@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
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { http, toHex } from 'viem';
|
|
2
|
+
import { http, isAddress, keccak256, toHex } from 'viem';
|
|
3
3
|
import { providerError, serializeRpcError } from './errors.js';
|
|
4
4
|
import { buildInjectedProviderScript, emitterName, rpcBridgeName, } from './injected-provider.js';
|
|
5
|
+
import { createWalletPersona, mockWalletPersona, } from './wallet-personas.js';
|
|
5
6
|
/**
|
|
6
7
|
* Adapter: EIP-1193 RpcClient over a plain JSON-RPC URL (viem http
|
|
7
8
|
* transport). URL-backed chains serve reads, eth_sendRawTransaction, and
|
|
@@ -14,8 +15,9 @@ export function httpRpcClient(url, options = {}) {
|
|
|
14
15
|
retryCount: options.retryCount ?? 0,
|
|
15
16
|
timeout: options.timeout,
|
|
16
17
|
})({});
|
|
18
|
+
const rpcTransport = transport;
|
|
17
19
|
return {
|
|
18
|
-
request: (request) =>
|
|
20
|
+
request: (request) => rpcTransport.request(request),
|
|
19
21
|
};
|
|
20
22
|
}
|
|
21
23
|
const DEFAULT_PROVIDER_INFO = {
|
|
@@ -24,11 +26,13 @@ const DEFAULT_PROVIDER_INFO = {
|
|
|
24
26
|
icon: 'data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="%23111827"/><path d="M17 34h30v14H17z" fill="%2338bdf8"/><path d="M20 18h24v16H20z" fill="%23f59e0b"/><circle cx="43" cy="41" r="3" fill="%23111827"/></svg>',
|
|
25
27
|
rdns: 'dev.invisible-wallet.mock',
|
|
26
28
|
};
|
|
29
|
+
const DEFAULT_SOLANA_PUBLIC_KEY = '26qv4GCcx98RihuK3c4T6ozB3J7L6VwCuFVc7Ta2A3Uo';
|
|
27
30
|
const defaultAdditionalProviderInfo = (index) => ({
|
|
28
31
|
uuid: `00000000-0000-4000-8000-${String(index + 2).padStart(12, '0')}`,
|
|
29
32
|
name: `Mock Wallet ${index + 2}`,
|
|
30
33
|
icon: DEFAULT_PROVIDER_INFO.icon,
|
|
31
34
|
rdns: `dev.invisible-wallet.mock.${index + 2}`,
|
|
35
|
+
flags: { isMetaMask: true, isMock: true },
|
|
32
36
|
});
|
|
33
37
|
const SIGNING_METHODS = new Set([
|
|
34
38
|
'eth_sendTransaction',
|
|
@@ -37,16 +41,26 @@ const SIGNING_METHODS = new Set([
|
|
|
37
41
|
'eth_signTypedData_v3',
|
|
38
42
|
'eth_signTypedData_v4',
|
|
39
43
|
'personal_sign',
|
|
44
|
+
'solana_signIn',
|
|
45
|
+
'solana_signAllTransactions',
|
|
46
|
+
'solana_signAndSendAllTransactions',
|
|
47
|
+
'solana_signAndSendTransaction',
|
|
48
|
+
'solana_signMessage',
|
|
49
|
+
'solana_signTransaction',
|
|
40
50
|
// A batch is a spend: 4100 while disconnected, approval-gated, covered by
|
|
41
51
|
// the default simulateRejection() set.
|
|
42
52
|
'wallet_sendCalls',
|
|
43
53
|
]);
|
|
44
54
|
// Methods that open a wallet prompt in a real wallet but do not sign.
|
|
45
55
|
const PROMPT_METHODS = new Set([
|
|
56
|
+
'coinbase_fetchPermissions',
|
|
57
|
+
'wallet_addSubAccount',
|
|
46
58
|
'wallet_addEthereumChain',
|
|
59
|
+
'wallet_connect',
|
|
47
60
|
'wallet_requestPermissions',
|
|
48
61
|
'wallet_switchEthereumChain',
|
|
49
62
|
'wallet_watchAsset',
|
|
63
|
+
'solana_requestAccounts',
|
|
50
64
|
]);
|
|
51
65
|
const APPROVAL_GATED_METHODS = new Set([
|
|
52
66
|
...SIGNING_METHODS,
|
|
@@ -56,6 +70,37 @@ const APPROVAL_GATED_METHODS = new Set([
|
|
|
56
70
|
// approve, and it must not slip through the unguarded default forward.
|
|
57
71
|
'eth_sendRawTransaction',
|
|
58
72
|
]);
|
|
73
|
+
const UNLOCK_REQUIRED_METHODS = new Set([
|
|
74
|
+
...APPROVAL_GATED_METHODS,
|
|
75
|
+
'coinbase_fetchPermission',
|
|
76
|
+
'wallet_getSubAccounts',
|
|
77
|
+
'wallet_getCapabilities',
|
|
78
|
+
]);
|
|
79
|
+
const HARDWARE_WALLET_DEFAULT_DELAY_MS = 750;
|
|
80
|
+
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
81
|
+
const inferHardwareWalletRequiredApp = (method) => method.startsWith('solana_') ? 'Solana' : 'Ethereum';
|
|
82
|
+
const hardwareWalletRequiredApp = (method, hardwareWallet) => hardwareWallet.requiredApps[method] ??
|
|
83
|
+
hardwareWallet.requiredApp ??
|
|
84
|
+
inferHardwareWalletRequiredApp(method);
|
|
85
|
+
const normalizeHardwareWalletSimulation = (options) => {
|
|
86
|
+
const input = options === true
|
|
87
|
+
? { enabled: true }
|
|
88
|
+
: options === false || options === undefined
|
|
89
|
+
? { enabled: false }
|
|
90
|
+
: options;
|
|
91
|
+
const approvalDelayMs = input.approvalDelayMs ?? HARDWARE_WALLET_DEFAULT_DELAY_MS;
|
|
92
|
+
if (!Number.isFinite(approvalDelayMs) || approvalDelayMs < 0) {
|
|
93
|
+
throw new Error('hardwareWallet.approvalDelayMs must be a non-negative finite number.');
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
enabled: input.enabled ?? (options !== undefined),
|
|
97
|
+
approvalDelayMs,
|
|
98
|
+
deviceState: input.deviceState ?? 'ready',
|
|
99
|
+
methods: [...(input.methods ?? SIGNING_METHODS)],
|
|
100
|
+
requiredApp: input.requiredApp,
|
|
101
|
+
requiredApps: { ...(input.requiredApps ?? {}) },
|
|
102
|
+
};
|
|
103
|
+
};
|
|
59
104
|
const normalizeParams = (params) => {
|
|
60
105
|
if (params === undefined) {
|
|
61
106
|
return [];
|
|
@@ -87,6 +132,136 @@ const parseDappChainId = (chainId) => {
|
|
|
87
132
|
throw providerError(-32602, `Invalid chainId "${chainId}".`);
|
|
88
133
|
}
|
|
89
134
|
};
|
|
135
|
+
const parseCoinbaseChainId = (chainId, label = 'chainId') => {
|
|
136
|
+
if (typeof chainId === 'number' && Number.isSafeInteger(chainId) && chainId >= 0) {
|
|
137
|
+
return toHex(chainId);
|
|
138
|
+
}
|
|
139
|
+
if (typeof chainId === 'string' && chainId.startsWith('0x')) {
|
|
140
|
+
return parseDappChainId(chainId);
|
|
141
|
+
}
|
|
142
|
+
throw providerError(-32602, `${label} must be a hexadecimal string or non-negative integer.`);
|
|
143
|
+
};
|
|
144
|
+
const isRecord = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
145
|
+
const assertRpcAddress = (value, label) => {
|
|
146
|
+
if (typeof value === 'string' && isAddress(value)) {
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
throw providerError(-32602, `${label} must be a valid address.`);
|
|
150
|
+
};
|
|
151
|
+
const assertRpcHex = (value, label, options = {}) => {
|
|
152
|
+
const pattern = options.allowEmpty ? /^0x[0-9a-fA-F]*$/ : /^0x[0-9a-fA-F]+$/;
|
|
153
|
+
if (typeof value === 'string' && pattern.test(value)) {
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
throw providerError(-32602, `${label} must be a 0x-prefixed hex string.`);
|
|
157
|
+
};
|
|
158
|
+
const assertRpcHash = (value, label) => {
|
|
159
|
+
if (typeof value === 'string' && /^0x[0-9a-fA-F]{64}$/.test(value)) {
|
|
160
|
+
return value;
|
|
161
|
+
}
|
|
162
|
+
throw providerError(-32602, `${label} must be a 32-byte 0x-prefixed hex string.`);
|
|
163
|
+
};
|
|
164
|
+
const assertRpcObject = (value, method) => {
|
|
165
|
+
if (isRecord(value)) {
|
|
166
|
+
return value;
|
|
167
|
+
}
|
|
168
|
+
throw providerError(-32602, `${method} requires an object parameter.`);
|
|
169
|
+
};
|
|
170
|
+
const requireConfigAddress = (value, label) => {
|
|
171
|
+
if (typeof value === 'string' && isAddress(value)) {
|
|
172
|
+
return value;
|
|
173
|
+
}
|
|
174
|
+
throw new Error(`${label} must be a valid address.`);
|
|
175
|
+
};
|
|
176
|
+
const requireConfigHex = (value, label, options = {}) => {
|
|
177
|
+
const pattern = options.hash
|
|
178
|
+
? /^0x[0-9a-fA-F]{64}$/
|
|
179
|
+
: options.allowEmpty
|
|
180
|
+
? /^0x[0-9a-fA-F]*$/
|
|
181
|
+
: /^0x[0-9a-fA-F]+$/;
|
|
182
|
+
if (typeof value === 'string' && pattern.test(value)) {
|
|
183
|
+
return value;
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`${label} must be a ${options.hash ? '32-byte ' : ''}0x-prefixed hex string.`);
|
|
186
|
+
};
|
|
187
|
+
const requireFiniteNumber = (value, label) => {
|
|
188
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
throw new Error(`${label} must be a finite number.`);
|
|
192
|
+
};
|
|
193
|
+
const requireConfigString = (value, label) => {
|
|
194
|
+
if (typeof value === 'string') {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
throw new Error(`${label} must be a string.`);
|
|
198
|
+
};
|
|
199
|
+
const sameAddress = (left, right) => left.toLowerCase() === right.toLowerCase();
|
|
200
|
+
const cloneCoinbasePermission = (permission) => ({
|
|
201
|
+
createdAt: permission.createdAt,
|
|
202
|
+
permissionHash: permission.permissionHash,
|
|
203
|
+
signature: permission.signature,
|
|
204
|
+
spendPermission: { ...permission.spendPermission },
|
|
205
|
+
});
|
|
206
|
+
const cloneCoinbaseSubAccount = (subAccount) => ({
|
|
207
|
+
address: subAccount.address,
|
|
208
|
+
...(subAccount.factory ? { factory: subAccount.factory } : {}),
|
|
209
|
+
...(subAccount.factoryData ? { factoryData: subAccount.factoryData } : {}),
|
|
210
|
+
});
|
|
211
|
+
const normalizeCoinbasePermission = (permission, defaultChainId) => ({
|
|
212
|
+
createdAt: requireFiniteNumber(permission.createdAt, 'coinbase.permissions[].createdAt'),
|
|
213
|
+
permissionHash: requireConfigHex(permission.permissionHash, 'coinbase.permissions[].permissionHash', { hash: true }),
|
|
214
|
+
signature: requireConfigHex(permission.signature, 'coinbase.permissions[].signature'),
|
|
215
|
+
chainId: permission.chainId === undefined ? defaultChainId : normalizeChainId(permission.chainId),
|
|
216
|
+
spendPermission: {
|
|
217
|
+
account: requireConfigAddress(permission.spendPermission?.account, 'coinbase.permissions[].spendPermission.account'),
|
|
218
|
+
spender: requireConfigAddress(permission.spendPermission?.spender, 'coinbase.permissions[].spendPermission.spender'),
|
|
219
|
+
token: requireConfigAddress(permission.spendPermission?.token, 'coinbase.permissions[].spendPermission.token'),
|
|
220
|
+
allowance: requireConfigString(permission.spendPermission?.allowance, 'coinbase.permissions[].spendPermission.allowance'),
|
|
221
|
+
period: requireFiniteNumber(permission.spendPermission?.period, 'coinbase.permissions[].spendPermission.period'),
|
|
222
|
+
start: requireFiniteNumber(permission.spendPermission?.start, 'coinbase.permissions[].spendPermission.start'),
|
|
223
|
+
end: requireFiniteNumber(permission.spendPermission?.end, 'coinbase.permissions[].spendPermission.end'),
|
|
224
|
+
salt: requireConfigString(permission.spendPermission?.salt, 'coinbase.permissions[].spendPermission.salt'),
|
|
225
|
+
extraData: requireConfigHex(permission.spendPermission?.extraData, 'coinbase.permissions[].spendPermission.extraData', { allowEmpty: true }),
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
const normalizeCoinbaseSubAccount = (subAccount, defaultChainId, defaultAccount) => ({
|
|
229
|
+
address: requireConfigAddress(subAccount.address, 'coinbase.subAccounts[].address'),
|
|
230
|
+
chainId: subAccount.chainId === undefined ? defaultChainId : normalizeChainId(subAccount.chainId),
|
|
231
|
+
account: subAccount.account === undefined
|
|
232
|
+
? defaultAccount
|
|
233
|
+
: requireConfigAddress(subAccount.account, 'coinbase.subAccounts[].account'),
|
|
234
|
+
...(subAccount.domain ? { domain: subAccount.domain } : {}),
|
|
235
|
+
...(subAccount.factory
|
|
236
|
+
? { factory: requireConfigAddress(subAccount.factory, 'coinbase.subAccounts[].factory') }
|
|
237
|
+
: {}),
|
|
238
|
+
...(subAccount.factoryData
|
|
239
|
+
? {
|
|
240
|
+
factoryData: requireConfigHex(subAccount.factoryData, 'coinbase.subAccounts[].factoryData', { allowEmpty: true }),
|
|
241
|
+
}
|
|
242
|
+
: {}),
|
|
243
|
+
});
|
|
244
|
+
const normalizeCoinbaseWalletSimulation = (options, defaultEnabled, defaultChainId, defaultAccount) => {
|
|
245
|
+
if (options === false) {
|
|
246
|
+
return { enabled: false, permissions: [], subAccounts: [] };
|
|
247
|
+
}
|
|
248
|
+
const input = options === true ? { enabled: true } : (options ?? {});
|
|
249
|
+
return {
|
|
250
|
+
enabled: input.enabled ?? (options === undefined ? defaultEnabled : true),
|
|
251
|
+
permissions: [...(input.permissions ?? [])].map((permission) => normalizeCoinbasePermission(permission, defaultChainId)),
|
|
252
|
+
subAccounts: [...(input.subAccounts ?? [])].map((subAccount) => normalizeCoinbaseSubAccount(subAccount, defaultChainId, defaultAccount)),
|
|
253
|
+
...(input.factory
|
|
254
|
+
? { factory: requireConfigAddress(input.factory, 'coinbase.factory') }
|
|
255
|
+
: {}),
|
|
256
|
+
...(input.factoryData
|
|
257
|
+
? { factoryData: requireConfigHex(input.factoryData, 'coinbase.factoryData', { allowEmpty: true }) }
|
|
258
|
+
: {}),
|
|
259
|
+
};
|
|
260
|
+
};
|
|
261
|
+
const generatedSubAccountAddress = (owner, chainId, index, accountConfig) => {
|
|
262
|
+
const seed = JSON.stringify({ owner: owner.toLowerCase(), chainId, index, accountConfig });
|
|
263
|
+
return `0x${keccak256(toHex(seed)).slice(-40)}`;
|
|
264
|
+
};
|
|
90
265
|
// Bounded eth_chainId probe used when trustDappRpcUrls wires up a
|
|
91
266
|
// dapp-supplied RPC URL — a hung endpoint must not stall the dapp's promise.
|
|
92
267
|
const probeChainId = async (url, timeoutMs = 5_000) => {
|
|
@@ -132,7 +307,10 @@ export class MockWalletController {
|
|
|
132
307
|
accounts;
|
|
133
308
|
chainId;
|
|
134
309
|
connected;
|
|
310
|
+
unlocked;
|
|
135
311
|
approveRequests;
|
|
312
|
+
hardwareWallet;
|
|
313
|
+
coinbase;
|
|
136
314
|
rejectionQueue = [];
|
|
137
315
|
holdQueue = [];
|
|
138
316
|
approvalQueue = [];
|
|
@@ -156,13 +334,16 @@ export class MockWalletController {
|
|
|
156
334
|
shownCallsStatusIds = [];
|
|
157
335
|
sentTransactions = [];
|
|
158
336
|
sentTransactionRequests = [];
|
|
337
|
+
watchedAssets = [];
|
|
159
338
|
constructor(page, rpcClient, options) {
|
|
160
339
|
this.page = page;
|
|
161
340
|
this.rpcClient = rpcClient;
|
|
162
341
|
this.accounts = [...options.accounts];
|
|
163
342
|
this.chainId = normalizeChainId(options.chainId);
|
|
164
343
|
this.connected = options.connected ?? true;
|
|
344
|
+
this.unlocked = options.unlocked ?? true;
|
|
165
345
|
this.approveRequests = options.autoApprove ?? true;
|
|
346
|
+
this.hardwareWallet = normalizeHardwareWalletSimulation(options.hardwareWallet);
|
|
166
347
|
this.trustDappRpcUrls = options.trustDappRpcUrls ?? false;
|
|
167
348
|
const eip5792 = typeof options.eip5792 === 'boolean' ? { enabled: options.eip5792 } : (options.eip5792 ?? {});
|
|
168
349
|
this.eip5792 = {
|
|
@@ -202,18 +383,24 @@ export class MockWalletController {
|
|
|
202
383
|
if (this.accounts.length === 0) {
|
|
203
384
|
throw new Error('MockWalletController requires at least one account.');
|
|
204
385
|
}
|
|
205
|
-
const primaryProviderInfo = {
|
|
206
|
-
...
|
|
386
|
+
const primaryProviderInfo = createWalletPersona({
|
|
387
|
+
...mockWalletPersona(),
|
|
388
|
+
...options.persona,
|
|
207
389
|
...options.providerInfo,
|
|
208
|
-
};
|
|
390
|
+
});
|
|
209
391
|
this.providerInfos = [
|
|
210
392
|
primaryProviderInfo,
|
|
211
|
-
...(options.additionalProviders ?? []).map((provider, index) => ({
|
|
393
|
+
...(options.additionalProviders ?? []).map((provider, index) => createWalletPersona({
|
|
212
394
|
...defaultAdditionalProviderInfo(index),
|
|
213
395
|
...provider,
|
|
214
396
|
})),
|
|
397
|
+
...(options.additionalPersonas ?? []).map((provider, index) => createWalletPersona({
|
|
398
|
+
...defaultAdditionalProviderInfo(index + (options.additionalProviders?.length ?? 0)),
|
|
399
|
+
...provider,
|
|
400
|
+
})),
|
|
215
401
|
];
|
|
216
402
|
this.providerInfo = primaryProviderInfo;
|
|
403
|
+
this.coinbase = normalizeCoinbaseWalletSimulation(options.coinbase, this.providerInfos.some((provider) => provider.flags?.isCoinbaseWallet === true), this.chainId, this.primaryAccount);
|
|
217
404
|
}
|
|
218
405
|
providerInfo;
|
|
219
406
|
providerInfos;
|
|
@@ -231,6 +418,36 @@ export class MockWalletController {
|
|
|
231
418
|
get backedChainIds() {
|
|
232
419
|
return [...this.chainBackends.keys()];
|
|
233
420
|
}
|
|
421
|
+
get coinbasePermissions() {
|
|
422
|
+
return this.coinbase.permissions.map(cloneCoinbasePermission);
|
|
423
|
+
}
|
|
424
|
+
get coinbaseSubAccounts() {
|
|
425
|
+
return this.coinbase.subAccounts.map((subAccount) => ({
|
|
426
|
+
...cloneCoinbaseSubAccount(subAccount),
|
|
427
|
+
chainId: subAccount.chainId,
|
|
428
|
+
account: subAccount.account,
|
|
429
|
+
...(subAccount.domain ? { domain: subAccount.domain } : {}),
|
|
430
|
+
}));
|
|
431
|
+
}
|
|
432
|
+
get solanaAccounts() {
|
|
433
|
+
if (!this.connected || !this.unlocked) {
|
|
434
|
+
return [];
|
|
435
|
+
}
|
|
436
|
+
const seen = new Set();
|
|
437
|
+
const accounts = [];
|
|
438
|
+
for (const provider of this.providerInfos) {
|
|
439
|
+
if (!provider.solana) {
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
const publicKey = provider.solana.publicKey ?? DEFAULT_SOLANA_PUBLIC_KEY;
|
|
443
|
+
if (seen.has(publicKey)) {
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
seen.add(publicKey);
|
|
447
|
+
accounts.push({ publicKey, pubkey: publicKey, address: publicKey });
|
|
448
|
+
}
|
|
449
|
+
return accounts;
|
|
450
|
+
}
|
|
234
451
|
/**
|
|
235
452
|
* Test-side chain registration (Synpress addNetwork analogue): registers
|
|
236
453
|
* the backend and marks the chain known — no approval gate, no probe, and
|
|
@@ -302,6 +519,7 @@ export class MockWalletController {
|
|
|
302
519
|
autoApprove: this.approveRequests,
|
|
303
520
|
chainId: this.chainId,
|
|
304
521
|
connected: this.connected,
|
|
522
|
+
unlocked: this.unlocked,
|
|
305
523
|
providers: this.providerInfos,
|
|
306
524
|
allowedOrigins: this.allowedOrigins,
|
|
307
525
|
};
|
|
@@ -312,6 +530,25 @@ export class MockWalletController {
|
|
|
312
530
|
autoApprove(enabled = true) {
|
|
313
531
|
this.approveRequests = enabled;
|
|
314
532
|
}
|
|
533
|
+
configureHardwareWallet(options) {
|
|
534
|
+
this.hardwareWallet = normalizeHardwareWalletSimulation(options);
|
|
535
|
+
}
|
|
536
|
+
configureCoinbaseWallet(options) {
|
|
537
|
+
this.coinbase = normalizeCoinbaseWalletSimulation(options, this.coinbase.enabled, this.chainId, this.primaryAccount);
|
|
538
|
+
}
|
|
539
|
+
setHardwareWalletState(state) {
|
|
540
|
+
this.hardwareWallet = {
|
|
541
|
+
...this.hardwareWallet,
|
|
542
|
+
enabled: true,
|
|
543
|
+
deviceState: state,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
setHardwareWalletApprovalDelay(approvalDelayMs) {
|
|
547
|
+
this.hardwareWallet = normalizeHardwareWalletSimulation({
|
|
548
|
+
...this.hardwareWallet,
|
|
549
|
+
approvalDelayMs,
|
|
550
|
+
});
|
|
551
|
+
}
|
|
315
552
|
/**
|
|
316
553
|
* One-shot: the next atomicRequired wallet_sendCalls while the atomic
|
|
317
554
|
* capability is 'ready' throws 5750 (user rejected the EOA upgrade)
|
|
@@ -379,6 +616,22 @@ export class MockWalletController {
|
|
|
379
616
|
}
|
|
380
617
|
throw new Error(`Timed out after ${timeoutMs}ms waiting for the page to submit a transaction.`);
|
|
381
618
|
}
|
|
619
|
+
/**
|
|
620
|
+
* Resolves with the next approved wallet_watchAsset request after this
|
|
621
|
+
* call. Invoke before triggering the dapp action, then await it.
|
|
622
|
+
*/
|
|
623
|
+
async waitForNextWatchedAsset(options = {}) {
|
|
624
|
+
const timeoutMs = options.timeoutMs ?? 15_000;
|
|
625
|
+
const baseline = this.watchedAssets.length;
|
|
626
|
+
const deadline = Date.now() + timeoutMs;
|
|
627
|
+
while (Date.now() < deadline) {
|
|
628
|
+
if (this.watchedAssets.length > baseline) {
|
|
629
|
+
return this.watchedAssets[baseline];
|
|
630
|
+
}
|
|
631
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
632
|
+
}
|
|
633
|
+
throw new Error(`Timed out after ${timeoutMs}ms waiting for the page to watch an asset.`);
|
|
634
|
+
}
|
|
382
635
|
/**
|
|
383
636
|
* Replaces the account set (and reconnects a disconnected wallet — unlike
|
|
384
637
|
* switchAccount, which only reorders). Accounts are validated against the
|
|
@@ -394,7 +647,7 @@ export class MockWalletController {
|
|
|
394
647
|
}
|
|
395
648
|
this.accounts = [...accounts];
|
|
396
649
|
this.connected = true;
|
|
397
|
-
await this.emit('accountsChanged', this.accounts);
|
|
650
|
+
await this.emit('accountsChanged', this.unlocked ? this.accounts : []);
|
|
398
651
|
}
|
|
399
652
|
/**
|
|
400
653
|
* Re-selects one of the wallet's existing accounts: moves it to index 0
|
|
@@ -413,10 +666,28 @@ export class MockWalletController {
|
|
|
413
666
|
}
|
|
414
667
|
const [selected] = this.accounts.splice(index, 1);
|
|
415
668
|
this.accounts.unshift(selected);
|
|
416
|
-
if (this.connected) {
|
|
669
|
+
if (this.connected && this.unlocked) {
|
|
417
670
|
await this.emit('accountsChanged', this.accounts);
|
|
418
671
|
}
|
|
419
672
|
}
|
|
673
|
+
get isUnlocked() {
|
|
674
|
+
return this.unlocked;
|
|
675
|
+
}
|
|
676
|
+
async setUnlocked(unlocked) {
|
|
677
|
+
if (this.unlocked === unlocked) {
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
this.unlocked = unlocked;
|
|
681
|
+
if (this.connected) {
|
|
682
|
+
await this.emit('accountsChanged', unlocked ? this.accounts : []);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
async lock() {
|
|
686
|
+
await this.setUnlocked(false);
|
|
687
|
+
}
|
|
688
|
+
async unlock() {
|
|
689
|
+
await this.setUnlocked(true);
|
|
690
|
+
}
|
|
420
691
|
async disconnect() {
|
|
421
692
|
this.connected = false;
|
|
422
693
|
await this.emit('accountsChanged', []);
|
|
@@ -425,7 +696,7 @@ export class MockWalletController {
|
|
|
425
696
|
async reconnect() {
|
|
426
697
|
this.connected = true;
|
|
427
698
|
await this.emit('connect', { chainId: this.chainId });
|
|
428
|
-
await this.emit('accountsChanged', this.accounts);
|
|
699
|
+
await this.emit('accountsChanged', this.unlocked ? this.accounts : []);
|
|
429
700
|
}
|
|
430
701
|
async switchNetwork(chainId) {
|
|
431
702
|
const normalized = normalizeChainId(chainId);
|
|
@@ -774,6 +1045,26 @@ export class MockWalletController {
|
|
|
774
1045
|
throw providerError(4100, `The wallet is not available to origin "${origin}" (allowedOrigins: ${this.allowedOrigins.join(', ')}).`);
|
|
775
1046
|
}
|
|
776
1047
|
}
|
|
1048
|
+
async assertHardwareWalletReady(method) {
|
|
1049
|
+
if (!this.hardwareWallet.enabled || !this.hardwareWallet.methods.includes(method)) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
switch (this.hardwareWallet.deviceState) {
|
|
1053
|
+
case 'ready':
|
|
1054
|
+
if (this.hardwareWallet.approvalDelayMs > 0) {
|
|
1055
|
+
await wait(this.hardwareWallet.approvalDelayMs);
|
|
1056
|
+
}
|
|
1057
|
+
return;
|
|
1058
|
+
case 'locked':
|
|
1059
|
+
throw providerError(4001, 'Hardware wallet is locked. Unlock the device and try again.');
|
|
1060
|
+
case 'wrong-app':
|
|
1061
|
+
throw providerError(4001, `Open the ${hardwareWalletRequiredApp(method, this.hardwareWallet)} app on your hardware wallet and try again.`);
|
|
1062
|
+
case 'blind-signing-disabled':
|
|
1063
|
+
throw providerError(4001, 'Blind signing is disabled on your hardware wallet.');
|
|
1064
|
+
case 'disconnected':
|
|
1065
|
+
throw providerError(4900, 'Hardware wallet disconnected.');
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
777
1068
|
async assertUserApproved(method, params) {
|
|
778
1069
|
const hold = this.consumeRule(this.holdQueue, method);
|
|
779
1070
|
if (hold) {
|
|
@@ -790,11 +1081,11 @@ export class MockWalletController {
|
|
|
790
1081
|
(!rule.match || rule.match(method, params)));
|
|
791
1082
|
if (armedIndex !== -1) {
|
|
792
1083
|
this.approvalQueue.splice(armedIndex, 1);
|
|
793
|
-
return;
|
|
794
1084
|
}
|
|
795
|
-
if (!this.approveRequests && APPROVAL_GATED_METHODS.has(method)) {
|
|
1085
|
+
else if (!this.approveRequests && APPROVAL_GATED_METHODS.has(method)) {
|
|
796
1086
|
throw providerError(4001, 'User rejected the request.');
|
|
797
1087
|
}
|
|
1088
|
+
await this.assertHardwareWalletReady(method);
|
|
798
1089
|
}
|
|
799
1090
|
permissionResponse() {
|
|
800
1091
|
return [
|
|
@@ -804,15 +1095,286 @@ export class MockWalletController {
|
|
|
804
1095
|
},
|
|
805
1096
|
];
|
|
806
1097
|
}
|
|
1098
|
+
requestedPermissionKeys(params, method) {
|
|
1099
|
+
if (params.length === 0 || params[0] === undefined) {
|
|
1100
|
+
return ['eth_accounts'];
|
|
1101
|
+
}
|
|
1102
|
+
const request = assertRpcObject(params[0], method);
|
|
1103
|
+
const keys = Object.keys(request);
|
|
1104
|
+
if (keys.length === 0) {
|
|
1105
|
+
throw providerError(-32602, `${method} requires at least one permission.`);
|
|
1106
|
+
}
|
|
1107
|
+
return keys;
|
|
1108
|
+
}
|
|
1109
|
+
assertSupportedPermissions(params, method) {
|
|
1110
|
+
const unsupported = this.requestedPermissionKeys(params, method).filter((permission) => permission !== 'eth_accounts');
|
|
1111
|
+
if (unsupported.length > 0) {
|
|
1112
|
+
throw providerError(4200, `The mock wallet does not support permission "${unsupported[0]}".`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
async handleRequestPermissions(params) {
|
|
1116
|
+
this.assertSupportedPermissions(params, 'wallet_requestPermissions');
|
|
1117
|
+
await this.assertUserApproved('wallet_requestPermissions', params);
|
|
1118
|
+
const wasConnected = this.connected;
|
|
1119
|
+
this.connected = true;
|
|
1120
|
+
if (!wasConnected) {
|
|
1121
|
+
await this.emit('connect', { chainId: this.chainId });
|
|
1122
|
+
}
|
|
1123
|
+
await this.emit('accountsChanged', this.accounts);
|
|
1124
|
+
return this.permissionResponse();
|
|
1125
|
+
}
|
|
1126
|
+
async handleRevokePermissions(params) {
|
|
1127
|
+
this.assertSupportedPermissions(params, 'wallet_revokePermissions');
|
|
1128
|
+
this.connected = false;
|
|
1129
|
+
await this.emit('accountsChanged', []);
|
|
1130
|
+
return null;
|
|
1131
|
+
}
|
|
1132
|
+
assertCoinbaseEnabled(method) {
|
|
1133
|
+
if (!this.coinbase.enabled) {
|
|
1134
|
+
throw providerError(4200, `The mock wallet does not support the method "${method}".`);
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
assertCoinbaseConnected() {
|
|
1138
|
+
if (!this.connected) {
|
|
1139
|
+
throw providerError(4100, 'The requested account and/or method has not been authorized by the user.');
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
assertAuthorizedAccount(account) {
|
|
1143
|
+
if (!this.accounts.some((authorized) => sameAddress(authorized, account))) {
|
|
1144
|
+
throw providerError(4100, `The requested account ${account} has not been authorized by the user.`);
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
async buildCoinbaseSiweCapability(capability) {
|
|
1148
|
+
const config = assertRpcObject(capability, 'wallet_connect signInWithEthereum');
|
|
1149
|
+
const nonce = config.nonce;
|
|
1150
|
+
if (typeof nonce !== 'string' || nonce.length === 0) {
|
|
1151
|
+
throw providerError(-32602, 'signInWithEthereum.nonce must be a non-empty string.');
|
|
1152
|
+
}
|
|
1153
|
+
const chainId = parseCoinbaseChainId(config.chainId, 'signInWithEthereum.chainId');
|
|
1154
|
+
if (chainId !== this.chainId) {
|
|
1155
|
+
throw providerError(-32602, `signInWithEthereum.chainId ${chainId} does not match the active chain ${this.chainId}.`);
|
|
1156
|
+
}
|
|
1157
|
+
const domain = typeof config.domain === 'string' && config.domain.length > 0
|
|
1158
|
+
? config.domain
|
|
1159
|
+
: 'web3-tester.local';
|
|
1160
|
+
const uri = typeof config.uri === 'string' && config.uri.length > 0
|
|
1161
|
+
? config.uri
|
|
1162
|
+
: `https://${domain}`;
|
|
1163
|
+
const statement = typeof config.statement === 'string' && config.statement.length > 0
|
|
1164
|
+
? config.statement
|
|
1165
|
+
: 'Sign in with Coinbase Wallet.';
|
|
1166
|
+
const resources = Array.isArray(config.resources)
|
|
1167
|
+
? config.resources.filter((resource) => typeof resource === 'string')
|
|
1168
|
+
: [];
|
|
1169
|
+
const messageLines = [
|
|
1170
|
+
`${domain} wants you to sign in with your Ethereum account:`,
|
|
1171
|
+
this.primaryAccount,
|
|
1172
|
+
'',
|
|
1173
|
+
statement,
|
|
1174
|
+
'',
|
|
1175
|
+
`URI: ${uri}`,
|
|
1176
|
+
'Version: 1',
|
|
1177
|
+
`Chain ID: ${Number(BigInt(chainId))}`,
|
|
1178
|
+
`Nonce: ${nonce}`,
|
|
1179
|
+
'Issued At: 1970-01-01T00:00:00.000Z',
|
|
1180
|
+
];
|
|
1181
|
+
if (resources.length > 0) {
|
|
1182
|
+
messageLines.push('Resources:', ...resources.map((resource) => `- ${resource}`));
|
|
1183
|
+
}
|
|
1184
|
+
const message = messageLines.join('\n');
|
|
1185
|
+
const signature = (await this.activeRpcClient.request({
|
|
1186
|
+
method: 'personal_sign',
|
|
1187
|
+
params: [toHex(message), this.primaryAccount],
|
|
1188
|
+
}));
|
|
1189
|
+
return { message, signature };
|
|
1190
|
+
}
|
|
1191
|
+
async handleWalletConnect(params) {
|
|
1192
|
+
this.assertCoinbaseEnabled('wallet_connect');
|
|
1193
|
+
const request = params[0] === undefined ? {} : assertRpcObject(params[0], 'wallet_connect');
|
|
1194
|
+
await this.assertUserApproved('wallet_connect', params);
|
|
1195
|
+
if (!this.connected) {
|
|
1196
|
+
this.connected = true;
|
|
1197
|
+
await this.emit('connect', { chainId: this.chainId });
|
|
1198
|
+
await this.emit('accountsChanged', this.accounts);
|
|
1199
|
+
}
|
|
1200
|
+
const capabilities = isRecord(request.capabilities) ? request.capabilities : undefined;
|
|
1201
|
+
const result = {
|
|
1202
|
+
accounts: this.accounts.map((address) => ({ address })),
|
|
1203
|
+
chainId: this.chainId,
|
|
1204
|
+
isConnected: true,
|
|
1205
|
+
};
|
|
1206
|
+
if (capabilities?.signInWithEthereum !== undefined) {
|
|
1207
|
+
result.capabilities = {
|
|
1208
|
+
signInWithEthereum: await this.buildCoinbaseSiweCapability(capabilities.signInWithEthereum),
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
return result;
|
|
1212
|
+
}
|
|
1213
|
+
handleWalletGetSubAccounts(params) {
|
|
1214
|
+
this.assertCoinbaseEnabled('wallet_getSubAccounts');
|
|
1215
|
+
this.assertCoinbaseConnected();
|
|
1216
|
+
const request = assertRpcObject(params[0], 'wallet_getSubAccounts');
|
|
1217
|
+
const account = assertRpcAddress(request.account, 'wallet_getSubAccounts.account');
|
|
1218
|
+
this.assertAuthorizedAccount(account);
|
|
1219
|
+
if (typeof request.domain !== 'string' || request.domain.length === 0) {
|
|
1220
|
+
throw providerError(-32602, 'wallet_getSubAccounts.domain must be a non-empty string.');
|
|
1221
|
+
}
|
|
1222
|
+
const subAccounts = this.coinbase.subAccounts
|
|
1223
|
+
.filter((subAccount) => subAccount.chainId === this.chainId &&
|
|
1224
|
+
sameAddress(subAccount.account, account) &&
|
|
1225
|
+
subAccount.domain === request.domain)
|
|
1226
|
+
.map(cloneCoinbaseSubAccount);
|
|
1227
|
+
return { subAccounts };
|
|
1228
|
+
}
|
|
1229
|
+
async handleWalletAddSubAccount(params) {
|
|
1230
|
+
this.assertCoinbaseEnabled('wallet_addSubAccount');
|
|
1231
|
+
this.assertCoinbaseConnected();
|
|
1232
|
+
const request = assertRpcObject(params[0], 'wallet_addSubAccount');
|
|
1233
|
+
const accountConfig = assertRpcObject(request.account, 'wallet_addSubAccount.account');
|
|
1234
|
+
const type = accountConfig.type;
|
|
1235
|
+
if (type !== 'create' && type !== 'deployed') {
|
|
1236
|
+
throw providerError(-32602, 'wallet_addSubAccount.account.type must be "create" or "deployed".');
|
|
1237
|
+
}
|
|
1238
|
+
if (request.domain !== undefined && typeof request.domain !== 'string') {
|
|
1239
|
+
throw providerError(-32602, 'wallet_addSubAccount.domain must be a string when provided.');
|
|
1240
|
+
}
|
|
1241
|
+
let chainId = this.chainId;
|
|
1242
|
+
let address;
|
|
1243
|
+
if (type === 'deployed') {
|
|
1244
|
+
address = assertRpcAddress(accountConfig.address, 'wallet_addSubAccount.account.address');
|
|
1245
|
+
chainId = parseCoinbaseChainId(accountConfig.chainId, 'wallet_addSubAccount.account.chainId');
|
|
1246
|
+
}
|
|
1247
|
+
else {
|
|
1248
|
+
if (!Array.isArray(accountConfig.keys) || accountConfig.keys.length === 0) {
|
|
1249
|
+
throw providerError(-32602, 'wallet_addSubAccount.account.keys must be a non-empty array.');
|
|
1250
|
+
}
|
|
1251
|
+
for (const key of accountConfig.keys) {
|
|
1252
|
+
if (!isRecord(key) || typeof key.type !== 'string' || typeof key.publicKey !== 'string') {
|
|
1253
|
+
throw providerError(-32602, 'wallet_addSubAccount.account.keys entries require type and publicKey strings.');
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
address =
|
|
1257
|
+
typeof accountConfig.address === 'string' && isAddress(accountConfig.address)
|
|
1258
|
+
? accountConfig.address
|
|
1259
|
+
: generatedSubAccountAddress(this.primaryAccount, chainId, this.coinbase.subAccounts.length, accountConfig);
|
|
1260
|
+
}
|
|
1261
|
+
const factory = request.factory !== undefined
|
|
1262
|
+
? assertRpcAddress(request.factory, 'wallet_addSubAccount.factory')
|
|
1263
|
+
: accountConfig.factory !== undefined
|
|
1264
|
+
? assertRpcAddress(accountConfig.factory, 'wallet_addSubAccount.account.factory')
|
|
1265
|
+
: this.coinbase.factory;
|
|
1266
|
+
const factoryData = request.factoryData !== undefined
|
|
1267
|
+
? assertRpcHex(request.factoryData, 'wallet_addSubAccount.factoryData', { allowEmpty: true })
|
|
1268
|
+
: accountConfig.factoryData !== undefined
|
|
1269
|
+
? assertRpcHex(accountConfig.factoryData, 'wallet_addSubAccount.account.factoryData', {
|
|
1270
|
+
allowEmpty: true,
|
|
1271
|
+
})
|
|
1272
|
+
: this.coinbase.factoryData;
|
|
1273
|
+
await this.assertUserApproved('wallet_addSubAccount', params);
|
|
1274
|
+
const subAccount = {
|
|
1275
|
+
address,
|
|
1276
|
+
chainId,
|
|
1277
|
+
account: this.primaryAccount,
|
|
1278
|
+
...(typeof request.domain === 'string' ? { domain: request.domain } : {}),
|
|
1279
|
+
...(factory ? { factory } : {}),
|
|
1280
|
+
...(factoryData ? { factoryData } : {}),
|
|
1281
|
+
};
|
|
1282
|
+
this.coinbase.subAccounts.push(subAccount);
|
|
1283
|
+
return {
|
|
1284
|
+
address,
|
|
1285
|
+
chainId,
|
|
1286
|
+
...(factory ? { factory } : {}),
|
|
1287
|
+
...(factoryData ? { factoryData } : {}),
|
|
1288
|
+
};
|
|
1289
|
+
}
|
|
1290
|
+
async handleCoinbaseFetchPermissions(params) {
|
|
1291
|
+
this.assertCoinbaseEnabled('coinbase_fetchPermissions');
|
|
1292
|
+
this.assertCoinbaseConnected();
|
|
1293
|
+
const request = assertRpcObject(params[0], 'coinbase_fetchPermissions');
|
|
1294
|
+
const spender = assertRpcAddress(request.spender, 'coinbase_fetchPermissions.spender');
|
|
1295
|
+
const chainId = parseDappChainId(request.chainId);
|
|
1296
|
+
const account = request.account === undefined
|
|
1297
|
+
? undefined
|
|
1298
|
+
: assertRpcAddress(request.account, 'coinbase_fetchPermissions.account');
|
|
1299
|
+
if (account) {
|
|
1300
|
+
this.assertAuthorizedAccount(account);
|
|
1301
|
+
}
|
|
1302
|
+
const pageOptions = request.pageOptions === undefined
|
|
1303
|
+
? {}
|
|
1304
|
+
: assertRpcObject(request.pageOptions, 'coinbase_fetchPermissions.pageOptions');
|
|
1305
|
+
const pageSize = typeof pageOptions.pageSize === 'number' && Number.isSafeInteger(pageOptions.pageSize)
|
|
1306
|
+
? pageOptions.pageSize
|
|
1307
|
+
: 50;
|
|
1308
|
+
if (pageSize <= 0) {
|
|
1309
|
+
throw providerError(-32602, 'coinbase_fetchPermissions.pageOptions.pageSize must be positive.');
|
|
1310
|
+
}
|
|
1311
|
+
const cursor = pageOptions.cursor === undefined
|
|
1312
|
+
? 0
|
|
1313
|
+
: typeof pageOptions.cursor === 'string' && /^\d+$/.test(pageOptions.cursor)
|
|
1314
|
+
? Number(pageOptions.cursor)
|
|
1315
|
+
: (() => {
|
|
1316
|
+
throw providerError(-32602, 'coinbase_fetchPermissions.pageOptions.cursor must be a decimal string.');
|
|
1317
|
+
})();
|
|
1318
|
+
await this.assertUserApproved('coinbase_fetchPermissions', params);
|
|
1319
|
+
const nowSeconds = Math.floor(Date.now() / 1000);
|
|
1320
|
+
const matching = this.coinbase.permissions
|
|
1321
|
+
.filter((permission) => permission.chainId === chainId &&
|
|
1322
|
+
sameAddress(permission.spendPermission.spender, spender) &&
|
|
1323
|
+
(account === undefined || sameAddress(permission.spendPermission.account, account)) &&
|
|
1324
|
+
(permission.spendPermission.end === 0 || permission.spendPermission.end > nowSeconds))
|
|
1325
|
+
.sort((left, right) => left.createdAt - right.createdAt);
|
|
1326
|
+
const page = matching.slice(cursor, cursor + pageSize);
|
|
1327
|
+
const nextCursor = cursor + page.length < matching.length ? String(cursor + page.length) : undefined;
|
|
1328
|
+
return {
|
|
1329
|
+
permissions: page.map(cloneCoinbasePermission),
|
|
1330
|
+
pageDescription: {
|
|
1331
|
+
pageSize: page.length,
|
|
1332
|
+
...(nextCursor ? { nextCursor } : {}),
|
|
1333
|
+
},
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
handleCoinbaseFetchPermission(params) {
|
|
1337
|
+
this.assertCoinbaseEnabled('coinbase_fetchPermission');
|
|
1338
|
+
const request = assertRpcObject(params[0], 'coinbase_fetchPermission');
|
|
1339
|
+
const permissionHash = assertRpcHash(request.permissionHash, 'coinbase_fetchPermission.permissionHash');
|
|
1340
|
+
const permission = this.coinbase.permissions.find((candidate) => candidate.permissionHash.toLowerCase() === permissionHash.toLowerCase());
|
|
1341
|
+
if (!permission) {
|
|
1342
|
+
throw providerError(-32603, `No Coinbase spend permission found for ${permissionHash}.`);
|
|
1343
|
+
}
|
|
1344
|
+
return { permission: cloneCoinbasePermission(permission) };
|
|
1345
|
+
}
|
|
1346
|
+
async handleWatchAsset(params) {
|
|
1347
|
+
const request = assertRpcObject(params[0], 'wallet_watchAsset');
|
|
1348
|
+
if (typeof request.type !== 'string' || request.type.length === 0) {
|
|
1349
|
+
throw providerError(-32602, 'wallet_watchAsset.type must be a non-empty string.');
|
|
1350
|
+
}
|
|
1351
|
+
const options = assertRpcObject(request.options, 'wallet_watchAsset.options');
|
|
1352
|
+
if (request.type === 'ERC20' && !isAddress(String(options.address ?? ''))) {
|
|
1353
|
+
throw providerError(-32602, 'wallet_watchAsset.options.address must be a valid address.');
|
|
1354
|
+
}
|
|
1355
|
+
await this.assertUserApproved('wallet_watchAsset', params);
|
|
1356
|
+
this.watchedAssets.push({
|
|
1357
|
+
chainId: this.chainId,
|
|
1358
|
+
type: request.type,
|
|
1359
|
+
options: { ...options },
|
|
1360
|
+
request,
|
|
1361
|
+
});
|
|
1362
|
+
return true;
|
|
1363
|
+
}
|
|
807
1364
|
async handleRpcRequest(request) {
|
|
808
1365
|
const { method } = request;
|
|
809
1366
|
const params = normalizeParams(request.params);
|
|
1367
|
+
if (!this.unlocked && UNLOCK_REQUIRED_METHODS.has(method)) {
|
|
1368
|
+
throw providerError(4100, 'The wallet is locked. Unlock the wallet and try again.');
|
|
1369
|
+
}
|
|
810
1370
|
if (!this.connected && SIGNING_METHODS.has(method)) {
|
|
811
1371
|
throw providerError(4100, 'The requested account and/or method has not been authorized by the user.');
|
|
812
1372
|
}
|
|
813
1373
|
switch (method) {
|
|
814
1374
|
case 'eth_accounts':
|
|
815
|
-
return this.connected ? this.accounts : [];
|
|
1375
|
+
return this.connected && this.unlocked ? this.accounts : [];
|
|
1376
|
+
case 'solana_getAccounts':
|
|
1377
|
+
return this.solanaAccounts;
|
|
816
1378
|
case 'eth_requestAccounts':
|
|
817
1379
|
await this.assertUserApproved(method, params);
|
|
818
1380
|
if (!this.connected) {
|
|
@@ -821,21 +1383,37 @@ export class MockWalletController {
|
|
|
821
1383
|
await this.emit('accountsChanged', this.accounts);
|
|
822
1384
|
}
|
|
823
1385
|
return this.accounts;
|
|
1386
|
+
case 'wallet_connect':
|
|
1387
|
+
return this.handleWalletConnect(params);
|
|
1388
|
+
case 'solana_requestAccounts':
|
|
1389
|
+
await this.assertUserApproved(method, params);
|
|
1390
|
+
if (!this.connected) {
|
|
1391
|
+
this.connected = true;
|
|
1392
|
+
await this.emit('connect', { chainId: this.chainId });
|
|
1393
|
+
await this.emit('accountsChanged', this.accounts);
|
|
1394
|
+
}
|
|
1395
|
+
return this.solanaAccounts;
|
|
824
1396
|
case 'eth_chainId':
|
|
825
1397
|
return this.chainId;
|
|
826
1398
|
case 'net_version':
|
|
827
1399
|
return String(Number(BigInt(this.chainId)));
|
|
1400
|
+
case 'eth_subscribe':
|
|
1401
|
+
case 'eth_unsubscribe':
|
|
1402
|
+
throw providerError(4200, `The mock wallet does not support the method "${method}".`);
|
|
828
1403
|
case 'wallet_getPermissions':
|
|
829
|
-
return this.connected ? this.permissionResponse() : [];
|
|
1404
|
+
return this.connected && this.unlocked ? this.permissionResponse() : [];
|
|
830
1405
|
case 'wallet_requestPermissions':
|
|
831
|
-
|
|
832
|
-
this.connected = true;
|
|
833
|
-
await this.emit('accountsChanged', this.accounts);
|
|
834
|
-
return this.permissionResponse();
|
|
1406
|
+
return this.handleRequestPermissions(params);
|
|
835
1407
|
case 'wallet_revokePermissions':
|
|
836
|
-
this.
|
|
837
|
-
|
|
838
|
-
return
|
|
1408
|
+
return this.handleRevokePermissions(params);
|
|
1409
|
+
case 'wallet_addSubAccount':
|
|
1410
|
+
return this.handleWalletAddSubAccount(params);
|
|
1411
|
+
case 'wallet_getSubAccounts':
|
|
1412
|
+
return this.handleWalletGetSubAccounts(params);
|
|
1413
|
+
case 'coinbase_fetchPermissions':
|
|
1414
|
+
return this.handleCoinbaseFetchPermissions(params);
|
|
1415
|
+
case 'coinbase_fetchPermission':
|
|
1416
|
+
return this.handleCoinbaseFetchPermission(params);
|
|
839
1417
|
// Per-handler order, library-wide: param validation (-32602) -> state
|
|
840
1418
|
// checks (4902) -> approval -> execution. Real MetaMask returns 4902
|
|
841
1419
|
// for an unknown chain without ever showing a prompt.
|
|
@@ -910,14 +1488,14 @@ export class MockWalletController {
|
|
|
910
1488
|
return null;
|
|
911
1489
|
}
|
|
912
1490
|
case 'wallet_watchAsset':
|
|
913
|
-
|
|
914
|
-
return true;
|
|
1491
|
+
return this.handleWatchAsset(params);
|
|
915
1492
|
case 'wallet_getCapabilities': {
|
|
916
1493
|
this.assertEip5792Enabled(method);
|
|
917
1494
|
// Spec privacy rule: only answer for the connected wallet's accounts.
|
|
918
1495
|
const [address, chainIdFilter] = params;
|
|
919
1496
|
const requested = typeof address === 'string' ? address.toLowerCase() : '';
|
|
920
1497
|
if (!this.connected ||
|
|
1498
|
+
!this.unlocked ||
|
|
921
1499
|
!this.accounts.some((account) => account.toLowerCase() === requested)) {
|
|
922
1500
|
throw providerError(4100, 'The requested account and/or method has not been authorized by the user.');
|
|
923
1501
|
}
|
|
@@ -956,9 +1534,9 @@ export class MockWalletController {
|
|
|
956
1534
|
}
|
|
957
1535
|
case 'metamask_getProviderState':
|
|
958
1536
|
return {
|
|
959
|
-
accounts: this.connected ? this.accounts : [],
|
|
1537
|
+
accounts: this.connected && this.unlocked ? this.accounts : [],
|
|
960
1538
|
chainId: this.chainId,
|
|
961
|
-
isUnlocked:
|
|
1539
|
+
isUnlocked: this.unlocked,
|
|
962
1540
|
networkVersion: String(Number(BigInt(this.chainId))),
|
|
963
1541
|
};
|
|
964
1542
|
case 'eth_sendTransaction': {
|
|
@@ -1008,12 +1586,23 @@ export class MockWalletController {
|
|
|
1008
1586
|
case 'personal_sign':
|
|
1009
1587
|
await this.assertUserApproved(method, params);
|
|
1010
1588
|
return this.activeRpcClient.request({ method, params });
|
|
1589
|
+
case 'solana_signAllTransactions':
|
|
1590
|
+
case 'solana_signAndSendAllTransactions':
|
|
1591
|
+
case 'solana_signAndSendTransaction':
|
|
1592
|
+
case 'solana_signIn':
|
|
1593
|
+
case 'solana_signMessage':
|
|
1594
|
+
case 'solana_signTransaction':
|
|
1595
|
+
await this.assertUserApproved(method, params);
|
|
1596
|
+
return null;
|
|
1011
1597
|
default:
|
|
1012
1598
|
// Unknown wallet-namespace methods are the wallet's responsibility;
|
|
1013
1599
|
// forwarding them to the node would leak a confusing -32601.
|
|
1014
1600
|
if (method.startsWith('wallet_')) {
|
|
1015
1601
|
throw providerError(4200, `The mock wallet does not support the method "${method}".`);
|
|
1016
1602
|
}
|
|
1603
|
+
if (method.startsWith('coinbase_')) {
|
|
1604
|
+
throw providerError(4200, `The mock wallet does not support the method "${method}".`);
|
|
1605
|
+
}
|
|
1017
1606
|
return this.activeRpcClient.request({ method, params });
|
|
1018
1607
|
}
|
|
1019
1608
|
}
|