@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.
Files changed (59) hide show
  1. package/README.md +317 -11
  2. package/dist/anvil.d.ts.map +1 -1
  3. package/dist/anvil.js.map +1 -1
  4. package/dist/benchmark.d.ts +55 -0
  5. package/dist/benchmark.d.ts.map +1 -0
  6. package/dist/benchmark.js +168 -0
  7. package/dist/benchmark.js.map +1 -0
  8. package/dist/fixtures.js +2 -2
  9. package/dist/fixtures.js.map +1 -1
  10. package/dist/index.d.ts +13 -4
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +5 -1
  13. package/dist/index.js.map +1 -1
  14. package/dist/injected-provider.d.ts.map +1 -1
  15. package/dist/injected-provider.js +748 -25
  16. package/dist/injected-provider.js.map +1 -1
  17. package/dist/mock-wallet-controller.d.ts +150 -2
  18. package/dist/mock-wallet-controller.d.ts.map +1 -1
  19. package/dist/mock-wallet-controller.js +613 -24
  20. package/dist/mock-wallet-controller.js.map +1 -1
  21. package/dist/private-key-rpc-client.d.ts +144 -132
  22. package/dist/private-key-rpc-client.d.ts.map +1 -1
  23. package/dist/private-key-rpc-client.js +142 -20
  24. package/dist/private-key-rpc-client.js.map +1 -1
  25. package/dist/real-wallet-cache.d.ts +38 -0
  26. package/dist/real-wallet-cache.d.ts.map +1 -1
  27. package/dist/real-wallet-cache.js +143 -57
  28. package/dist/real-wallet-cache.js.map +1 -1
  29. package/dist/real-wallet-extension-fixtures.d.ts +35 -0
  30. package/dist/real-wallet-extension-fixtures.d.ts.map +1 -0
  31. package/dist/real-wallet-extension-fixtures.js +96 -0
  32. package/dist/real-wallet-extension-fixtures.js.map +1 -0
  33. package/dist/real-wallet-extension.d.ts +86 -0
  34. package/dist/real-wallet-extension.d.ts.map +1 -0
  35. package/dist/real-wallet-extension.js +245 -0
  36. package/dist/real-wallet-extension.js.map +1 -0
  37. package/dist/real-wallet-fixtures.d.ts.map +1 -1
  38. package/dist/real-wallet-fixtures.js +46 -31
  39. package/dist/real-wallet-fixtures.js.map +1 -1
  40. package/dist/real-wallet.d.ts +5 -14
  41. package/dist/real-wallet.d.ts.map +1 -1
  42. package/dist/real-wallet.js +668 -435
  43. package/dist/real-wallet.js.map +1 -1
  44. package/dist/safe.d.ts +311 -0
  45. package/dist/safe.d.ts.map +1 -0
  46. package/dist/safe.js +743 -0
  47. package/dist/safe.js.map +1 -0
  48. package/dist/types.d.ts +35 -1
  49. package/dist/types.d.ts.map +1 -1
  50. package/dist/wallet-personas.d.ts +99 -0
  51. package/dist/wallet-personas.d.ts.map +1 -0
  52. package/dist/wallet-personas.js +666 -0
  53. package/dist/wallet-personas.js.map +1 -0
  54. package/dist/walletconnect.d.ts +176 -9
  55. package/dist/walletconnect.d.ts.map +1 -1
  56. package/dist/walletconnect.js +514 -74
  57. package/dist/walletconnect.js.map +1 -1
  58. package/examples/playwright.config.ts +8 -0
  59. package/package.json +29 -3
@@ -17,6 +17,95 @@ export const buildInjectedProviderScript = (config) => `
17
17
 
18
18
  const toNetworkVersion = (chainId) => String(Number(BigInt(chainId)));
19
19
 
20
+ const walletError = (code, message) => {
21
+ const error = new Error(message);
22
+ error.code = code;
23
+ return error;
24
+ };
25
+
26
+ const createPublicKey = (value) => Object.freeze({
27
+ toString: () => value,
28
+ toBase58: () => value,
29
+ equals: (other) => {
30
+ const otherValue =
31
+ typeof other === 'string'
32
+ ? other
33
+ : typeof other?.toBase58 === 'function'
34
+ ? other.toBase58()
35
+ : typeof other?.toString === 'function'
36
+ ? other.toString()
37
+ : '';
38
+ return otherValue === value;
39
+ },
40
+ toBytes: () => base58Decode(value),
41
+ toJSON: () => value,
42
+ });
43
+
44
+ const base58Decode = (value) => {
45
+ const alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz';
46
+ const bytes = [0];
47
+ for (const char of String(value)) {
48
+ const carryStart = alphabet.indexOf(char);
49
+ if (carryStart === -1) {
50
+ return new TextEncoder().encode(String(value)).slice(0, 32);
51
+ }
52
+ let carry = carryStart;
53
+ for (let index = 0; index < bytes.length; index += 1) {
54
+ const next = bytes[index] * 58 + carry;
55
+ bytes[index] = next & 0xff;
56
+ carry = next >> 8;
57
+ }
58
+ while (carry > 0) {
59
+ bytes.push(carry & 0xff);
60
+ carry >>= 8;
61
+ }
62
+ }
63
+ for (const char of String(value)) {
64
+ if (char !== '1') break;
65
+ bytes.push(0);
66
+ }
67
+ return new Uint8Array(bytes.reverse());
68
+ };
69
+
70
+ const bytesFrom = (input) => {
71
+ if (input instanceof Uint8Array) {
72
+ return [...input];
73
+ }
74
+ if (input instanceof ArrayBuffer) {
75
+ return [...new Uint8Array(input)];
76
+ }
77
+ if (ArrayBuffer.isView(input)) {
78
+ return [...new Uint8Array(input.buffer, input.byteOffset, input.byteLength)];
79
+ }
80
+ if (Array.isArray(input)) {
81
+ return input.map((value) => Number(value) & 255);
82
+ }
83
+ if (typeof input === 'string') {
84
+ return [...new TextEncoder().encode(input)];
85
+ }
86
+ return [...new TextEncoder().encode(JSON.stringify(input ?? null))];
87
+ };
88
+
89
+ const deterministicSignature = (publicKey, payload) => {
90
+ const seed = [...new TextEncoder().encode(publicKey), ...bytesFrom(payload)];
91
+ const signature = new Uint8Array(64);
92
+ for (let index = 0; index < signature.length; index += 1) {
93
+ const a = seed[index % seed.length] ?? 0;
94
+ const b = seed[(index * 7 + 13) % seed.length] ?? 0;
95
+ signature[index] = (a + b + index * 17) & 255;
96
+ }
97
+ return signature;
98
+ };
99
+
100
+ const hexToBytes = (hex) => {
101
+ const value = String(hex).replace(/^0x/u, '');
102
+ const bytes = new Uint8Array(Math.floor(value.length / 2));
103
+ for (let index = 0; index < bytes.length; index += 1) {
104
+ bytes[index] = Number.parseInt(value.slice(index * 2, index * 2 + 2), 16);
105
+ }
106
+ return bytes;
107
+ };
108
+
20
109
  // EIP-1193 connectivity means "can the provider reach the chain", which is
21
110
  // independent of account authorization. The controller's disconnect()
22
111
  // simulates losing the chain via the 'disconnect' event.
@@ -47,7 +136,9 @@ export const buildInjectedProviderScript = (config) => `
47
136
  // One distinct provider object per announced wallet so selector tests can
48
137
  // detect a dapp talking to the wrong provider. They share request handling
49
138
  // and state, but each has its own identity and listener registry.
50
- const createProviderEntry = (info) => {
139
+ const createProviderEntry = (providerConfig) => {
140
+ const { evm = true, flags = {}, aliases = [], solana, ...info } = providerConfig;
141
+ const providerInfo = Object.freeze({ ...info });
51
142
  const listeners = new Map();
52
143
 
53
144
  const getListeners = (event) => {
@@ -57,10 +148,46 @@ export const buildInjectedProviderScript = (config) => `
57
148
  return listeners.get(event);
58
149
  };
59
150
 
151
+ const responseForPayload = (payload, result) => ({
152
+ id: payload.id,
153
+ jsonrpc: payload.jsonrpc ?? '2.0',
154
+ result,
155
+ });
156
+
157
+ const errorResponseForPayload = (payload, error) => {
158
+ const rpcError = {
159
+ code: typeof error?.code === 'number' ? error.code : -32603,
160
+ message:
161
+ typeof error?.message === 'string' ? error.message : 'Internal JSON-RPC error',
162
+ };
163
+ if (error?.data !== undefined) {
164
+ rpcError.data = error.data;
165
+ }
166
+ return {
167
+ id: payload.id,
168
+ jsonrpc: payload.jsonrpc ?? '2.0',
169
+ error: rpcError,
170
+ };
171
+ };
172
+
173
+ const requestBatch = (payloads) =>
174
+ Promise.all(payloads.map((payload) => request(payload)));
175
+
176
+ const requestBatchResponses = (payloads) =>
177
+ Promise.all(
178
+ payloads.map(async (payload) => {
179
+ try {
180
+ return responseForPayload(payload, await request(payload));
181
+ } catch (error) {
182
+ return errorResponseForPayload(payload, error);
183
+ }
184
+ }),
185
+ );
186
+
60
187
  const provider = {
61
- isMetaMask: true,
62
- isMock: true,
63
- selectedAddress: config.connected && config.accounts.length > 0 ? config.accounts[0] : null,
188
+ info: providerInfo,
189
+ selectedAddress:
190
+ config.connected && config.unlocked && config.accounts.length > 0 ? config.accounts[0] : null,
64
191
  chainId: config.chainId,
65
192
  networkVersion: toNetworkVersion(config.chainId),
66
193
  request,
@@ -73,19 +200,37 @@ export const buildInjectedProviderScript = (config) => `
73
200
 
74
201
  const payload = methodOrPayload;
75
202
  const callback = paramsOrCallback;
76
- request(payload)
77
- .then((result) => callback?.(null, { id: payload.id, jsonrpc: '2.0', result }))
203
+ if (typeof callback !== 'function') {
204
+ return Array.isArray(payload) ? requestBatch(payload) : request(payload);
205
+ }
206
+ const promise = Array.isArray(payload)
207
+ ? requestBatchResponses(payload)
208
+ : request(payload).then((result) => responseForPayload(payload, result));
209
+ promise
210
+ .then((response) => callback?.(null, response))
78
211
  .catch((error) => callback?.(error, null));
212
+ return undefined;
79
213
  },
80
214
  sendAsync: (payload, callback) => {
81
- request(payload)
82
- .then((result) => callback(null, { id: payload.id, jsonrpc: '2.0', result }))
215
+ const promise = Array.isArray(payload)
216
+ ? requestBatchResponses(payload)
217
+ : request(payload).then((result) => responseForPayload(payload, result));
218
+ promise
219
+ .then((response) => callback(null, response))
83
220
  .catch((error) => callback(error, null));
84
221
  },
85
222
  on: (event, handler) => {
86
223
  getListeners(event).add(handler);
224
+ if (event === 'connect' && config.connected && !chainDisconnected) {
225
+ setTimeout(() => {
226
+ if (listeners.get(event)?.has(handler)) {
227
+ handler({ chainId: provider.chainId });
228
+ }
229
+ }, 0);
230
+ }
87
231
  return provider;
88
232
  },
233
+ addListener: (event, handler) => provider.on(event, handler),
89
234
  once: (event, handler) => {
90
235
  const wrapped = (payload) => {
91
236
  provider.removeListener(event, wrapped);
@@ -98,6 +243,7 @@ export const buildInjectedProviderScript = (config) => `
98
243
  listeners.get(event)?.delete(handler);
99
244
  return provider;
100
245
  },
246
+ off: (event, handler) => provider.removeListener(event, handler),
101
247
  removeAllListeners: (event) => {
102
248
  if (event) {
103
249
  listeners.delete(event);
@@ -106,15 +252,534 @@ export const buildInjectedProviderScript = (config) => `
106
252
  }
107
253
  return provider;
108
254
  },
109
- _metamask: {
110
- isUnlocked: async () => true,
255
+ listeners: (event) => [...(listeners.get(event) ?? [])],
256
+ listenerCount: (event) => listeners.get(event)?.size ?? 0,
257
+ };
258
+
259
+ for (const [flag, value] of Object.entries(flags)) {
260
+ Object.defineProperty(provider, flag, {
261
+ value: Boolean(value),
262
+ configurable: true,
263
+ enumerable: true,
264
+ });
265
+ }
266
+
267
+ if (flags.isMetaMask) {
268
+ Object.defineProperty(provider, '_metamask', {
269
+ value: {
270
+ isUnlocked: async () => {
271
+ try {
272
+ const state = await request({ method: 'metamask_getProviderState' });
273
+ return state.isUnlocked === true;
274
+ } catch {
275
+ return config.unlocked === true;
276
+ }
277
+ },
278
+ },
279
+ configurable: true,
280
+ enumerable: true,
281
+ });
282
+ }
283
+
284
+ return { info: providerInfo, provider, listeners, aliases, solana, evm: evm !== false };
285
+ };
286
+
287
+ const createSolanaProvider = (entry) => {
288
+ const solanaConfig = entry.solana;
289
+ if (!solanaConfig) {
290
+ return undefined;
291
+ }
292
+
293
+ const listeners = new Map();
294
+ const standardListeners = new Map();
295
+ const publicKeyValue =
296
+ solanaConfig.publicKey ?? '26qv4GCcx98RihuK3c4T6ozB3J7L6VwCuFVc7Ta2A3Uo';
297
+ const publicKey = createPublicKey(publicKeyValue);
298
+ const solanaChains = Object.freeze(
299
+ solanaConfig.chains ?? ['solana:mainnet', 'solana:devnet', 'solana:testnet'],
300
+ );
301
+ const solanaAccountFeatures = Object.freeze([
302
+ 'solana:signAndSendTransaction',
303
+ 'solana:signIn',
304
+ 'solana:signMessage',
305
+ 'solana:signTransaction',
306
+ ]);
307
+ const standardAccount = Object.freeze({
308
+ address: publicKeyValue,
309
+ publicKey: base58Decode(publicKeyValue),
310
+ chains: solanaChains,
311
+ features: solanaAccountFeatures,
312
+ label: entry.info.name,
313
+ icon: entry.info.icon,
314
+ });
315
+ let connected = false;
316
+ let trusted = false;
317
+ let hiddenByController = false;
318
+
319
+ const getListeners = (event) => {
320
+ if (!listeners.has(event)) {
321
+ listeners.set(event, new Set());
322
+ }
323
+ return listeners.get(event);
324
+ };
325
+
326
+ const emitSolana = (event, payload) => {
327
+ const callbacks = listeners.get(event);
328
+ if (!callbacks) {
329
+ return;
330
+ }
331
+ for (const callback of [...callbacks]) {
332
+ callback(payload);
333
+ }
334
+ };
335
+
336
+ const emitStandardChange = (properties) => {
337
+ const callbacks = standardListeners.get('change');
338
+ if (!callbacks) {
339
+ return;
340
+ }
341
+ for (const callback of [...callbacks]) {
342
+ callback(properties);
343
+ }
344
+ };
345
+
346
+ const standardOn = (event, handler) => {
347
+ if (!standardListeners.has(event)) {
348
+ standardListeners.set(event, new Set());
349
+ }
350
+ standardListeners.get(event).add(handler);
351
+ return () => standardListeners.get(event)?.delete(handler);
352
+ };
353
+
354
+ const ensureConnected = () => {
355
+ if (!connected) {
356
+ throw walletError(4100, 'The Solana wallet is not connected.');
357
+ }
358
+ };
359
+
360
+ const visibleAccounts = () => (connected ? [publicKeyValue] : []);
361
+ const visibleAccountObjects = () =>
362
+ connected
363
+ ? [
364
+ Object.freeze({
365
+ publicKey,
366
+ pubkey: publicKeyValue,
367
+ address: publicKeyValue,
368
+ }),
369
+ ]
370
+ : [];
371
+
372
+ const ensureStandardAccount = (account) => {
373
+ if (account?.address && account.address !== publicKeyValue) {
374
+ throw walletError(4100, 'The Solana wallet cannot use the requested account.');
375
+ }
376
+ };
377
+
378
+ const transactionsFromParams = (params) => {
379
+ if (Array.isArray(params)) {
380
+ return params;
381
+ }
382
+ if (Array.isArray(params?.transactions)) {
383
+ return params.transactions;
384
+ }
385
+ if (Array.isArray(params?.message)) {
386
+ return params.message;
387
+ }
388
+ if (Array.isArray(params?.[0])) {
389
+ return params[0];
390
+ }
391
+ return [];
392
+ };
393
+
394
+ const buildSignInMessage = (input = {}) => {
395
+ const address = input.address ?? publicKeyValue;
396
+ const domain = input.domain ?? window.location.host ?? 'localhost';
397
+ const statement = input.statement ?? 'Sign in with Solana.';
398
+ const lines = [
399
+ \`\${domain} wants you to sign in with your Solana account:\`,
400
+ address,
401
+ '',
402
+ statement,
403
+ ];
404
+ const fields = [
405
+ ['URI', input.uri],
406
+ ['Version', input.version],
407
+ ['Chain ID', input.chainId],
408
+ ['Nonce', input.nonce],
409
+ ['Issued At', input.issuedAt],
410
+ ['Expiration Time', input.expirationTime],
411
+ ['Not Before', input.notBefore],
412
+ ['Request ID', input.requestId],
413
+ ];
414
+ for (const [label, value] of fields) {
415
+ if (value !== undefined) {
416
+ lines.push(\`\${label}: \${value}\`);
417
+ }
418
+ }
419
+ if (Array.isArray(input.resources) && input.resources.length > 0) {
420
+ lines.push('Resources:');
421
+ for (const resource of input.resources) {
422
+ lines.push(\`- \${resource}\`);
423
+ }
424
+ }
425
+ return new TextEncoder().encode(lines.join('\\n'));
426
+ };
427
+
428
+ const provider = {
429
+ info: entry.info,
430
+ publicKey: null,
431
+ isConnected: false,
432
+ connect: async (options = {}) => {
433
+ if (options?.onlyIfTrusted && !trusted) {
434
+ throw walletError(4001, 'User rejected the request.');
435
+ }
436
+ if (!trusted) {
437
+ await request({
438
+ method: 'solana_requestAccounts',
439
+ params: { publicKey: publicKeyValue },
440
+ });
441
+ }
442
+ const wasConnected = connected;
443
+ connected = true;
444
+ trusted = true;
445
+ provider.isConnected = true;
446
+ provider.publicKey = publicKey;
447
+ if (!wasConnected) {
448
+ emitSolana('connect', publicKey);
449
+ emitStandardChange({ accounts: standardWallet.accounts });
450
+ }
451
+ return { publicKey };
452
+ },
453
+ disconnect: async () => {
454
+ const wasConnected = connected;
455
+ connected = false;
456
+ hiddenByController = false;
457
+ provider.isConnected = false;
458
+ provider.publicKey = null;
459
+ if (wasConnected) {
460
+ emitSolana('disconnect');
461
+ emitStandardChange({ accounts: standardWallet.accounts });
462
+ }
111
463
  },
464
+ request: async ({ method, params } = {}) => {
465
+ switch (method) {
466
+ case 'connect':
467
+ return provider.connect(params ?? {});
468
+ case 'disconnect':
469
+ await provider.disconnect();
470
+ return null;
471
+ case 'getAccounts':
472
+ return visibleAccounts();
473
+ case 'requestAccounts':
474
+ await provider.connect(params ?? {});
475
+ return visibleAccounts();
476
+ case 'solana_getAccounts':
477
+ return visibleAccountObjects();
478
+ case 'solana_requestAccounts':
479
+ await provider.connect(params ?? {});
480
+ return visibleAccountObjects();
481
+ case 'signMessage':
482
+ case 'solana_signMessage':
483
+ return provider.signMessage(params?.message ?? params?.[0] ?? new Uint8Array());
484
+ case 'signTransaction':
485
+ case 'solana_signTransaction':
486
+ return provider.signTransaction(params?.transaction ?? params?.message ?? params?.[0]);
487
+ case 'signAllTransactions':
488
+ case 'solana_signAllTransactions':
489
+ return provider.signAllTransactions(transactionsFromParams(params));
490
+ case 'signAndSendTransaction':
491
+ case 'solana_signAndSendTransaction':
492
+ return provider.signAndSendTransaction(params?.transaction ?? params?.message ?? params?.[0]);
493
+ case 'signAndSendAllTransactions':
494
+ case 'solana_signAndSendAllTransactions':
495
+ return provider.signAndSendAllTransactions(transactionsFromParams(params));
496
+ case 'signIn':
497
+ case 'solana_signIn':
498
+ return provider.signIn(params ?? {});
499
+ default:
500
+ throw walletError(4200, \`The Solana wallet does not support the method "\${String(method)}".\`);
501
+ }
502
+ },
503
+ signIn: async (input = {}) => {
504
+ if (!connected) {
505
+ await provider.connect();
506
+ }
507
+ const signedMessage = buildSignInMessage(input);
508
+ await request({
509
+ method: 'solana_signIn',
510
+ params: { input, publicKey: publicKeyValue, message: [...signedMessage] },
511
+ });
512
+ return {
513
+ address: input.address ?? publicKeyValue,
514
+ publicKey,
515
+ signedMessage,
516
+ signature: deterministicSignature(publicKeyValue, signedMessage),
517
+ };
518
+ },
519
+ signMessage: async (message) => {
520
+ ensureConnected();
521
+ await request({
522
+ method: 'solana_signMessage',
523
+ params: { message, pubkey: publicKeyValue },
524
+ });
525
+ return {
526
+ publicKey,
527
+ signature: deterministicSignature(publicKeyValue, message),
528
+ };
529
+ },
530
+ signTransaction: async (transaction) => {
531
+ ensureConnected();
532
+ await request({
533
+ method: 'solana_signTransaction',
534
+ params: { transaction, pubkey: publicKeyValue },
535
+ });
536
+ return transaction;
537
+ },
538
+ signAllTransactions: async (transactions) => {
539
+ ensureConnected();
540
+ await request({
541
+ method: 'solana_signAllTransactions',
542
+ params: { transactions, pubkey: publicKeyValue },
543
+ });
544
+ return transactions;
545
+ },
546
+ signAndSendTransaction: async (transaction) => {
547
+ ensureConnected();
548
+ await request({
549
+ method: 'solana_signAndSendTransaction',
550
+ params: { transaction, pubkey: publicKeyValue },
551
+ });
552
+ const signature = deterministicSignature(publicKeyValue, transaction);
553
+ return {
554
+ signature: Array.from(signature)
555
+ .map((byte) => byte.toString(16).padStart(2, '0'))
556
+ .join(''),
557
+ };
558
+ },
559
+ signAndSendAllTransactions: async (transactions) => {
560
+ ensureConnected();
561
+ const list = Array.isArray(transactions) ? transactions : [];
562
+ await request({
563
+ method: 'solana_signAndSendAllTransactions',
564
+ params: { transactions: list, pubkey: publicKeyValue },
565
+ });
566
+ return {
567
+ publicKey,
568
+ signatures: list.map((transaction) =>
569
+ Array.from(deterministicSignature(publicKeyValue, transaction))
570
+ .map((byte) => byte.toString(16).padStart(2, '0'))
571
+ .join(''),
572
+ ),
573
+ };
574
+ },
575
+ on: (event, handler) => {
576
+ getListeners(event).add(handler);
577
+ return provider;
578
+ },
579
+ addListener: (event, handler) => provider.on(event, handler),
580
+ once: (event, handler) => {
581
+ const wrapped = (payload) => {
582
+ provider.removeListener(event, wrapped);
583
+ handler(payload);
584
+ };
585
+ provider.on(event, wrapped);
586
+ return provider;
587
+ },
588
+ removeListener: (event, handler) => {
589
+ listeners.get(event)?.delete(handler);
590
+ return provider;
591
+ },
592
+ off: (event, handler) => provider.removeListener(event, handler),
593
+ removeAllListeners: (event) => {
594
+ if (event) {
595
+ listeners.delete(event);
596
+ } else {
597
+ listeners.clear();
598
+ }
599
+ return provider;
600
+ },
601
+ listeners: (event) => [...(listeners.get(event) ?? [])],
602
+ listenerCount: (event) => listeners.get(event)?.size ?? 0,
603
+ };
604
+
605
+ const hideFromController = () => {
606
+ if (!connected) {
607
+ return;
608
+ }
609
+
610
+ connected = false;
611
+ hiddenByController = true;
612
+ provider.isConnected = false;
613
+ provider.publicKey = null;
614
+ emitSolana('accountChanged', null);
615
+ emitStandardChange({ accounts: standardWallet.accounts });
616
+ };
617
+
618
+ const restoreFromController = () => {
619
+ if (!hiddenByController) {
620
+ return;
621
+ }
622
+
623
+ hiddenByController = false;
624
+ connected = true;
625
+ trusted = true;
626
+ provider.isConnected = true;
627
+ provider.publicKey = publicKey;
628
+ emitSolana('accountChanged', publicKey);
629
+ emitStandardChange({ accounts: standardWallet.accounts });
630
+ };
631
+
632
+ const disconnectFromController = () => {
633
+ const wasVisible = connected;
634
+ const shouldEmitDisconnect = connected || hiddenByController;
635
+ connected = false;
636
+ hiddenByController = false;
637
+ provider.isConnected = false;
638
+ provider.publicKey = null;
639
+ if (wasVisible) {
640
+ emitSolana('accountChanged', null);
641
+ emitStandardChange({ accounts: standardWallet.accounts });
642
+ }
643
+ if (shouldEmitDisconnect) {
644
+ emitSolana('disconnect');
645
+ }
646
+ };
647
+
648
+ const handleControllerEvent = (event, payload) => {
649
+ if (event === 'accountsChanged') {
650
+ if (Array.isArray(payload) && payload.length > 0) {
651
+ restoreFromController();
652
+ } else {
653
+ hideFromController();
654
+ }
655
+ }
656
+ if (event === 'disconnect') {
657
+ disconnectFromController();
658
+ }
112
659
  };
113
660
 
114
- return { info, provider, listeners };
661
+ const standardWallet = Object.freeze({
662
+ version: '1.0.0',
663
+ name: entry.info.name,
664
+ icon: entry.info.icon,
665
+ chains: solanaChains,
666
+ get accounts() {
667
+ return connected ? Object.freeze([standardAccount]) : Object.freeze([]);
668
+ },
669
+ features: Object.freeze({
670
+ 'standard:connect': Object.freeze({
671
+ version: '1.0.0',
672
+ connect: async (input = {}) => {
673
+ await provider.connect({ onlyIfTrusted: input?.silent === true });
674
+ return { accounts: standardWallet.accounts };
675
+ },
676
+ }),
677
+ 'standard:disconnect': Object.freeze({
678
+ version: '1.0.0',
679
+ disconnect: async () => {
680
+ await provider.disconnect();
681
+ },
682
+ }),
683
+ 'standard:events': Object.freeze({
684
+ version: '1.0.0',
685
+ on: standardOn,
686
+ }),
687
+ 'solana:signIn': Object.freeze({
688
+ version: '1.0.0',
689
+ signIn: async (...inputs) => {
690
+ const signInInputs = inputs.length > 0 ? inputs : [{}];
691
+ return Promise.all(signInInputs.map(async (input) => {
692
+ const result = await provider.signIn(input);
693
+ return {
694
+ account: standardAccount,
695
+ signedMessage: result.signedMessage,
696
+ signature: result.signature,
697
+ };
698
+ }));
699
+ },
700
+ }),
701
+ 'solana:signMessage': Object.freeze({
702
+ version: '1.1.0',
703
+ signMessage: async (...inputs) => {
704
+ return Promise.all(inputs.map(async (input) => {
705
+ ensureStandardAccount(input.account);
706
+ const result = await provider.signMessage(input.message);
707
+ return {
708
+ signedMessage: input.message,
709
+ signature: result.signature,
710
+ };
711
+ }));
712
+ },
713
+ }),
714
+ 'solana:signTransaction': Object.freeze({
715
+ version: '1.0.0',
716
+ supportedTransactionVersions: Object.freeze(['legacy', 0]),
717
+ signTransaction: async (...inputs) => {
718
+ return Promise.all(inputs.map(async (input) => {
719
+ ensureStandardAccount(input.account);
720
+ const signedTransaction = await provider.signTransaction(input.transaction);
721
+ return {
722
+ signedTransaction,
723
+ };
724
+ }));
725
+ },
726
+ }),
727
+ 'solana:signAndSendTransaction': Object.freeze({
728
+ version: '1.0.0',
729
+ supportedTransactionVersions: Object.freeze(['legacy', 0]),
730
+ signAndSendTransaction: async (...inputs) => {
731
+ return Promise.all(inputs.map(async (input) => {
732
+ ensureStandardAccount(input.account);
733
+ const result = await provider.signAndSendTransaction(input.transaction);
734
+ return {
735
+ signature: hexToBytes(result.signature),
736
+ };
737
+ }));
738
+ },
739
+ }),
740
+ }),
741
+ });
742
+
743
+ for (const [flag, value] of Object.entries(solanaConfig.flags ?? {})) {
744
+ Object.defineProperty(provider, flag, {
745
+ value: Boolean(value),
746
+ configurable: true,
747
+ enumerable: true,
748
+ });
749
+ }
750
+
751
+ return { provider, aliases: solanaConfig.aliases ?? [], standardWallet, handleControllerEvent };
752
+ };
753
+
754
+ const registerStandardWallet = (wallet) => {
755
+ const register = (api) => {
756
+ if (api && typeof api.register === 'function') {
757
+ api.register(wallet);
758
+ }
759
+ };
760
+ window.addEventListener('wallet-standard:app-ready', (event) => register(event.detail));
761
+ window.dispatchEvent(
762
+ new CustomEvent('wallet-standard:register-wallet', {
763
+ detail: register,
764
+ }),
765
+ );
115
766
  };
116
767
 
117
768
  const entries = config.providers.map(createProviderEntry);
769
+ const evmEntries = entries.filter((entry) => entry.evm);
770
+ const solanaEntries = entries
771
+ .map((entry) => createSolanaProvider(entry))
772
+ .filter(Boolean);
773
+ if (evmEntries.length > 1) {
774
+ const providers = evmEntries.map((entry) => entry.provider);
775
+ for (const entry of evmEntries) {
776
+ Object.defineProperty(entry.provider, 'providers', {
777
+ value: providers,
778
+ configurable: true,
779
+ enumerable: true,
780
+ });
781
+ }
782
+ }
118
783
 
119
784
  const emit = (event, payload) => {
120
785
  if (event === 'connect') {
@@ -124,7 +789,7 @@ export const buildInjectedProviderScript = (config) => `
124
789
  chainDisconnected = true;
125
790
  }
126
791
 
127
- for (const entry of entries) {
792
+ for (const entry of evmEntries) {
128
793
  if (event === 'accountsChanged') {
129
794
  entry.provider.selectedAddress =
130
795
  Array.isArray(payload) && payload.length > 0 ? payload[0] : null;
@@ -136,18 +801,29 @@ export const buildInjectedProviderScript = (config) => `
136
801
  }
137
802
 
138
803
  const callbacks = entry.listeners.get(event);
139
- if (!callbacks) {
140
- continue;
804
+ if (callbacks) {
805
+ for (const callback of [...callbacks]) {
806
+ callback(payload);
807
+ }
141
808
  }
142
809
 
143
- for (const callback of [...callbacks]) {
144
- callback(payload);
810
+ if (event === 'chainChanged') {
811
+ const networkCallbacks = entry.listeners.get('networkChanged');
812
+ if (networkCallbacks) {
813
+ for (const callback of [...networkCallbacks]) {
814
+ callback(entry.provider.networkVersion);
815
+ }
816
+ }
145
817
  }
146
818
  }
819
+
820
+ for (const entry of solanaEntries) {
821
+ entry.handleControllerEvent(event, payload);
822
+ }
147
823
  };
148
824
 
149
825
  const announceProviders = () => {
150
- for (const entry of entries) {
826
+ for (const entry of evmEntries) {
151
827
  window.dispatchEvent(
152
828
  new CustomEvent('eip6963:announceProvider', {
153
829
  detail: Object.freeze({
@@ -159,12 +835,58 @@ export const buildInjectedProviderScript = (config) => `
159
835
  }
160
836
  };
161
837
 
162
- Object.defineProperty(window, 'ethereum', {
163
- value: entries[0].provider,
164
- configurable: true,
165
- enumerable: true,
166
- writable: true,
167
- });
838
+ if (evmEntries[0]) {
839
+ Object.defineProperty(window, 'ethereum', {
840
+ value: evmEntries[0].provider,
841
+ configurable: true,
842
+ enumerable: true,
843
+ writable: true,
844
+ });
845
+ }
846
+
847
+ const assignProviderAlias = (path, provider) => {
848
+ const parts = String(path).split('.').filter(Boolean);
849
+ if (parts.length === 0 || (parts.length === 1 && parts[0] === 'ethereum')) {
850
+ return;
851
+ }
852
+
853
+ let target = window;
854
+ for (const part of parts.slice(0, -1)) {
855
+ const current = target[part];
856
+ if (
857
+ current === null ||
858
+ (typeof current !== 'object' && typeof current !== 'function')
859
+ ) {
860
+ Object.defineProperty(target, part, {
861
+ value: {},
862
+ configurable: true,
863
+ enumerable: true,
864
+ writable: true,
865
+ });
866
+ }
867
+ target = target[part];
868
+ }
869
+
870
+ Object.defineProperty(target, parts[parts.length - 1], {
871
+ value: provider,
872
+ configurable: true,
873
+ enumerable: true,
874
+ writable: true,
875
+ });
876
+ };
877
+
878
+ for (const entry of evmEntries) {
879
+ for (const alias of entry.aliases) {
880
+ assignProviderAlias(alias, entry.provider);
881
+ }
882
+ }
883
+
884
+ for (const entry of solanaEntries) {
885
+ for (const alias of entry.aliases) {
886
+ assignProviderAlias(alias, entry.provider);
887
+ }
888
+ registerStandardWallet(entry.standardWallet);
889
+ }
168
890
 
169
891
  Object.defineProperty(window, '${EMITTER_NAME}', {
170
892
  value: emit,
@@ -188,8 +910,9 @@ export const buildInjectedProviderScript = (config) => `
188
910
  }
189
911
 
190
912
  const state = response.result;
191
- for (const entry of entries) {
192
- entry.provider.selectedAddress = state.accounts.length > 0 ? state.accounts[0] : null;
913
+ for (const entry of evmEntries) {
914
+ entry.provider.selectedAddress =
915
+ state.isUnlocked && state.accounts.length > 0 ? state.accounts[0] : null;
193
916
  entry.provider.chainId = state.chainId;
194
917
  entry.provider.networkVersion = toNetworkVersion(state.chainId);
195
918
  }