@rango-dev/provider-walletconnect-2 0.57.0 → 0.57.1-next.0
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.
- package/CHANGELOG.md +4 -0
- package/dist/adapter/adapter.d.ts +39 -0
- package/dist/adapter/adapter.d.ts.map +1 -0
- package/dist/adapter/index.d.ts +3 -0
- package/dist/adapter/index.d.ts.map +1 -0
- package/dist/adapter/modal-cache.d.ts +14 -0
- package/dist/adapter/modal-cache.d.ts.map +1 -0
- package/dist/adapter/modal.d.ts +25 -0
- package/dist/adapter/modal.d.ts.map +1 -0
- package/dist/adapter/networks.d.ts +60 -0
- package/dist/adapter/networks.d.ts.map +1 -0
- package/dist/adapter/provider-client.d.ts +3 -0
- package/dist/adapter/provider-client.d.ts.map +1 -0
- package/dist/adapter/registry.d.ts +4 -0
- package/dist/adapter/registry.d.ts.map +1 -0
- package/dist/adapter/session-cache.d.ts +17 -0
- package/dist/adapter/session-cache.d.ts.map +1 -0
- package/dist/adapter/task-queue.d.ts +11 -0
- package/dist/adapter/task-queue.d.ts.map +1 -0
- package/dist/chunk-GEAT645O.js +2 -0
- package/dist/chunk-GEAT645O.js.map +7 -0
- package/dist/constants.d.ts +4 -48
- package/dist/constants.d.ts.map +1 -0
- package/dist/evm-3KFL5N77.js +2 -0
- package/dist/evm-3KFL5N77.js.map +7 -0
- package/dist/mod.d.ts +4 -0
- package/dist/mod.d.ts.map +1 -0
- package/dist/mod.js +2 -0
- package/dist/mod.js.map +7 -0
- package/dist/namespaces/evm/hooks.d.ts +18 -0
- package/dist/namespaces/evm/hooks.d.ts.map +1 -0
- package/dist/namespaces/evm/namespace.d.ts +4 -0
- package/dist/namespaces/evm/namespace.d.ts.map +1 -0
- package/dist/namespaces/subscriber.d.ts +24 -0
- package/dist/namespaces/subscriber.d.ts.map +1 -0
- package/dist/provider.d.ts +3 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/session/accounts.d.ts +17 -0
- package/dist/session/accounts.d.ts.map +1 -0
- package/dist/session/bip122.d.ts +23 -0
- package/dist/session/bip122.d.ts.map +1 -0
- package/dist/session/chain-state.d.ts +6 -0
- package/dist/session/chain-state.d.ts.map +1 -0
- package/dist/session/evm-chain.d.ts +29 -0
- package/dist/session/evm-chain.d.ts.map +1 -0
- package/dist/session/evm.d.ts +25 -0
- package/dist/session/evm.d.ts.map +1 -0
- package/dist/session/lifecycle.d.ts +30 -0
- package/dist/session/lifecycle.d.ts.map +1 -0
- package/dist/session/lookup.d.ts +6 -0
- package/dist/session/lookup.d.ts.map +1 -0
- package/dist/session/proposals.d.ts +27 -0
- package/dist/session/proposals.d.ts.map +1 -0
- package/dist/session/restore-cache.d.ts +6 -0
- package/dist/session/restore-cache.d.ts.map +1 -0
- package/dist/session/teardown.d.ts +24 -0
- package/dist/session/teardown.d.ts.map +1 -0
- package/dist/signer.d.ts +2 -2
- package/dist/signer.d.ts.map +1 -0
- package/dist/signers/evm.d.ts +6 -4
- package/dist/signers/evm.d.ts.map +1 -0
- package/dist/types.d.ts +28 -11
- package/dist/types.d.ts.map +1 -0
- package/dist/utils.d.ts +17 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/wcConstants.d.ts +61 -0
- package/dist/wcConstants.d.ts.map +1 -0
- package/package.json +18 -13
- package/readme.md +14 -2
- package/src/adapter/adapter.ts +289 -0
- package/src/adapter/index.ts +2 -0
- package/src/adapter/modal-cache.ts +23 -0
- package/src/adapter/modal.ts +121 -0
- package/src/adapter/networks.ts +85 -0
- package/src/adapter/provider-client.ts +13 -0
- package/src/adapter/registry.ts +17 -0
- package/src/adapter/session-cache.ts +38 -0
- package/src/adapter/task-queue.ts +32 -0
- package/src/constants.ts +41 -76
- package/src/mod.ts +10 -0
- package/src/namespaces/evm/hooks.ts +148 -0
- package/src/namespaces/evm/namespace.ts +102 -0
- package/src/namespaces/subscriber.ts +106 -0
- package/src/provider.ts +35 -0
- package/src/session/accounts.ts +44 -0
- package/src/session/bip122.ts +225 -0
- package/src/session/chain-state.ts +38 -0
- package/src/session/evm-chain.ts +185 -0
- package/src/session/evm.ts +198 -0
- package/src/session/lifecycle.ts +241 -0
- package/src/session/lookup.ts +54 -0
- package/src/session/proposals.ts +96 -0
- package/src/session/restore-cache.ts +38 -0
- package/src/session/teardown.ts +181 -0
- package/src/signer.ts +9 -17
- package/src/signers/evm.ts +19 -17
- package/src/types.ts +31 -12
- package/src/utils.ts +68 -0
- package/src/wcConstants.ts +94 -0
- package/dist/chunk-6V3WUFOU.js +0 -2
- package/dist/chunk-6V3WUFOU.js.map +0 -7
- package/dist/chunk-BPSEZJUF.js +0 -2
- package/dist/chunk-BPSEZJUF.js.map +0 -7
- package/dist/chunk-DXVBX5XQ.js +0 -2
- package/dist/chunk-DXVBX5XQ.js.map +0 -7
- package/dist/dist-6GI3ECE5.js +0 -139
- package/dist/dist-6GI3ECE5.js.map +0 -7
- package/dist/evm-3ID4XY4Y.js +0 -2
- package/dist/evm-3ID4XY4Y.js.map +0 -7
- package/dist/helpers.d.ts +0 -31
- package/dist/index.d.ts +0 -21
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -7
- package/dist/session.d.ts +0 -63
- package/dist/signers/solana.d.ts +0 -14
- package/dist/solana-MFA7C5XI.js +0 -2
- package/dist/solana-MFA7C5XI.js.map +0 -7
- package/src/helpers.ts +0 -238
- package/src/index.ts +0 -234
- package/src/session.ts +0 -376
- package/src/signers/solana.ts +0 -160
- 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
|
+
}
|