@marigoldlabs/web3-tester 0.4.1 → 0.4.2
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/README.md +317 -11
- package/dist/anvil.d.ts.map +1 -1
- package/dist/anvil.js.map +1 -1
- package/dist/benchmark.d.ts +55 -0
- package/dist/benchmark.d.ts.map +1 -0
- package/dist/benchmark.js +168 -0
- package/dist/benchmark.js.map +1 -0
- package/dist/fixtures.js +2 -2
- package/dist/fixtures.js.map +1 -1
- package/dist/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -1
- package/dist/index.js.map +1 -1
- package/dist/injected-provider.d.ts.map +1 -1
- package/dist/injected-provider.js +748 -25
- package/dist/injected-provider.js.map +1 -1
- package/dist/mock-wallet-controller.d.ts +150 -2
- package/dist/mock-wallet-controller.d.ts.map +1 -1
- package/dist/mock-wallet-controller.js +613 -24
- package/dist/mock-wallet-controller.js.map +1 -1
- package/dist/private-key-rpc-client.d.ts +144 -132
- package/dist/private-key-rpc-client.d.ts.map +1 -1
- package/dist/private-key-rpc-client.js +142 -20
- package/dist/private-key-rpc-client.js.map +1 -1
- package/dist/real-wallet-cache.d.ts +38 -0
- package/dist/real-wallet-cache.d.ts.map +1 -1
- package/dist/real-wallet-cache.js +143 -57
- package/dist/real-wallet-cache.js.map +1 -1
- package/dist/real-wallet-extension-fixtures.d.ts +35 -0
- package/dist/real-wallet-extension-fixtures.d.ts.map +1 -0
- package/dist/real-wallet-extension-fixtures.js +96 -0
- package/dist/real-wallet-extension-fixtures.js.map +1 -0
- package/dist/real-wallet-extension.d.ts +86 -0
- package/dist/real-wallet-extension.d.ts.map +1 -0
- package/dist/real-wallet-extension.js +245 -0
- package/dist/real-wallet-extension.js.map +1 -0
- package/dist/real-wallet-fixtures.d.ts.map +1 -1
- package/dist/real-wallet-fixtures.js +46 -31
- package/dist/real-wallet-fixtures.js.map +1 -1
- package/dist/real-wallet.d.ts +5 -14
- package/dist/real-wallet.d.ts.map +1 -1
- package/dist/real-wallet.js +668 -435
- package/dist/real-wallet.js.map +1 -1
- package/dist/safe.d.ts +311 -0
- package/dist/safe.d.ts.map +1 -0
- package/dist/safe.js +743 -0
- package/dist/safe.js.map +1 -0
- package/dist/types.d.ts +35 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/wallet-personas.d.ts +99 -0
- package/dist/wallet-personas.d.ts.map +1 -0
- package/dist/wallet-personas.js +666 -0
- package/dist/wallet-personas.js.map +1 -0
- package/dist/walletconnect.d.ts +176 -9
- package/dist/walletconnect.d.ts.map +1 -1
- package/dist/walletconnect.js +514 -74
- package/dist/walletconnect.js.map +1 -1
- package/examples/playwright.config.ts +8 -0
- package/package.json +29 -3
package/dist/walletconnect.js
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { toHex } from 'viem';
|
|
2
2
|
import { serializeRpcError } from './errors.js';
|
|
3
|
+
import { createWalletPersona, walletConnectMetadataForPersona, } from './wallet-personas.js';
|
|
4
|
+
const removeWalletConnectListener = (client, event, handler) => {
|
|
5
|
+
if (client.off) {
|
|
6
|
+
client.off(event, handler);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
client.removeListener?.(event, handler);
|
|
10
|
+
};
|
|
3
11
|
export const DEFAULT_WALLETCONNECT_METHODS = [
|
|
4
12
|
'eth_sendTransaction',
|
|
5
13
|
'eth_sendRawTransaction',
|
|
@@ -16,11 +24,39 @@ export const DEFAULT_WALLETCONNECT_METHODS = [
|
|
|
16
24
|
'wallet_getPermissions',
|
|
17
25
|
'wallet_requestPermissions',
|
|
18
26
|
'wallet_watchAsset',
|
|
27
|
+
'wallet_getCapabilities',
|
|
28
|
+
'wallet_sendCalls',
|
|
29
|
+
'wallet_getCallsStatus',
|
|
30
|
+
'wallet_showCallsStatus',
|
|
31
|
+
];
|
|
32
|
+
export const DEFAULT_WALLETCONNECT_COINBASE_METHODS = [
|
|
33
|
+
'wallet_connect',
|
|
34
|
+
'wallet_addSubAccount',
|
|
35
|
+
'wallet_getSubAccounts',
|
|
36
|
+
'coinbase_fetchPermissions',
|
|
37
|
+
'coinbase_fetchPermission',
|
|
19
38
|
];
|
|
20
39
|
const DEFAULT_EVENTS = ['chainChanged', 'accountsChanged'];
|
|
40
|
+
export const DEFAULT_WALLETCONNECT_SOLANA_METHODS = [
|
|
41
|
+
'solana_getAccounts',
|
|
42
|
+
'solana_requestAccounts',
|
|
43
|
+
'solana_signIn',
|
|
44
|
+
'solana_signAllTransactions',
|
|
45
|
+
'solana_signAndSendAllTransactions',
|
|
46
|
+
'solana_signAndSendTransaction',
|
|
47
|
+
'solana_signMessage',
|
|
48
|
+
'solana_signTransaction',
|
|
49
|
+
];
|
|
50
|
+
const DEFAULT_SOLANA_EVENTS = ['accountsChanged'];
|
|
51
|
+
const DEFAULT_SOLANA_CHAINS = [
|
|
52
|
+
'solana:mainnet',
|
|
53
|
+
'solana:devnet',
|
|
54
|
+
'solana:testnet',
|
|
55
|
+
];
|
|
21
56
|
// Injectable so the missing-optional-peer hint is hermetically testable
|
|
22
57
|
// (the peers are devDependencies here, so they always resolve in-repo).
|
|
23
58
|
let loadModule = (specifier) => import(specifier);
|
|
59
|
+
let walletConnectStoragePrefixCounter = 0;
|
|
24
60
|
/** @internal Test hook: stub the dynamic importer; call with no args to restore. */
|
|
25
61
|
export const __setWalletConnectModuleLoader = (loader) => {
|
|
26
62
|
loadModule = loader ?? ((specifier) => import(specifier));
|
|
@@ -29,7 +65,206 @@ const parseCaipChainId = (caip) => {
|
|
|
29
65
|
const match = typeof caip === 'string' ? /^eip155:(\d+)$/.exec(caip) : null;
|
|
30
66
|
return match ? toHex(BigInt(match[1])) : undefined;
|
|
31
67
|
};
|
|
68
|
+
const parseSolanaCaipChainId = (caip) => typeof caip === 'string' && /^solana:[a-z0-9-]+$/i.test(caip) ? caip : undefined;
|
|
32
69
|
const toCaipChainId = (chainId) => `eip155:${Number(BigInt(chainId))}`;
|
|
70
|
+
const isAddressString = (value) => typeof value === 'string' && /^0x[0-9a-fA-F]{40}$/.test(value);
|
|
71
|
+
const eip155AccountsFor = (chains, accounts) => chains.flatMap((chainId) => accounts.map((account) => `${chainId}:${account}`));
|
|
72
|
+
const arraysEqual = (left = [], right = []) => left.length === right.length && left.every((value, index) => value === right[index]);
|
|
73
|
+
const bytesFrom = (input) => {
|
|
74
|
+
if (input instanceof Uint8Array)
|
|
75
|
+
return [...input];
|
|
76
|
+
if (input instanceof ArrayBuffer)
|
|
77
|
+
return [...new Uint8Array(input)];
|
|
78
|
+
if (ArrayBuffer.isView(input)) {
|
|
79
|
+
return [...new Uint8Array(input.buffer, input.byteOffset, input.byteLength)];
|
|
80
|
+
}
|
|
81
|
+
if (Array.isArray(input))
|
|
82
|
+
return input.map((value) => Number(value) & 255);
|
|
83
|
+
if (typeof input === 'string')
|
|
84
|
+
return [...new TextEncoder().encode(input)];
|
|
85
|
+
return [...new TextEncoder().encode(JSON.stringify(input ?? null))];
|
|
86
|
+
};
|
|
87
|
+
const deterministicSignature = (publicKey, payload) => {
|
|
88
|
+
const seed = [...new TextEncoder().encode(publicKey), ...bytesFrom(payload)];
|
|
89
|
+
const signature = new Uint8Array(64);
|
|
90
|
+
for (let index = 0; index < signature.length; index += 1) {
|
|
91
|
+
const a = seed[index % seed.length] ?? 0;
|
|
92
|
+
const b = seed[(index * 7 + 13) % seed.length] ?? 0;
|
|
93
|
+
signature[index] = (a + b + index * 17) & 255;
|
|
94
|
+
}
|
|
95
|
+
return signature;
|
|
96
|
+
};
|
|
97
|
+
const base58Encode = (input) => {
|
|
98
|
+
const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
|
|
99
|
+
const digits = [0];
|
|
100
|
+
for (const byte of input) {
|
|
101
|
+
let carry = byte;
|
|
102
|
+
for (let index = 0; index < digits.length; index += 1) {
|
|
103
|
+
const next = digits[index] * 256 + carry;
|
|
104
|
+
digits[index] = next % 58;
|
|
105
|
+
carry = Math.floor(next / 58);
|
|
106
|
+
}
|
|
107
|
+
while (carry > 0) {
|
|
108
|
+
digits.push(carry % 58);
|
|
109
|
+
carry = Math.floor(carry / 58);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const byte of input) {
|
|
113
|
+
if (byte !== 0)
|
|
114
|
+
break;
|
|
115
|
+
digits.push(0);
|
|
116
|
+
}
|
|
117
|
+
return digits
|
|
118
|
+
.reverse()
|
|
119
|
+
.map((digit) => alphabet[digit])
|
|
120
|
+
.join('');
|
|
121
|
+
};
|
|
122
|
+
const signatureFor = (publicKey, payload) => base58Encode(deterministicSignature(publicKey, payload));
|
|
123
|
+
const resolveSolanaOptions = (option, persona) => {
|
|
124
|
+
if (option === false) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const input = typeof option === 'object' ? option : {};
|
|
128
|
+
const personaSolana = persona?.solana;
|
|
129
|
+
if (!personaSolana && option !== true && option === undefined) {
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
chains: input.chains ?? personaSolana?.chains ?? DEFAULT_SOLANA_CHAINS,
|
|
134
|
+
publicKey: input.publicKey ??
|
|
135
|
+
personaSolana?.publicKey ??
|
|
136
|
+
'26qv4GCcx98RihuK3c4T6ozB3J7L6VwCuFVc7Ta2A3Uo',
|
|
137
|
+
methods: input.methods ?? DEFAULT_WALLETCONNECT_SOLANA_METHODS,
|
|
138
|
+
events: input.events ?? DEFAULT_SOLANA_EVENTS,
|
|
139
|
+
};
|
|
140
|
+
};
|
|
141
|
+
const rpcError = (code, message) => {
|
|
142
|
+
const error = new Error(message);
|
|
143
|
+
error.code = code;
|
|
144
|
+
return error;
|
|
145
|
+
};
|
|
146
|
+
const normalizeObjectParams = (params) => {
|
|
147
|
+
const candidate = Array.isArray(params) ? params[0] : params;
|
|
148
|
+
return candidate && typeof candidate === 'object' ? candidate : {};
|
|
149
|
+
};
|
|
150
|
+
const normalizeJsonRpcParams = (params) => {
|
|
151
|
+
if (params === undefined || Array.isArray(params))
|
|
152
|
+
return params;
|
|
153
|
+
if (params && typeof params === 'object')
|
|
154
|
+
return params;
|
|
155
|
+
return [params];
|
|
156
|
+
};
|
|
157
|
+
const assertSolanaPubkey = (params, publicKey) => {
|
|
158
|
+
const requested = params.pubkey;
|
|
159
|
+
if (requested !== undefined && requested !== publicKey) {
|
|
160
|
+
throw rpcError(4100, `The requested Solana account ${String(requested)} is not authorized.`);
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const solanaAccountsResult = (solana) => [
|
|
164
|
+
{ pubkey: solana.publicKey },
|
|
165
|
+
];
|
|
166
|
+
const buildSolanaSignInMessage = (publicKey, input = {}) => {
|
|
167
|
+
const address = input.address ?? publicKey;
|
|
168
|
+
const domain = input.domain ?? 'localhost';
|
|
169
|
+
const lines = [
|
|
170
|
+
`${domain} wants you to sign in with your Solana account:`,
|
|
171
|
+
String(address),
|
|
172
|
+
];
|
|
173
|
+
if (input.statement !== undefined) {
|
|
174
|
+
lines.push('', String(input.statement));
|
|
175
|
+
}
|
|
176
|
+
const fields = [
|
|
177
|
+
['URI', input.uri],
|
|
178
|
+
['Version', input.version],
|
|
179
|
+
['Chain ID', input.chainId],
|
|
180
|
+
['Nonce', input.nonce],
|
|
181
|
+
['Issued At', input.issuedAt],
|
|
182
|
+
['Expiration Time', input.expirationTime],
|
|
183
|
+
['Not Before', input.notBefore],
|
|
184
|
+
['Request ID', input.requestId],
|
|
185
|
+
];
|
|
186
|
+
for (const [label, value] of fields) {
|
|
187
|
+
if (value !== undefined) {
|
|
188
|
+
lines.push(`${label}: ${String(value)}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (Array.isArray(input.resources) && input.resources.length > 0) {
|
|
192
|
+
lines.push('Resources:');
|
|
193
|
+
for (const resource of input.resources) {
|
|
194
|
+
lines.push(`- ${String(resource)}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return new TextEncoder().encode(lines.join('\n'));
|
|
198
|
+
};
|
|
199
|
+
const handleSolanaSessionRequest = async (wallet, solana, event, context) => {
|
|
200
|
+
const method = event.params.request.method;
|
|
201
|
+
if (!solana.methods.includes(method)) {
|
|
202
|
+
throw rpcError(4200, `The mock wallet does not support the method "${method}".`);
|
|
203
|
+
}
|
|
204
|
+
const params = normalizeObjectParams(event.params.request.params);
|
|
205
|
+
switch (method) {
|
|
206
|
+
case 'solana_getAccounts': {
|
|
207
|
+
const accounts = (await wallet.handleExternalRequest({ method: 'eth_accounts', params: [] }, context));
|
|
208
|
+
return accounts && accounts.length > 0 ? solanaAccountsResult(solana) : [];
|
|
209
|
+
}
|
|
210
|
+
case 'solana_requestAccounts':
|
|
211
|
+
await wallet.handleExternalRequest({ method, params: [params] }, context);
|
|
212
|
+
return solanaAccountsResult(solana);
|
|
213
|
+
case 'solana_signIn': {
|
|
214
|
+
const input = normalizeObjectParams(params.input ?? params);
|
|
215
|
+
if (input.address !== undefined && input.address !== solana.publicKey) {
|
|
216
|
+
throw rpcError(4100, `The requested Solana account ${String(input.address)} is not authorized.`);
|
|
217
|
+
}
|
|
218
|
+
const signedMessage = buildSolanaSignInMessage(solana.publicKey, input);
|
|
219
|
+
await wallet.handleExternalRequest({
|
|
220
|
+
method,
|
|
221
|
+
params: [{ input, publicKey: solana.publicKey, message: [...signedMessage] }],
|
|
222
|
+
}, context);
|
|
223
|
+
return {
|
|
224
|
+
address: input.address ?? solana.publicKey,
|
|
225
|
+
publicKey: solana.publicKey,
|
|
226
|
+
signedMessage: [...signedMessage],
|
|
227
|
+
signature: signatureFor(solana.publicKey, signedMessage),
|
|
228
|
+
signatureType: 'ed25519',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
case 'solana_signMessage':
|
|
232
|
+
assertSolanaPubkey(params, solana.publicKey);
|
|
233
|
+
await wallet.handleExternalRequest({ method, params: [params] }, context);
|
|
234
|
+
return {
|
|
235
|
+
signature: signatureFor(solana.publicKey, params.message ?? ''),
|
|
236
|
+
};
|
|
237
|
+
case 'solana_signTransaction':
|
|
238
|
+
await wallet.handleExternalRequest({ method, params: [params] }, context);
|
|
239
|
+
return {
|
|
240
|
+
signature: signatureFor(solana.publicKey, params.transaction ?? params),
|
|
241
|
+
...(typeof params.transaction === 'string' ? { transaction: params.transaction } : {}),
|
|
242
|
+
};
|
|
243
|
+
case 'solana_signAllTransactions': {
|
|
244
|
+
await wallet.handleExternalRequest({ method, params: [params] }, context);
|
|
245
|
+
const transactions = Array.isArray(params.transactions) ? params.transactions : [];
|
|
246
|
+
return {
|
|
247
|
+
transactions,
|
|
248
|
+
signatures: transactions.map((transaction) => signatureFor(solana.publicKey, transaction)),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
case 'solana_signAndSendAllTransactions': {
|
|
252
|
+
await wallet.handleExternalRequest({ method, params: [params] }, context);
|
|
253
|
+
const transactions = Array.isArray(params.transactions) ? params.transactions : [];
|
|
254
|
+
return {
|
|
255
|
+
publicKey: solana.publicKey,
|
|
256
|
+
signatures: transactions.map((transaction) => signatureFor(solana.publicKey, transaction)),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
case 'solana_signAndSendTransaction':
|
|
260
|
+
await wallet.handleExternalRequest({ method, params: [params] }, context);
|
|
261
|
+
return {
|
|
262
|
+
signature: signatureFor(solana.publicKey, params.transaction ?? params),
|
|
263
|
+
};
|
|
264
|
+
default:
|
|
265
|
+
throw rpcError(4200, `The mock wallet does not support the method "${method}".`);
|
|
266
|
+
}
|
|
267
|
+
};
|
|
33
268
|
/**
|
|
34
269
|
* The session_request loop, exported for hermetic tests: validates the CAIP
|
|
35
270
|
* chain against the approved set, switches the wallet when an approved
|
|
@@ -42,8 +277,26 @@ export const createSessionRequestHandler = (wallet, config, respond) => {
|
|
|
42
277
|
return async (event) => {
|
|
43
278
|
const { id, topic, params, verifyContext } = event;
|
|
44
279
|
try {
|
|
280
|
+
const context = config.enforceOrigins
|
|
281
|
+
? { origin: verifyContext?.verified?.origin }
|
|
282
|
+
: { bypassOriginCheck: true };
|
|
283
|
+
const solanaChain = parseSolanaCaipChainId(params.chainId);
|
|
284
|
+
if (solanaChain) {
|
|
285
|
+
if (!config.solana || !config.solana.chains.includes(solanaChain)) {
|
|
286
|
+
await respond(topic, {
|
|
287
|
+
id,
|
|
288
|
+
jsonrpc: '2.0',
|
|
289
|
+
error: { code: 5100, message: 'Requested chain is not approved for this session.' },
|
|
290
|
+
});
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
const result = await handleSolanaSessionRequest(wallet, config.solana, event, context);
|
|
294
|
+
await respond(topic, { id, jsonrpc: '2.0', result });
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
45
297
|
const chainId = parseCaipChainId(params.chainId);
|
|
46
|
-
|
|
298
|
+
const approvedChains = config.getChains?.() ?? config.chains;
|
|
299
|
+
if (!chainId || !approvedChains.includes(chainId)) {
|
|
47
300
|
await respond(topic, {
|
|
48
301
|
id,
|
|
49
302
|
jsonrpc: '2.0',
|
|
@@ -54,9 +307,7 @@ export const createSessionRequestHandler = (wallet, config, respond) => {
|
|
|
54
307
|
if (chainId !== wallet.currentChainId) {
|
|
55
308
|
await wallet.switchNetwork(chainId);
|
|
56
309
|
}
|
|
57
|
-
const result = await wallet.handleExternalRequest({ method: params.request.method, params: params.request.params },
|
|
58
|
-
? { origin: verifyContext?.verified?.origin }
|
|
59
|
-
: { bypassOriginCheck: true });
|
|
310
|
+
const result = await wallet.handleExternalRequest({ method: params.request.method, params: normalizeJsonRpcParams(params.request.params) }, context);
|
|
60
311
|
await respond(topic, { id, jsonrpc: '2.0', result });
|
|
61
312
|
}
|
|
62
313
|
catch (error) {
|
|
@@ -73,32 +324,140 @@ export const createSessionRequestHandler = (wallet, config, respond) => {
|
|
|
73
324
|
}
|
|
74
325
|
};
|
|
75
326
|
};
|
|
327
|
+
export const createSessionAuthenticateHandler = (wallet, config, auth) => {
|
|
328
|
+
return async (event) => {
|
|
329
|
+
const { id, params, verifyContext } = event;
|
|
330
|
+
const context = config.enforceOrigins
|
|
331
|
+
? { origin: verifyContext?.verified?.origin }
|
|
332
|
+
: { bypassOriginCheck: true };
|
|
333
|
+
const requestedChains = Array.isArray(params.authPayload.chains)
|
|
334
|
+
? params.authPayload.chains.map(parseCaipChainId)
|
|
335
|
+
: [];
|
|
336
|
+
const approvedChains = config.getChains?.() ?? config.chains;
|
|
337
|
+
const unsupported = requestedChains.some((chainId) => !chainId || !approvedChains.includes(chainId));
|
|
338
|
+
if (requestedChains.length === 0 || unsupported) {
|
|
339
|
+
await auth.reject(id, auth.getSdkError('UNSUPPORTED_CHAINS')).catch(() => undefined);
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const proposalPayload = {
|
|
344
|
+
origin: verifyContext?.verified?.origin,
|
|
345
|
+
requester: params.requester?.metadata,
|
|
346
|
+
authPayload: params.authPayload,
|
|
347
|
+
};
|
|
348
|
+
const accounts = (await wallet.handleExternalRequest({ method: 'eth_requestAccounts', params: [proposalPayload] }, context));
|
|
349
|
+
const auths = [];
|
|
350
|
+
for (const chainId of requestedChains) {
|
|
351
|
+
const caip = toCaipChainId(chainId);
|
|
352
|
+
for (const account of accounts) {
|
|
353
|
+
const iss = `did:pkh:${caip}:${account}`;
|
|
354
|
+
const message = auth.formatAuthMessage({ request: params.authPayload, iss });
|
|
355
|
+
const signature = (await wallet.handleExternalRequest({ method: 'personal_sign', params: [toHex(message), account] }, context));
|
|
356
|
+
auths.push(auth.buildAuthObject(params.authPayload, { t: 'eip191', s: signature, m: message }, iss));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
const result = await auth.approve(id, auths);
|
|
360
|
+
if (result.session) {
|
|
361
|
+
auth.onSession?.({
|
|
362
|
+
topic: result.session.topic,
|
|
363
|
+
namespaces: result.session.namespaces ?? {},
|
|
364
|
+
peerMetadata: result.session.peer?.metadata ??
|
|
365
|
+
params.requester?.metadata ??
|
|
366
|
+
{ name: '', description: '', url: '', icons: [] },
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
await auth.reject(id, auth.getSdkError('USER_REJECTED')).catch(() => undefined);
|
|
372
|
+
}
|
|
373
|
+
};
|
|
374
|
+
};
|
|
375
|
+
const DEFAULT_URI_SELECTORS = ['wui-qr-code', '[uri^="wc:"]', '[data-uri^="wc:"]', '[href^="wc:"]'];
|
|
376
|
+
const DEFAULT_URI_ATTRIBUTES = ['uri', 'data-uri', 'href', 'value'];
|
|
377
|
+
const DEFAULT_TEXT_SELECTORS = ['textarea', 'input', 'code', 'pre', '[data-wc-uri]'];
|
|
378
|
+
const DEFAULT_COPY_BUTTON_SELECTORS = ['[data-testid="copy-wc2-uri"]'];
|
|
379
|
+
const URI_PROBE_MS = 250;
|
|
380
|
+
const uniq = (values) => [
|
|
381
|
+
...new Set(values.filter((value) => Boolean(value))),
|
|
382
|
+
];
|
|
383
|
+
const extractWalletConnectUri = (value) => {
|
|
384
|
+
if (typeof value !== 'string')
|
|
385
|
+
return undefined;
|
|
386
|
+
const trimmed = value.trim();
|
|
387
|
+
const decoded = /wc%3a/i.test(trimmed)
|
|
388
|
+
? decodeURIComponent(trimmed)
|
|
389
|
+
: trimmed;
|
|
390
|
+
const match = /wc:[^\s"'<>`]+/.exec(decoded);
|
|
391
|
+
return match?.[0];
|
|
392
|
+
};
|
|
393
|
+
const readLocatorText = async (locator) => {
|
|
394
|
+
return locator
|
|
395
|
+
.first()
|
|
396
|
+
.evaluate((element) => {
|
|
397
|
+
if (element instanceof HTMLInputElement ||
|
|
398
|
+
element instanceof HTMLTextAreaElement ||
|
|
399
|
+
element instanceof HTMLSelectElement) {
|
|
400
|
+
return element.value;
|
|
401
|
+
}
|
|
402
|
+
return element.textContent ?? '';
|
|
403
|
+
}, undefined, { timeout: URI_PROBE_MS })
|
|
404
|
+
.catch(() => undefined);
|
|
405
|
+
};
|
|
406
|
+
const readClipboard = async (page) => page
|
|
407
|
+
.evaluate(() => navigator.clipboard?.readText?.())
|
|
408
|
+
.catch(() => undefined);
|
|
76
409
|
/**
|
|
77
|
-
* Polls
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
410
|
+
* Polls common WalletConnect QR/modal surfaces for a pairing URI. Defaults
|
|
411
|
+
* keep the AppKit/W3M `wui-qr-code[uri]` contract, then fall back to generic
|
|
412
|
+
* URI attributes, text/value-bearing elements, and AppKit's copy button.
|
|
413
|
+
* For unusual modals pass selectors/textSelectors/copyButtonSelector or use
|
|
414
|
+
* the connect() getUri hook.
|
|
82
415
|
*/
|
|
83
416
|
export async function getWalletConnectUri(page, options = {}) {
|
|
84
417
|
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
85
|
-
const
|
|
418
|
+
const selectors = uniq([
|
|
419
|
+
options.selector,
|
|
420
|
+
...(options.selectors ?? []),
|
|
421
|
+
...DEFAULT_URI_SELECTORS,
|
|
422
|
+
]);
|
|
423
|
+
const attributes = options.attributes ?? DEFAULT_URI_ATTRIBUTES;
|
|
424
|
+
const textSelectors = uniq([...(options.textSelectors ?? []), ...DEFAULT_TEXT_SELECTORS]);
|
|
425
|
+
const copyButtonSelectors = uniq([
|
|
426
|
+
options.copyButtonSelector,
|
|
427
|
+
...(options.copyButtonSelectors ?? []),
|
|
428
|
+
...DEFAULT_COPY_BUTTON_SELECTORS,
|
|
429
|
+
]);
|
|
86
430
|
const deadline = Date.now() + timeoutMs;
|
|
87
431
|
while (Date.now() < deadline) {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
432
|
+
for (const selector of selectors) {
|
|
433
|
+
const locator = page.locator(selector).first();
|
|
434
|
+
for (const attribute of attributes) {
|
|
435
|
+
const uri = extractWalletConnectUri(await locator.getAttribute(attribute, { timeout: URI_PROBE_MS }).catch(() => null));
|
|
436
|
+
if (uri)
|
|
437
|
+
return uri;
|
|
438
|
+
}
|
|
439
|
+
const uri = extractWalletConnectUri(await readLocatorText(locator));
|
|
440
|
+
if (uri)
|
|
441
|
+
return uri;
|
|
442
|
+
}
|
|
443
|
+
for (const selector of textSelectors) {
|
|
444
|
+
const uri = extractWalletConnectUri(await readLocatorText(page.locator(selector)));
|
|
445
|
+
if (uri)
|
|
446
|
+
return uri;
|
|
447
|
+
}
|
|
448
|
+
for (const selector of copyButtonSelectors) {
|
|
449
|
+
const clicked = await page.locator(selector).first().click({ timeout: URI_PROBE_MS }).then(() => true, () => false);
|
|
450
|
+
if (!clicked)
|
|
451
|
+
continue;
|
|
452
|
+
const uri = extractWalletConnectUri(await readClipboard(page));
|
|
453
|
+
if (uri)
|
|
454
|
+
return uri;
|
|
97
455
|
}
|
|
98
456
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
99
457
|
}
|
|
100
|
-
throw new Error(`Timed out after ${timeoutMs}ms waiting for a wc: pairing URI
|
|
101
|
-
|
|
458
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for a wc: pairing URI. ` +
|
|
459
|
+
`Probed selectors: ${[...selectors, ...textSelectors].join(', ')}. ` +
|
|
460
|
+
'For unusual modals pass selector/selectors/textSelectors/copyButtonSelector or supply a getUri(page) hook to connect().');
|
|
102
461
|
}
|
|
103
462
|
export class WalletConnectWallet {
|
|
104
463
|
/** Underlying SignClient — escape hatch for protocol-level access. */
|
|
@@ -106,11 +465,14 @@ export class WalletConnectWallet {
|
|
|
106
465
|
wallet;
|
|
107
466
|
utils;
|
|
108
467
|
chains;
|
|
468
|
+
evm;
|
|
109
469
|
methods;
|
|
110
470
|
events;
|
|
471
|
+
solana;
|
|
111
472
|
enforceOrigins;
|
|
112
473
|
sessionList = [];
|
|
113
474
|
requestHandler;
|
|
475
|
+
authHandler;
|
|
114
476
|
deleteHandler;
|
|
115
477
|
unsubscribeProviderEvents;
|
|
116
478
|
closed = false;
|
|
@@ -118,16 +480,40 @@ export class WalletConnectWallet {
|
|
|
118
480
|
this.client = client;
|
|
119
481
|
this.utils = utils;
|
|
120
482
|
this.wallet = options.wallet;
|
|
483
|
+
const persona = options.persona ? createWalletPersona(options.persona) : undefined;
|
|
484
|
+
this.evm = options.evm ?? persona?.evm !== false;
|
|
121
485
|
this.chains = (options.chains ?? [options.wallet.currentChainId]).map((chainId) => toHex(typeof chainId === 'number' ? chainId : BigInt(chainId)));
|
|
122
|
-
this.methods =
|
|
486
|
+
this.methods =
|
|
487
|
+
options.methods ??
|
|
488
|
+
(persona?.flags?.isCoinbaseWallet === true
|
|
489
|
+
? [...DEFAULT_WALLETCONNECT_METHODS, ...DEFAULT_WALLETCONNECT_COINBASE_METHODS]
|
|
490
|
+
: DEFAULT_WALLETCONNECT_METHODS);
|
|
123
491
|
this.events = options.events ?? DEFAULT_EVENTS;
|
|
492
|
+
this.solana = resolveSolanaOptions(options.solana, persona);
|
|
124
493
|
this.enforceOrigins = options.enforceOrigins ?? true;
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
494
|
+
this.requestHandler = createSessionRequestHandler(this.wallet, {
|
|
495
|
+
chains: this.chains,
|
|
496
|
+
getChains: () => this.chains,
|
|
497
|
+
enforceOrigins: this.enforceOrigins,
|
|
498
|
+
solana: this.solana,
|
|
499
|
+
}, (topic, response) => this.client.respond({ topic, response }));
|
|
130
500
|
client.on('session_request', this.requestHandler);
|
|
501
|
+
// One-Click Auth / SIWE uses a separate WC event and suppresses
|
|
502
|
+
// sign-client's plain session_proposal fallback once a listener exists.
|
|
503
|
+
// Keep the fallback available through sessionAuthenticate: false.
|
|
504
|
+
if (options.sessionAuthenticate !== false && this.evm) {
|
|
505
|
+
this.authHandler = createSessionAuthenticateHandler(this.wallet, { chains: this.chains, getChains: () => this.chains, enforceOrigins: this.enforceOrigins }, {
|
|
506
|
+
approve: (id, auths) => this.client.approveSessionAuthenticate({ id, auths }),
|
|
507
|
+
buildAuthObject: (requestPayload, signature, iss) => this.utils.buildAuthObject(requestPayload, signature, iss),
|
|
508
|
+
formatAuthMessage: (args) => this.client.formatAuthMessage(args),
|
|
509
|
+
getSdkError: (code) => this.utils.getSdkError(code),
|
|
510
|
+
onSession: (session) => {
|
|
511
|
+
this.sessionList.push(session);
|
|
512
|
+
},
|
|
513
|
+
reject: (id, reason) => this.client.rejectSessionAuthenticate({ id, reason }),
|
|
514
|
+
});
|
|
515
|
+
client.on('session_authenticate', this.authHandler);
|
|
516
|
+
}
|
|
131
517
|
this.deleteHandler = ({ topic }) => {
|
|
132
518
|
const index = this.sessionList.findIndex((session) => session.topic === topic);
|
|
133
519
|
if (index !== -1) {
|
|
@@ -160,11 +546,16 @@ export class WalletConnectWallet {
|
|
|
160
546
|
const client = await SignClient.init({
|
|
161
547
|
projectId: options.projectId,
|
|
162
548
|
...(options.relayUrl ? { relayUrl: options.relayUrl } : {}),
|
|
549
|
+
customStoragePrefix: options.customStoragePrefix ??
|
|
550
|
+
`web3-tester-wallet-${walletConnectStoragePrefixCounter++}`,
|
|
163
551
|
metadata: {
|
|
164
552
|
name: 'web3-tester Wallet',
|
|
165
553
|
description: 'Headless WalletConnect wallet for E2E tests',
|
|
166
554
|
url: 'https://github.com/AndyMarigoldLabs/web3-tester',
|
|
167
555
|
icons: [],
|
|
556
|
+
...(options.persona
|
|
557
|
+
? walletConnectMetadataForPersona(createWalletPersona(options.persona))
|
|
558
|
+
: {}),
|
|
168
559
|
...options.metadata,
|
|
169
560
|
},
|
|
170
561
|
// A request parked by holdNextRequest must not starve delivery of
|
|
@@ -190,7 +581,7 @@ export class WalletConnectWallet {
|
|
|
190
581
|
const proposal = await new Promise((resolve, reject) => {
|
|
191
582
|
const cleanup = () => {
|
|
192
583
|
clearTimeout(timer);
|
|
193
|
-
(this.client
|
|
584
|
+
removeWalletConnectListener(this.client, 'session_proposal', handler);
|
|
194
585
|
};
|
|
195
586
|
const timer = setTimeout(() => {
|
|
196
587
|
cleanup();
|
|
@@ -213,19 +604,21 @@ export class WalletConnectWallet {
|
|
|
213
604
|
// simulateRejection, and holds all apply with zero new machinery.
|
|
214
605
|
let accounts;
|
|
215
606
|
try {
|
|
216
|
-
|
|
217
|
-
method: 'eth_requestAccounts',
|
|
218
|
-
params: [
|
|
219
|
-
{
|
|
220
|
-
origin: proposal.verifyContext?.verified?.origin,
|
|
221
|
-
proposer: proposal.params?.proposer?.metadata,
|
|
222
|
-
requiredNamespaces: proposal.params?.requiredNamespaces,
|
|
223
|
-
optionalNamespaces: proposal.params?.optionalNamespaces,
|
|
224
|
-
},
|
|
225
|
-
],
|
|
226
|
-
}, this.enforceOrigins
|
|
607
|
+
const context = this.enforceOrigins
|
|
227
608
|
? { origin: proposal.verifyContext?.verified?.origin }
|
|
228
|
-
: { bypassOriginCheck: true }
|
|
609
|
+
: { bypassOriginCheck: true };
|
|
610
|
+
const proposalPayload = {
|
|
611
|
+
origin: proposal.verifyContext?.verified?.origin,
|
|
612
|
+
proposer: proposal.params?.proposer?.metadata,
|
|
613
|
+
requiredNamespaces: proposal.params?.requiredNamespaces,
|
|
614
|
+
optionalNamespaces: proposal.params?.optionalNamespaces,
|
|
615
|
+
};
|
|
616
|
+
accounts = this.evm
|
|
617
|
+
? (await this.wallet.handleExternalRequest({ method: 'eth_requestAccounts', params: [proposalPayload] }, context))
|
|
618
|
+
: [];
|
|
619
|
+
if (!this.evm && this.solana) {
|
|
620
|
+
await this.wallet.handleExternalRequest({ method: 'solana_requestAccounts', params: [proposalPayload] }, context);
|
|
621
|
+
}
|
|
229
622
|
}
|
|
230
623
|
catch (error) {
|
|
231
624
|
await this.client
|
|
@@ -235,16 +628,29 @@ export class WalletConnectWallet {
|
|
|
235
628
|
}
|
|
236
629
|
let namespaces;
|
|
237
630
|
try {
|
|
631
|
+
const supportedNamespaces = {};
|
|
632
|
+
if (this.evm) {
|
|
633
|
+
supportedNamespaces.eip155 = {
|
|
634
|
+
chains: this.chains.map(toCaipChainId),
|
|
635
|
+
methods: [...this.methods],
|
|
636
|
+
events: [...this.events],
|
|
637
|
+
accounts: this.chains.flatMap((chainId) => accounts.map((account) => `${toCaipChainId(chainId)}:${account}`)),
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
if (this.solana) {
|
|
641
|
+
supportedNamespaces.solana = {
|
|
642
|
+
chains: [...this.solana.chains],
|
|
643
|
+
methods: [...this.solana.methods],
|
|
644
|
+
events: [...this.solana.events],
|
|
645
|
+
accounts: this.solana.chains.map((chain) => `${chain}:${this.solana.publicKey}`),
|
|
646
|
+
};
|
|
647
|
+
}
|
|
648
|
+
if (Object.keys(supportedNamespaces).length === 0) {
|
|
649
|
+
throw new Error('No WalletConnect namespaces are enabled.');
|
|
650
|
+
}
|
|
238
651
|
namespaces = this.utils.buildApprovedNamespaces({
|
|
239
652
|
proposal: proposal.params,
|
|
240
|
-
supportedNamespaces
|
|
241
|
-
eip155: {
|
|
242
|
-
chains: this.chains.map(toCaipChainId),
|
|
243
|
-
methods: [...this.methods],
|
|
244
|
-
events: [...this.events],
|
|
245
|
-
accounts: this.chains.flatMap((chainId) => accounts.map((account) => `${toCaipChainId(chainId)}:${account}`)),
|
|
246
|
-
},
|
|
247
|
-
},
|
|
653
|
+
supportedNamespaces,
|
|
248
654
|
});
|
|
249
655
|
}
|
|
250
656
|
catch (error) {
|
|
@@ -295,8 +701,11 @@ export class WalletConnectWallet {
|
|
|
295
701
|
new Promise((resolve) => setTimeout(resolve, 5_000)),
|
|
296
702
|
]).catch(() => undefined);
|
|
297
703
|
this.unsubscribeProviderEvents();
|
|
298
|
-
(this.client
|
|
299
|
-
(this.client
|
|
704
|
+
removeWalletConnectListener(this.client, 'session_request', this.requestHandler);
|
|
705
|
+
removeWalletConnectListener(this.client, 'session_delete', this.deleteHandler);
|
|
706
|
+
if (this.authHandler) {
|
|
707
|
+
removeWalletConnectListener(this.client, 'session_authenticate', this.authHandler);
|
|
708
|
+
}
|
|
300
709
|
await this.client.core?.relayer?.transportClose?.().catch(() => undefined);
|
|
301
710
|
this.client.core?.heartbeat?.stop?.();
|
|
302
711
|
}
|
|
@@ -307,28 +716,30 @@ export class WalletConnectWallet {
|
|
|
307
716
|
for (const session of [...this.sessionList]) {
|
|
308
717
|
try {
|
|
309
718
|
if (event === 'chainChanged') {
|
|
719
|
+
const namespaces = session.namespaces;
|
|
720
|
+
const eip155 = namespaces.eip155;
|
|
721
|
+
if (!eip155)
|
|
722
|
+
continue;
|
|
310
723
|
const chainId = payload;
|
|
724
|
+
const caip = toCaipChainId(chainId);
|
|
311
725
|
if (!this.chains.includes(chainId)) {
|
|
312
|
-
// Extend the session namespace first (MetaMask-mobile behavior).
|
|
313
726
|
this.chains = [...this.chains, chainId];
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
eip155
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
accounts
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
session.namespaces = extended;
|
|
331
|
-
}
|
|
727
|
+
}
|
|
728
|
+
if (!(eip155.chains ?? []).includes(caip)) {
|
|
729
|
+
// Extend the session namespace first (MetaMask-mobile behavior).
|
|
730
|
+
const extended = {
|
|
731
|
+
...namespaces,
|
|
732
|
+
eip155: {
|
|
733
|
+
...eip155,
|
|
734
|
+
chains: uniq([...(eip155.chains ?? []), caip]),
|
|
735
|
+
accounts: uniq([
|
|
736
|
+
...(eip155.accounts ?? []),
|
|
737
|
+
...this.wallet.currentAccounts.map((account) => `${caip}:${account}`),
|
|
738
|
+
]),
|
|
739
|
+
},
|
|
740
|
+
};
|
|
741
|
+
await this.client.update({ topic: session.topic, namespaces: extended });
|
|
742
|
+
session.namespaces = extended;
|
|
332
743
|
}
|
|
333
744
|
await this.client.emit({
|
|
334
745
|
topic: session.topic,
|
|
@@ -337,11 +748,40 @@ export class WalletConnectWallet {
|
|
|
337
748
|
});
|
|
338
749
|
}
|
|
339
750
|
else if (event === 'accountsChanged') {
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
751
|
+
const namespaces = session.namespaces;
|
|
752
|
+
if (namespaces.eip155) {
|
|
753
|
+
const accounts = Array.isArray(payload) ? payload.filter(isAddressString) : [];
|
|
754
|
+
if (accounts.length > 0) {
|
|
755
|
+
const caipChains = (namespaces.eip155.chains ?? this.chains.map(toCaipChainId)).filter((chainId) => typeof chainId === 'string');
|
|
756
|
+
const nextAccounts = eip155AccountsFor(caipChains, accounts);
|
|
757
|
+
if (!arraysEqual(namespaces.eip155.accounts, nextAccounts)) {
|
|
758
|
+
const updated = {
|
|
759
|
+
...namespaces,
|
|
760
|
+
eip155: {
|
|
761
|
+
...namespaces.eip155,
|
|
762
|
+
accounts: nextAccounts,
|
|
763
|
+
},
|
|
764
|
+
};
|
|
765
|
+
await this.client.update({ topic: session.topic, namespaces: updated });
|
|
766
|
+
session.namespaces = updated;
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
await this.client.emit({
|
|
770
|
+
topic: session.topic,
|
|
771
|
+
event: { name: 'accountsChanged', data: payload },
|
|
772
|
+
chainId: toCaipChainId(this.wallet.currentChainId),
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (this.solana && namespaces.solana) {
|
|
776
|
+
const accounts = Array.isArray(payload) && payload.length > 0 ? [this.solana.publicKey] : [];
|
|
777
|
+
for (const chainId of namespaces.solana.chains ?? this.solana.chains) {
|
|
778
|
+
await this.client.emit({
|
|
779
|
+
topic: session.topic,
|
|
780
|
+
event: { name: 'accountsChanged', data: accounts },
|
|
781
|
+
chainId,
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
}
|
|
345
785
|
}
|
|
346
786
|
else if (event === 'disconnect') {
|
|
347
787
|
await this.client
|