@rango-dev/provider-walletconnect-2 0.57.0 → 0.57.1-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/adapter/adapter.d.ts +39 -0
  3. package/dist/adapter/adapter.d.ts.map +1 -0
  4. package/dist/adapter/index.d.ts +3 -0
  5. package/dist/adapter/index.d.ts.map +1 -0
  6. package/dist/adapter/modal-cache.d.ts +14 -0
  7. package/dist/adapter/modal-cache.d.ts.map +1 -0
  8. package/dist/adapter/modal.d.ts +25 -0
  9. package/dist/adapter/modal.d.ts.map +1 -0
  10. package/dist/adapter/networks.d.ts +60 -0
  11. package/dist/adapter/networks.d.ts.map +1 -0
  12. package/dist/adapter/provider-client.d.ts +3 -0
  13. package/dist/adapter/provider-client.d.ts.map +1 -0
  14. package/dist/adapter/registry.d.ts +4 -0
  15. package/dist/adapter/registry.d.ts.map +1 -0
  16. package/dist/adapter/session-cache.d.ts +17 -0
  17. package/dist/adapter/session-cache.d.ts.map +1 -0
  18. package/dist/adapter/task-queue.d.ts +11 -0
  19. package/dist/adapter/task-queue.d.ts.map +1 -0
  20. package/dist/chunk-GEAT645O.js +2 -0
  21. package/dist/chunk-GEAT645O.js.map +7 -0
  22. package/dist/constants.d.ts +4 -48
  23. package/dist/constants.d.ts.map +1 -0
  24. package/dist/evm-3KFL5N77.js +2 -0
  25. package/dist/evm-3KFL5N77.js.map +7 -0
  26. package/dist/mod.d.ts +4 -0
  27. package/dist/mod.d.ts.map +1 -0
  28. package/dist/mod.js +2 -0
  29. package/dist/mod.js.map +7 -0
  30. package/dist/namespaces/evm/hooks.d.ts +18 -0
  31. package/dist/namespaces/evm/hooks.d.ts.map +1 -0
  32. package/dist/namespaces/evm/namespace.d.ts +4 -0
  33. package/dist/namespaces/evm/namespace.d.ts.map +1 -0
  34. package/dist/namespaces/subscriber.d.ts +24 -0
  35. package/dist/namespaces/subscriber.d.ts.map +1 -0
  36. package/dist/provider.d.ts +3 -0
  37. package/dist/provider.d.ts.map +1 -0
  38. package/dist/session/accounts.d.ts +17 -0
  39. package/dist/session/accounts.d.ts.map +1 -0
  40. package/dist/session/bip122.d.ts +23 -0
  41. package/dist/session/bip122.d.ts.map +1 -0
  42. package/dist/session/chain-state.d.ts +6 -0
  43. package/dist/session/chain-state.d.ts.map +1 -0
  44. package/dist/session/evm-chain.d.ts +29 -0
  45. package/dist/session/evm-chain.d.ts.map +1 -0
  46. package/dist/session/evm.d.ts +25 -0
  47. package/dist/session/evm.d.ts.map +1 -0
  48. package/dist/session/lifecycle.d.ts +30 -0
  49. package/dist/session/lifecycle.d.ts.map +1 -0
  50. package/dist/session/lookup.d.ts +6 -0
  51. package/dist/session/lookup.d.ts.map +1 -0
  52. package/dist/session/proposals.d.ts +27 -0
  53. package/dist/session/proposals.d.ts.map +1 -0
  54. package/dist/session/restore-cache.d.ts +6 -0
  55. package/dist/session/restore-cache.d.ts.map +1 -0
  56. package/dist/session/teardown.d.ts +24 -0
  57. package/dist/session/teardown.d.ts.map +1 -0
  58. package/dist/signer.d.ts +2 -2
  59. package/dist/signer.d.ts.map +1 -0
  60. package/dist/signers/evm.d.ts +6 -4
  61. package/dist/signers/evm.d.ts.map +1 -0
  62. package/dist/types.d.ts +28 -11
  63. package/dist/types.d.ts.map +1 -0
  64. package/dist/utils.d.ts +17 -0
  65. package/dist/utils.d.ts.map +1 -0
  66. package/dist/wcConstants.d.ts +61 -0
  67. package/dist/wcConstants.d.ts.map +1 -0
  68. package/package.json +18 -13
  69. package/readme.md +14 -2
  70. package/src/adapter/adapter.ts +289 -0
  71. package/src/adapter/index.ts +2 -0
  72. package/src/adapter/modal-cache.ts +23 -0
  73. package/src/adapter/modal.ts +121 -0
  74. package/src/adapter/networks.ts +85 -0
  75. package/src/adapter/provider-client.ts +13 -0
  76. package/src/adapter/registry.ts +17 -0
  77. package/src/adapter/session-cache.ts +38 -0
  78. package/src/adapter/task-queue.ts +32 -0
  79. package/src/constants.ts +41 -76
  80. package/src/mod.ts +10 -0
  81. package/src/namespaces/evm/hooks.ts +148 -0
  82. package/src/namespaces/evm/namespace.ts +102 -0
  83. package/src/namespaces/subscriber.ts +106 -0
  84. package/src/provider.ts +35 -0
  85. package/src/session/accounts.ts +44 -0
  86. package/src/session/bip122.ts +225 -0
  87. package/src/session/chain-state.ts +38 -0
  88. package/src/session/evm-chain.ts +185 -0
  89. package/src/session/evm.ts +198 -0
  90. package/src/session/lifecycle.ts +241 -0
  91. package/src/session/lookup.ts +54 -0
  92. package/src/session/proposals.ts +96 -0
  93. package/src/session/restore-cache.ts +38 -0
  94. package/src/session/teardown.ts +181 -0
  95. package/src/signer.ts +9 -17
  96. package/src/signers/evm.ts +19 -17
  97. package/src/types.ts +31 -12
  98. package/src/utils.ts +68 -0
  99. package/src/wcConstants.ts +94 -0
  100. package/dist/chunk-6V3WUFOU.js +0 -2
  101. package/dist/chunk-6V3WUFOU.js.map +0 -7
  102. package/dist/chunk-BPSEZJUF.js +0 -2
  103. package/dist/chunk-BPSEZJUF.js.map +0 -7
  104. package/dist/chunk-DXVBX5XQ.js +0 -2
  105. package/dist/chunk-DXVBX5XQ.js.map +0 -7
  106. package/dist/dist-6GI3ECE5.js +0 -139
  107. package/dist/dist-6GI3ECE5.js.map +0 -7
  108. package/dist/evm-3ID4XY4Y.js +0 -2
  109. package/dist/evm-3ID4XY4Y.js.map +0 -7
  110. package/dist/helpers.d.ts +0 -31
  111. package/dist/index.d.ts +0 -21
  112. package/dist/index.js +0 -2
  113. package/dist/index.js.map +0 -7
  114. package/dist/session.d.ts +0 -63
  115. package/dist/signers/solana.d.ts +0 -14
  116. package/dist/solana-MFA7C5XI.js +0 -2
  117. package/dist/solana-MFA7C5XI.js.map +0 -7
  118. package/src/helpers.ts +0 -238
  119. package/src/index.ts +0 -234
  120. package/src/session.ts +0 -376
  121. package/src/signers/solana.ts +0 -160
  122. package/src/wc-types.d.ts +0 -31
@@ -0,0 +1,185 @@
1
+ import type { Chain } from '@hub3js/evm';
2
+ import type { ISignClient, SessionTypes } from '@walletconnect/types';
3
+ import type { BlockchainMeta } from 'rango-types';
4
+
5
+ import { error as logError } from '@rango-dev/logging-core';
6
+ import { ChainId } from 'caip';
7
+
8
+ import {
9
+ chainReferenceToHex,
10
+ parseChainReference,
11
+ resolveNetworkName,
12
+ } from '../utils.js';
13
+ import { EthereumRPCMethods, NAMESPACES } from '../wcConstants.js';
14
+
15
+ import { getAccountsFromSession } from './accounts.js';
16
+ import { getPersistedChainId, persistCurrentChainId } from './chain-state.js';
17
+ import {
18
+ filterEvmAccounts,
19
+ ignoreNamespaceMethods,
20
+ switchOrAddEvmChain,
21
+ updateSessionAccounts,
22
+ } from './evm.js';
23
+
24
+ export type EvmChainDeps = {
25
+ meta: BlockchainMeta[];
26
+ getSession: (namespace: 'evm') => SessionTypes.Struct | null;
27
+ cacheSession: (namespace: 'evm', session: SessionTypes.Struct) => void;
28
+ getClient: () => Promise<ISignClient>;
29
+ ensureSession: (options: {
30
+ namespace: 'evm';
31
+ chainReference?: string;
32
+ }) => Promise<SessionTypes.Struct>;
33
+ };
34
+
35
+ export async function resolveActiveChainReference(
36
+ client: ISignClient,
37
+ session: SessionTypes.Struct
38
+ ): Promise<string | undefined> {
39
+ const persistedChainId = await getPersistedChainId(client);
40
+ if (persistedChainId) {
41
+ return persistedChainId;
42
+ }
43
+
44
+ const accounts = getAccountsFromSession(session);
45
+ const evmAccounts = filterEvmAccounts(accounts);
46
+ return evmAccounts[0]?.chainId;
47
+ }
48
+
49
+ export async function getCurrentChainId(
50
+ client: ISignClient,
51
+ session: SessionTypes.Struct
52
+ ): Promise<`0x${string}`> {
53
+ const reference = await resolveActiveChainReference(client, session);
54
+ if (!reference) {
55
+ throw new Error(
56
+ 'Unable to determine EVM chain id from WalletConnect session.'
57
+ );
58
+ }
59
+
60
+ return chainReferenceToHex(reference);
61
+ }
62
+
63
+ /**
64
+ * Returns the session to keep using: `updateSessionAccounts` may rebuild it, and
65
+ * the caller's copy is stale from that point on.
66
+ */
67
+ export async function switchEvmNetwork(options: {
68
+ client: ISignClient;
69
+ session: SessionTypes.Struct;
70
+ meta: BlockchainMeta[];
71
+ requestedChainId: string;
72
+ currentChainId: string;
73
+ }): Promise<SessionTypes.Struct> {
74
+ const { client, session, meta, requestedChainId, currentChainId } = options;
75
+
76
+ const requestedReference = parseChainReference(requestedChainId);
77
+ if (!requestedReference) {
78
+ const error = new Error(`There is no match for ${requestedChainId}`);
79
+ logError(error);
80
+ throw error;
81
+ }
82
+
83
+ const chainIdStr = new ChainId({
84
+ namespace: NAMESPACES.ETHEREUM,
85
+ reference: requestedReference,
86
+ }).toString();
87
+
88
+ const requestedNetwork = resolveNetworkName(requestedChainId, meta);
89
+ const currentNetwork = resolveNetworkName(currentChainId, meta);
90
+
91
+ const evmNamespace = session.namespaces[NAMESPACES.ETHEREUM];
92
+ const authorizedChains = evmNamespace?.chains || [];
93
+ const authorizedMethods = evmNamespace?.methods || [];
94
+ let activeSession = session;
95
+
96
+ if (
97
+ authorizedMethods.includes(EthereumRPCMethods.SWITCH_CHAIN) &&
98
+ !ignoreNamespaceMethods(session)
99
+ ) {
100
+ if (!requestedNetwork || !currentNetwork) {
101
+ const error = new Error(`Chain ${requestedReference} is not configured.`);
102
+ logError(error);
103
+ throw error;
104
+ }
105
+
106
+ activeSession = await updateSessionAccounts(
107
+ client,
108
+ session,
109
+ requestedNetwork,
110
+ currentNetwork,
111
+ meta
112
+ );
113
+ await switchOrAddEvmChain(
114
+ client,
115
+ activeSession,
116
+ meta,
117
+ requestedNetwork,
118
+ currentNetwork
119
+ );
120
+ } else if (!authorizedChains.includes(chainIdStr)) {
121
+ const error = new Error(`Chain ${requestedReference} is not configured.`);
122
+ logError(error);
123
+ throw error;
124
+ }
125
+
126
+ await persistCurrentChainId(client, requestedReference);
127
+ return activeSession;
128
+ }
129
+
130
+ export async function switchToChainIfNeeded(
131
+ deps: EvmChainDeps,
132
+ requestedReference: string
133
+ ): Promise<void> {
134
+ const session = deps.getSession('evm');
135
+ if (!session) {
136
+ throw new Error('WalletConnect session is not available.');
137
+ }
138
+
139
+ const client = await deps.getClient();
140
+ const currentReference = await resolveActiveChainReference(client, session);
141
+ if (currentReference === requestedReference) {
142
+ await persistCurrentChainId(client, requestedReference);
143
+ return;
144
+ }
145
+
146
+ const currentChainId = currentReference
147
+ ? chainReferenceToHex(currentReference)
148
+ : chainReferenceToHex(requestedReference);
149
+
150
+ deps.cacheSession(
151
+ 'evm',
152
+ await switchEvmNetwork({
153
+ client,
154
+ session,
155
+ meta: deps.meta,
156
+ requestedChainId: requestedReference,
157
+ currentChainId,
158
+ })
159
+ );
160
+ }
161
+
162
+ export async function ensureConnectedToChain(
163
+ deps: EvmChainDeps,
164
+ chain?: string | Chain
165
+ ): Promise<SessionTypes.Struct> {
166
+ const requestedReference = chain ? parseChainReference(chain) : undefined;
167
+
168
+ if (!deps.getSession('evm')) {
169
+ await deps.ensureSession({
170
+ namespace: 'evm',
171
+ chainReference: requestedReference,
172
+ });
173
+ }
174
+
175
+ if (requestedReference) {
176
+ await switchToChainIfNeeded(deps, requestedReference);
177
+ }
178
+
179
+ const session = deps.getSession('evm');
180
+ if (!session) {
181
+ throw new Error('WalletConnect session is not available.');
182
+ }
183
+
184
+ return session;
185
+ }
@@ -0,0 +1,198 @@
1
+ import type { ISignClient, SessionTypes } from '@walletconnect/types';
2
+ import type { BlockchainMeta } from 'rango-types';
3
+
4
+ import { debug } from '@rango-dev/logging-core';
5
+ import {
6
+ convertEvmBlockchainMetaToEvmChainInfo,
7
+ isEvmAddress,
8
+ } from '@rango-dev/wallets-shared';
9
+ import { AccountId, ChainId } from 'caip';
10
+ import { isEvmBlockchain } from 'rango-types';
11
+
12
+ import { EthereumRPCMethods, NAMESPACES } from '../wcConstants.js';
13
+
14
+ import { getChainIdByNetworkName } from './chain-state.js';
15
+
16
+ export function getCurrentEvmAccountAddress(
17
+ session: SessionTypes.Struct
18
+ ): string | undefined {
19
+ return session.namespaces[NAMESPACES.ETHEREUM]?.accounts
20
+ ?.map((account) => new AccountId(account).address)
21
+ ?.filter((address) => isEvmAddress(address))?.[0];
22
+ }
23
+
24
+ export function getEvmAccount(
25
+ network: string,
26
+ address: string,
27
+ meta: BlockchainMeta[]
28
+ ): string | undefined {
29
+ const currentChainId = getChainIdByNetworkName(network, meta);
30
+ if (!currentChainId) {
31
+ return undefined;
32
+ }
33
+
34
+ return AccountId.format({
35
+ chainId: {
36
+ namespace: NAMESPACES.ETHEREUM,
37
+ reference: currentChainId,
38
+ },
39
+ address,
40
+ });
41
+ }
42
+
43
+ // It's enough to return only connected network for the EVM networks and ignore others
44
+ export function filterEvmAccounts(
45
+ accounts: {
46
+ address: string;
47
+ chainId: string;
48
+ }[],
49
+ currentChainId?: string
50
+ ) {
51
+ let firstEvmAddress = false;
52
+ return accounts
53
+ .filter(({ address, chainId }) => {
54
+ const isEvm = isEvmAddress(address);
55
+ if (!isEvm) {
56
+ return false;
57
+ }
58
+ if (currentChainId && chainId !== currentChainId) {
59
+ return false;
60
+ }
61
+ if (!currentChainId) {
62
+ if (firstEvmAddress) {
63
+ return false;
64
+ }
65
+ firstEvmAddress = true;
66
+ }
67
+ return true;
68
+ })
69
+ .map(({ address, chainId }) => ({
70
+ accounts: [address],
71
+ chainId: chainId,
72
+ }));
73
+ }
74
+
75
+ export async function switchOrAddEvmChain(
76
+ client: ISignClient,
77
+ session: SessionTypes.Struct,
78
+ meta: BlockchainMeta[],
79
+ requestedNetwork: string,
80
+ currentNetwork: string
81
+ ) {
82
+ const evmBlockchains = meta.filter(isEvmBlockchain);
83
+ const evmNetworksChainInfo =
84
+ convertEvmBlockchainMetaToEvmChainInfo(evmBlockchains);
85
+ const targetChain = evmNetworksChainInfo[requestedNetwork];
86
+ const targetBlockchain = meta.find(
87
+ (blockchain: BlockchainMeta) => blockchain.name === requestedNetwork
88
+ );
89
+ const chainIdInHex = targetBlockchain?.chainId;
90
+
91
+ const currentChainId = getChainIdByNetworkName(currentNetwork, meta);
92
+ const currentChainEip = ChainId.format({
93
+ namespace: NAMESPACES.ETHEREUM,
94
+ reference: String(currentChainId),
95
+ });
96
+
97
+ try {
98
+ await client.request({
99
+ topic: session.topic,
100
+ request: {
101
+ method: EthereumRPCMethods.SWITCH_CHAIN,
102
+ params: [
103
+ {
104
+ chainId: chainIdInHex,
105
+ },
106
+ ],
107
+ },
108
+ // It's required to pass current chain, otherwise it won't work
109
+ chainId: currentChainEip,
110
+ });
111
+ } catch (err) {
112
+ // EIP-1193 "Unrecognized chain ID" - the wallet doesn't have this chain yet.
113
+ const addChainError = 4902;
114
+ const { code, message } = (err ?? {}) as {
115
+ code?: number;
116
+ message?: string;
117
+ };
118
+ if (code === addChainError || message?.includes(String(addChainError))) {
119
+ await client.request({
120
+ topic: session.topic,
121
+ request: {
122
+ method: EthereumRPCMethods.ADD_CHAIN,
123
+ params: [targetChain],
124
+ },
125
+ // It's required to pass current chain, otherwise it won't work
126
+ chainId: currentChainEip,
127
+ });
128
+ } else {
129
+ throw err;
130
+ }
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Adds the requested and current chains' accounts to the session so sign-client
136
+ * accepts a request carrying their CAIP chain id, and returns the session to use
137
+ * from here on.
138
+ *
139
+ * The record is rebuilt rather than pushed into: the struct is shared with the
140
+ * store and the adapter cache, and the write below can fail (it is logged, not
141
+ * thrown), which would otherwise leave both holding accounts the store never
142
+ * accepted.
143
+ */
144
+ export async function updateSessionAccounts(
145
+ client: ISignClient,
146
+ session: SessionTypes.Struct,
147
+ requestedNetwork: string,
148
+ currentNetwork: string,
149
+ meta: BlockchainMeta[]
150
+ ): Promise<SessionTypes.Struct> {
151
+ const evmNamespace = session.namespaces[NAMESPACES.ETHEREUM];
152
+ const currentAccountAddress = getCurrentEvmAccountAddress(session);
153
+ if (!evmNamespace || !currentAccountAddress) {
154
+ return session;
155
+ }
156
+
157
+ const existing = evmNamespace.accounts ?? [];
158
+ const wanted = [
159
+ getEvmAccount(requestedNetwork, currentAccountAddress, meta),
160
+ getEvmAccount(currentNetwork, currentAccountAddress, meta),
161
+ ].filter(
162
+ (account): account is string => !!account && !existing.includes(account)
163
+ );
164
+
165
+ if (!wanted.length) {
166
+ return session;
167
+ }
168
+
169
+ const updated: SessionTypes.Struct = {
170
+ ...session,
171
+ namespaces: {
172
+ ...session.namespaces,
173
+ [NAMESPACES.ETHEREUM]: {
174
+ ...evmNamespace,
175
+ accounts: [...existing, ...new Set(wanted)],
176
+ },
177
+ },
178
+ };
179
+
180
+ try {
181
+ await client.session.update(session.topic, {
182
+ namespaces: updated.namespaces,
183
+ });
184
+ } catch (err) {
185
+ debug(err instanceof Error ? err : new Error(String(err)));
186
+ return session;
187
+ }
188
+
189
+ return updated;
190
+ }
191
+
192
+ export function ignoreNamespaceMethods(session: SessionTypes.Struct): boolean {
193
+ const WALLETS_WITH_WRONG_NAMESPACE_METHODS = ['trust', '1inch'];
194
+ const peerName = session.peer?.metadata?.name;
195
+ return WALLETS_WITH_WRONG_NAMESPACE_METHODS.some((name) =>
196
+ peerName?.toLowerCase()?.includes(name)
197
+ );
198
+ }
@@ -0,0 +1,241 @@
1
+ import type { AppKitModal } from '../adapter/modal.js';
2
+ import type {
3
+ ConnectParams,
4
+ CreateSessionParams,
5
+ Environments,
6
+ SignClientInstance,
7
+ WalletConnectNamespace,
8
+ } from '../types.js';
9
+ import type { ISignClient, SessionTypes } from '@walletconnect/types';
10
+ import type UniversalProvider from '@walletconnect/universal-provider';
11
+
12
+ import { debug } from '@rango-dev/logging-core';
13
+ import { timeout } from '@rango-dev/wallets-shared';
14
+
15
+ import { prepareModalForNamespace } from '../adapter/modal.js';
16
+ import { PING_TIMEOUT, WC_NAMESPACE_TO_CAIP } from '../wcConstants.js';
17
+
18
+ import { getAccountsFromSession } from './accounts.js';
19
+ import { filterBip122Accounts, resolveBip122Session } from './bip122.js';
20
+ import { getPersistedChainId } from './chain-state.js';
21
+ import { filterEvmAccounts } from './evm.js';
22
+ import { findSessionByNamespace } from './lookup.js';
23
+ import { generateOptionalNamespace } from './proposals.js';
24
+ import {
25
+ cleanupStaleSessionsForNamespace,
26
+ hasActivePairing,
27
+ hasActiveSession,
28
+ purgeOrphanedPairings,
29
+ purgeOrphanedSessions,
30
+ removeSessionRecord,
31
+ } from './teardown.js';
32
+
33
+ export type RestoreWalletConnectSessionOptions = {
34
+ validateAccounts?: boolean;
35
+ };
36
+
37
+ export async function restoreWalletConnectSession(
38
+ client: SignClientInstance,
39
+ namespace: WalletConnectNamespace,
40
+ options: RestoreWalletConnectSessionOptions = {}
41
+ ): Promise<SessionTypes.Struct | undefined> {
42
+ const session = await restoreNamespaceSession(client, namespace);
43
+ if (!session) {
44
+ return undefined;
45
+ }
46
+
47
+ /*
48
+ * Never wait for `bip122_addressesChanged` here. The wallet only emits it just
49
+ * after approval, so a stored session that still has no account will never be
50
+ * given one - waiting would stall for the full timeout and fail anyway. Report
51
+ * it as unrestorable instead and let the caller negotiate a fresh session.
52
+ */
53
+ if (namespace === 'utxo' && filterBip122Accounts(session).length === 0) {
54
+ return undefined;
55
+ }
56
+
57
+ if (options.validateAccounts) {
58
+ const isValid = await hasNamespaceAccounts(client, namespace, session);
59
+ if (!isValid) {
60
+ return undefined;
61
+ }
62
+ }
63
+
64
+ return session;
65
+ }
66
+
67
+ async function hasNamespaceAccounts(
68
+ client: SignClientInstance,
69
+ namespace: WalletConnectNamespace,
70
+ session: SessionTypes.Struct
71
+ ): Promise<boolean> {
72
+ if (namespace === 'utxo') {
73
+ return filterBip122Accounts(session).length > 0;
74
+ }
75
+
76
+ const accounts = getAccountsFromSession(session);
77
+ const currentChainId = await getPersistedChainId(client);
78
+ return filterEvmAccounts(accounts, currentChainId).length > 0;
79
+ }
80
+
81
+ /**
82
+ * Ping the session topic and return it if still valid.
83
+ * Returns undefined (instead of throwing) when storage contains stale records.
84
+ */
85
+ export async function restoreNamespaceSession(
86
+ client: SignClientInstance,
87
+ namespace: WalletConnectNamespace
88
+ ): Promise<SessionTypes.Struct | undefined> {
89
+ const session = findSessionByNamespace(client, namespace);
90
+ if (!session?.topic) {
91
+ return undefined;
92
+ }
93
+
94
+ if (!hasActiveSession(client, session.topic)) {
95
+ await removeSessionRecord(client, session);
96
+ return undefined;
97
+ }
98
+
99
+ if (session.pairingTopic && !hasActivePairing(client, session.pairingTopic)) {
100
+ await removeSessionRecord(client, session);
101
+ return undefined;
102
+ }
103
+
104
+ try {
105
+ await timeout(
106
+ client.ping({
107
+ topic: session.topic,
108
+ }),
109
+ PING_TIMEOUT
110
+ );
111
+ } catch (error) {
112
+ debug(error instanceof Error ? error : new Error(String(error)));
113
+ await removeSessionRecord(client, session);
114
+ return undefined;
115
+ }
116
+
117
+ return findSessionByNamespace(client, namespace) ?? session;
118
+ }
119
+
120
+ /**
121
+ * Clears unusable records ahead of a connect: orphaned sessions across every
122
+ * namespace, plus `namespace`'s own stale ones.
123
+ *
124
+ * A *live* session belonging to another namespace is deliberately left alone -
125
+ * EVM and UTXO each own an independent session, and only the namespace being
126
+ * disconnected drops its own.
127
+ */
128
+ export async function prepareWalletConnectNamespace(
129
+ client: UniversalProvider['client'],
130
+ namespace: WalletConnectNamespace
131
+ ): Promise<void> {
132
+ await purgeOrphanedSessions(client);
133
+ await cleanupStaleSessionsForNamespace(client, namespace);
134
+ await purgeOrphanedPairings(client);
135
+ }
136
+
137
+ /**
138
+ * Restores `namespace`'s session, or negotiates a new one.
139
+ *
140
+ * Callers are expected to have run {@link prepareWalletConnectNamespace} first -
141
+ * this does not clean up on its own.
142
+ */
143
+ export async function connectWalletConnectSession(
144
+ client: UniversalProvider['client'],
145
+ web3Modal: AppKitModal,
146
+ params: ConnectParams
147
+ ): Promise<SessionTypes.Struct> {
148
+ const { chains, namespace, chainReference } = params;
149
+
150
+ const restored = await restoreWalletConnectSession(client, namespace);
151
+ if (restored) {
152
+ return restored;
153
+ }
154
+
155
+ const proposal = generateOptionalNamespace(
156
+ chains,
157
+ [namespace],
158
+ chainReference
159
+ );
160
+ const session = await createSession(
161
+ client,
162
+ web3Modal,
163
+ { optionalNamespaces: proposal ?? {} },
164
+ {
165
+ envs: params.envs,
166
+ namespace,
167
+ }
168
+ );
169
+
170
+ /*
171
+ * Only a just-approved session can be waited on: bitcoin wallets routinely
172
+ * approve bip122 with no account and send it moments later. A restored session
173
+ * never reaches here, so the wait can't be spent on one that will never
174
+ * receive the event.
175
+ */
176
+ if (namespace === 'utxo') {
177
+ return resolveBip122Session(client, session);
178
+ }
179
+
180
+ return session;
181
+ }
182
+
183
+ async function createSession(
184
+ client: ISignClient,
185
+ web3Modal: AppKitModal,
186
+ options: CreateSessionParams,
187
+ configs: {
188
+ envs: Environments;
189
+ namespace: WalletConnectNamespace;
190
+ }
191
+ ): Promise<SessionTypes.Struct> {
192
+ const { optionalNamespaces, pairingTopic } = options;
193
+
194
+ try {
195
+ const { uri, approval } = await client.connect({
196
+ optionalNamespaces,
197
+ pairingTopic,
198
+ });
199
+
200
+ let onCloseModal;
201
+ if (uri) {
202
+ const redirectLink = configs.envs.DISABLE_MODAL_AND_OPEN_LINK;
203
+ if (redirectLink) {
204
+ const url = `${redirectLink}/wc?uri=${encodeURIComponent(uri)}`;
205
+ window.open(url, '_blank', 'noreferrer noopener');
206
+ } else {
207
+ await prepareModalForNamespace(web3Modal, configs.namespace);
208
+ /*
209
+ * `namespace` scopes the wallet list AppKit renders. It isn't in
210
+ * AppKit's `OpenOptions` type but is honored at runtime, so it goes
211
+ * through a pre-built object (an inline literal would trip the
212
+ * excess-property check). `WC_NAMESPACE_TO_CAIP` is the single source
213
+ * for the alias -> CAIP prefix instead of sniffing the payload keys.
214
+ */
215
+ const openOptions = {
216
+ uri,
217
+ namespace: WC_NAMESPACE_TO_CAIP[configs.namespace],
218
+ };
219
+ await web3Modal.open(openOptions);
220
+
221
+ onCloseModal = new Promise((_, reject) => {
222
+ web3Modal.subscribeState((state) => {
223
+ if (!state.open) {
224
+ reject(new Error('Modal has been closed.'));
225
+ }
226
+ });
227
+ });
228
+ }
229
+ }
230
+
231
+ const session = approval();
232
+
233
+ if (onCloseModal) {
234
+ const result = await Promise.race([session, onCloseModal]);
235
+ return result as SessionTypes.Struct;
236
+ }
237
+ return await session;
238
+ } finally {
239
+ void web3Modal.close();
240
+ }
241
+ }
@@ -0,0 +1,54 @@
1
+ import type { WalletConnectNamespace } from '../types.js';
2
+ import type { ISignClient, SessionTypes } from '@walletconnect/types';
3
+
4
+ import { WC_NAMESPACE_TO_CAIP } from '../wcConstants.js';
5
+
6
+ export function getCaipNamespace(namespace: WalletConnectNamespace): string {
7
+ return WC_NAMESPACE_TO_CAIP[namespace];
8
+ }
9
+
10
+ export function findSessionByNamespace(
11
+ client: ISignClient,
12
+ namespace: WalletConnectNamespace
13
+ ): SessionTypes.Struct | undefined {
14
+ const caipNamespace = getCaipNamespace(namespace);
15
+
16
+ return client.session.getAll().find((session) => {
17
+ const ns = session.namespaces[caipNamespace];
18
+ if ((ns?.accounts?.length ?? 0) > 0) {
19
+ return true;
20
+ }
21
+
22
+ return namespace === 'utxo' && (ns?.chains?.length ?? 0) > 0;
23
+ });
24
+ }
25
+
26
+ export function getSessionNamespace(
27
+ session: SessionTypes.Struct
28
+ ): WalletConnectNamespace | undefined {
29
+ const hasAccounts = (namespace: WalletConnectNamespace) =>
30
+ (session.namespaces[WC_NAMESPACE_TO_CAIP[namespace]]?.accounts?.length ??
31
+ 0) > 0;
32
+ const hasChains = (namespace: WalletConnectNamespace) =>
33
+ (session.namespaces[WC_NAMESPACE_TO_CAIP[namespace]]?.chains?.length ?? 0) >
34
+ 0;
35
+
36
+ /*
37
+ * Prefer a namespace the wallet shared accounts for; fall back to one that
38
+ * only negotiated chains (utxo can be chains-only before its account settles).
39
+ */
40
+ if (hasAccounts('evm')) {
41
+ return 'evm';
42
+ }
43
+ if (hasAccounts('utxo')) {
44
+ return 'utxo';
45
+ }
46
+ if (hasChains('utxo')) {
47
+ return 'utxo';
48
+ }
49
+ if (hasChains('evm')) {
50
+ return 'evm';
51
+ }
52
+
53
+ return undefined;
54
+ }