@mysten-incubation/dev-wallet 0.5.1 → 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 (68) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/README.md +7 -4
  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/standalone/assets/{in-memory-adapter-Ban2kRYS.js → in-memory-adapter-uuWG4K4k.js} +1 -1
  16. package/dist/standalone/assets/main-CKYv0eR6.js +7 -0
  17. package/dist/standalone/assets/{webcrypto-adapter-Ddqn-RAu.js → webcrypto-adapter-5ShaPc_M.js} +1 -1
  18. package/dist/standalone/bookmarklet.js +1 -1
  19. package/dist/standalone/index.html +1 -1
  20. package/dist/ui/dev-wallet-balances.d.mts.map +1 -1
  21. package/dist/ui/dev-wallet-balances.mjs +20 -13
  22. package/dist/ui/dev-wallet-balances.mjs.map +1 -1
  23. package/dist/ui/dev-wallet-connect-guide.d.mts +29 -0
  24. package/dist/ui/dev-wallet-connect-guide.d.mts.map +1 -0
  25. package/dist/ui/dev-wallet-connect-guide.mjs +386 -0
  26. package/dist/ui/dev-wallet-connect-guide.mjs.map +1 -0
  27. package/dist/ui/dev-wallet-connected-apps.d.mts +27 -0
  28. package/dist/ui/dev-wallet-connected-apps.d.mts.map +1 -0
  29. package/dist/ui/dev-wallet-connected-apps.mjs +164 -0
  30. package/dist/ui/dev-wallet-connected-apps.mjs.map +1 -0
  31. package/dist/ui/dev-wallet-panel.d.mts.map +1 -1
  32. package/dist/ui/dev-wallet-panel.mjs +0 -9
  33. package/dist/ui/dev-wallet-panel.mjs.map +1 -1
  34. package/dist/ui/dev-wallet-settings.d.mts +8 -2
  35. package/dist/ui/dev-wallet-settings.d.mts.map +1 -1
  36. package/dist/ui/dev-wallet-settings.mjs +86 -171
  37. package/dist/ui/dev-wallet-settings.mjs.map +1 -1
  38. package/dist/ui/dev-wallet-standalone.d.mts +7 -2
  39. package/dist/ui/dev-wallet-standalone.d.mts.map +1 -1
  40. package/dist/ui/dev-wallet-standalone.mjs +68 -222
  41. package/dist/ui/dev-wallet-standalone.mjs.map +1 -1
  42. package/dist/ui/index.d.mts +3 -1
  43. package/dist/ui/index.mjs +3 -1
  44. package/dist/ui/wallet-controller.mjs +15 -3
  45. package/dist/ui/wallet-controller.mjs.map +1 -1
  46. package/dist/wallet/dev-wallet.d.mts +15 -3
  47. package/dist/wallet/dev-wallet.d.mts.map +1 -1
  48. package/dist/wallet/dev-wallet.mjs +94 -30
  49. package/dist/wallet/dev-wallet.mjs.map +1 -1
  50. package/dist/wallet/persisted.mjs +20 -0
  51. package/dist/wallet/persisted.mjs.map +1 -0
  52. package/package.json +6 -6
  53. package/src/app/main.ts +6 -2
  54. package/src/client/connected-apps.ts +79 -0
  55. package/src/client/dev-wallet-client.ts +2 -1
  56. package/src/client/index.ts +2 -0
  57. package/src/client/request-handler.ts +24 -1
  58. package/src/ui/dev-wallet-balances.ts +25 -13
  59. package/src/ui/dev-wallet-connect-guide.ts +428 -0
  60. package/src/ui/dev-wallet-connected-apps.ts +191 -0
  61. package/src/ui/dev-wallet-panel.ts +0 -9
  62. package/src/ui/dev-wallet-settings.ts +101 -179
  63. package/src/ui/dev-wallet-standalone.ts +80 -231
  64. package/src/ui/index.ts +2 -0
  65. package/src/ui/wallet-controller.ts +23 -3
  66. package/src/wallet/dev-wallet.ts +130 -39
  67. package/src/wallet/persisted.ts +24 -0
  68. 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);
@@ -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`