@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,388 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { Signer } from '@mysten/sui/cryptography';
5
+ import type { PublicKey, SignatureScheme } from '@mysten/sui/cryptography';
6
+ import { Ed25519PublicKey } from '@mysten/sui/keypairs/ed25519';
7
+ import { Secp256k1PublicKey } from '@mysten/sui/keypairs/secp256k1';
8
+ import { Secp256r1PublicKey } from '@mysten/sui/keypairs/secp256r1';
9
+ import { fromBase64, toBase64 } from '@mysten/sui/utils';
10
+ import type { ImportAccountOptions, ManagedAccount } from '../types.js';
11
+ import { BaseSignerAdapter } from './base-adapter.js';
12
+ import { buildManagedAccount } from './build-managed-account.js';
13
+
14
+ /**
15
+ * Wallet features advertised by CLI-backed accounts.
16
+ * Personal message signing is not supported because the `sui keytool sign`
17
+ * command only accepts BCS-serialized TransactionData.
18
+ */
19
+ const CLI_WALLET_FEATURES = ['sui:signTransaction', 'sui:signAndExecuteTransaction'] as const;
20
+
21
+ /** Cache TTL for the CLI server accounts fetch (milliseconds). */
22
+ const ACCOUNTS_CACHE_TTL_MS = 5000;
23
+
24
+ /** Thrown when the server rejects a request with a 401 or 403 status. */
25
+ class AuthError extends Error {
26
+ constructor() {
27
+ super('Unauthorized');
28
+ this.name = 'AuthError';
29
+ }
30
+ }
31
+
32
+ /** Map CLI `keyScheme` strings to SDK {@link SignatureScheme} values. */
33
+ const KEY_SCHEME_MAP: Record<string, SignatureScheme> = {
34
+ ed25519: 'ED25519',
35
+ secp256k1: 'Secp256k1',
36
+ secp256r1: 'Secp256r1',
37
+ };
38
+
39
+ /** Account metadata returned by the server `/api/v1/accounts` endpoint. */
40
+ interface ServerAccountInfo {
41
+ suiAddress: string;
42
+ publicBase64Key: string;
43
+ keyScheme: string;
44
+ alias: string | null;
45
+ }
46
+
47
+ function publicKeyFromSuiBytes(publicBase64Key: string, scheme: SignatureScheme): PublicKey {
48
+ const allBytes = fromBase64(publicBase64Key);
49
+ const rawBytes = allBytes.slice(1);
50
+
51
+ switch (scheme) {
52
+ case 'ED25519':
53
+ return new Ed25519PublicKey(rawBytes);
54
+ case 'Secp256k1':
55
+ return new Secp256k1PublicKey(rawBytes);
56
+ case 'Secp256r1':
57
+ return new Secp256r1PublicKey(rawBytes);
58
+ default:
59
+ throw new Error(`Unsupported key scheme: ${scheme}`);
60
+ }
61
+ }
62
+
63
+ /**
64
+ * A {@link Signer} that delegates transaction signing to a server HTTP endpoint
65
+ * backed by `sui keytool sign`. Private keys never enter JavaScript.
66
+ *
67
+ * `signPersonalMessage()` is not supported — `sui keytool sign` only accepts
68
+ * BCS-serialized `TransactionData`.
69
+ */
70
+ export class CliProxySigner extends Signer {
71
+ #address: string;
72
+ #publicKey: PublicKey;
73
+ #scheme: SignatureScheme;
74
+ #serverOrigin: string;
75
+ #authToken: string | null;
76
+
77
+ constructor(options: {
78
+ address: string;
79
+ publicKey: PublicKey;
80
+ scheme: SignatureScheme;
81
+ serverOrigin: string;
82
+ authToken?: string | null;
83
+ }) {
84
+ super();
85
+ this.#address = options.address;
86
+ this.#publicKey = options.publicKey;
87
+ this.#scheme = options.scheme;
88
+ this.#serverOrigin = options.serverOrigin;
89
+ this.#authToken = options.authToken ?? null;
90
+ }
91
+
92
+ async sign(_bytes: Uint8Array): Promise<Uint8Array<ArrayBuffer>> {
93
+ throw new Error(
94
+ 'CliProxySigner does not support direct digest signing. ' +
95
+ 'Transaction signing is handled via the sui CLI.',
96
+ );
97
+ }
98
+
99
+ getKeyScheme(): SignatureScheme {
100
+ return this.#scheme;
101
+ }
102
+
103
+ getPublicKey(): PublicKey {
104
+ return this.#publicKey;
105
+ }
106
+
107
+ override toSuiAddress(): string {
108
+ return this.#address;
109
+ }
110
+
111
+ override async signTransaction(bytes: Uint8Array) {
112
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
113
+ if (this.#authToken) {
114
+ headers['Authorization'] = `Bearer ${this.#authToken}`;
115
+ }
116
+
117
+ const res = await fetch(`${this.#serverOrigin}/api/v1/sign-transaction`, {
118
+ method: 'POST',
119
+ headers,
120
+ body: JSON.stringify({
121
+ address: this.#address,
122
+ txBytes: toBase64(bytes),
123
+ }),
124
+ });
125
+
126
+ if (!res.ok) {
127
+ const body = await res.json().catch(() => ({}));
128
+ throw new Error(`Transaction signing failed: ${body.error ?? res.statusText}`);
129
+ }
130
+
131
+ const { suiSignature } = await res.json();
132
+ if (typeof suiSignature !== 'string' || suiSignature.length === 0) {
133
+ throw new Error('Transaction signing failed: server returned invalid signature');
134
+ }
135
+ return {
136
+ bytes: toBase64(bytes),
137
+ signature: suiSignature,
138
+ };
139
+ }
140
+
141
+ override async signPersonalMessage(_bytes: Uint8Array): Promise<never> {
142
+ throw new Error(
143
+ 'Personal message signing is not supported in CLI mode. ' +
144
+ 'The sui keytool sign command only supports TransactionData. ' +
145
+ 'Use --adapter=webcrypto or --adapter=memory for personal message signing.',
146
+ );
147
+ }
148
+ }
149
+
150
+ /**
151
+ * {@link SignerAdapter} that delegates signing to a server running the `sui` CLI.
152
+ *
153
+ * Authentication uses a token-in-URL approach: the CLI server prints a URL with
154
+ * a token, which gets stored in `localStorage` for subsequent page loads. Imported
155
+ * account addresses are also persisted so they survive CLI restarts.
156
+ */
157
+ export class RemoteCliAdapter extends BaseSignerAdapter {
158
+ readonly id = 'remote-cli';
159
+ readonly name = 'Remote CLI Signer';
160
+ readonly allowAutoSign = false;
161
+
162
+ protected override getDefaultLabel(): string {
163
+ return `CLI Account ${this.getAccounts().length + 1}`;
164
+ }
165
+
166
+ // Keys are scoped per origin to prevent cross-site leakage on shared localhost setups.
167
+ // localStorage is used rather than sessionStorage because popup wallet flows
168
+ // (DevWalletClient) may open new tabs that need access to the token.
169
+ static storageKey(suffix: string): string {
170
+ const origin = typeof location !== 'undefined' ? location.origin : 'unknown';
171
+ return `dev-wallet:${suffix}:${origin}`;
172
+ }
173
+
174
+ static get STORAGE_KEY(): string {
175
+ return RemoteCliAdapter.storageKey('cli-imported-addresses');
176
+ }
177
+
178
+ static get TOKEN_KEY(): string {
179
+ return RemoteCliAdapter.storageKey('cli-token');
180
+ }
181
+
182
+ #serverOrigin: string;
183
+ #authToken: string | null = null;
184
+ #allServerAccounts: ServerAccountInfo[] = [];
185
+ #lastFetch: { time: number; result: ServerAccountInfo[] } | null = null;
186
+
187
+ constructor(options?: { serverOrigin?: string; token?: string }) {
188
+ super();
189
+ const origin = options?.serverOrigin ?? '';
190
+ if (origin) {
191
+ try {
192
+ new URL(origin);
193
+ } catch {
194
+ throw new Error(`Invalid serverOrigin URL: "${origin}"`);
195
+ }
196
+ }
197
+ this.#serverOrigin = origin;
198
+ if (options?.token) {
199
+ this.#authToken = options.token;
200
+ }
201
+ }
202
+
203
+ get isPaired(): boolean {
204
+ return this.#authToken !== null;
205
+ }
206
+
207
+ async initialize(): Promise<void> {
208
+ if (!this.#authToken && typeof localStorage !== 'undefined') {
209
+ const stored = localStorage.getItem(RemoteCliAdapter.TOKEN_KEY);
210
+ if (stored) {
211
+ this.#authToken = stored;
212
+ }
213
+ }
214
+
215
+ if (this.#authToken) {
216
+ try {
217
+ await this.#fetchServerAccounts();
218
+ await this.#restoreImportedAccounts();
219
+ } catch (error) {
220
+ if (error instanceof AuthError) {
221
+ this.#authToken = null;
222
+ if (typeof localStorage !== 'undefined') {
223
+ localStorage.removeItem(RemoteCliAdapter.TOKEN_KEY);
224
+ }
225
+ } else {
226
+ console.warn('[dev-wallet] CLI adapter initialization failed:', error);
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ async listAvailableAccounts(): Promise<
233
+ Array<{ address: string; scheme: string; alias: string | null }>
234
+ > {
235
+ await this.#fetchServerAccounts();
236
+ const importedAddresses = new Set(this._accounts.map((a) => a.address));
237
+ return this.#allServerAccounts
238
+ .filter((info) => {
239
+ const scheme = KEY_SCHEME_MAP[info.keyScheme];
240
+ return scheme && !importedAddresses.has(info.suiAddress);
241
+ })
242
+ .map((info) => ({
243
+ address: info.suiAddress,
244
+ scheme: info.keyScheme,
245
+ alias: info.alias,
246
+ }));
247
+ }
248
+
249
+ async importAccount(options: ImportAccountOptions): Promise<ManagedAccount> {
250
+ const address = options.address;
251
+ if (!address) {
252
+ throw new Error('Remote CLI adapter requires an address to import');
253
+ }
254
+
255
+ if (this._accounts.some((a) => a.address === address)) {
256
+ throw new Error(`Account ${address} is already imported`);
257
+ }
258
+
259
+ await this.#fetchServerAccounts();
260
+ const info = this.#allServerAccounts.find((a) => a.suiAddress === address);
261
+ if (!info) {
262
+ throw new Error(`Account ${address} not found on server`);
263
+ }
264
+
265
+ const scheme = KEY_SCHEME_MAP[info.keyScheme];
266
+ if (!scheme) {
267
+ throw new Error(`Unsupported key scheme: ${info.keyScheme}`);
268
+ }
269
+
270
+ const account = this.#serverAccountToManaged(info, scheme, options.label);
271
+ this.addAccount(account);
272
+ this.#saveImportedAddresses();
273
+ return account;
274
+ }
275
+
276
+ async removeAccount(address: string): Promise<boolean> {
277
+ const removed = this.removeAccountByAddress(address);
278
+ if (removed) {
279
+ this.#saveImportedAddresses();
280
+ }
281
+ return removed;
282
+ }
283
+
284
+ override destroy(): void {
285
+ this.#allServerAccounts = [];
286
+ this.#authToken = null;
287
+ if (typeof localStorage !== 'undefined') {
288
+ localStorage.removeItem(RemoteCliAdapter.TOKEN_KEY);
289
+ localStorage.removeItem(RemoteCliAdapter.STORAGE_KEY);
290
+ }
291
+ super.destroy();
292
+ }
293
+
294
+ #authHeaders(): Record<string, string> {
295
+ const headers: Record<string, string> = { 'Content-Type': 'application/json' };
296
+ if (this.#authToken) {
297
+ headers['Authorization'] = `Bearer ${this.#authToken}`;
298
+ }
299
+ return headers;
300
+ }
301
+
302
+ async #fetchServerAccounts(forceRefresh = false): Promise<void> {
303
+ if (
304
+ !forceRefresh &&
305
+ this.#lastFetch &&
306
+ Date.now() - this.#lastFetch.time < ACCOUNTS_CACHE_TTL_MS
307
+ ) {
308
+ this.#allServerAccounts = this.#lastFetch.result;
309
+ return;
310
+ }
311
+ const res = await fetch(`${this.#serverOrigin}/api/v1/accounts`, {
312
+ headers: this.#authHeaders(),
313
+ });
314
+ if (res.status === 401 || res.status === 403) {
315
+ throw new AuthError();
316
+ }
317
+ if (!res.ok) {
318
+ throw new Error(`Failed to fetch accounts: ${res.statusText}`);
319
+ }
320
+
321
+ const { accounts } = (await res.json()) as { accounts: ServerAccountInfo[] };
322
+ this.#allServerAccounts = accounts;
323
+ this.#lastFetch = { time: Date.now(), result: accounts };
324
+ }
325
+
326
+ async #restoreImportedAccounts(): Promise<void> {
327
+ const saved = this.#loadImportedAddresses();
328
+ if (saved.length === 0) return;
329
+
330
+ const existingAddresses = new Set(this.getAccounts().map((a) => a.address));
331
+ const restored: ManagedAccount[] = [];
332
+
333
+ for (const address of saved) {
334
+ if (existingAddresses.has(address)) continue;
335
+
336
+ const info = this.#allServerAccounts.find((a) => a.suiAddress === address);
337
+ if (!info) continue; // Account no longer on server — skip
338
+
339
+ const scheme = KEY_SCHEME_MAP[info.keyScheme];
340
+ if (!scheme) continue;
341
+
342
+ restored.push(this.#serverAccountToManaged(info, scheme));
343
+ }
344
+
345
+ if (restored.length > 0) {
346
+ this.setInitialAccounts([...this.getAccounts(), ...restored]);
347
+ }
348
+ }
349
+
350
+ #loadImportedAddresses(): string[] {
351
+ if (typeof localStorage === 'undefined') return [];
352
+ try {
353
+ const raw = localStorage.getItem(RemoteCliAdapter.STORAGE_KEY);
354
+ if (!raw) return [];
355
+ const parsed: unknown = JSON.parse(raw);
356
+ if (!Array.isArray(parsed)) return [];
357
+ return parsed.filter((v): v is string => typeof v === 'string');
358
+ } catch {
359
+ return [];
360
+ }
361
+ }
362
+
363
+ #saveImportedAddresses(): void {
364
+ if (typeof localStorage === 'undefined') return;
365
+ const addresses = this._accounts.map((a) => a.address);
366
+ localStorage.setItem(RemoteCliAdapter.STORAGE_KEY, JSON.stringify(addresses));
367
+ }
368
+
369
+ #serverAccountToManaged(
370
+ info: ServerAccountInfo,
371
+ scheme: SignatureScheme,
372
+ label?: string,
373
+ ): ManagedAccount {
374
+ const publicKey = publicKeyFromSuiBytes(info.publicBase64Key, scheme);
375
+
376
+ const signer = new CliProxySigner({
377
+ address: info.suiAddress,
378
+ publicKey,
379
+ scheme,
380
+ serverOrigin: this.#serverOrigin,
381
+ authToken: this.#authToken,
382
+ });
383
+
384
+ const accountLabel = label ?? info.alias ?? this.getDefaultLabel();
385
+
386
+ return buildManagedAccount(signer, info.suiAddress, accountLabel, CLI_WALLET_FEATURES);
387
+ }
388
+ }
@@ -0,0 +1,96 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import type { ExportedWebCryptoKeypair } from '@mysten/signers/webcrypto';
5
+ import { WebCryptoSigner } from '@mysten/signers/webcrypto';
6
+
7
+ import type { CreateAccountOptions, ManagedAccount } from '../types.js';
8
+ import { BaseSignerAdapter } from './base-adapter.js';
9
+ import { buildManagedAccount } from './build-managed-account.js';
10
+ import { IDBStore } from './idb-store.js';
11
+
12
+ interface StoredAccountMeta {
13
+ address: string;
14
+ label: string;
15
+ }
16
+
17
+ const DEFAULT_DB_NAME = 'dev-wallet-webcrypto';
18
+ const DEFAULT_STORE_NAME = 'accounts';
19
+ const META_KEY = '__account_meta__';
20
+
21
+ export class WebCryptoSignerAdapter extends BaseSignerAdapter {
22
+ readonly id = 'webcrypto';
23
+ readonly name = 'WebCrypto Signer';
24
+
25
+ #store: IDBStore;
26
+
27
+ constructor(options?: { dbName?: string; storeName?: string }) {
28
+ super();
29
+ this.#store = new IDBStore(
30
+ options?.dbName ?? DEFAULT_DB_NAME,
31
+ options?.storeName ?? DEFAULT_STORE_NAME,
32
+ );
33
+ }
34
+
35
+ async initialize(): Promise<void> {
36
+ const allEntries = await this.#store.entries<
37
+ string,
38
+ ExportedWebCryptoKeypair | StoredAccountMeta[]
39
+ >();
40
+ const meta: StoredAccountMeta[] = (await this.#store.get<StoredAccountMeta[]>(META_KEY)) ?? [];
41
+
42
+ const accounts = [];
43
+
44
+ for (const [key, value] of allEntries) {
45
+ if (key === META_KEY) continue;
46
+
47
+ const address = key as string;
48
+ try {
49
+ const exported = value as ExportedWebCryptoKeypair;
50
+ const signer = WebCryptoSigner.import(exported);
51
+ const accountMeta = meta.find((m) => m.address === address);
52
+ accounts.push(buildManagedAccount(signer, address, accountMeta?.label ?? 'Account'));
53
+ } catch (error) {
54
+ console.warn(`[dev-wallet] Failed to load account ${address}, skipping:`, error);
55
+ }
56
+ }
57
+
58
+ this.setInitialAccounts(accounts);
59
+ }
60
+
61
+ async createAccount(options?: CreateAccountOptions): Promise<ManagedAccount> {
62
+ const signer = await WebCryptoSigner.generate();
63
+ const address = signer.getPublicKey().toSuiAddress();
64
+ const label = options?.label ?? this.getDefaultLabel();
65
+
66
+ const managedAccount = buildManagedAccount(signer, address, label);
67
+
68
+ await this.#store.set(address, signer.export());
69
+ this.addAccount(managedAccount);
70
+ await this.#saveMeta();
71
+
72
+ return managedAccount;
73
+ }
74
+
75
+ async renameAccount(address: string, label: string): Promise<boolean> {
76
+ if (!this._performRename(address, label)) return false;
77
+ await this.#saveMeta();
78
+ return true;
79
+ }
80
+
81
+ async removeAccount(address: string): Promise<boolean> {
82
+ if (!this.removeAccountByAddress(address)) return false;
83
+
84
+ await this.#store.del(address);
85
+ await this.#saveMeta();
86
+ return true;
87
+ }
88
+
89
+ async #saveMeta(): Promise<void> {
90
+ const meta: StoredAccountMeta[] = this.getAccounts().map((a) => ({
91
+ address: a.address,
92
+ label: a.label,
93
+ }));
94
+ await this.#store.set(META_KEY, meta);
95
+ }
96
+ }
@@ -0,0 +1,77 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ /**
5
+ * Bookmarklet entry point — bundled into an IIFE and served at /bookmarklet.js
6
+ * by the dev-wallet server (local CLI or hosted).
7
+ *
8
+ * When injected into a page via bookmarklet, this script registers a
9
+ * DevWalletClient with the wallet-standard registry, making the dev wallet
10
+ * appear in dApp wallet pickers.
11
+ *
12
+ * IMPORTANT: This uses the wallet-standard window event protocol
13
+ * (`wallet-standard:register-wallet`) instead of the app-side
14
+ * `getWallets().register()` API. This is necessary because IIFE bundles
15
+ * get their own module-level singleton of `@wallet-standard/app`, which
16
+ * is separate from the host page's singleton. The window event protocol
17
+ * bridges this gap — the host page's `getWallets()` listener picks up
18
+ * the event and adds the wallet to its own registry.
19
+ */
20
+
21
+ import type { Wallet } from '@mysten/wallet-standard';
22
+ import { DevWalletClient } from '../client/dev-wallet-client.js';
23
+
24
+ /**
25
+ * Register a wallet using the wallet-standard window event protocol.
26
+ * This dispatches `wallet-standard:register-wallet` which works across
27
+ * bundle boundaries (unlike `getWallets().register()` which is per-module).
28
+ */
29
+ function registerWalletViaEvent(wallet: Wallet) {
30
+ const callback = ({ register }: { register: (...wallets: Wallet[]) => void }) => register(wallet);
31
+
32
+ try {
33
+ window.dispatchEvent(
34
+ new CustomEvent('wallet-standard:register-wallet', {
35
+ bubbles: false,
36
+ cancelable: false,
37
+ composed: false,
38
+ detail: callback,
39
+ }),
40
+ );
41
+ } catch (error) {
42
+ console.error('wallet-standard:register-wallet event could not be dispatched\n', error);
43
+ }
44
+
45
+ try {
46
+ window.addEventListener('wallet-standard:app-ready', ({ detail: api }: any) => callback(api));
47
+ } catch (error) {
48
+ console.error('wallet-standard:app-ready event listener could not be added\n', error);
49
+ }
50
+ }
51
+
52
+ (function () {
53
+ if ((window as unknown as Record<string, unknown>).__DEV_WALLET_INJECTED__) {
54
+ console.log('[Dev Wallet] Already injected, skipping.');
55
+ return;
56
+ }
57
+ (window as unknown as Record<string, unknown>).__DEV_WALLET_INJECTED__ = true;
58
+
59
+ // Derive the wallet server origin from this script's URL.
60
+ // When loaded via <script src="https://wallet.example.com/bookmarklet.js">,
61
+ // document.currentScript.src gives us the full URL — works on any host.
62
+ const src =
63
+ document.currentScript instanceof HTMLScriptElement ? document.currentScript.src : null;
64
+
65
+ if (!src) {
66
+ console.error('[Dev Wallet] Could not determine wallet origin from script src.');
67
+ return;
68
+ }
69
+
70
+ const origin = new URL(src).origin;
71
+
72
+ // Create the wallet instance but register via the window event protocol
73
+ // so the host page's wallet-standard registry picks it up.
74
+ const wallet = new DevWalletClient({ origin });
75
+ registerWalletViaEvent(wallet);
76
+ console.log(`[Dev Wallet] Registered (origin: ${origin})`);
77
+ })();
@@ -0,0 +1,48 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Dev Wallet</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+ body {
14
+ font-family:
15
+ ui-sans-serif,
16
+ system-ui,
17
+ -apple-system,
18
+ BlinkMacSystemFont,
19
+ 'Segoe UI',
20
+ Roboto,
21
+ 'Helvetica Neue',
22
+ Arial,
23
+ sans-serif;
24
+ background: oklch(0.175 0.028 283);
25
+ color: oklch(0.91 0 0);
26
+ min-height: 100vh;
27
+ }
28
+ #app {
29
+ display: flex;
30
+ flex-direction: column;
31
+ align-items: center;
32
+ justify-content: center;
33
+ min-height: 100vh;
34
+ padding: 20px;
35
+ }
36
+ .loading {
37
+ color: oklch(0.62 0.04 280);
38
+ font-size: 14px;
39
+ }
40
+ </style>
41
+ </head>
42
+ <body>
43
+ <div id="app">
44
+ <p class="loading">Loading wallet...</p>
45
+ </div>
46
+ <script type="module" src="./main.ts"></script>
47
+ </body>
48
+ </html>