@mysten-incubation/dev-wallet 0.0.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 (213) hide show
  1. package/README.md +250 -0
  2. package/dist/_virtual/_@oxc-project_runtime@0.107.0/helpers/decorate.mjs +10 -0
  3. package/dist/adapters/base-adapter.d.mts +33 -0
  4. package/dist/adapters/base-adapter.d.mts.map +1 -0
  5. package/dist/adapters/base-adapter.mjs +77 -0
  6. package/dist/adapters/base-adapter.mjs.map +1 -0
  7. package/dist/adapters/browser.d.mts +8 -0
  8. package/dist/adapters/browser.mjs +8 -0
  9. package/dist/adapters/build-managed-account.d.mts +12 -0
  10. package/dist/adapters/build-managed-account.d.mts.map +1 -0
  11. package/dist/adapters/build-managed-account.mjs +30 -0
  12. package/dist/adapters/build-managed-account.mjs.map +1 -0
  13. package/dist/adapters/idb-store.mjs +73 -0
  14. package/dist/adapters/idb-store.mjs.map +1 -0
  15. package/dist/adapters/in-memory-adapter.d.mts +16 -0
  16. package/dist/adapters/in-memory-adapter.d.mts.map +1 -0
  17. package/dist/adapters/in-memory-adapter.mjs +44 -0
  18. package/dist/adapters/in-memory-adapter.mjs.map +1 -0
  19. package/dist/adapters/passkey-adapter.d.mts +31 -0
  20. package/dist/adapters/passkey-adapter.d.mts.map +1 -0
  21. package/dist/adapters/passkey-adapter.mjs +87 -0
  22. package/dist/adapters/passkey-adapter.mjs.map +1 -0
  23. package/dist/adapters/remote-cli-adapter.d.mts +65 -0
  24. package/dist/adapters/remote-cli-adapter.d.mts.map +1 -0
  25. package/dist/adapters/remote-cli-adapter.mjs +299 -0
  26. package/dist/adapters/remote-cli-adapter.mjs.map +1 -0
  27. package/dist/adapters/webcrypto-adapter.d.mts +20 -0
  28. package/dist/adapters/webcrypto-adapter.d.mts.map +1 -0
  29. package/dist/adapters/webcrypto-adapter.mjs +68 -0
  30. package/dist/adapters/webcrypto-adapter.mjs.map +1 -0
  31. package/dist/bin/cli.d.mts +1 -0
  32. package/dist/bin/cli.mjs +183 -0
  33. package/dist/bin/cli.mjs.map +1 -0
  34. package/dist/client/dev-wallet-client.d.mts +57 -0
  35. package/dist/client/dev-wallet-client.d.mts.map +1 -0
  36. package/dist/client/dev-wallet-client.mjs +223 -0
  37. package/dist/client/dev-wallet-client.mjs.map +1 -0
  38. package/dist/client/index.d.mts +3 -0
  39. package/dist/client/index.mjs +4 -0
  40. package/dist/client/request-handler.d.mts +42 -0
  41. package/dist/client/request-handler.d.mts.map +1 -0
  42. package/dist/client/request-handler.mjs +115 -0
  43. package/dist/client/request-handler.mjs.map +1 -0
  44. package/dist/index.d.mts +5 -0
  45. package/dist/index.mjs +4 -0
  46. package/dist/react/components.d.mts +32 -0
  47. package/dist/react/components.d.mts.map +1 -0
  48. package/dist/react/components.mjs +53 -0
  49. package/dist/react/components.mjs.map +1 -0
  50. package/dist/react/context.d.mts +22 -0
  51. package/dist/react/context.d.mts.map +1 -0
  52. package/dist/react/context.mjs +25 -0
  53. package/dist/react/context.mjs.map +1 -0
  54. package/dist/react/index.d.mts +4 -0
  55. package/dist/react/index.mjs +5 -0
  56. package/dist/react/useDevWallet.d.mts +38 -0
  57. package/dist/react/useDevWallet.d.mts.map +1 -0
  58. package/dist/react/useDevWallet.mjs +89 -0
  59. package/dist/react/useDevWallet.mjs.map +1 -0
  60. package/dist/server/cli-signing-middleware.d.mts +22 -0
  61. package/dist/server/cli-signing-middleware.d.mts.map +1 -0
  62. package/dist/server/cli-signing-middleware.mjs +117 -0
  63. package/dist/server/cli-signing-middleware.mjs.map +1 -0
  64. package/dist/server/index.d.mts +2 -0
  65. package/dist/server/index.mjs +3 -0
  66. package/dist/standalone/assets/in-memory-adapter-CBF4h2KD.js +1 -0
  67. package/dist/standalone/assets/main-VE4olb-Q.js +2841 -0
  68. package/dist/standalone/assets/webcrypto-adapter-DysAubFb.js +1 -0
  69. package/dist/standalone/bookmarklet.js +5 -0
  70. package/dist/standalone/index.html +48 -0
  71. package/dist/types.d.mts +55 -0
  72. package/dist/types.d.mts.map +1 -0
  73. package/dist/ui/copy-controller.mjs +43 -0
  74. package/dist/ui/copy-controller.mjs.map +1 -0
  75. package/dist/ui/dev-wallet-account-selector.d.mts +24 -0
  76. package/dist/ui/dev-wallet-account-selector.d.mts.map +1 -0
  77. package/dist/ui/dev-wallet-account-selector.mjs +268 -0
  78. package/dist/ui/dev-wallet-account-selector.mjs.map +1 -0
  79. package/dist/ui/dev-wallet-accounts.d.mts +29 -0
  80. package/dist/ui/dev-wallet-accounts.d.mts.map +1 -0
  81. package/dist/ui/dev-wallet-accounts.mjs +524 -0
  82. package/dist/ui/dev-wallet-accounts.mjs.map +1 -0
  83. package/dist/ui/dev-wallet-balances.d.mts +26 -0
  84. package/dist/ui/dev-wallet-balances.d.mts.map +1 -0
  85. package/dist/ui/dev-wallet-balances.mjs +130 -0
  86. package/dist/ui/dev-wallet-balances.mjs.map +1 -0
  87. package/dist/ui/dev-wallet-connect.d.mts +31 -0
  88. package/dist/ui/dev-wallet-connect.d.mts.map +1 -0
  89. package/dist/ui/dev-wallet-connect.mjs +225 -0
  90. package/dist/ui/dev-wallet-connect.mjs.map +1 -0
  91. package/dist/ui/dev-wallet-dropdown.d.mts +27 -0
  92. package/dist/ui/dev-wallet-dropdown.d.mts.map +1 -0
  93. package/dist/ui/dev-wallet-dropdown.mjs +121 -0
  94. package/dist/ui/dev-wallet-dropdown.mjs.map +1 -0
  95. package/dist/ui/dev-wallet-network-badge.d.mts +21 -0
  96. package/dist/ui/dev-wallet-network-badge.d.mts.map +1 -0
  97. package/dist/ui/dev-wallet-network-badge.mjs +121 -0
  98. package/dist/ui/dev-wallet-network-badge.mjs.map +1 -0
  99. package/dist/ui/dev-wallet-new-account.d.mts +31 -0
  100. package/dist/ui/dev-wallet-new-account.d.mts.map +1 -0
  101. package/dist/ui/dev-wallet-new-account.mjs +577 -0
  102. package/dist/ui/dev-wallet-new-account.mjs.map +1 -0
  103. package/dist/ui/dev-wallet-objects.d.mts +26 -0
  104. package/dist/ui/dev-wallet-objects.d.mts.map +1 -0
  105. package/dist/ui/dev-wallet-objects.mjs +276 -0
  106. package/dist/ui/dev-wallet-objects.mjs.map +1 -0
  107. package/dist/ui/dev-wallet-panel.d.mts +22 -0
  108. package/dist/ui/dev-wallet-panel.d.mts.map +1 -0
  109. package/dist/ui/dev-wallet-panel.mjs +192 -0
  110. package/dist/ui/dev-wallet-panel.mjs.map +1 -0
  111. package/dist/ui/dev-wallet-popup.d.mts +36 -0
  112. package/dist/ui/dev-wallet-popup.d.mts.map +1 -0
  113. package/dist/ui/dev-wallet-popup.mjs +137 -0
  114. package/dist/ui/dev-wallet-popup.mjs.map +1 -0
  115. package/dist/ui/dev-wallet-settings.d.mts +33 -0
  116. package/dist/ui/dev-wallet-settings.d.mts.map +1 -0
  117. package/dist/ui/dev-wallet-settings.mjs +635 -0
  118. package/dist/ui/dev-wallet-settings.mjs.map +1 -0
  119. package/dist/ui/dev-wallet-signing-modal.d.mts +32 -0
  120. package/dist/ui/dev-wallet-signing-modal.d.mts.map +1 -0
  121. package/dist/ui/dev-wallet-signing-modal.mjs +115 -0
  122. package/dist/ui/dev-wallet-signing-modal.mjs.map +1 -0
  123. package/dist/ui/dev-wallet-signing.d.mts +25 -0
  124. package/dist/ui/dev-wallet-signing.d.mts.map +1 -0
  125. package/dist/ui/dev-wallet-signing.mjs +544 -0
  126. package/dist/ui/dev-wallet-signing.mjs.map +1 -0
  127. package/dist/ui/dev-wallet-standalone.d.mts +23 -0
  128. package/dist/ui/dev-wallet-standalone.d.mts.map +1 -0
  129. package/dist/ui/dev-wallet-standalone.mjs +129 -0
  130. package/dist/ui/dev-wallet-standalone.mjs.map +1 -0
  131. package/dist/ui/dev-wallet-tab-bar.d.mts +19 -0
  132. package/dist/ui/dev-wallet-tab-bar.d.mts.map +1 -0
  133. package/dist/ui/dev-wallet-tab-bar.mjs +143 -0
  134. package/dist/ui/dev-wallet-tab-bar.mjs.map +1 -0
  135. package/dist/ui/index.d.mts +17 -0
  136. package/dist/ui/index.mjs +18 -0
  137. package/dist/ui/mount.d.mts +11 -0
  138. package/dist/ui/mount.d.mts.map +1 -0
  139. package/dist/ui/mount.mjs +17 -0
  140. package/dist/ui/mount.mjs.map +1 -0
  141. package/dist/ui/styles.mjs +250 -0
  142. package/dist/ui/styles.mjs.map +1 -0
  143. package/dist/ui/transaction-analyzer.mjs +58 -0
  144. package/dist/ui/transaction-analyzer.mjs.map +1 -0
  145. package/dist/ui/utils.mjs +98 -0
  146. package/dist/ui/utils.mjs.map +1 -0
  147. package/dist/ui/wallet-controller.mjs +275 -0
  148. package/dist/ui/wallet-controller.mjs.map +1 -0
  149. package/dist/wallet/constants.mjs +11 -0
  150. package/dist/wallet/constants.mjs.map +1 -0
  151. package/dist/wallet/dev-wallet.d.mts +118 -0
  152. package/dist/wallet/dev-wallet.d.mts.map +1 -0
  153. package/dist/wallet/dev-wallet.mjs +424 -0
  154. package/dist/wallet/dev-wallet.mjs.map +1 -0
  155. package/dist/wallet/initializer.d.mts +63 -0
  156. package/dist/wallet/initializer.d.mts.map +1 -0
  157. package/dist/wallet/initializer.mjs +75 -0
  158. package/dist/wallet/initializer.mjs.map +1 -0
  159. package/dist/wallet/signing.d.mts +35 -0
  160. package/dist/wallet/signing.d.mts.map +1 -0
  161. package/dist/wallet/signing.mjs +69 -0
  162. package/dist/wallet/signing.mjs.map +1 -0
  163. package/package.json +124 -0
  164. package/src/adapters/base-adapter.ts +93 -0
  165. package/src/adapters/browser.ts +15 -0
  166. package/src/adapters/build-managed-account.ts +33 -0
  167. package/src/adapters/idb-store.ts +87 -0
  168. package/src/adapters/in-memory-adapter.ts +51 -0
  169. package/src/adapters/passkey-adapter.ts +120 -0
  170. package/src/adapters/remote-cli-adapter.ts +388 -0
  171. package/src/adapters/webcrypto-adapter.ts +96 -0
  172. package/src/app/bookmarklet-entry.ts +77 -0
  173. package/src/app/index.html +48 -0
  174. package/src/app/main.ts +245 -0
  175. package/src/app/tsconfig.json +3 -0
  176. package/src/bin/cli.ts +214 -0
  177. package/src/client/dev-wallet-client.ts +280 -0
  178. package/src/client/index.ts +7 -0
  179. package/src/client/request-handler.ts +161 -0
  180. package/src/index.ts +19 -0
  181. package/src/react/components.ts +64 -0
  182. package/src/react/context.ts +38 -0
  183. package/src/react/index.ts +16 -0
  184. package/src/react/useDevWallet.ts +118 -0
  185. package/src/server/cli-signing-middleware.ts +146 -0
  186. package/src/server/index.ts +8 -0
  187. package/src/types.ts +58 -0
  188. package/src/ui/copy-controller.ts +49 -0
  189. package/src/ui/dev-wallet-account-selector.ts +283 -0
  190. package/src/ui/dev-wallet-accounts.ts +578 -0
  191. package/src/ui/dev-wallet-balances.ts +171 -0
  192. package/src/ui/dev-wallet-connect.ts +259 -0
  193. package/src/ui/dev-wallet-dropdown.ts +138 -0
  194. package/src/ui/dev-wallet-network-badge.ts +128 -0
  195. package/src/ui/dev-wallet-new-account.ts +662 -0
  196. package/src/ui/dev-wallet-objects.ts +336 -0
  197. package/src/ui/dev-wallet-panel.ts +207 -0
  198. package/src/ui/dev-wallet-popup.ts +169 -0
  199. package/src/ui/dev-wallet-settings.ts +692 -0
  200. package/src/ui/dev-wallet-signing-modal.ts +137 -0
  201. package/src/ui/dev-wallet-signing.ts +624 -0
  202. package/src/ui/dev-wallet-standalone.ts +136 -0
  203. package/src/ui/dev-wallet-tab-bar.ts +151 -0
  204. package/src/ui/index.ts +26 -0
  205. package/src/ui/mount.ts +21 -0
  206. package/src/ui/styles.ts +252 -0
  207. package/src/ui/transaction-analyzer.ts +60 -0
  208. package/src/ui/utils.ts +118 -0
  209. package/src/ui/wallet-controller.ts +340 -0
  210. package/src/wallet/constants.ts +17 -0
  211. package/src/wallet/dev-wallet.ts +597 -0
  212. package/src/wallet/initializer.ts +124 -0
  213. package/src/wallet/signing.ts +85 -0
@@ -0,0 +1,597 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { ClientWithCoreApi } from '@mysten/sui/client';
5
+ import { SuiGrpcClient } from '@mysten/sui/grpc';
6
+ import { mitt, type Emitter } from '@mysten/utils';
7
+ import type {
8
+ StandardConnectFeature,
9
+ StandardConnectMethod,
10
+ StandardDisconnectFeature,
11
+ StandardDisconnectMethod,
12
+ StandardEventsFeature,
13
+ StandardEventsOnMethod,
14
+ SuiFeatures,
15
+ SuiSignAndExecuteTransactionMethod,
16
+ SuiSignPersonalMessageMethod,
17
+ SuiSignTransactionMethod,
18
+ Wallet,
19
+ WalletAccount,
20
+ WalletIcon,
21
+ } from '@mysten/wallet-standard';
22
+ import { getWallets, ReadonlyWalletAccount, SUI_CHAINS } from '@mysten/wallet-standard';
23
+
24
+ import type { SignerAdapter } from '../types.js';
25
+ import { DEFAULT_WALLET_ICON, getNetworkFromChain, type WalletEventsMap } from './constants.js';
26
+ import { type SigningResult, executeSigning } from './signing.js';
27
+
28
+ const DEFAULT_WALLET_NAME = 'Dev Wallet';
29
+ const NETWORK_STORAGE_KEY = 'dev-wallet:networks';
30
+
31
+ /**
32
+ * Default gRPC endpoint URLs for standard Sui networks (devnet, testnet, localnet).
33
+ */
34
+ export const DEFAULT_NETWORK_URLS = {
35
+ devnet: 'https://fullnode.devnet.sui.io:443',
36
+ testnet: 'https://fullnode.testnet.sui.io:443',
37
+ localnet: 'http://127.0.0.1:9000',
38
+ } as const;
39
+
40
+ /** A pending signing request waiting for user approval via the wallet UI. */
41
+ export interface WalletRequest {
42
+ id: string;
43
+ type: 'sign-transaction' | 'sign-and-execute-transaction' | 'sign-personal-message';
44
+ account: WalletAccount;
45
+ chain: string;
46
+ /** Transaction JSON string for sign/execute, Uint8Array for personal messages. */
47
+ data: string | Uint8Array;
48
+ resolve: (result: SigningResult) => void;
49
+ reject: (error: Error) => void;
50
+ }
51
+
52
+ /** Public view of a pending signing request, omitting internal resolve/reject callbacks. */
53
+ export type PendingSigningRequest = Omit<WalletRequest, 'resolve' | 'reject'>;
54
+
55
+ export interface ConnectRequest {
56
+ id: string;
57
+ resolve: (result: { accounts: readonly WalletAccount[] }) => void;
58
+ reject: (error: Error) => void;
59
+ }
60
+
61
+ /** Public view of a pending connect request, omitting internal resolve/reject callbacks. */
62
+ export type PendingConnectRequest = Omit<ConnectRequest, 'resolve' | 'reject'>;
63
+
64
+ /**
65
+ * Auto-approval policy for signing requests.
66
+ * `true` approves all requests; a function is called per-request and returns `true` to approve.
67
+ */
68
+ export type AutoApprovePolicy =
69
+ | boolean
70
+ | ((request: {
71
+ type: WalletRequest['type'];
72
+ account: WalletAccount;
73
+ chain: string;
74
+ data: string | Uint8Array;
75
+ }) => boolean);
76
+
77
+ export interface DevWalletConfig {
78
+ adapters: SignerAdapter[];
79
+ /** Network URLs by name (network → gRPC endpoint). Defaults to devnet, testnet, and localnet. */
80
+ networks?: Record<string, string>;
81
+ /** Display name for the wallet. Defaults to 'Dev Wallet'. */
82
+ name?: string;
83
+ icon?: WalletIcon;
84
+ /** Initially active network. Defaults to the first key in `networks`. */
85
+ activeNetwork?: string;
86
+ /**
87
+ * When set, matching requests are signed immediately without queuing for user approval.
88
+ * `true` approves everything; a function is called per-request for fine-grained control.
89
+ */
90
+ autoApprove?: AutoApprovePolicy;
91
+ /** When true, connect requests resolve immediately with all accounts. */
92
+ autoConnect?: boolean;
93
+ /** Factory to create a client for a given network. Defaults to SuiGrpcClient. */
94
+ clientFactory?: (network: string, url: string) => ClientWithCoreApi;
95
+ /**
96
+ * Persist network URLs to localStorage so custom configurations survive page reloads
97
+ * and are shared with popup windows.
98
+ */
99
+ persistNetworks?: boolean;
100
+ }
101
+
102
+ /**
103
+ * wallet-standard Wallet implementation for development. All signing requests are queued
104
+ * and require explicit approval — either via the UI or programmatically via `approveRequest()`.
105
+ */
106
+ export class DevWallet implements Wallet {
107
+ readonly #adapters: SignerAdapter[];
108
+ #networkUrls: Record<string, string>;
109
+ #clients: Record<string, ClientWithCoreApi>;
110
+ #activeNetwork: string;
111
+ readonly #name: string;
112
+ readonly #icon: WalletIcon;
113
+ readonly #autoApprove: AutoApprovePolicy;
114
+ readonly #autoConnect: boolean;
115
+ readonly #clientFactory: (network: string, url: string) => ClientWithCoreApi;
116
+ readonly #persistNetworks: boolean;
117
+ #accounts: ReadonlyWalletAccount[];
118
+ #events: Emitter<WalletEventsMap>;
119
+ #unsubscribeAdapters: (() => void)[];
120
+ #pendingRequest: WalletRequest | null;
121
+ #requestListeners: Set<(request: PendingSigningRequest | null) => void>;
122
+ #pendingConnect: ConnectRequest | null;
123
+ #connectListeners: Set<(request: PendingConnectRequest | null) => void>;
124
+ #destroyed = false;
125
+
126
+ constructor(config: DevWalletConfig) {
127
+ if (config.adapters.length === 0) {
128
+ console.warn('[dev-wallet] No adapters provided. The wallet will have no accounts.');
129
+ }
130
+ this.#adapters = config.adapters;
131
+ this.#persistNetworks = config.persistNetworks ?? false;
132
+ this.#networkUrls = this.#loadNetworkUrls(config.networks ?? DEFAULT_NETWORK_URLS);
133
+ this.#clients = {};
134
+ this.#activeNetwork = config.activeNetwork ?? Object.keys(this.#networkUrls)[0] ?? '';
135
+ this.#name = config.name ?? DEFAULT_WALLET_NAME;
136
+ this.#icon = config.icon ?? DEFAULT_WALLET_ICON;
137
+ this.#autoApprove = config.autoApprove ?? false;
138
+ this.#autoConnect = config.autoConnect ?? false;
139
+ this.#clientFactory =
140
+ config.clientFactory ?? ((network, url) => new SuiGrpcClient({ baseUrl: url, network }));
141
+ this.#accounts = this.#aggregateAccounts();
142
+ this.#events = mitt();
143
+ this.#unsubscribeAdapters = [];
144
+ this.#pendingRequest = null;
145
+ this.#requestListeners = new Set();
146
+ this.#pendingConnect = null;
147
+ this.#connectListeners = new Set();
148
+
149
+ this.#setupAdapterListeners();
150
+ }
151
+
152
+ get version() {
153
+ return '1.0.0' as const;
154
+ }
155
+
156
+ get name() {
157
+ return this.#name;
158
+ }
159
+
160
+ get icon() {
161
+ return this.#icon;
162
+ }
163
+
164
+ get chains() {
165
+ return SUI_CHAINS;
166
+ }
167
+
168
+ get accounts(): readonly ReadonlyWalletAccount[] {
169
+ return this.#accounts;
170
+ }
171
+
172
+ get features(): StandardConnectFeature &
173
+ StandardEventsFeature &
174
+ StandardDisconnectFeature &
175
+ SuiFeatures {
176
+ return {
177
+ 'standard:connect': {
178
+ version: '1.0.0',
179
+ connect: this.#connect,
180
+ },
181
+ 'standard:events': {
182
+ version: '1.0.0',
183
+ on: this.#on,
184
+ },
185
+ 'standard:disconnect': {
186
+ version: '1.0.0',
187
+ disconnect: this.#disconnect,
188
+ },
189
+ 'sui:signPersonalMessage': {
190
+ version: '1.1.0',
191
+ signPersonalMessage: this.#signPersonalMessage,
192
+ },
193
+ 'sui:signTransaction': {
194
+ version: '2.0.0',
195
+ signTransaction: this.#signTransaction,
196
+ },
197
+ 'sui:signAndExecuteTransaction': {
198
+ version: '2.0.0',
199
+ signAndExecuteTransaction: this.#signAndExecuteTransaction,
200
+ },
201
+ };
202
+ }
203
+
204
+ /** The current pending signing request, or null if none. */
205
+ get pendingRequest(): PendingSigningRequest | null {
206
+ return this.#pendingRequest;
207
+ }
208
+
209
+ get adapters(): readonly SignerAdapter[] {
210
+ return this.#adapters;
211
+ }
212
+
213
+ getAdapterForAccount(address: string): SignerAdapter | undefined {
214
+ return this.#adapters.find((a) => a.getAccount(address) !== undefined);
215
+ }
216
+
217
+ /** Eagerly creates and returns clients for all configured networks. Prefer `getClient()` or `activeClient` for single-network access. */
218
+ get clients(): Record<string, ClientWithCoreApi> {
219
+ for (const name of Object.keys(this.#networkUrls)) {
220
+ this.#ensureClient(name);
221
+ }
222
+ return { ...this.#clients };
223
+ }
224
+
225
+ getClient(network: string): ClientWithCoreApi {
226
+ return this.#ensureClient(network);
227
+ }
228
+
229
+ get networkUrls(): Record<string, string> {
230
+ return { ...this.#networkUrls };
231
+ }
232
+
233
+ get activeNetwork(): string {
234
+ return this.#activeNetwork;
235
+ }
236
+
237
+ get activeClient(): ClientWithCoreApi | null {
238
+ if (!this.#activeNetwork || !this.#networkUrls[this.#activeNetwork]) return null;
239
+ return this.#ensureClient(this.#activeNetwork);
240
+ }
241
+
242
+ get availableNetworks(): string[] {
243
+ return Object.keys(this.#networkUrls);
244
+ }
245
+
246
+ /** Switch the active network. Emits a `change` event. */
247
+ setActiveNetwork(network: string): void {
248
+ if (!this.#networkUrls[network]) {
249
+ throw new Error(
250
+ `No client for network "${network}". Available: ${this.availableNetworks.join(', ')}`,
251
+ );
252
+ }
253
+ this.#activeNetwork = network;
254
+ this.#events.emit('change', { accounts: this.#accounts });
255
+ }
256
+
257
+ addNetwork(name: string, url: string): void {
258
+ let parsed: URL;
259
+ try {
260
+ parsed = new URL(url);
261
+ } catch {
262
+ throw new Error(`Invalid URL: ${url}`);
263
+ }
264
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
265
+ throw new Error(`Invalid URL: must use http or https (got ${parsed.protocol})`);
266
+ }
267
+ this.#networkUrls[name] = url;
268
+ this.#clients[name] = this.#clientFactory(name, url);
269
+ this.#saveNetworkUrls();
270
+ this.#events.emit('change', { accounts: this.#accounts });
271
+ }
272
+
273
+ removeNetwork(name: string): void {
274
+ delete this.#networkUrls[name];
275
+ delete this.#clients[name];
276
+ if (this.#activeNetwork === name) {
277
+ this.#activeNetwork = Object.keys(this.#networkUrls)[0] ?? '';
278
+ }
279
+ this.#saveNetworkUrls();
280
+ this.#events.emit('change', { accounts: this.#accounts });
281
+ }
282
+
283
+ /** Register this wallet with the wallet-standard registry. Returns an unregister function. */
284
+ register(): () => void {
285
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') {
286
+ console.warn(
287
+ '[@mysten-incubation/dev-wallet] DevWallet is intended for development only. Do not use in production.',
288
+ );
289
+ }
290
+ const walletsApi = getWallets();
291
+ return walletsApi.register(this);
292
+ }
293
+
294
+ /** Reject any pending requests, unsubscribe from adapters, destroy them, and clear all listeners. */
295
+ destroy(): void {
296
+ this.#destroyed = true;
297
+ for (const unsub of this.#unsubscribeAdapters) {
298
+ unsub();
299
+ }
300
+ this.#unsubscribeAdapters = [];
301
+ for (const adapter of this.#adapters) {
302
+ adapter.destroy();
303
+ }
304
+ if (this.#pendingRequest) {
305
+ this.#pendingRequest.reject(new Error('Wallet destroyed.'));
306
+ this.#pendingRequest = null;
307
+ }
308
+ if (this.#pendingConnect) {
309
+ this.#pendingConnect.reject(new Error('Wallet destroyed.'));
310
+ this.#pendingConnect = null;
311
+ }
312
+ this.#requestListeners.clear();
313
+ this.#connectListeners.clear();
314
+ this.#events.all.clear();
315
+ }
316
+
317
+ /** Sign the pending request using the account's adapter signer and resolve the promise. */
318
+ async approveRequest(): Promise<void> {
319
+ const request = this.#pendingRequest;
320
+ if (!request) {
321
+ throw new Error('No pending request to approve.');
322
+ }
323
+
324
+ try {
325
+ const result = await this.#executeSigning(
326
+ request.type,
327
+ request.account,
328
+ request.chain,
329
+ request.data,
330
+ );
331
+ this.#pendingRequest = null;
332
+ this.#notifyRequestListeners();
333
+ request.resolve(result);
334
+ } catch (error) {
335
+ this.#pendingRequest = null;
336
+ this.#notifyRequestListeners();
337
+ request.reject(error instanceof Error ? error : new Error(String(error)));
338
+ }
339
+ }
340
+
341
+ rejectRequest(reason?: string): void {
342
+ const request = this.#pendingRequest;
343
+ if (!request) {
344
+ throw new Error('No pending request to reject.');
345
+ }
346
+ this.#pendingRequest = null;
347
+ this.#notifyRequestListeners();
348
+ request.reject(new Error(reason ?? 'Request rejected by user.'));
349
+ }
350
+
351
+ /** Subscribe to pending request changes. Returns an unsubscribe function. */
352
+ onRequestChange(callback: (request: PendingSigningRequest | null) => void): () => void {
353
+ this.#requestListeners.add(callback);
354
+ return () => {
355
+ this.#requestListeners.delete(callback);
356
+ };
357
+ }
358
+
359
+ /** The current pending connect request, or null if none. */
360
+ get pendingConnect(): PendingConnectRequest | null {
361
+ return this.#pendingConnect;
362
+ }
363
+
364
+ /** Expose the given addresses to the dApp. Pass an empty array to expose all accounts. */
365
+ approveConnect(selectedAddresses: string[]): void {
366
+ const request = this.#pendingConnect;
367
+ if (!request) {
368
+ throw new Error('No pending connect request to approve.');
369
+ }
370
+
371
+ const selected =
372
+ selectedAddresses.length > 0
373
+ ? this.#accounts.filter((a) => selectedAddresses.includes(a.address))
374
+ : [...this.#accounts];
375
+
376
+ this.#pendingConnect = null;
377
+ this.#notifyConnectListeners();
378
+ request.resolve({ accounts: selected });
379
+ }
380
+
381
+ rejectConnect(reason?: string): void {
382
+ const request = this.#pendingConnect;
383
+ if (!request) {
384
+ throw new Error('No pending connect request to reject.');
385
+ }
386
+ this.#pendingConnect = null;
387
+ this.#notifyConnectListeners();
388
+ request.reject(new Error(reason ?? 'Connection rejected by user.'));
389
+ }
390
+
391
+ /** Subscribe to pending connect request changes. Returns an unsubscribe function. */
392
+ onConnectChange(callback: (request: PendingConnectRequest | null) => void): () => void {
393
+ this.#connectListeners.add(callback);
394
+ return () => {
395
+ this.#connectListeners.delete(callback);
396
+ };
397
+ }
398
+
399
+ #notifyRequestListeners() {
400
+ const request = this.#pendingRequest;
401
+ for (const listener of this.#requestListeners) {
402
+ listener(request);
403
+ }
404
+ }
405
+
406
+ #notifyConnectListeners() {
407
+ const request = this.#pendingConnect;
408
+ for (const listener of this.#connectListeners) {
409
+ listener(request);
410
+ }
411
+ }
412
+
413
+ #aggregateAccounts(): ReadonlyWalletAccount[] {
414
+ return this.#adapters.flatMap((a) => a.getAccounts().map((acc) => acc.walletAccount));
415
+ }
416
+
417
+ #setupAdapterListeners() {
418
+ for (const adapter of this.#adapters) {
419
+ const unsub = adapter.onAccountsChanged(() => {
420
+ this.#accounts = this.#aggregateAccounts();
421
+ this.#events.emit('change', { accounts: this.#accounts });
422
+ });
423
+ this.#unsubscribeAdapters.push(unsub);
424
+ }
425
+ }
426
+
427
+ #on: StandardEventsOnMethod = (event, listener) => {
428
+ this.#events.on(event, listener);
429
+ return () => this.#events.off(event, listener);
430
+ };
431
+
432
+ #connect: StandardConnectMethod = async () => {
433
+ // Auto-connect: return all accounts immediately
434
+ if (this.#autoConnect) {
435
+ return { accounts: this.accounts };
436
+ }
437
+
438
+ // Queue a connect request for user to select accounts
439
+ if (this.#pendingConnect) {
440
+ return Promise.reject(new Error('A connect request is already pending.'));
441
+ }
442
+
443
+ return new Promise((resolve, reject) => {
444
+ this.#pendingConnect = {
445
+ id: crypto.randomUUID(),
446
+ resolve,
447
+ reject,
448
+ };
449
+ this.#notifyConnectListeners();
450
+ });
451
+ };
452
+
453
+ #disconnect: StandardDisconnectMethod = async () => {
454
+ // Disconnecting ends the dApp session but does not affect wallet state.
455
+ // The wallet retains its accounts and adapter subscriptions.
456
+ };
457
+
458
+ #getClientForChain(chain: string) {
459
+ const network = getNetworkFromChain(chain);
460
+ if (!network) {
461
+ throw new Error(`Invalid chain identifier: ${chain}`);
462
+ }
463
+ return this.#ensureClient(network);
464
+ }
465
+
466
+ #loadNetworkUrls(defaults: Record<string, string>): Record<string, string> {
467
+ if (!this.#persistNetworks || typeof localStorage === 'undefined') {
468
+ return { ...defaults };
469
+ }
470
+ try {
471
+ const raw = localStorage.getItem(NETWORK_STORAGE_KEY);
472
+ if (raw) {
473
+ const parsed = JSON.parse(raw);
474
+ if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) {
475
+ return parsed as Record<string, string>;
476
+ }
477
+ }
478
+ } catch {
479
+ // Corrupted data — fall through to defaults
480
+ }
481
+ return { ...defaults };
482
+ }
483
+
484
+ #saveNetworkUrls(): void {
485
+ if (!this.#persistNetworks || typeof localStorage === 'undefined') return;
486
+ localStorage.setItem(NETWORK_STORAGE_KEY, JSON.stringify(this.#networkUrls));
487
+ }
488
+
489
+ #ensureClient(network: string): ClientWithCoreApi {
490
+ if (!this.#clients[network]) {
491
+ const url = this.#networkUrls[network];
492
+ if (!url) {
493
+ throw new Error(
494
+ `No client for network "${network}". Available: ${this.availableNetworks.join(', ')}`,
495
+ );
496
+ }
497
+ this.#clients[network] = this.#clientFactory(network, url);
498
+ }
499
+ return this.#clients[network];
500
+ }
501
+
502
+ #getSigner(address: string) {
503
+ for (const adapter of this.#adapters) {
504
+ const account = adapter.getAccount(address);
505
+ if (account) return account.signer;
506
+ }
507
+ const allAddresses = this.#adapters.flatMap((a) => a.getAccounts().map((acc) => acc.address));
508
+ throw new Error(
509
+ `No account found for address "${address}". Available addresses: ${allAddresses.join(', ')}`,
510
+ );
511
+ }
512
+
513
+ #shouldAutoApprove(
514
+ type: WalletRequest['type'],
515
+ account: WalletAccount,
516
+ chain: string,
517
+ data: string | Uint8Array,
518
+ ): boolean {
519
+ // Check adapter-level opt-out first — CLI adapters never auto-sign
520
+ const adapter = this.getAdapterForAccount(account.address);
521
+ if (adapter?.allowAutoSign === false) {
522
+ return false;
523
+ }
524
+
525
+ if (this.#autoApprove === true) {
526
+ return true;
527
+ }
528
+ if (typeof this.#autoApprove === 'function') {
529
+ return this.#autoApprove({ type, account, chain, data });
530
+ }
531
+ return false;
532
+ }
533
+
534
+ async #executeSigning(
535
+ type: WalletRequest['type'],
536
+ account: WalletAccount,
537
+ chain: string,
538
+ data: string | Uint8Array,
539
+ ) {
540
+ const signer = this.#getSigner(account.address);
541
+ const client = type !== 'sign-personal-message' ? this.#getClientForChain(chain) : undefined;
542
+ return executeSigning({ type, signer, data, client });
543
+ }
544
+
545
+ #enqueueRequest<T extends WalletRequest['type']>(
546
+ type: T,
547
+ account: WalletAccount,
548
+ chain: string,
549
+ data: string | Uint8Array,
550
+ ): Promise<Extract<SigningResult, { type: T }>> {
551
+ type Result = Extract<SigningResult, { type: T }>;
552
+ if (this.#destroyed) {
553
+ return Promise.reject(new Error('Wallet has been destroyed.'));
554
+ }
555
+ if (this.#shouldAutoApprove(type, account, chain, data)) {
556
+ return this.#executeSigning(type, account, chain, data) as Promise<Result>;
557
+ }
558
+
559
+ // Only one request at a time — subsequent requests are immediately rejected.
560
+ // DApps that batch transactions should serialize their signing calls.
561
+ if (this.#pendingRequest) {
562
+ return Promise.reject(new Error('A signing request is already pending.'));
563
+ }
564
+
565
+ return new Promise<Result>((resolve, reject) => {
566
+ this.#pendingRequest = {
567
+ id: crypto.randomUUID(),
568
+ type,
569
+ account,
570
+ chain,
571
+ data,
572
+ resolve: resolve as (result: SigningResult) => void,
573
+ reject,
574
+ };
575
+ this.#notifyRequestListeners();
576
+ });
577
+ }
578
+
579
+ #signPersonalMessage: SuiSignPersonalMessageMethod = async (input) => {
580
+ return this.#enqueueRequest(
581
+ 'sign-personal-message',
582
+ input.account,
583
+ input.chain ?? 'sui:unknown',
584
+ input.message,
585
+ );
586
+ };
587
+
588
+ #signTransaction: SuiSignTransactionMethod = async (input) => {
589
+ const txJson = await input.transaction.toJSON();
590
+ return this.#enqueueRequest('sign-transaction', input.account, input.chain, txJson);
591
+ };
592
+
593
+ #signAndExecuteTransaction: SuiSignAndExecuteTransactionMethod = async (input) => {
594
+ const txJson = await input.transaction.toJSON();
595
+ return this.#enqueueRequest('sign-and-execute-transaction', input.account, input.chain, txJson);
596
+ };
597
+ }