@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
package/src/app/main.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  // Copyright (c) Mysten Labs, Inc.
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
 
4
+ import { getFaucetHost } from '@mysten/sui/faucet';
4
5
  import { fromBase64, toBase64 } from '@mysten/sui/utils';
5
6
 
6
7
  import { RemoteCliAdapter } from '../adapters/remote-cli-adapter.js';
7
8
  import type { SignerAdapter } from '../types.js';
9
+ import { ConnectedAppsStore } from '../client/connected-apps.js';
8
10
  import { parseWalletRequest } from '../client/request-handler.js';
9
11
  import { getNetworkFromChain } from '../wallet/constants.js';
10
12
  import { DevWallet } from '../wallet/dev-wallet.js';
@@ -97,7 +99,8 @@ async function createWallet(): Promise<DevWallet> {
97
99
  return new DevWallet({
98
100
  adapters,
99
101
  activeNetwork: 'devnet',
100
- persistNetworks: true,
102
+ persistState: true,
103
+ faucets: { devnet: getFaucetHost('devnet'), localnet: getFaucetHost('localnet') },
101
104
  });
102
105
  }
103
106
 
@@ -112,6 +115,7 @@ async function handlePopupRequest(hash: string) {
112
115
  const request = parseWalletRequest({
113
116
  adapters: [...wallet.adapters],
114
117
  jwtSecretKey,
118
+ connectedApps: new ConnectedAppsStore(),
115
119
  getClient: (network) => {
116
120
  try {
117
121
  return wallet.getClient(network);
@@ -201,7 +205,7 @@ async function showStandaloneUI() {
201
205
 
202
206
  const el = document.createElement('dev-wallet-standalone') as DevWalletStandalone;
203
207
  el.wallet = wallet;
204
- el.bookmarkletOrigin = window.location.origin;
208
+ el.connectedApps = new ConnectedAppsStore();
205
209
  app.appendChild(el);
206
210
  } catch (error) {
207
211
  showErrorMessage(app, 'Failed to initialize wallet', error);
@@ -0,0 +1,79 @@
1
+ // Copyright (c) Mysten Labs, Inc.
2
+ // SPDX-License-Identifier: Apache-2.0
3
+
4
+ import { readPersisted } from '../wallet/persisted.js';
5
+
6
+ /** localStorage key for the connected-apps list. Bump the suffix on shape changes. */
7
+ export const CONNECTED_APPS_STORAGE_KEY = 'dev-wallet:connected-apps:v1';
8
+
9
+ /** A dApp the user approved a connection for in the standalone wallet. */
10
+ export interface ConnectedApp {
11
+ /** The dApp's origin — the identity sessions are scoped to. */
12
+ origin: string;
13
+ /** Display name the dApp sent (its page title). */
14
+ name: string;
15
+ /** Addresses shared with the dApp. */
16
+ accounts: string[];
17
+ /** Milliseconds since epoch of the latest approval. */
18
+ connectedAt: number;
19
+ }
20
+
21
+ function isConnectedApp(value: unknown): value is ConnectedApp {
22
+ if (typeof value !== 'object' || value === null) return false;
23
+ const app = value as Record<string, unknown>;
24
+ return (
25
+ typeof app.origin === 'string' &&
26
+ typeof app.name === 'string' &&
27
+ Array.isArray(app.accounts) &&
28
+ app.accounts.every((a) => typeof a === 'string') &&
29
+ typeof app.connectedAt === 'number'
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Connected dApps for a standalone wallet, stored in the wallet origin's
35
+ * localStorage so the popup (which records connections) and the wallet page
36
+ * (which lists them) share it. Removing an app makes the popup reject that
37
+ * origin's signing requests until it reconnects.
38
+ */
39
+ export class ConnectedAppsStore {
40
+ readonly #storage: Storage;
41
+
42
+ constructor(storage: Storage = localStorage) {
43
+ this.#storage = storage;
44
+ }
45
+
46
+ /** Connected apps, most recently connected first. */
47
+ list(): ConnectedApp[] {
48
+ const apps = readPersisted(this.#storage, CONNECTED_APPS_STORAGE_KEY, (value) =>
49
+ Array.isArray(value) && value.every(isConnectedApp) ? value : null,
50
+ );
51
+ return (apps ?? []).sort((a, b) => b.connectedAt - a.connectedAt);
52
+ }
53
+
54
+ has(origin: string): boolean {
55
+ return this.list().some((app) => app.origin === origin);
56
+ }
57
+
58
+ /** Add an app, or replace the entry for its origin. */
59
+ record(app: ConnectedApp): void {
60
+ this.#write([app, ...this.list().filter((a) => a.origin !== app.origin)]);
61
+ }
62
+
63
+ remove(origin: string): void {
64
+ this.#write(this.list().filter((a) => a.origin !== origin));
65
+ }
66
+
67
+ /** Call `callback` when another window (e.g. the signing popup) changes the list. */
68
+ subscribe(callback: () => void): () => void {
69
+ const onStorage = (e: StorageEvent) => {
70
+ if (e.key === CONNECTED_APPS_STORAGE_KEY || e.key === null) callback();
71
+ };
72
+ window.addEventListener('storage', onStorage);
73
+ return () => window.removeEventListener('storage', onStorage);
74
+ }
75
+
76
+ #write(apps: ConnectedApp[]): void {
77
+ this.#storage.setItem(CONNECTED_APPS_STORAGE_KEY, JSON.stringify(apps));
78
+ }
79
+ }
@@ -223,7 +223,8 @@ export class DevWalletClient implements Wallet {
223
223
 
224
224
  #createChannel(): DappPostMessageChannel {
225
225
  return new DappPostMessageChannel({
226
- appName: this.#name,
226
+ // Identifies the requesting dApp in the wallet's approval popup.
227
+ appName: globalThis.document?.title || globalThis.location?.host || this.#name,
227
228
  hostOrigin: this.#origin,
228
229
  });
229
230
  }
@@ -5,3 +5,5 @@ export { DevWalletClient, devWalletClientInitializer } from './dev-wallet-client
5
5
  export type { DevWalletClientOptions } from './dev-wallet-client.js';
6
6
  export { parseWalletRequest } from './request-handler.js';
7
7
  export type { PendingWalletRequest, HandleRequestOptions } from './request-handler.js';
8
+ export { ConnectedAppsStore, CONNECTED_APPS_STORAGE_KEY } from './connected-apps.js';
9
+ export type { ConnectedApp } from './connected-apps.js';
@@ -6,6 +6,7 @@ import { fromBase64, toBase64 } from '@mysten/sui/utils';
6
6
  import { createJwtSession, WalletPostMessageChannel } from '@mysten/window-wallet-core';
7
7
 
8
8
  import type { SignerAdapter } from '../types.js';
9
+ import type { ConnectedAppsStore } from './connected-apps.js';
9
10
  import { getNetworkFromChain } from '../wallet/constants.js';
10
11
  import { executeSigning } from '../wallet/signing.js';
11
12
 
@@ -41,6 +42,12 @@ export interface HandleRequestOptions {
41
42
  getClient?: (network: string) => ClientWithCoreApi | undefined;
42
43
  /** URL hash containing the encoded request. Defaults to window.location.hash. */
43
44
  hash?: string;
45
+ /**
46
+ * Tracks connected dApps. When set, approved connects are recorded here and
47
+ * signing requests from an origin that isn't in the store are rejected, so
48
+ * removing an app disconnects it.
49
+ */
50
+ connectedApps?: ConnectedAppsStore;
44
51
  }
45
52
 
46
53
  export function parseWalletRequest(options: HandleRequestOptions): PendingWalletRequest {
@@ -53,6 +60,7 @@ export function parseWalletRequest(options: HandleRequestOptions): PendingWallet
53
60
  const channel = WalletPostMessageChannel.fromUrlHash(hash);
54
61
  const requestData = channel.getRequestData();
55
62
  const payload = requestData.payload;
63
+ const appOrigin = new URL(requestData.appUrl).origin;
56
64
 
57
65
  if (payload.type === 'connect') {
58
66
  return {
@@ -78,10 +86,17 @@ export function parseWalletRequest(options: HandleRequestOptions): PendingWallet
78
86
  secretKey: options.jwtSecretKey,
79
87
  expirationTime: '7d',
80
88
  issuer: 'dev-wallet',
81
- audience: new URL(requestData.appUrl).origin,
89
+ audience: appOrigin,
82
90
  },
83
91
  );
84
92
 
93
+ options.connectedApps?.record({
94
+ origin: appOrigin,
95
+ name: requestData.appName,
96
+ accounts: accounts.map((a) => a.address),
97
+ connectedAt: Date.now(),
98
+ });
99
+
85
100
  channel.sendMessage({
86
101
  type: 'resolve',
87
102
  data: { type: 'connect', session },
@@ -116,6 +131,14 @@ export function parseWalletRequest(options: HandleRequestOptions): PendingWallet
116
131
  return;
117
132
  }
118
133
 
134
+ if (options.connectedApps && !options.connectedApps.has(appOrigin)) {
135
+ channel.sendMessage({
136
+ type: 'reject',
137
+ reason: `${appOrigin} was disconnected from the wallet. Reconnect to continue.`,
138
+ });
139
+ return;
140
+ }
141
+
119
142
  let account;
120
143
  for (const adapter of options.adapters) {
121
144
  account = adapter.getAccount(payload.address);
@@ -52,6 +52,10 @@ export interface DevstackNetworkInfo {
52
52
  export interface RegisterDevstackDevWalletConfig {
53
53
  /** Wallet-app origin (the dev-wallet connection's `walletUrl`). */
54
54
  readonly serverOrigin: string;
55
+ /** Browser page origins accepted by the devstack wallet server. When the
56
+ * current page is not in this list, the HTTP adapter will be CORS-blocked;
57
+ * this is non-secret metadata used only for a local diagnostic. */
58
+ readonly allowedOrigins?: ReadonlyArray<string>;
55
59
  /** Bearer pairing token (read by the Vite dev server from the wallet's
56
60
  * `0o600` side-channel token file), or null. */
57
61
  readonly token?: string | null;
@@ -97,6 +101,56 @@ declare global {
97
101
  var __devstackDevWalletPromise__: Promise<RegisterDevstackDevWalletResult> | undefined;
98
102
  }
99
103
 
104
+ const currentPageOrigin = (): string | null => {
105
+ const location = (globalThis as { location?: { origin?: unknown } }).location;
106
+ return typeof location?.origin === 'string' && location.origin.length > 0
107
+ ? location.origin
108
+ : null;
109
+ };
110
+
111
+ const originMatchesAllowed = (origin: string, allowed: string): boolean => {
112
+ if (!allowed.includes('*')) return origin === allowed;
113
+ let originUrl: URL;
114
+ let patternUrl: URL;
115
+ try {
116
+ originUrl = new URL(origin);
117
+ patternUrl = new URL(allowed.replace('*', 'devstack-origin-probe'));
118
+ } catch {
119
+ return false;
120
+ }
121
+ if (originUrl.origin !== origin) return false;
122
+ if (originUrl.protocol !== patternUrl.protocol || originUrl.port !== patternUrl.port) {
123
+ return false;
124
+ }
125
+ const originLabels = originUrl.hostname.toLowerCase().split('.');
126
+ const patternLabels = patternUrl.hostname.toLowerCase().split('.');
127
+ return (
128
+ originLabels.length === patternLabels.length &&
129
+ patternLabels[0] === 'devstack-origin-probe' &&
130
+ originLabels[0]!.length > 0 &&
131
+ originLabels.slice(1).join('.') === patternLabels.slice(1).join('.')
132
+ );
133
+ };
134
+
135
+ const assertPageOriginAllowed = (allowedOrigins: ReadonlyArray<string> | undefined): void => {
136
+ if (allowedOrigins === undefined || allowedOrigins.length === 0) return;
137
+ const origin = currentPageOrigin();
138
+ if (origin === null || origin === 'null') return;
139
+ if (allowedOrigins.some((allowed) => originMatchesAllowed(origin, allowed))) return;
140
+
141
+ const recommended = allowedOrigins.find((allowed) => !allowed.includes('*')) ?? allowedOrigins[0];
142
+ const allowedList = allowedOrigins.join(', ');
143
+ const guidance =
144
+ recommended === undefined
145
+ ? ''
146
+ : ` Open ${recommended}, or add wallet({ allowedOrigins: ['${origin}'] }) if this origin is intentional.`;
147
+ const message =
148
+ `[devstack] dev wallet is not available from page origin ${origin}. ` +
149
+ `Allowed origins: ${allowedList}.${guidance}`;
150
+ console.error(message);
151
+ throw new Error(`devstack dev wallet page origin ${origin} is not allowlisted`);
152
+ };
153
+
100
154
  /**
101
155
  * Construct + register the devstack dev wallet on the current page and wire
102
156
  * the Playwright `connectAs` slot. Idempotent: a second call is a no-op and
@@ -138,6 +192,7 @@ async function registerDevstackDevWalletImpl(
138
192
  ): Promise<RegisterDevstackDevWalletResult> {
139
193
  const { mountUI = true, autoApprove = false } = config;
140
194
  globalThis.__DEV_WALLET_INJECTED__ = true;
195
+ assertPageOriginAllowed(config.allowedOrigins);
141
196
 
142
197
  // Resolve the FULL network set up front so it can be advertised both on
143
198
  // the wallet (its `chains`) and on every devstack account (per-account
@@ -117,7 +117,6 @@ export class DevWalletBalances extends LitElement {
117
117
  margin-bottom: 0;
118
118
  }
119
119
 
120
- .manage-btn,
121
120
  .action-btn {
122
121
  height: 30px;
123
122
  padding: 0 10px;
@@ -129,18 +128,12 @@ export class DevWalletBalances extends LitElement {
129
128
  font-weight: var(--dev-wallet-font-weight-medium);
130
129
  }
131
130
 
132
- .manage-btn:hover,
133
131
  .action-btn:hover:not(:disabled) {
134
132
  background: var(--dev-wallet-bg-hover);
135
133
  border-color: var(--dev-wallet-border-strong);
136
134
  color: var(--dev-wallet-foreground);
137
135
  }
138
136
 
139
- .manage-btn:disabled {
140
- opacity: 0.48;
141
- cursor: not-allowed;
142
- }
143
-
144
137
  .balance-hero {
145
138
  display: flex;
146
139
  align-items: center;
@@ -210,6 +203,10 @@ export class DevWalletBalances extends LitElement {
210
203
  color: var(--dev-wallet-foreground);
211
204
  }
212
205
 
206
+ .empty-copy a {
207
+ color: var(--dev-wallet-foreground);
208
+ }
209
+
213
210
  .empty-copy {
214
211
  margin-top: 6px;
215
212
  font-size: 12px;
@@ -271,14 +268,25 @@ export class DevWalletBalances extends LitElement {
271
268
  }
272
269
 
273
270
  /** Request SUI from the active network's faucet for the active address,
274
- * then refresh balances. No-op when no `faucetHost` is configured (live
275
- * mainnet / fork stacks have none). */
271
+ * wait for the faucet's transfers to land, then refresh balances. No-op
272
+ * when no `faucetHost` is configured (live mainnet / fork stacks have none). */
276
273
  async #requestFromFaucet() {
277
274
  if (!this.faucetHost || !this.address || this._funding) return;
278
275
  this._funding = true;
279
276
  this._fundError = null;
280
277
  try {
281
- await requestSuiFromFaucetV2({ host: this.faucetHost, recipient: this.address });
278
+ const result = await requestSuiFromFaucetV2({
279
+ host: this.faucetHost,
280
+ recipient: this.address,
281
+ });
282
+ const client = this.client;
283
+ if (client) {
284
+ const digests = new Set(result.coins_sent?.map((c) => c.transferTxDigest) ?? []);
285
+ // Best effort: the funds were sent even if waiting times out.
286
+ await Promise.allSettled(
287
+ [...digests].map((digest) => client.core.waitForTransaction({ digest, timeout: 30_000 })),
288
+ );
289
+ }
282
290
  this.refresh();
283
291
  } catch (error) {
284
292
  this._fundError =
@@ -346,7 +354,6 @@ export class DevWalletBalances extends LitElement {
346
354
  : nothing}
347
355
  <div class="coins-header">
348
356
  <h3 class="section-header">Coins · ${this._balances.length}</h3>
349
- <button class="manage-btn" disabled>Manage</button>
350
357
  </div>
351
358
  ${this._loading
352
359
  ? html`<div class="loading" part="loading" aria-live="polite">Loading...</div>`
@@ -361,8 +368,13 @@ export class DevWalletBalances extends LitElement {
361
368
  ${this.faucetHost
362
369
  ? html`Use the Faucet button above to fund ${formatAddress(this.address)} on
363
370
  ${networkLabel}, then refresh.`
364
- : html`Fund ${formatAddress(this.address)} from your local faucet or devstack
365
- seed, then refresh balances here.`}
371
+ : this.network === 'testnet'
372
+ ? html`Get testnet SUI for ${formatAddress(this.address)} at
373
+ <a href="https://faucet.sui.io" target="_blank" rel="noopener noreferrer"
374
+ >faucet.sui.io</a
375
+ >, then refresh.`
376
+ : html`Fund ${formatAddress(this.address)} from your local faucet or devstack
377
+ seed, then refresh balances here.`}
366
378
  </div>
367
379
  </div>`
368
380
  : html`