Skip to content

Commit eee1ecc

Browse files
committed
optimize client init
1 parent e8a69e8 commit eee1ecc

File tree

10 files changed

+513
-322
lines changed

10 files changed

+513
-322
lines changed

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/src/__clients__/core.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -191,22 +191,37 @@ export class TurnkeyClient {
191191

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

196201
if (this.config.passkeyConfig) {
197-
this.passkeyStamper = new CrossPlatformPasskeyStamper(
202+
const passkeyStamper = new CrossPlatformPasskeyStamper(
198203
this.config.passkeyConfig,
199204
);
200-
await this.passkeyStamper.init();
205+
initTasks.push(
206+
passkeyStamper.init().then(() => {
207+
this.passkeyStamper = passkeyStamper;
208+
}),
209+
);
201210
}
202211

203212
if (
204213
this.config.walletConfig?.features?.auth ||
205214
this.config.walletConfig?.features?.connecting
206215
) {
207-
this.walletManager = await createWalletManager(this.config.walletConfig);
216+
initTasks.push(
217+
createWalletManager(this.config.walletConfig).then((manager) => {
218+
this.walletManager = manager;
219+
}),
220+
);
208221
}
209222

223+
await Promise.all(initTasks);
224+
210225
// Initialize the HTTP client with the appropriate stampers
211226
// Note: not passing anything here since we want to use the configured stampers and this.config
212227
this.httpClient = this.createHttpClient();

packages/core/src/__types__/external-wallets.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,10 @@ export interface WalletProvider {
6969
info: WalletProviderInfo;
7070
provider: WalletRpcProvider;
7171
connectedAddresses: string[];
72+
73+
// WalletConnect specific
7274
uri?: string;
75+
isLoading?: boolean;
7376
}
7477

7578
/** @internal */

packages/core/src/__wallet__/mobile/manager.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,15 @@ export class MobileWalletManager {
5252

5353
if (cfg.walletConnect && enableWalletConnect) {
5454
this.wcClient = new WalletConnectClient();
55-
const wcUnified = new WalletConnectWallet(this.wcClient);
55+
const wcUnified = new WalletConnectWallet(this.wcClient, undefined, {
56+
ethereumNamespaces,
57+
solanaNamespaces,
58+
});
5659

5760
this.wallets[WalletInterfaceType.WalletConnect] = wcUnified;
5861

5962
// add async init step to the initializer queue
60-
this.initializers.push(() =>
61-
wcUnified.init({ ethereumNamespaces, solanaNamespaces }),
62-
);
63+
this.initializers.push(() => wcUnified.init());
6364

6465
// register WalletConnect as a wallet interface for each enabled chain
6566
if (enableWalletConnectEvm) {

packages/core/src/__wallet__/wallet-connect/base.ts

Lines changed: 83 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,17 @@ import {
1616
WalletConnectInterface,
1717
SwitchableChain,
1818
} from "../../__types__";
19-
import type { WalletConnectClient } from "./client";
2019
import type { SessionTypes } from "@walletconnect/types";
20+
import type { WalletConnectClient } from "./client";
2121
import { Transaction } from "ethers";
2222

2323
type WalletConnectChangeEvent =
2424
| { type: "disconnect" }
2525
| { type: "chainChanged"; chainId?: string }
2626
| { type: "update" }
27-
| { type: "proposalExpired" };
27+
| { type: "proposalExpired" }
28+
| { type: "initialized" }
29+
| { type: "failed"; error?: unknown };
2830

2931
export class WalletConnectWallet implements WalletConnectInterface {
3032
readonly interfaceType = WalletInterfaceType.WalletConnect;
@@ -37,6 +39,7 @@ export class WalletConnectWallet implements WalletConnectInterface {
3739

3840
private uri?: string;
3941
private isRegeneratingUri = false;
42+
private isInitialized = false;
4043

4144
private changeListeners = new Set<
4245
(event?: WalletConnectChangeEvent) => void
@@ -60,8 +63,29 @@ export class WalletConnectWallet implements WalletConnectInterface {
6063
* updating `this.uri` so the UI can present a fresh QR/deeplink.
6164
*
6265
* @param client - The low-level WalletConnect client used for session/RPC.
66+
* @param ensureReady - Optional callback to ensure WalletConnect is initialized before operations.
67+
* @param namespaces - Optional namespace configuration to set up configured chains.
6368
*/
64-
constructor(private client: WalletConnectClient) {
69+
constructor(
70+
private client: WalletConnectClient,
71+
private ensureReady?: () => Promise<void>,
72+
namespaces?: {
73+
ethereumNamespaces: string[];
74+
solanaNamespaces: string[];
75+
},
76+
) {
77+
if (namespaces) {
78+
this.ethereumNamespaces = namespaces.ethereumNamespaces;
79+
if (this.ethereumNamespaces.length > 0) {
80+
this.ethChain = this.ethereumNamespaces[0]!;
81+
}
82+
83+
this.solanaNamespaces = namespaces.solanaNamespaces;
84+
if (this.solanaNamespaces.length > 0) {
85+
this.solChain = this.solanaNamespaces[0]!;
86+
}
87+
}
88+
6589
// session updated (actual update to the session for example adding a chain to namespaces)
6690
this.client.onSessionUpdate(() => {
6791
this.notifyChange({ type: "update" });
@@ -112,59 +136,57 @@ export class WalletConnectWallet implements WalletConnectInterface {
112136
}
113137

114138
/**
115-
* Initializes WalletConnect pairing flow with the specified namespaces.
139+
* Initializes WalletConnect pairing flow.
116140
*
117-
* - Saves the requested chain namespaces (e.g., `["eip155:1", "eip155:137", "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"]`).
118141
* - If an active session already has connected accounts, pairing is skipped.
119142
* - Otherwise initiates a pairing and stores the resulting URI.
143+
* - Namespaces should be set via constructor for this to work.
120144
*
121-
* @param opts.ethereumNamespaces - List of EVM CAIP IDs (e.g., "eip155:1").
122-
* @param opts.solanaNamespaces - List of Solana CAIP IDs (e.g., "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp").
123-
* @throws {Error} If no namespaces are provided for either chain.
145+
* @throws {Error} If no namespaces were configured in constructor.
124146
*/
125-
async init(opts: {
126-
ethereumNamespaces: string[];
127-
solanaNamespaces: string[];
128-
}): Promise<void> {
129-
this.ethereumNamespaces = opts.ethereumNamespaces;
130-
if (this.ethereumNamespaces.length > 0) {
131-
this.ethChain = this.ethereumNamespaces[0]!;
132-
}
147+
async init(): Promise<void> {
148+
try {
149+
if (
150+
this.ethereumNamespaces.length === 0 &&
151+
this.solanaNamespaces.length === 0
152+
) {
153+
throw new Error(
154+
"At least one namespace must be enabled for WalletConnect",
155+
);
156+
}
133157

134-
this.solanaNamespaces = opts.solanaNamespaces;
135-
if (this.solanaNamespaces.length > 0) {
136-
this.solChain = this.solanaNamespaces[0]!;
137-
}
158+
// we don't want to create more than one active session
159+
// so we don't make a pair request if one is already active
160+
// since pairing would mean initializing a new session
161+
const session = this.client.getSession();
162+
if (hasConnectedAccounts(session)) {
163+
this.isInitialized = true;
164+
// we notify that initialization is complete
165+
this.notifyChange({ type: "initialized" });
166+
return;
167+
}
138168

139-
if (
140-
this.ethereumNamespaces.length === 0 &&
141-
this.solanaNamespaces.length === 0
142-
) {
143-
throw new Error(
144-
"At least one namespace must be enabled for WalletConnect",
145-
);
146-
}
169+
const namespaces = this.buildNamespaces();
147170

148-
// we don't want to create more than one active session
149-
// so we don't make a pair request if one is already active
150-
// since pairing would mean initializing a new session
151-
const session = this.client.getSession();
152-
if (hasConnectedAccounts(session)) {
153-
return;
171+
await this.client.pair(namespaces).then((newUri) => {
172+
this.uri = newUri;
173+
this.isInitialized = true;
174+
// we notify that initialization is complete
175+
this.notifyChange({ type: "initialized" });
176+
});
177+
} catch (error) {
178+
// we emit a failed event
179+
this.notifyChange({ type: "failed", error });
180+
throw error;
154181
}
155-
156-
const namespaces = this.buildNamespaces();
157-
158-
await this.client.pair(namespaces).then((newUri) => {
159-
this.uri = newUri;
160-
});
161182
}
162183

163184
/**
164185
* Returns WalletConnect providers with associated chain/account metadata.
165186
*
166187
* - Builds an EVM provider (if Ethereum namespaces are enabled).
167188
* - Builds a Solana provider (if Solana namespaces are enabled).
189+
* - Before initialization, returns placeholder providers with isLoading: true.
168190
*
169191
* @returns A promise resolving to an array of WalletProvider objects.
170192
*/
@@ -194,12 +216,18 @@ export class WalletConnectWallet implements WalletConnectInterface {
194216
*
195217
* - Calls `approve()` on the underlying client when pairing is pending.
196218
* - Throws if the approved session contains no connected accounts.
219+
* - Waits for WalletConnect initialization if still in progress.
197220
*
198221
* @param _provider - Unused (present for interface compatibility).
199222
* @returns A promise that resolves with the connected wallet's address.
200223
* @throws {Error} If the session contains no accounts.
201224
*/
202225
async connectWalletAccount(provider: WalletProvider): Promise<string> {
226+
// we ensure WalletConnect is fully initialized before connecting
227+
if (this.ensureReady) {
228+
await this.ensureReady();
229+
}
230+
203231
const session = await this.client.approve();
204232

205233
let address: string | undefined;
@@ -222,14 +250,15 @@ export class WalletConnectWallet implements WalletConnectInterface {
222250
}
223251

224252
/**
225-
* Switches the users WalletConnect session to a new EVM chain.
253+
* Switches the user's WalletConnect session to a new EVM chain.
226254
*
227255
* - Ethereum-only: only supported for providers on the Ethereum namespace.
228256
* - No add-then-switch: WalletConnect cannot add chains mid-session. The target chain
229257
* must be present in `ethereumNamespaces` negotiated at pairing time. To support a new chain,
230258
* you must include it in the walletConfig.
231259
* - Accepts a hex chain ID (e.g., "0x1"). If a `SwitchableChain` is passed, only its `id`
232260
* (hex chain ID) is used; metadata is ignored for WalletConnect.
261+
* - Waits for WalletConnect initialization if still in progress.
233262
*
234263
* @param provider - The WalletProvider returned by `getProviders()`.
235264
* @param chainOrId - Hex chain ID (e.g., "0x1") or a `SwitchableChain` (its `id` is used).
@@ -241,6 +270,11 @@ export class WalletConnectWallet implements WalletConnectInterface {
241270
provider: WalletProvider,
242271
chainOrId: string | SwitchableChain,
243272
): Promise<void> {
273+
// we ensure WalletConnect is fully initialized
274+
if (this.ensureReady) {
275+
await this.ensureReady();
276+
}
277+
244278
if (provider.chainInfo.namespace !== Chain.Ethereum) {
245279
throw new Error("Only EVM wallets support chain switching");
246280
}
@@ -408,12 +442,18 @@ export class WalletConnectWallet implements WalletConnectInterface {
408442
*
409443
* - Ethereum: signs a fixed challenge and recovers the compressed secp256k1 public key.
410444
* - Solana: decodes the base58-encoded address to raw bytes.
445+
* - Waits for WalletConnect initialization if still in progress.
411446
*
412447
* @param provider - The WalletProvider to fetch the key from.
413448
* @returns A compressed public key as a hex string.
414449
* @throws {Error} If no account is available or the namespace is unsupported.
415450
*/
416451
async getPublicKey(provider: WalletProvider): Promise<string> {
452+
// we ensure WalletConnect is fully initialized
453+
if (this.ensureReady) {
454+
await this.ensureReady();
455+
}
456+
417457
const session = this.client.getSession();
418458

419459
if (provider.chainInfo.namespace === Chain.Ethereum) {
@@ -547,6 +587,7 @@ export class WalletConnectWallet implements WalletConnectInterface {
547587
provider: this.makeProvider(this.ethChain),
548588
connectedAddresses: address ? [address] : [],
549589
...(this.uri && { uri: this.uri }),
590+
isLoading: !this.isInitialized,
550591
};
551592
}
552593

@@ -574,6 +615,7 @@ export class WalletConnectWallet implements WalletConnectInterface {
574615
provider: this.makeProvider(this.solChain),
575616
connectedAddresses: address ? [address] : [],
576617
...(this.uri && { uri: this.uri }),
618+
isLoading: !this.isInitialized,
577619
};
578620
}
579621

packages/core/src/__wallet__/wallet-connect/client.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,11 @@ export class WalletConnectClient {
202202
* @returns The most recent session, or `null` if none are active.
203203
*/
204204
getSession(): SessionTypes.Struct | null {
205+
// we return null if the client hasn't been initialized yet
206+
if (!this.client?.session) {
207+
return null;
208+
}
209+
205210
const sessions = this.client.session.getAll();
206211
return sessions.length ? sessions[sessions.length - 1]! : null;
207212
}

0 commit comments

Comments
 (0)