@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.
- package/README.md +250 -0
- package/dist/_virtual/_@oxc-project_runtime@0.107.0/helpers/decorate.mjs +10 -0
- package/dist/adapters/base-adapter.d.mts +33 -0
- package/dist/adapters/base-adapter.d.mts.map +1 -0
- package/dist/adapters/base-adapter.mjs +77 -0
- package/dist/adapters/base-adapter.mjs.map +1 -0
- package/dist/adapters/browser.d.mts +8 -0
- package/dist/adapters/browser.mjs +8 -0
- package/dist/adapters/build-managed-account.d.mts +12 -0
- package/dist/adapters/build-managed-account.d.mts.map +1 -0
- package/dist/adapters/build-managed-account.mjs +30 -0
- package/dist/adapters/build-managed-account.mjs.map +1 -0
- package/dist/adapters/idb-store.mjs +73 -0
- package/dist/adapters/idb-store.mjs.map +1 -0
- package/dist/adapters/in-memory-adapter.d.mts +16 -0
- package/dist/adapters/in-memory-adapter.d.mts.map +1 -0
- package/dist/adapters/in-memory-adapter.mjs +44 -0
- package/dist/adapters/in-memory-adapter.mjs.map +1 -0
- package/dist/adapters/passkey-adapter.d.mts +31 -0
- package/dist/adapters/passkey-adapter.d.mts.map +1 -0
- package/dist/adapters/passkey-adapter.mjs +87 -0
- package/dist/adapters/passkey-adapter.mjs.map +1 -0
- package/dist/adapters/remote-cli-adapter.d.mts +65 -0
- package/dist/adapters/remote-cli-adapter.d.mts.map +1 -0
- package/dist/adapters/remote-cli-adapter.mjs +299 -0
- package/dist/adapters/remote-cli-adapter.mjs.map +1 -0
- package/dist/adapters/webcrypto-adapter.d.mts +20 -0
- package/dist/adapters/webcrypto-adapter.d.mts.map +1 -0
- package/dist/adapters/webcrypto-adapter.mjs +68 -0
- package/dist/adapters/webcrypto-adapter.mjs.map +1 -0
- package/dist/bin/cli.d.mts +1 -0
- package/dist/bin/cli.mjs +183 -0
- package/dist/bin/cli.mjs.map +1 -0
- package/dist/client/dev-wallet-client.d.mts +57 -0
- package/dist/client/dev-wallet-client.d.mts.map +1 -0
- package/dist/client/dev-wallet-client.mjs +223 -0
- package/dist/client/dev-wallet-client.mjs.map +1 -0
- package/dist/client/index.d.mts +3 -0
- package/dist/client/index.mjs +4 -0
- package/dist/client/request-handler.d.mts +42 -0
- package/dist/client/request-handler.d.mts.map +1 -0
- package/dist/client/request-handler.mjs +115 -0
- package/dist/client/request-handler.mjs.map +1 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +4 -0
- package/dist/react/components.d.mts +32 -0
- package/dist/react/components.d.mts.map +1 -0
- package/dist/react/components.mjs +53 -0
- package/dist/react/components.mjs.map +1 -0
- package/dist/react/context.d.mts +22 -0
- package/dist/react/context.d.mts.map +1 -0
- package/dist/react/context.mjs +25 -0
- package/dist/react/context.mjs.map +1 -0
- package/dist/react/index.d.mts +4 -0
- package/dist/react/index.mjs +5 -0
- package/dist/react/useDevWallet.d.mts +38 -0
- package/dist/react/useDevWallet.d.mts.map +1 -0
- package/dist/react/useDevWallet.mjs +89 -0
- package/dist/react/useDevWallet.mjs.map +1 -0
- package/dist/server/cli-signing-middleware.d.mts +22 -0
- package/dist/server/cli-signing-middleware.d.mts.map +1 -0
- package/dist/server/cli-signing-middleware.mjs +117 -0
- package/dist/server/cli-signing-middleware.mjs.map +1 -0
- package/dist/server/index.d.mts +2 -0
- package/dist/server/index.mjs +3 -0
- package/dist/standalone/assets/in-memory-adapter-CBF4h2KD.js +1 -0
- package/dist/standalone/assets/main-VE4olb-Q.js +2841 -0
- package/dist/standalone/assets/webcrypto-adapter-DysAubFb.js +1 -0
- package/dist/standalone/bookmarklet.js +5 -0
- package/dist/standalone/index.html +48 -0
- package/dist/types.d.mts +55 -0
- package/dist/types.d.mts.map +1 -0
- package/dist/ui/copy-controller.mjs +43 -0
- package/dist/ui/copy-controller.mjs.map +1 -0
- package/dist/ui/dev-wallet-account-selector.d.mts +24 -0
- package/dist/ui/dev-wallet-account-selector.d.mts.map +1 -0
- package/dist/ui/dev-wallet-account-selector.mjs +268 -0
- package/dist/ui/dev-wallet-account-selector.mjs.map +1 -0
- package/dist/ui/dev-wallet-accounts.d.mts +29 -0
- package/dist/ui/dev-wallet-accounts.d.mts.map +1 -0
- package/dist/ui/dev-wallet-accounts.mjs +524 -0
- package/dist/ui/dev-wallet-accounts.mjs.map +1 -0
- package/dist/ui/dev-wallet-balances.d.mts +26 -0
- package/dist/ui/dev-wallet-balances.d.mts.map +1 -0
- package/dist/ui/dev-wallet-balances.mjs +130 -0
- package/dist/ui/dev-wallet-balances.mjs.map +1 -0
- package/dist/ui/dev-wallet-connect.d.mts +31 -0
- package/dist/ui/dev-wallet-connect.d.mts.map +1 -0
- package/dist/ui/dev-wallet-connect.mjs +225 -0
- package/dist/ui/dev-wallet-connect.mjs.map +1 -0
- package/dist/ui/dev-wallet-dropdown.d.mts +27 -0
- package/dist/ui/dev-wallet-dropdown.d.mts.map +1 -0
- package/dist/ui/dev-wallet-dropdown.mjs +121 -0
- package/dist/ui/dev-wallet-dropdown.mjs.map +1 -0
- package/dist/ui/dev-wallet-network-badge.d.mts +21 -0
- package/dist/ui/dev-wallet-network-badge.d.mts.map +1 -0
- package/dist/ui/dev-wallet-network-badge.mjs +121 -0
- package/dist/ui/dev-wallet-network-badge.mjs.map +1 -0
- package/dist/ui/dev-wallet-new-account.d.mts +31 -0
- package/dist/ui/dev-wallet-new-account.d.mts.map +1 -0
- package/dist/ui/dev-wallet-new-account.mjs +577 -0
- package/dist/ui/dev-wallet-new-account.mjs.map +1 -0
- package/dist/ui/dev-wallet-objects.d.mts +26 -0
- package/dist/ui/dev-wallet-objects.d.mts.map +1 -0
- package/dist/ui/dev-wallet-objects.mjs +276 -0
- package/dist/ui/dev-wallet-objects.mjs.map +1 -0
- package/dist/ui/dev-wallet-panel.d.mts +22 -0
- package/dist/ui/dev-wallet-panel.d.mts.map +1 -0
- package/dist/ui/dev-wallet-panel.mjs +192 -0
- package/dist/ui/dev-wallet-panel.mjs.map +1 -0
- package/dist/ui/dev-wallet-popup.d.mts +36 -0
- package/dist/ui/dev-wallet-popup.d.mts.map +1 -0
- package/dist/ui/dev-wallet-popup.mjs +137 -0
- package/dist/ui/dev-wallet-popup.mjs.map +1 -0
- package/dist/ui/dev-wallet-settings.d.mts +33 -0
- package/dist/ui/dev-wallet-settings.d.mts.map +1 -0
- package/dist/ui/dev-wallet-settings.mjs +635 -0
- package/dist/ui/dev-wallet-settings.mjs.map +1 -0
- package/dist/ui/dev-wallet-signing-modal.d.mts +32 -0
- package/dist/ui/dev-wallet-signing-modal.d.mts.map +1 -0
- package/dist/ui/dev-wallet-signing-modal.mjs +115 -0
- package/dist/ui/dev-wallet-signing-modal.mjs.map +1 -0
- package/dist/ui/dev-wallet-signing.d.mts +25 -0
- package/dist/ui/dev-wallet-signing.d.mts.map +1 -0
- package/dist/ui/dev-wallet-signing.mjs +544 -0
- package/dist/ui/dev-wallet-signing.mjs.map +1 -0
- package/dist/ui/dev-wallet-standalone.d.mts +23 -0
- package/dist/ui/dev-wallet-standalone.d.mts.map +1 -0
- package/dist/ui/dev-wallet-standalone.mjs +129 -0
- package/dist/ui/dev-wallet-standalone.mjs.map +1 -0
- package/dist/ui/dev-wallet-tab-bar.d.mts +19 -0
- package/dist/ui/dev-wallet-tab-bar.d.mts.map +1 -0
- package/dist/ui/dev-wallet-tab-bar.mjs +143 -0
- package/dist/ui/dev-wallet-tab-bar.mjs.map +1 -0
- package/dist/ui/index.d.mts +17 -0
- package/dist/ui/index.mjs +18 -0
- package/dist/ui/mount.d.mts +11 -0
- package/dist/ui/mount.d.mts.map +1 -0
- package/dist/ui/mount.mjs +17 -0
- package/dist/ui/mount.mjs.map +1 -0
- package/dist/ui/styles.mjs +250 -0
- package/dist/ui/styles.mjs.map +1 -0
- package/dist/ui/transaction-analyzer.mjs +58 -0
- package/dist/ui/transaction-analyzer.mjs.map +1 -0
- package/dist/ui/utils.mjs +98 -0
- package/dist/ui/utils.mjs.map +1 -0
- package/dist/ui/wallet-controller.mjs +275 -0
- package/dist/ui/wallet-controller.mjs.map +1 -0
- package/dist/wallet/constants.mjs +11 -0
- package/dist/wallet/constants.mjs.map +1 -0
- package/dist/wallet/dev-wallet.d.mts +118 -0
- package/dist/wallet/dev-wallet.d.mts.map +1 -0
- package/dist/wallet/dev-wallet.mjs +424 -0
- package/dist/wallet/dev-wallet.mjs.map +1 -0
- package/dist/wallet/initializer.d.mts +63 -0
- package/dist/wallet/initializer.d.mts.map +1 -0
- package/dist/wallet/initializer.mjs +75 -0
- package/dist/wallet/initializer.mjs.map +1 -0
- package/dist/wallet/signing.d.mts +35 -0
- package/dist/wallet/signing.d.mts.map +1 -0
- package/dist/wallet/signing.mjs +69 -0
- package/dist/wallet/signing.mjs.map +1 -0
- package/package.json +124 -0
- package/src/adapters/base-adapter.ts +93 -0
- package/src/adapters/browser.ts +15 -0
- package/src/adapters/build-managed-account.ts +33 -0
- package/src/adapters/idb-store.ts +87 -0
- package/src/adapters/in-memory-adapter.ts +51 -0
- package/src/adapters/passkey-adapter.ts +120 -0
- package/src/adapters/remote-cli-adapter.ts +388 -0
- package/src/adapters/webcrypto-adapter.ts +96 -0
- package/src/app/bookmarklet-entry.ts +77 -0
- package/src/app/index.html +48 -0
- package/src/app/main.ts +245 -0
- package/src/app/tsconfig.json +3 -0
- package/src/bin/cli.ts +214 -0
- package/src/client/dev-wallet-client.ts +280 -0
- package/src/client/index.ts +7 -0
- package/src/client/request-handler.ts +161 -0
- package/src/index.ts +19 -0
- package/src/react/components.ts +64 -0
- package/src/react/context.ts +38 -0
- package/src/react/index.ts +16 -0
- package/src/react/useDevWallet.ts +118 -0
- package/src/server/cli-signing-middleware.ts +146 -0
- package/src/server/index.ts +8 -0
- package/src/types.ts +58 -0
- package/src/ui/copy-controller.ts +49 -0
- package/src/ui/dev-wallet-account-selector.ts +283 -0
- package/src/ui/dev-wallet-accounts.ts +578 -0
- package/src/ui/dev-wallet-balances.ts +171 -0
- package/src/ui/dev-wallet-connect.ts +259 -0
- package/src/ui/dev-wallet-dropdown.ts +138 -0
- package/src/ui/dev-wallet-network-badge.ts +128 -0
- package/src/ui/dev-wallet-new-account.ts +662 -0
- package/src/ui/dev-wallet-objects.ts +336 -0
- package/src/ui/dev-wallet-panel.ts +207 -0
- package/src/ui/dev-wallet-popup.ts +169 -0
- package/src/ui/dev-wallet-settings.ts +692 -0
- package/src/ui/dev-wallet-signing-modal.ts +137 -0
- package/src/ui/dev-wallet-signing.ts +624 -0
- package/src/ui/dev-wallet-standalone.ts +136 -0
- package/src/ui/dev-wallet-tab-bar.ts +151 -0
- package/src/ui/index.ts +26 -0
- package/src/ui/mount.ts +21 -0
- package/src/ui/styles.ts +252 -0
- package/src/ui/transaction-analyzer.ts +60 -0
- package/src/ui/utils.ts +118 -0
- package/src/ui/wallet-controller.ts +340 -0
- package/src/wallet/constants.ts +17 -0
- package/src/wallet/dev-wallet.ts +597 -0
- package/src/wallet/initializer.ts +124 -0
- package/src/wallet/signing.ts +85 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import { execFile } from 'node:child_process';
|
|
5
|
+
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
|
|
8
|
+
import { isValidSuiAddress } from '@mysten/sui/utils';
|
|
9
|
+
import { Hono } from 'hono';
|
|
10
|
+
|
|
11
|
+
const execFileAsync = promisify(execFile);
|
|
12
|
+
|
|
13
|
+
function isValidBase64(value: unknown): value is string {
|
|
14
|
+
if (typeof value !== 'string' || value.length === 0) return false;
|
|
15
|
+
return /^[A-Za-z0-9+/]+=*$/.test(value) && value.length <= 1_000_000;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MAX_BODY_SIZE = 2 * 1024 * 1024; // 2 MB
|
|
19
|
+
|
|
20
|
+
function generateToken(): string {
|
|
21
|
+
return randomBytes(32).toString('hex');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface CliSigningMiddlewareOptions {
|
|
25
|
+
/** The port the server is listening on, used for DNS rebinding protection. */
|
|
26
|
+
port: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface CliSigningMiddlewareResult {
|
|
30
|
+
app: Hono;
|
|
31
|
+
token: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a Hono sub-app that exposes signing endpoints backed by the `sui` CLI.
|
|
36
|
+
*
|
|
37
|
+
* Uses token-in-URL auth: a 256-bit random token is embedded in the URL printed
|
|
38
|
+
* to the terminal and sent as `Authorization: Bearer <token>` on subsequent requests.
|
|
39
|
+
* All CLI calls use `execFile` to prevent shell injection.
|
|
40
|
+
*/
|
|
41
|
+
export function createCliSigningMiddleware(
|
|
42
|
+
options: CliSigningMiddlewareOptions,
|
|
43
|
+
): CliSigningMiddlewareResult {
|
|
44
|
+
const { port } = options;
|
|
45
|
+
const token = generateToken();
|
|
46
|
+
|
|
47
|
+
function isAuthenticated(authHeader: string | undefined): boolean {
|
|
48
|
+
if (!token) return false;
|
|
49
|
+
if (!authHeader || !/^bearer /i.test(authHeader)) return false;
|
|
50
|
+
const provided = authHeader.slice(7);
|
|
51
|
+
if (provided.length !== token.length) return false;
|
|
52
|
+
return timingSafeEqual(Buffer.from(provided), Buffer.from(token));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const app = new Hono();
|
|
56
|
+
|
|
57
|
+
// DNS rebinding protection — reject non-localhost hosts
|
|
58
|
+
app.use('*', async (c, next) => {
|
|
59
|
+
const host = c.req.header('host') ?? '';
|
|
60
|
+
const allowedHosts = new Set([`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`]);
|
|
61
|
+
if (host && !allowedHosts.has(host)) {
|
|
62
|
+
return c.json({ error: 'Requests must originate from localhost.' }, 403);
|
|
63
|
+
}
|
|
64
|
+
return await next();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Token authentication for API routes
|
|
68
|
+
app.use('/api/*', async (c, next) => {
|
|
69
|
+
if (!isAuthenticated(c.req.header('authorization'))) {
|
|
70
|
+
return c.json(
|
|
71
|
+
{ error: 'Authentication required. Open the token URL from the terminal.' },
|
|
72
|
+
401,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
return await next();
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// GET /api/v1/accounts — list accounts from sui CLI keystore
|
|
79
|
+
app.get('/api/v1/accounts', async (c) => {
|
|
80
|
+
try {
|
|
81
|
+
const { stdout } = await execFileAsync('sui', ['keytool', 'list', '--json']);
|
|
82
|
+
const accounts = JSON.parse(stdout);
|
|
83
|
+
return c.json({ accounts });
|
|
84
|
+
} catch (err) {
|
|
85
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
86
|
+
const isExecError = message.includes('ENOENT') || message.includes('not found');
|
|
87
|
+
return c.json(
|
|
88
|
+
{
|
|
89
|
+
error: isExecError
|
|
90
|
+
? 'Failed to list accounts: sui CLI not found.'
|
|
91
|
+
: 'Failed to list accounts.',
|
|
92
|
+
},
|
|
93
|
+
500,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// POST /api/v1/sign-transaction — sign BCS-serialized TransactionData
|
|
99
|
+
app.post('/api/v1/sign-transaction', async (c) => {
|
|
100
|
+
const contentLength = c.req.header('content-length');
|
|
101
|
+
if (contentLength && parseInt(contentLength, 10) > MAX_BODY_SIZE) {
|
|
102
|
+
return c.json({ error: 'Request body too large' }, 413);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let body: { address?: unknown; txBytes?: unknown };
|
|
106
|
+
try {
|
|
107
|
+
body = await c.req.json();
|
|
108
|
+
} catch {
|
|
109
|
+
return c.json({ error: 'Invalid JSON body' }, 400);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (typeof body.address !== 'string' || !isValidSuiAddress(body.address)) {
|
|
113
|
+
return c.json({ error: 'Invalid address format. Expected 0x-prefixed hex.' }, 400);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!isValidBase64(body.txBytes)) {
|
|
117
|
+
return c.json({ error: 'Invalid txBytes. Expected non-empty base64 string.' }, 400);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const { stdout } = await execFileAsync('sui', [
|
|
122
|
+
'keytool',
|
|
123
|
+
'sign',
|
|
124
|
+
'--address',
|
|
125
|
+
body.address,
|
|
126
|
+
'--data',
|
|
127
|
+
body.txBytes as string,
|
|
128
|
+
'--json',
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
const result = JSON.parse(stdout);
|
|
132
|
+
return c.json({ suiSignature: result.suiSignature, digest: result.digest });
|
|
133
|
+
} catch (err) {
|
|
134
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
135
|
+
console.error('[dev-wallet] CLI signing error:', message);
|
|
136
|
+
|
|
137
|
+
if (message.includes('not found') || message.includes('Cannot find key')) {
|
|
138
|
+
return c.json({ error: `Address not found in keystore: ${body.address}` }, 404);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return c.json({ error: 'Signing failed due to a CLI error.' }, 500);
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return { app, token };
|
|
146
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
export { createCliSigningMiddleware } from './cli-signing-middleware.js';
|
|
5
|
+
export type {
|
|
6
|
+
CliSigningMiddlewareOptions,
|
|
7
|
+
CliSigningMiddlewareResult,
|
|
8
|
+
} from './cli-signing-middleware.js';
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { Signer } from '@mysten/sui/cryptography';
|
|
5
|
+
import type { ReadonlyWalletAccount } from '@mysten/wallet-standard';
|
|
6
|
+
|
|
7
|
+
export interface ManagedAccount {
|
|
8
|
+
address: string;
|
|
9
|
+
label: string;
|
|
10
|
+
signer: Signer;
|
|
11
|
+
walletAccount: ReadonlyWalletAccount;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface CreateAccountOptions {
|
|
15
|
+
label?: string;
|
|
16
|
+
[key: string]: unknown;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ImportAccountOptions {
|
|
20
|
+
/** Keypair to import — used by adapters that accept raw keypairs. */
|
|
21
|
+
signer?: Signer;
|
|
22
|
+
/** Address to import — used by adapters that look up keys from an external source (e.g. CLI keystore). */
|
|
23
|
+
address?: string;
|
|
24
|
+
label?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pluggable interface for managing accounts and providing signers to a {@link DevWallet}.
|
|
29
|
+
* Implementations handle key generation, storage, and lifecycle.
|
|
30
|
+
*/
|
|
31
|
+
export interface SignerAdapter {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly name: string;
|
|
34
|
+
/**
|
|
35
|
+
* Whether accounts from this adapter are eligible for auto-signing.
|
|
36
|
+
* Defaults to `true`. CLI-based adapters set this to `false` to ensure
|
|
37
|
+
* transactions always require explicit user approval.
|
|
38
|
+
*/
|
|
39
|
+
readonly allowAutoSign?: boolean;
|
|
40
|
+
|
|
41
|
+
/** Load keys from storage. Must be called before use. */
|
|
42
|
+
initialize(): Promise<void>;
|
|
43
|
+
getAccounts(): ManagedAccount[];
|
|
44
|
+
getAccount(address: string): ManagedAccount | undefined;
|
|
45
|
+
|
|
46
|
+
createAccount?(options?: CreateAccountOptions): Promise<ManagedAccount>;
|
|
47
|
+
importAccount?(options: ImportAccountOptions): Promise<ManagedAccount>;
|
|
48
|
+
/** List accounts available for import (e.g. from a CLI keystore). */
|
|
49
|
+
listAvailableAccounts?(): Promise<
|
|
50
|
+
Array<{ address: string; scheme: string; alias?: string | null }>
|
|
51
|
+
>;
|
|
52
|
+
removeAccount?(address: string): Promise<boolean>;
|
|
53
|
+
renameAccount?(address: string, label: string): Promise<boolean>;
|
|
54
|
+
|
|
55
|
+
/** Subscribe to account list changes. Returns an unsubscribe function. */
|
|
56
|
+
onAccountsChanged(callback: (accounts: ManagedAccount[]) => void): () => void;
|
|
57
|
+
destroy(): void;
|
|
58
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { ReactiveController, ReactiveControllerHost } from 'lit';
|
|
5
|
+
|
|
6
|
+
import { copyToClipboard } from './utils.js';
|
|
7
|
+
|
|
8
|
+
export class CopyController implements ReactiveController {
|
|
9
|
+
host: ReactiveControllerHost;
|
|
10
|
+
copied = false;
|
|
11
|
+
copiedValue: string | null = null;
|
|
12
|
+
#timeout: ReturnType<typeof setTimeout> | null = null;
|
|
13
|
+
#duration: number;
|
|
14
|
+
|
|
15
|
+
constructor(host: ReactiveControllerHost, duration = 1500) {
|
|
16
|
+
this.#duration = duration;
|
|
17
|
+
this.host = host;
|
|
18
|
+
host.addController(this);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
hostConnected(): void {}
|
|
22
|
+
|
|
23
|
+
hostDisconnected(): void {
|
|
24
|
+
if (this.#timeout) {
|
|
25
|
+
clearTimeout(this.#timeout);
|
|
26
|
+
this.#timeout = null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async copy(text: string): Promise<boolean> {
|
|
31
|
+
const ok = await copyToClipboard(text);
|
|
32
|
+
if (ok) {
|
|
33
|
+
this.copied = true;
|
|
34
|
+
this.copiedValue = text;
|
|
35
|
+
if (this.#timeout) clearTimeout(this.#timeout);
|
|
36
|
+
this.#timeout = setTimeout(() => {
|
|
37
|
+
this.copied = false;
|
|
38
|
+
this.copiedValue = null;
|
|
39
|
+
this.host.requestUpdate();
|
|
40
|
+
}, this.#duration);
|
|
41
|
+
this.host.requestUpdate();
|
|
42
|
+
}
|
|
43
|
+
return ok;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
isCopied(text: string): boolean {
|
|
47
|
+
return this.copied && this.copiedValue === text;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
// Copyright (c) Mysten Labs, Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
4
|
+
import type { ReadonlyWalletAccount } from '@mysten/wallet-standard';
|
|
5
|
+
import { css, html, LitElement, nothing } from 'lit';
|
|
6
|
+
import { customElement, property, state } from 'lit/decorators.js';
|
|
7
|
+
|
|
8
|
+
import type { SignerAdapter } from '../types.js';
|
|
9
|
+
import { dropdownItemStyles, sharedStyles } from './styles.js';
|
|
10
|
+
import { CopyController } from './copy-controller.js';
|
|
11
|
+
import { emitEvent, findAdapterForAddress, formatAddress } from './utils.js';
|
|
12
|
+
import './dev-wallet-dropdown.js';
|
|
13
|
+
|
|
14
|
+
/** Short display names for adapter badges. */
|
|
15
|
+
const ADAPTER_SHORT_NAMES: Record<string, string> = {
|
|
16
|
+
'WebCrypto Signer': 'WebCrypto',
|
|
17
|
+
'Remote CLI Signer': 'CLI',
|
|
18
|
+
'In-Memory Signer': 'Memory',
|
|
19
|
+
'Passkey Signer': 'Passkey',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
@customElement('dev-wallet-account-selector')
|
|
23
|
+
export class DevWalletAccountSelector extends LitElement {
|
|
24
|
+
static override styles = [
|
|
25
|
+
sharedStyles,
|
|
26
|
+
dropdownItemStyles,
|
|
27
|
+
css`
|
|
28
|
+
:host {
|
|
29
|
+
display: block;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.active-account {
|
|
33
|
+
display: flex;
|
|
34
|
+
align-items: center;
|
|
35
|
+
gap: 8px;
|
|
36
|
+
width: 100%;
|
|
37
|
+
padding: 8px 10px;
|
|
38
|
+
border-radius: var(--dev-wallet-radius-md);
|
|
39
|
+
background: var(--dev-wallet-secondary);
|
|
40
|
+
cursor: pointer;
|
|
41
|
+
transition: background 0.15s;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.active-account:hover {
|
|
45
|
+
background: var(--dev-wallet-border);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.avatar {
|
|
49
|
+
width: 28px;
|
|
50
|
+
height: 28px;
|
|
51
|
+
border-radius: 50%;
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
justify-content: center;
|
|
55
|
+
font-size: 12px;
|
|
56
|
+
font-weight: var(--dev-wallet-font-weight-semibold);
|
|
57
|
+
color: var(--dev-wallet-primary-foreground);
|
|
58
|
+
background: var(--dev-wallet-primary);
|
|
59
|
+
flex-shrink: 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.account-info {
|
|
63
|
+
flex: 1;
|
|
64
|
+
min-width: 0;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.account-label {
|
|
68
|
+
font-size: 13px;
|
|
69
|
+
font-weight: var(--dev-wallet-font-weight-semibold);
|
|
70
|
+
color: var(--dev-wallet-foreground);
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.account-address {
|
|
77
|
+
font-size: 11px;
|
|
78
|
+
font-family: var(--dev-wallet-font-mono);
|
|
79
|
+
color: var(--dev-wallet-muted-foreground);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.copy-btn {
|
|
83
|
+
width: 22px;
|
|
84
|
+
height: 22px;
|
|
85
|
+
display: flex;
|
|
86
|
+
align-items: center;
|
|
87
|
+
justify-content: center;
|
|
88
|
+
border-radius: var(--dev-wallet-radius-xs);
|
|
89
|
+
color: var(--dev-wallet-muted-foreground);
|
|
90
|
+
flex-shrink: 0;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
.copy-btn:hover {
|
|
95
|
+
background: var(--dev-wallet-border);
|
|
96
|
+
color: var(--dev-wallet-foreground);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.copy-btn.copied {
|
|
100
|
+
color: var(--dev-wallet-positive);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.chevron {
|
|
104
|
+
width: 14px;
|
|
105
|
+
height: 14px;
|
|
106
|
+
color: var(--dev-wallet-muted-foreground);
|
|
107
|
+
flex-shrink: 0;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.dropdown-item[aria-selected='true'] {
|
|
111
|
+
background: color-mix(in oklab, var(--dev-wallet-primary) 10%, transparent);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.dropdown-item .avatar {
|
|
115
|
+
width: 24px;
|
|
116
|
+
height: 24px;
|
|
117
|
+
font-size: 10px;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
.dropdown-item .account-label {
|
|
121
|
+
font-size: 12px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.dropdown-item .account-address {
|
|
125
|
+
font-size: 10px;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.dropdown-item .account-info {
|
|
129
|
+
min-width: 0;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.dropdown-item .account-label {
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
text-overflow: ellipsis;
|
|
135
|
+
white-space: nowrap;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.adapter-badge {
|
|
139
|
+
font-size: 9px;
|
|
140
|
+
padding: 1px 4px;
|
|
141
|
+
border-radius: var(--dev-wallet-radius-2xs);
|
|
142
|
+
background: var(--dev-wallet-secondary);
|
|
143
|
+
color: var(--dev-wallet-muted-foreground);
|
|
144
|
+
white-space: nowrap;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.selector-row {
|
|
148
|
+
display: flex;
|
|
149
|
+
gap: 4px;
|
|
150
|
+
align-items: center;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.selector-wrapper {
|
|
154
|
+
flex: 1;
|
|
155
|
+
min-width: 0;
|
|
156
|
+
--dropdown-max-height: 240px;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.empty-state {
|
|
160
|
+
padding: 16px;
|
|
161
|
+
text-align: center;
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
color: var(--dev-wallet-muted-foreground);
|
|
164
|
+
}
|
|
165
|
+
`,
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
@property({ attribute: false })
|
|
169
|
+
accounts: ReadonlyWalletAccount[] = [];
|
|
170
|
+
|
|
171
|
+
@property({ attribute: false })
|
|
172
|
+
adapters: SignerAdapter[] = [];
|
|
173
|
+
|
|
174
|
+
@property({ type: String })
|
|
175
|
+
activeAddress = '';
|
|
176
|
+
|
|
177
|
+
@state()
|
|
178
|
+
private _open = false;
|
|
179
|
+
|
|
180
|
+
#copy = new CopyController(this);
|
|
181
|
+
|
|
182
|
+
override render() {
|
|
183
|
+
const active = this.accounts.find((a) => a.address === this.activeAddress);
|
|
184
|
+
|
|
185
|
+
if (!active) {
|
|
186
|
+
return html`<div class="empty-state" part="empty-state">No account selected</div>`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const label = this.#getLabel(active);
|
|
190
|
+
const initial = (label[0] ?? '?').toUpperCase();
|
|
191
|
+
|
|
192
|
+
return html`
|
|
193
|
+
<div class="selector-row">
|
|
194
|
+
<dev-wallet-dropdown
|
|
195
|
+
full-width
|
|
196
|
+
class="selector-wrapper"
|
|
197
|
+
.open=${this._open}
|
|
198
|
+
@close=${() => (this._open = false)}
|
|
199
|
+
>
|
|
200
|
+
<button
|
|
201
|
+
slot="trigger"
|
|
202
|
+
class="active-account"
|
|
203
|
+
part="trigger"
|
|
204
|
+
aria-expanded=${this._open}
|
|
205
|
+
aria-haspopup="listbox"
|
|
206
|
+
@click=${() => (this._open = !this._open)}
|
|
207
|
+
>
|
|
208
|
+
<span class="avatar">${initial}</span>
|
|
209
|
+
<div class="account-info">
|
|
210
|
+
<div class="account-label">${label}</div>
|
|
211
|
+
<div class="account-address">${formatAddress(active.address)}</div>
|
|
212
|
+
</div>
|
|
213
|
+
<svg
|
|
214
|
+
class="chevron"
|
|
215
|
+
viewBox="0 0 24 24"
|
|
216
|
+
fill="none"
|
|
217
|
+
stroke="currentColor"
|
|
218
|
+
stroke-width="2.5"
|
|
219
|
+
>
|
|
220
|
+
<path d="M6 9l6 6 6-6" />
|
|
221
|
+
</svg>
|
|
222
|
+
</button>
|
|
223
|
+
<div slot="popover" role="listbox" aria-label="Select account">
|
|
224
|
+
${this.accounts.map((account) => {
|
|
225
|
+
const accountLabel = this.#getLabel(account);
|
|
226
|
+
const accountInitial = (accountLabel[0] ?? '?').toUpperCase();
|
|
227
|
+
const adapterName = this.#getAdapterName(account.address);
|
|
228
|
+
|
|
229
|
+
return html`
|
|
230
|
+
<button
|
|
231
|
+
class="dropdown-item"
|
|
232
|
+
role="option"
|
|
233
|
+
aria-selected=${account.address === this.activeAddress}
|
|
234
|
+
@click=${() => this.#select(account)}
|
|
235
|
+
>
|
|
236
|
+
<span class="avatar">${accountInitial}</span>
|
|
237
|
+
<div class="account-info">
|
|
238
|
+
<div class="account-label">${accountLabel}</div>
|
|
239
|
+
<div class="account-address">${formatAddress(account.address)}</div>
|
|
240
|
+
</div>
|
|
241
|
+
${adapterName ? html`<span class="adapter-badge">${adapterName}</span>` : nothing}
|
|
242
|
+
</button>
|
|
243
|
+
`;
|
|
244
|
+
})}
|
|
245
|
+
</div>
|
|
246
|
+
</dev-wallet-dropdown>
|
|
247
|
+
<button
|
|
248
|
+
class="copy-btn ${this.#copy.isCopied(active.address) ? 'copied' : ''}"
|
|
249
|
+
part="copy-button"
|
|
250
|
+
title="Copy address"
|
|
251
|
+
aria-label="Copy address"
|
|
252
|
+
@click=${(e: Event) => {
|
|
253
|
+
e.stopPropagation();
|
|
254
|
+
this.#copy.copy(active.address);
|
|
255
|
+
}}
|
|
256
|
+
>
|
|
257
|
+
${this.#copy.isCopied(active.address) ? '\u2713' : '\u2398'}
|
|
258
|
+
</button>
|
|
259
|
+
</div>
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
#getLabel(account: ReadonlyWalletAccount): string {
|
|
264
|
+
return account.label ?? `Account ${account.address.slice(0, 8)}`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
#getAdapterName(address: string): string | null {
|
|
268
|
+
const adapter = findAdapterForAddress(this.adapters, address);
|
|
269
|
+
if (!adapter) return null;
|
|
270
|
+
return ADAPTER_SHORT_NAMES[adapter.name] ?? adapter.name;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
#select(account: ReadonlyWalletAccount) {
|
|
274
|
+
this._open = false;
|
|
275
|
+
emitEvent(this, 'account-selected', { account });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
declare global {
|
|
280
|
+
interface HTMLElementTagNameMap {
|
|
281
|
+
'dev-wallet-account-selector': DevWalletAccountSelector;
|
|
282
|
+
}
|
|
283
|
+
}
|