Skip to content

Commit 3f6b07f

Browse files
committed
Merge pull-request #1090
2 parents fae0c3d + 558949c commit 3f6b07f

File tree

22 files changed

+1450
-1109
lines changed

22 files changed

+1450
-1109
lines changed

.changeset/rude-signs-like.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@turnkey/sdk-types": minor
3+
---
4+
5+
Added `WALLET_CONNECT_INITIALIZATION_ERROR` error code

.changeset/six-laws-see.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
"@turnkey/core": minor
3+
---
4+
5+
- Fixed `stamp*` methods for query endpoints in `httpClient` incorrectly formatting request body
6+
- Parallelized stamper and session initialization
7+
- Separated WalletConnect initialization from client init
8+
- Optimized `fetchWallet` by reducing redundant queries and running wallet/user fetches in parallel
9+
- Added optional `authenticatorAddresses` param to `fetchWalletAccounts()`
10+
- Updated to latest `@walletconnect/sign-client` for performance improvements

.changeset/wild-monkeys-report.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@turnkey/react-wallet-kit": minor
3+
---
4+
5+
- Fixed unnecessary re-renders by ensuring all `useCallback` hooks include only direct dependencies
6+
- ConnectWallet and Auth model updated to show WalletConnect loading state during initialization

examples/with-sdk-js/e2e/auth-component.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,8 @@ test.describe("auth with wallet", async () => {
140140
await page
141141
.getByTestId(walletKitSelectors.authComponent.walletAuthButton)
142142
.click();
143-
await expect(page.getByText("Select wallet provider")).toBeVisible();
143+
await expect(
144+
page.getByText(/Select wallet provider|Select chain/),
145+
).toBeVisible();
144146
});
145147
});

packages/core/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@
4343
"@turnkey/webauthn-stamper": "workspace:*",
4444
"@wallet-standard/app": "^1.1.0",
4545
"@wallet-standard/base": "^1.1.0",
46-
"@walletconnect/sign-client": "^2.21.8",
47-
"@walletconnect/types": "^2.21.8",
46+
"@walletconnect/sign-client": "^2.23.0",
47+
"@walletconnect/types": "^2.23.0",
4848
"cross-fetch": "^3.1.5",
4949
"ethers": "^6.10.0",
5050
"jwt-decode": "4.0.0",

packages/core/scripts/codegen.js

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,7 @@ const generateSDKClientFromSwagger = async (
369369
const inputType = `T${operationNameWithoutNamespace}Body`;
370370
const responseType = `T${operationNameWithoutNamespace}Response`;
371371

372-
// For query methods
372+
// for query methods, we use flat body structure
373373
if (methodType === "query") {
374374
codeBuffer.push(
375375
`\n\t${methodName} = async (input: SdkTypes.${inputType}${
@@ -434,8 +434,38 @@ const generateSDKClientFromSwagger = async (
434434
VERSIONED_ACTIVITY_TYPES[unversionedActivityType];
435435

436436
// generate a stamping method for each method
437-
codeBuffer.push(
438-
`\n\tstamp${operationNameWithoutNamespace} = async (input: SdkTypes.${inputType}, stampWith?: StamperType): Promise<TSignedRequest | undefined> => {
437+
438+
if (methodType === "noop") {
439+
// we skip stamp method generation for noop methods
440+
continue;
441+
} else if (methodType === "query") {
442+
// for query methods, we use flat body structure
443+
codeBuffer.push(
444+
`\n\tstamp${operationNameWithoutNamespace} = async (input: SdkTypes.${inputType}, stampWith?: StamperType): Promise<TSignedRequest | undefined> => {
445+
const activeStamper = this.getStamper(stampWith);
446+
if (!activeStamper) {
447+
return undefined;
448+
}
449+
450+
const fullUrl = this.config.apiBaseUrl + "${endpointPath}";
451+
const body = {
452+
...input,
453+
organizationId: input.organizationId
454+
};
455+
456+
const stringifiedBody = JSON.stringify(body);
457+
const stamp = await activeStamper.stamp(stringifiedBody);
458+
return {
459+
body: stringifiedBody,
460+
stamp: stamp,
461+
url: fullUrl,
462+
};
463+
}`,
464+
);
465+
} else {
466+
// for activity and activityDecision methods, use parameters wrapper and type field
467+
codeBuffer.push(
468+
`\n\tstamp${operationNameWithoutNamespace} = async (input: SdkTypes.${inputType}, stampWith?: StamperType): Promise<TSignedRequest | undefined> => {
439469
const activeStamper = this.getStamper(stampWith);
440470
if (!activeStamper) {
441471
return undefined;
@@ -454,7 +484,6 @@ const generateSDKClientFromSwagger = async (
454484
type: "${versionedActivityType ?? unversionedActivityType}"
455485
};
456486
457-
458487
const stringifiedBody = JSON.stringify(bodyWithType);
459488
const stamp = await activeStamper.stamp(stringifiedBody);
460489
return {
@@ -463,7 +492,8 @@ const generateSDKClientFromSwagger = async (
463492
url: fullUrl,
464493
};
465494
}`,
466-
);
495+
);
496+
}
467497
}
468498

469499
for (const endpointPath in authProxySwaggerSpec.paths) {

packages/core/src/__clients__/core.ts

Lines changed: 105 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
type v1CreatePolicyIntentV3,
1919
type v1BootProof,
2020
ProxyTSignupResponse,
21+
TGetWalletsResponse,
22+
TGetUserResponse,
2123
} from "@turnkey/sdk-types";
2224
import {
2325
DEFAULT_SESSION_EXPIRATION_IN_SECONDS,
@@ -191,22 +193,37 @@ export class TurnkeyClient {
191193

192194
// Initialize the API key stamper
193195
this.apiKeyStamper = new CrossPlatformApiKeyStamper(this.storageManager);
194-
await this.apiKeyStamper.init();
196+
197+
// we parallelize independent initializations:
198+
// - API key stamper init
199+
// - Passkey stamper creation and init (if configured)
200+
// - Wallet manager creation (if configured)
201+
const initTasks: Promise<void>[] = [this.apiKeyStamper.init()];
195202

196203
if (this.config.passkeyConfig) {
197-
this.passkeyStamper = new CrossPlatformPasskeyStamper(
204+
const passkeyStamper = new CrossPlatformPasskeyStamper(
198205
this.config.passkeyConfig,
199206
);
200-
await this.passkeyStamper.init();
207+
initTasks.push(
208+
passkeyStamper.init().then(() => {
209+
this.passkeyStamper = passkeyStamper;
210+
}),
211+
);
201212
}
202213

203214
if (
204215
this.config.walletConfig?.features?.auth ||
205216
this.config.walletConfig?.features?.connecting
206217
) {
207-
this.walletManager = await createWalletManager(this.config.walletConfig);
218+
initTasks.push(
219+
createWalletManager(this.config.walletConfig).then((manager) => {
220+
this.walletManager = manager;
221+
}),
222+
);
208223
}
209224

225+
await Promise.all(initTasks);
226+
210227
// Initialize the HTTP client with the appropriate stampers
211228
// Note: not passing anything here since we want to use the configured stampers and this.config
212229
this.httpClient = this.createHttpClient();
@@ -566,12 +583,13 @@ export class TurnkeyClient {
566583
expirationSeconds,
567584
});
568585

569-
await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey!);
570-
571-
await this.storeSession({
572-
sessionToken: sessionResponse.session,
573-
sessionKey,
574-
});
586+
await Promise.all([
587+
this.apiKeyStamper?.deleteKeyPair(generatedPublicKey!),
588+
this.storeSession({
589+
sessionToken: sessionResponse.session,
590+
sessionKey,
591+
}),
592+
]);
575593

576594
generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
577595

@@ -1119,12 +1137,13 @@ export class TurnkeyClient {
11191137
expirationSeconds,
11201138
});
11211139

1122-
await this.apiKeyStamper?.deleteKeyPair(generatedPublicKey!);
1123-
1124-
await this.storeSession({
1125-
sessionToken: sessionResponse.session,
1126-
sessionKey,
1127-
});
1140+
await Promise.all([
1141+
this.apiKeyStamper?.deleteKeyPair(generatedPublicKey!),
1142+
this.storeSession({
1143+
sessionToken: sessionResponse.session,
1144+
sessionKey,
1145+
}),
1146+
]);
11281147

11291148
generatedPublicKey = undefined; // Key pair was successfully used, set to null to prevent cleanup
11301149

@@ -1939,39 +1958,75 @@ export class TurnkeyClient {
19391958
async () => {
19401959
let embedded: EmbeddedWallet[] = [];
19411960

1961+
const organizationId =
1962+
organizationIdFromParams || session?.organizationId;
1963+
const userId = userIdFromParams || session?.userId;
1964+
1965+
// we start fetching user early if we have the required params (needed for connected wallets)
1966+
// this runs in parallel with the embedded wallet fetching below
1967+
let userPromise:
1968+
| Promise<{ ethereum: string[]; solana: string[] }>
1969+
| undefined;
1970+
if (organizationId && userId && this.walletManager?.connector) {
1971+
const signedUserRequest = await this.httpClient.stampGetUser(
1972+
{
1973+
userId,
1974+
organizationId,
1975+
},
1976+
stampWith,
1977+
);
1978+
if (!signedUserRequest) {
1979+
throw new TurnkeyError(
1980+
"Failed to stamp user request",
1981+
TurnkeyErrorCodes.INVALID_REQUEST,
1982+
);
1983+
}
1984+
userPromise = sendSignedRequest<TGetUserResponse>(
1985+
signedUserRequest,
1986+
).then((response) => getAuthenticatorAddresses(response.user));
1987+
}
1988+
19421989
// if connectedOnly is true, we skip fetching embedded wallets
19431990
if (!connectedOnly) {
1944-
const organizationId =
1945-
organizationIdFromParams || session?.organizationId;
1946-
19471991
if (!organizationId) {
19481992
throw new TurnkeyError(
19491993
"No organization ID provided and no active session found. Please log in first or pass in an organization ID.",
19501994
TurnkeyErrorCodes.INVALID_REQUEST,
19511995
);
19521996
}
19531997

1954-
const userId = userIdFromParams || session?.userId;
19551998
if (!userId) {
19561999
throw new TurnkeyError(
19572000
"No user ID provided and no active session found. Please log in first or pass in a user ID.",
19582001
TurnkeyErrorCodes.INVALID_REQUEST,
19592002
);
19602003
}
19612004

1962-
const accounts = await fetchAllWalletAccountsWithCursor(
1963-
this.httpClient,
1964-
organizationId,
1965-
stampWith,
1966-
);
1967-
1968-
const walletsRes = await this.httpClient.getWallets(
2005+
// we stamp the wallet request first
2006+
// this is done to avoid concurrent passkey prompts
2007+
const signedWalletsRequest = await this.httpClient.stampGetWallets(
19692008
{
19702009
organizationId,
19712010
},
19722011
stampWith,
19732012
);
19742013

2014+
if (!signedWalletsRequest) {
2015+
throw new TurnkeyError(
2016+
"Failed to stamp wallet request",
2017+
TurnkeyErrorCodes.INVALID_REQUEST,
2018+
);
2019+
}
2020+
2021+
const [accounts, walletsRes] = await Promise.all([
2022+
fetchAllWalletAccountsWithCursor(
2023+
this.httpClient,
2024+
organizationId,
2025+
stampWith,
2026+
),
2027+
sendSignedRequest<TGetWalletsResponse>(signedWalletsRequest),
2028+
]);
2029+
19752030
// create a map of walletId to EmbeddedWallet for easy lookup
19762031
const walletMap: Map<string, EmbeddedWallet> = new Map(
19772032
walletsRes.wallets.map((wallet) => [
@@ -2006,6 +2061,16 @@ export class TurnkeyClient {
20062061
groupedProviders.set(walletId, group);
20072062
}
20082063

2064+
// we fetch user once for all connected wallets to avoid duplicate `fetchUser` calls
2065+
// this is only done if we have `organizationId` and `userId`
2066+
// Note: this was started earlier in parallel with embedded wallet fetching for performance
2067+
let authenticatorAddresses:
2068+
| { ethereum: string[]; solana: string[] }
2069+
| undefined;
2070+
if (userPromise) {
2071+
authenticatorAddresses = await userPromise;
2072+
}
2073+
20092074
// has to be done in a for of loop so we can await each fetchWalletAccounts call individually
20102075
// otherwise await Promise.all would cause them all to fire at once breaking passkey only set ups
20112076
// (multiple wallet fetches at once causing "OperationError: A request is already pending.")
@@ -2032,6 +2097,7 @@ export class TurnkeyClient {
20322097
organizationId: organizationIdFromParams,
20332098
}),
20342099
...(userIdFromParams !== undefined && { userId: userIdFromParams }),
2100+
...(authenticatorAddresses && { authenticatorAddresses }),
20352101
});
20362102

20372103
wallet.accounts = accounts;
@@ -2064,6 +2130,7 @@ export class TurnkeyClient {
20642130
* @param params.stampWith - parameter to stamp the request with a specific stamper (StamperType.Passkey, StamperType.ApiKey, or StamperType.Wallet).
20652131
* @param params.organizationId - organization ID to target (defaults to the session's organization ID).
20662132
* @param params.userId - user ID to target (defaults to the session's user ID).
2133+
* @param params.authenticatorAddresses - optional authenticator addresses to avoid redundant user fetches (this is used for connected wallets to determine if a connected wallet is an authenticator)
20672134
*
20682135
* @returns A promise that resolves to an array of `v1WalletAccount` objects.
20692136
* @throws {TurnkeyError} If no active session is found or if there is an error fetching wallet accounts.
@@ -2159,9 +2226,12 @@ export class TurnkeyClient {
21592226
let ethereumAddresses: string[] = [];
21602227
let solanaAddresses: string[] = [];
21612228

2162-
// we only fetch the user if we have to the organizationId and userId
2163-
// if not that means `isAuthenticator` will always be false
2164-
if (organizationId && userId) {
2229+
if (params.authenticatorAddresses) {
2230+
({ ethereum: ethereumAddresses, solana: solanaAddresses } =
2231+
params.authenticatorAddresses);
2232+
} else if (organizationId && userId) {
2233+
// we only fetch the user if authenticator addresses aren't provided and we have the organizationId and userId
2234+
// if not, then that means `isAuthenticator` will always be false
21652235
const user = await this.fetchUser({
21662236
userId,
21672237
organizationId,
@@ -2680,7 +2750,7 @@ export class TurnkeyClient {
26802750
);
26812751
}
26822752

2683-
return userResponse.user as v1User;
2753+
return userResponse.user;
26842754
},
26852755
{
26862756
errorMessage: "Failed to fetch user",
@@ -4212,8 +4282,10 @@ export class TurnkeyClient {
42124282
async () => {
42134283
const session = await this.storageManager.getSession(sessionKey);
42144284
if (session) {
4215-
await this.apiKeyStamper?.deleteKeyPair(session.publicKey!);
4216-
await this.storageManager.clearSession(sessionKey);
4285+
await Promise.all([
4286+
this.apiKeyStamper?.deleteKeyPair(session.publicKey!),
4287+
this.storageManager.clearSession(sessionKey),
4288+
]);
42174289
} else {
42184290
throw new TurnkeyError(
42194291
`No session found with key: ${sessionKey}`,

0 commit comments

Comments
 (0)