@@ -18,6 +18,8 @@ import {
1818 type v1CreatePolicyIntentV3 ,
1919 type v1BootProof ,
2020 ProxyTSignupResponse ,
21+ TGetWalletsResponse ,
22+ TGetUserResponse ,
2123} from "@turnkey/sdk-types" ;
2224import {
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