@mysten-incubation/dev-wallet 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +12 -8
  3. package/dist/client/connected-apps.d.mts +35 -0
  4. package/dist/client/connected-apps.d.mts.map +1 -0
  5. package/dist/client/connected-apps.mjs +50 -0
  6. package/dist/client/connected-apps.mjs.map +1 -0
  7. package/dist/client/dev-wallet-client.mjs +1 -1
  8. package/dist/client/dev-wallet-client.mjs.map +1 -1
  9. package/dist/client/index.d.mts +2 -1
  10. package/dist/client/index.mjs +2 -1
  11. package/dist/client/request-handler.d.mts +7 -0
  12. package/dist/client/request-handler.d.mts.map +1 -1
  13. package/dist/client/request-handler.mjs +17 -2
  14. package/dist/client/request-handler.mjs.map +1 -1
  15. package/dist/inject/index.d.mts +4 -0
  16. package/dist/inject/index.d.mts.map +1 -1
  17. package/dist/inject/index.mjs +31 -0
  18. package/dist/inject/index.mjs.map +1 -1
  19. package/dist/standalone/assets/{in-memory-adapter-Ban2kRYS.js → in-memory-adapter-uuWG4K4k.js} +1 -1
  20. package/dist/standalone/assets/main-CKYv0eR6.js +7 -0
  21. package/dist/standalone/assets/{webcrypto-adapter-Ddqn-RAu.js → webcrypto-adapter-5ShaPc_M.js} +1 -1
  22. package/dist/standalone/bookmarklet.js +1 -1
  23. package/dist/standalone/index.html +1 -1
  24. package/dist/ui/dev-wallet-balances.d.mts.map +1 -1
  25. package/dist/ui/dev-wallet-balances.mjs +20 -13
  26. package/dist/ui/dev-wallet-balances.mjs.map +1 -1
  27. package/dist/ui/dev-wallet-connect-guide.d.mts +29 -0
  28. package/dist/ui/dev-wallet-connect-guide.d.mts.map +1 -0
  29. package/dist/ui/dev-wallet-connect-guide.mjs +386 -0
  30. package/dist/ui/dev-wallet-connect-guide.mjs.map +1 -0
  31. package/dist/ui/dev-wallet-connected-apps.d.mts +27 -0
  32. package/dist/ui/dev-wallet-connected-apps.d.mts.map +1 -0
  33. package/dist/ui/dev-wallet-connected-apps.mjs +164 -0
  34. package/dist/ui/dev-wallet-connected-apps.mjs.map +1 -0
  35. package/dist/ui/dev-wallet-panel.d.mts.map +1 -1
  36. package/dist/ui/dev-wallet-panel.mjs +0 -9
  37. package/dist/ui/dev-wallet-panel.mjs.map +1 -1
  38. package/dist/ui/dev-wallet-settings.d.mts +8 -2
  39. package/dist/ui/dev-wallet-settings.d.mts.map +1 -1
  40. package/dist/ui/dev-wallet-settings.mjs +86 -171
  41. package/dist/ui/dev-wallet-settings.mjs.map +1 -1
  42. package/dist/ui/dev-wallet-standalone.d.mts +7 -2
  43. package/dist/ui/dev-wallet-standalone.d.mts.map +1 -1
  44. package/dist/ui/dev-wallet-standalone.mjs +68 -222
  45. package/dist/ui/dev-wallet-standalone.mjs.map +1 -1
  46. package/dist/ui/index.d.mts +3 -1
  47. package/dist/ui/index.mjs +3 -1
  48. package/dist/ui/wallet-controller.mjs +15 -3
  49. package/dist/ui/wallet-controller.mjs.map +1 -1
  50. package/dist/wallet/dev-wallet.d.mts +15 -3
  51. package/dist/wallet/dev-wallet.d.mts.map +1 -1
  52. package/dist/wallet/dev-wallet.mjs +94 -30
  53. package/dist/wallet/dev-wallet.mjs.map +1 -1
  54. package/dist/wallet/persisted.mjs +20 -0
  55. package/dist/wallet/persisted.mjs.map +1 -0
  56. package/package.json +6 -6
  57. package/src/app/main.ts +6 -2
  58. package/src/client/connected-apps.ts +79 -0
  59. package/src/client/dev-wallet-client.ts +2 -1
  60. package/src/client/index.ts +2 -0
  61. package/src/client/request-handler.ts +24 -1
  62. package/src/inject/index.ts +55 -0
  63. package/src/ui/dev-wallet-balances.ts +25 -13
  64. package/src/ui/dev-wallet-connect-guide.ts +428 -0
  65. package/src/ui/dev-wallet-connected-apps.ts +191 -0
  66. package/src/ui/dev-wallet-panel.ts +0 -9
  67. package/src/ui/dev-wallet-settings.ts +101 -179
  68. package/src/ui/dev-wallet-standalone.ts +80 -231
  69. package/src/ui/index.ts +2 -0
  70. package/src/ui/wallet-controller.ts +23 -3
  71. package/src/wallet/dev-wallet.ts +130 -39
  72. package/src/wallet/persisted.ts +24 -0
  73. package/dist/standalone/assets/main-xfzarPod.js +0 -7
@@ -24,10 +24,75 @@ import { getWallets, ReadonlyWalletAccount } from '@mysten/wallet-standard';
24
24
  import { chainsForNetworks } from '../adapters/build-managed-account.js';
25
25
  import type { SignerAdapter } from '../types.js';
26
26
  import { DEFAULT_WALLET_ICON, getNetworkFromChain, type WalletEventsMap } from './constants.js';
27
+ import { readPersisted } from './persisted.js';
27
28
  import { type SigningResult, executeSigning } from './signing.js';
28
29
 
29
30
  const DEFAULT_WALLET_NAME = 'Dev Wallet';
30
- const NETWORK_STORAGE_KEY = 'dev-wallet:networks';
31
+ /** localStorage key for {@link PersistedStateV1}. Bump the suffix on shape changes. */
32
+ export const STATE_STORAGE_KEY = 'dev-wallet:state:v1';
33
+
34
+ /** Wallet state persisted across reloads when `persistState` is enabled. */
35
+ interface PersistedStateV1 {
36
+ version: 1;
37
+ networks: Record<string, string>;
38
+ faucets?: Record<string, string>;
39
+ activeNetwork?: string;
40
+ activeAccount?: string;
41
+ }
42
+
43
+ function isStringRecord(value: unknown): value is Record<string, string> {
44
+ return (
45
+ typeof value === 'object' &&
46
+ value !== null &&
47
+ !Array.isArray(value) &&
48
+ Object.values(value).every((v) => typeof v === 'string')
49
+ );
50
+ }
51
+
52
+ /** Throws unless `url` is an http(s) URL. `what` names it in the error, e.g. `network "localnet"`. */
53
+ function assertHttpUrl(url: string, what: string): void {
54
+ let parsed: URL;
55
+ try {
56
+ parsed = new URL(url);
57
+ } catch {
58
+ throw new Error(`Invalid URL for ${what}: ${url}`);
59
+ }
60
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
61
+ throw new Error(`Invalid URL for ${what}: must use http or https (got ${parsed.protocol})`);
62
+ }
63
+ }
64
+
65
+ /** Config faucets overlaid with persisted ones; `''` marks a removed faucet. */
66
+ function mergeFaucets(
67
+ config: Record<string, string>,
68
+ persisted: Record<string, string> | undefined,
69
+ ): Record<string, string> {
70
+ const merged = { ...config, ...persisted };
71
+ for (const [name, url] of Object.entries(merged)) {
72
+ if (url === '') delete merged[name];
73
+ }
74
+ return merged;
75
+ }
76
+
77
+ function decodePersistedState(parsed: unknown): PersistedStateV1 | null {
78
+ if (typeof parsed !== 'object' || parsed === null) return null;
79
+ const { version, networks, faucets, activeNetwork, activeAccount } = parsed as Record<
80
+ string,
81
+ unknown
82
+ >;
83
+ if (version !== 1) return null;
84
+ if (!isStringRecord(networks)) return null;
85
+ if (faucets !== undefined && !isStringRecord(faucets)) return null;
86
+ if (activeNetwork !== undefined && typeof activeNetwork !== 'string') return null;
87
+ if (activeAccount !== undefined && typeof activeAccount !== 'string') return null;
88
+ return {
89
+ version,
90
+ networks,
91
+ faucets,
92
+ activeNetwork,
93
+ activeAccount,
94
+ };
95
+ }
31
96
 
32
97
  /**
33
98
  * Default gRPC endpoint URLs for standard Sui networks (devnet, testnet, localnet).
@@ -100,10 +165,11 @@ export interface DevWalletConfig {
100
165
  /** Factory to create a client for a given network. Defaults to SuiGrpcClient. */
101
166
  clientFactory?: (network: string, url: string) => ClientWithCoreApi;
102
167
  /**
103
- * Persist network URLs to localStorage so custom configurations survive page reloads
104
- * and are shared with popup windows.
168
+ * Persist network URLs, the active network, and the UI's active account to
169
+ * localStorage so they survive page reloads and are shared with popup windows.
170
+ * Persisted values take precedence over `networks` and `activeNetwork`.
105
171
  */
106
- persistNetworks?: boolean;
172
+ persistState?: boolean;
107
173
  }
108
174
 
109
175
  /**
@@ -114,6 +180,8 @@ export class DevWallet implements Wallet {
114
180
  readonly #adapters: SignerAdapter[];
115
181
  #networkUrls: Record<string, string>;
116
182
  #faucetUrls: Record<string, string>;
183
+ /** Faucets from config; persisted faucets layer over these. */
184
+ readonly #configFaucets: Record<string, string>;
117
185
  #clients: Record<string, ClientWithCoreApi>;
118
186
  #activeNetwork: string;
119
187
  readonly #name: string;
@@ -121,7 +189,8 @@ export class DevWallet implements Wallet {
121
189
  readonly #autoApprove: AutoApprovePolicy;
122
190
  readonly #autoConnect: boolean;
123
191
  readonly #clientFactory: (network: string, url: string) => ClientWithCoreApi;
124
- readonly #persistNetworks: boolean;
192
+ readonly #persistState: boolean;
193
+ #activeAccount: string | null;
125
194
  #accounts: ReadonlyWalletAccount[];
126
195
  #selectedAddress: string | null = null;
127
196
  #events: Emitter<WalletEventsMap>;
@@ -143,11 +212,17 @@ export class DevWallet implements Wallet {
143
212
  console.warn('[dev-wallet] No adapters provided. The wallet will have no accounts.');
144
213
  }
145
214
  this.#adapters = config.adapters;
146
- this.#persistNetworks = config.persistNetworks ?? false;
147
- this.#networkUrls = this.#loadNetworkUrls(config.networks ?? DEFAULT_NETWORK_URLS);
148
- this.#faucetUrls = { ...config.faucets };
215
+ this.#persistState = config.persistState ?? false;
216
+ const persisted = this.#loadState();
217
+ this.#networkUrls = persisted?.networks ?? { ...(config.networks ?? DEFAULT_NETWORK_URLS) };
218
+ this.#configFaucets = { ...config.faucets };
219
+ this.#faucetUrls = mergeFaucets(this.#configFaucets, persisted?.faucets);
149
220
  this.#clients = {};
150
- this.#activeNetwork = config.activeNetwork ?? Object.keys(this.#networkUrls)[0] ?? '';
221
+ this.#activeNetwork =
222
+ persisted?.activeNetwork && persisted.activeNetwork in this.#networkUrls
223
+ ? persisted.activeNetwork
224
+ : (config.activeNetwork ?? Object.keys(this.#networkUrls)[0] ?? '');
225
+ this.#activeAccount = persisted?.activeAccount ?? null;
151
226
  this.#name = config.name ?? DEFAULT_WALLET_NAME;
152
227
  this.#icon = config.icon ?? DEFAULT_WALLET_ICON;
153
228
  this.#autoApprove = config.autoApprove ?? false;
@@ -330,25 +405,37 @@ export class DevWallet implements Wallet {
330
405
  );
331
406
  }
332
407
  this.#activeNetwork = network;
408
+ this.#saveState();
333
409
  this.#events.emit('change', { accounts: this.#exposedAccounts() });
334
410
  }
335
411
 
412
+ /** Address the wallet UI shows as active, or `null` when unset. Persisted
413
+ * with `persistState`. Unlike {@link setSelectedAccount}, this doesn't
414
+ * change which accounts dApps see. */
415
+ get activeAccount(): string | null {
416
+ return this.#activeAccount;
417
+ }
418
+
419
+ setActiveAccount(address: string | null): void {
420
+ this.#activeAccount = address;
421
+ this.#saveState();
422
+ }
423
+
424
+ /**
425
+ * Add a network or replace its URL. `faucet` sets the network's faucet
426
+ * endpoint; pass `null` to remove it, or omit it to leave it unchanged.
427
+ */
336
428
  addNetwork(name: string, url: string, faucet?: string | null): void {
337
- let parsed: URL;
338
- try {
339
- parsed = new URL(url);
340
- } catch {
341
- throw new Error(`Invalid URL: ${url}`);
342
- }
343
- if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
344
- throw new Error(`Invalid URL: must use http or https (got ${parsed.protocol})`);
345
- }
429
+ assertHttpUrl(url, `network "${name}"`);
430
+ if (faucet) assertHttpUrl(faucet, `the faucet of network "${name}"`);
346
431
  this.#networkUrls[name] = url;
347
432
  this.#clients[name] = this.#clientFactory(name, url);
348
- if (faucet !== undefined && faucet !== null) {
433
+ if (faucet) {
349
434
  this.#faucetUrls[name] = faucet;
435
+ } else if (faucet === null) {
436
+ delete this.#faucetUrls[name];
350
437
  }
351
- this.#saveNetworkUrls();
438
+ this.#saveState();
352
439
  this.#events.emit('change', { accounts: this.#exposedAccounts() });
353
440
  }
354
441
 
@@ -359,7 +446,7 @@ export class DevWallet implements Wallet {
359
446
  if (this.#activeNetwork === name) {
360
447
  this.#activeNetwork = Object.keys(this.#networkUrls)[0] ?? '';
361
448
  }
362
- this.#saveNetworkUrls();
449
+ this.#saveState();
363
450
  this.#events.emit('change', { accounts: this.#exposedAccounts() });
364
451
  }
365
452
 
@@ -576,27 +663,31 @@ export class DevWallet implements Wallet {
576
663
  return this.#ensureClient(network);
577
664
  }
578
665
 
579
- #loadNetworkUrls(defaults: Record<string, string>): Record<string, string> {
580
- if (!this.#persistNetworks || typeof localStorage === 'undefined') {
581
- return { ...defaults };
582
- }
583
- try {
584
- const raw = localStorage.getItem(NETWORK_STORAGE_KEY);
585
- if (raw) {
586
- const parsed = JSON.parse(raw);
587
- if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
588
- return parsed as Record<string, string>;
589
- }
590
- }
591
- } catch {
592
- // Corrupted data — fall through to defaults
666
+ #loadState(): PersistedStateV1 | null {
667
+ if (!this.#persistState || typeof localStorage === 'undefined') return null;
668
+ return readPersisted(localStorage, STATE_STORAGE_KEY, decodePersistedState);
669
+ }
670
+
671
+ /** Current faucets plus an empty-string marker for each config faucet the
672
+ * user removed, so the removal survives a reload. */
673
+ #persistableFaucets(): Record<string, string> {
674
+ const faucets = { ...this.#faucetUrls };
675
+ for (const name of Object.keys(this.#configFaucets)) {
676
+ if (!(name in faucets)) faucets[name] = '';
593
677
  }
594
- return { ...defaults };
678
+ return faucets;
595
679
  }
596
680
 
597
- #saveNetworkUrls(): void {
598
- if (!this.#persistNetworks || typeof localStorage === 'undefined') return;
599
- localStorage.setItem(NETWORK_STORAGE_KEY, JSON.stringify(this.#networkUrls));
681
+ #saveState(): void {
682
+ if (!this.#persistState || typeof localStorage === 'undefined') return;
683
+ const state: PersistedStateV1 = {
684
+ version: 1,
685
+ networks: this.#networkUrls,
686
+ faucets: this.#persistableFaucets(),
687
+ activeNetwork: this.#activeNetwork,
688
+ ...(this.#activeAccount ? { activeAccount: this.#activeAccount } : {}),
689
+ };
690
+ localStorage.setItem(STATE_STORAGE_KEY, JSON.stringify(state));
600
691
  }
601
692
 
602
693
  #ensureClient(network: string): ClientWithCoreApi {
@@ -0,0 +1,24 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Read a versioned JSON value from storage. A missing key is a plain miss;
6
+ * unparseable data, or data `decode` rejects (returns `null`), is a miss plus
7
+ * a warning, so a bad write never crashes the wallet.
8
+ */
9
+ export function readPersisted<T>(
10
+ storage: Storage,
11
+ key: string,
12
+ decode: (value: unknown) => T | null,
13
+ ): T | null {
14
+ const raw = storage.getItem(key);
15
+ if (raw === null) return null;
16
+ let value: T | null = null;
17
+ try {
18
+ value = decode(JSON.parse(raw));
19
+ } catch {
20
+ // Invalid JSON — handled below.
21
+ }
22
+ if (value === null) console.warn(`[dev-wallet] Ignoring invalid data in "${key}".`);
23
+ return value;
24
+ }