@unlink-xyz/cli 0.1.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 (50) hide show
  1. package/README.md +9 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.js +46 -0
  4. package/dist/commands/account.d.ts +2 -0
  5. package/dist/commands/account.js +83 -0
  6. package/dist/commands/balance.d.ts +2 -0
  7. package/dist/commands/balance.js +75 -0
  8. package/dist/commands/burner.d.ts +2 -0
  9. package/dist/commands/burner.js +114 -0
  10. package/dist/commands/config.d.ts +2 -0
  11. package/dist/commands/config.js +140 -0
  12. package/dist/commands/deposit.d.ts +2 -0
  13. package/dist/commands/deposit.js +58 -0
  14. package/dist/commands/history.d.ts +2 -0
  15. package/dist/commands/history.js +30 -0
  16. package/dist/commands/multisig.d.ts +2 -0
  17. package/dist/commands/multisig.js +343 -0
  18. package/dist/commands/notes.d.ts +2 -0
  19. package/dist/commands/notes.js +28 -0
  20. package/dist/commands/sync.d.ts +2 -0
  21. package/dist/commands/sync.js +51 -0
  22. package/dist/commands/transfer.d.ts +2 -0
  23. package/dist/commands/transfer.js +47 -0
  24. package/dist/commands/tx-status.d.ts +2 -0
  25. package/dist/commands/tx-status.js +31 -0
  26. package/dist/commands/wallet.d.ts +2 -0
  27. package/dist/commands/wallet.js +98 -0
  28. package/dist/commands/withdraw.d.ts +2 -0
  29. package/dist/commands/withdraw.js +47 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +6 -0
  32. package/dist/lib/context.d.ts +14 -0
  33. package/dist/lib/context.js +42 -0
  34. package/dist/lib/errors.d.ts +1 -0
  35. package/dist/lib/errors.js +24 -0
  36. package/dist/lib/multisig-store.d.ts +8 -0
  37. package/dist/lib/multisig-store.js +121 -0
  38. package/dist/lib/options.d.ts +40 -0
  39. package/dist/lib/options.js +109 -0
  40. package/dist/lib/output.d.ts +6 -0
  41. package/dist/lib/output.js +34 -0
  42. package/dist/lib/relay.d.ts +18 -0
  43. package/dist/lib/relay.js +35 -0
  44. package/dist/lib/tokens.d.ts +14 -0
  45. package/dist/lib/tokens.js +142 -0
  46. package/dist/storage/sqlite.d.ts +1 -0
  47. package/dist/storage/sqlite.js +1 -0
  48. package/dist/test-utils.d.ts +56 -0
  49. package/dist/test-utils.js +73 -0
  50. package/package.json +45 -0
@@ -0,0 +1,343 @@
1
+ import { createMultisigWallet, runSigningListener, signMultisig, } from "@unlink-xyz/multisig";
2
+ import { ethers } from "ethers";
3
+ import { createContext } from "../lib/context.js";
4
+ import { withErrorHandler } from "../lib/errors.js";
5
+ import { listMultisigAccounts, loadMultisigAccount, saveMultisigAccount, } from "../lib/multisig-store.js";
6
+ import { requireChainId, requireGatewayUrl, requirePoolAddress, requirePrivateKey, resolveOptions, } from "../lib/options.js";
7
+ import { log, output } from "../lib/output.js";
8
+ async function loadAccountView(options, groupId) {
9
+ const account = await loadMultisigAccount(options.dataDir, groupId);
10
+ const msWallet = createMultisigWallet({
11
+ gatewayUrl: account.gatewayUrl,
12
+ });
13
+ return { account, accountView: msWallet.toAccountView(account) };
14
+ }
15
+ function parseOptionalParticipantIndex(cmdOpts) {
16
+ const participantIndexRaw = cmdOpts["participantIndex"];
17
+ if (participantIndexRaw === undefined)
18
+ return undefined;
19
+ const participantIndex = Number(participantIndexRaw);
20
+ if (!Number.isInteger(participantIndex) || participantIndex < 1) {
21
+ throw new Error("--participant-index must be a positive integer");
22
+ }
23
+ return participantIndex;
24
+ }
25
+ export function registerMultisigCommands(program) {
26
+ const multisig = program
27
+ .command("multisig")
28
+ .description("Multisig wallet management (FROST threshold signatures)");
29
+ multisig
30
+ .command("create")
31
+ .description("Create a new multisig wallet (starts DKG ceremony)")
32
+ .requiredOption("-t, --threshold <n>", "Minimum signers required")
33
+ .requiredOption("-n, --participants <n>", "Total number of participants")
34
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
35
+ const options = resolveOptions(cmd.optsWithGlobals());
36
+ requireGatewayUrl(options);
37
+ const threshold = Number(cmdOpts["threshold"]);
38
+ const totalShares = Number(cmdOpts["participants"]);
39
+ const wallet = createMultisigWallet({
40
+ gatewayUrl: options.gatewayUrl,
41
+ });
42
+ const viewingKeyPair = {
43
+ privateKey: globalThis.crypto.getRandomValues(new Uint8Array(32)),
44
+ pubkey: globalThis.crypto.getRandomValues(new Uint8Array(32)),
45
+ };
46
+ log("Creating DKG session...", options);
47
+ const { groupId, complete } = await wallet.create({
48
+ threshold,
49
+ totalShares,
50
+ viewingKeyPair,
51
+ });
52
+ log(`Group ID (share with participants): ${groupId}`, options);
53
+ log("Waiting for all participants to join...", options);
54
+ const cliName = cmd.parent?.parent?.name() ?? "unlink-cli";
55
+ log(`Share this with participants:\n ${cliName} multisig join --code ${groupId} --gateway-url ${options.gatewayUrl}`, options);
56
+ const account = await complete();
57
+ await saveMultisigAccount(options.dataDir, account);
58
+ output({
59
+ groupId: account.groupId,
60
+ address: account.address,
61
+ participantIndex: account.participantIndex,
62
+ threshold: account.config.threshold,
63
+ totalShares: account.config.totalShares,
64
+ }, options);
65
+ }));
66
+ multisig
67
+ .command("join")
68
+ .description("Join an existing multisig wallet (DKG ceremony)")
69
+ .requiredOption("-c, --code <group-id>", "Group ID from the creator")
70
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
71
+ const options = resolveOptions(cmd.optsWithGlobals());
72
+ requireGatewayUrl(options);
73
+ const code = cmdOpts["code"];
74
+ const wallet = createMultisigWallet({
75
+ gatewayUrl: options.gatewayUrl,
76
+ });
77
+ log("Joining DKG session...", options);
78
+ const account = await wallet.join({ code });
79
+ await saveMultisigAccount(options.dataDir, account);
80
+ output({
81
+ groupId: account.groupId,
82
+ address: account.address,
83
+ participantIndex: account.participantIndex,
84
+ threshold: account.config.threshold,
85
+ totalShares: account.config.totalShares,
86
+ }, options);
87
+ }));
88
+ multisig
89
+ .command("create-session")
90
+ .description("Create a signing session for threshold signing")
91
+ .requiredOption("-g, --group-id <id>", "Multisig group ID")
92
+ .option("-p, --participant-index <n>", "Participant index override when multiple local participants exist for a group")
93
+ .requiredOption("-P, --participants <indices>", "Comma-separated participant indices")
94
+ .requiredOption("-m, --message <value>", "Message to sign (bigint)")
95
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
96
+ const options = resolveOptions(cmd.optsWithGlobals());
97
+ const groupId = cmdOpts["groupId"];
98
+ const message = BigInt(cmdOpts["message"]);
99
+ const participants = cmdOpts["participants"]
100
+ .split(",")
101
+ .map(Number);
102
+ const participantIndex = parseOptionalParticipantIndex(cmdOpts);
103
+ const account = await loadMultisigAccount(options.dataDir, groupId, participantIndex);
104
+ const wallet = createMultisigWallet({
105
+ gatewayUrl: account.gatewayUrl,
106
+ });
107
+ log("Creating signing session...", options);
108
+ const { code } = await wallet.createSigningSession({
109
+ participants,
110
+ message,
111
+ groupId: account.groupId,
112
+ });
113
+ const cliName = cmd.parent?.parent?.name() ?? "unlink-cli";
114
+ const signCmdBase = `${cliName} multisig sign -g ${account.groupId} ` +
115
+ `-s ${code} -m ${message.toString()}`;
116
+ const nextSteps = {
117
+ shareWithParticipants: `${signCmdBase} -p <participant-index>`,
118
+ signAsCurrentParticipant: `${signCmdBase} -p ${account.participantIndex}`,
119
+ };
120
+ log(`Share this with participants:\n ${nextSteps.shareWithParticipants}`, options);
121
+ log(`Run this to sign as this participant:\n ${nextSteps.signAsCurrentParticipant}`, options);
122
+ output({
123
+ sessionCode: code,
124
+ participants,
125
+ message: String(message),
126
+ nextSteps,
127
+ }, options);
128
+ }));
129
+ multisig
130
+ .command("sign")
131
+ .description("Sign a message using a multisig account")
132
+ .requiredOption("-g, --group-id <id>", "Multisig group ID")
133
+ .option("-p, --participant-index <n>", "Participant index override when multiple local participants exist for a group")
134
+ .requiredOption("-s, --session-code <code>", "Signing session code")
135
+ .requiredOption("-m, --message <value>", "Message to sign (bigint)")
136
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
137
+ const options = resolveOptions(cmd.optsWithGlobals());
138
+ const groupId = cmdOpts["groupId"];
139
+ const participantIndex = parseOptionalParticipantIndex(cmdOpts);
140
+ const signingSessionCode = cmdOpts["sessionCode"];
141
+ const message = BigInt(cmdOpts["message"]);
142
+ const account = await loadMultisigAccount(options.dataDir, groupId, participantIndex);
143
+ log("Participating in signing session...", options);
144
+ const signature = await signMultisig({
145
+ account,
146
+ message,
147
+ signingSessionCode,
148
+ });
149
+ output(signature, options);
150
+ }));
151
+ multisig
152
+ .command("list")
153
+ .description("List saved multisig accounts")
154
+ .action(withErrorHandler(async (_cmdOpts, cmd) => {
155
+ const options = resolveOptions(cmd.optsWithGlobals());
156
+ const refs = await listMultisigAccounts(options.dataDir);
157
+ if (refs.length === 0) {
158
+ output(options.json ? [] : "No multisig accounts found.", options);
159
+ return;
160
+ }
161
+ const accounts = await Promise.all(refs.map(async (ref) => {
162
+ const account = await loadMultisigAccount(options.dataDir, ref.groupId, ref.participantIndex);
163
+ return {
164
+ groupId: account.groupId,
165
+ address: account.address,
166
+ participantIndex: account.participantIndex,
167
+ threshold: account.config.threshold,
168
+ totalShares: account.config.totalShares,
169
+ };
170
+ }));
171
+ output(accounts, options);
172
+ }));
173
+ multisig
174
+ .command("listen")
175
+ .description("Listen for signing sessions and auto-sign (co-signer mode)")
176
+ .requiredOption("-g, --group-id <id>", "Multisig group ID")
177
+ .option("-p, --participant-index <n>", "Participant index override when multiple local participants exist for a group")
178
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
179
+ const options = resolveOptions(cmd.optsWithGlobals());
180
+ const groupId = cmdOpts["groupId"];
181
+ const participantIndex = parseOptionalParticipantIndex(cmdOpts);
182
+ const account = await loadMultisigAccount(options.dataDir, groupId, participantIndex);
183
+ log(`Listening for signing sessions on group ${groupId} (participant ${account.participantIndex})...`, options);
184
+ log("Press Ctrl+C to stop.", options);
185
+ const controller = new AbortController();
186
+ process.on("SIGINT", () => controller.abort());
187
+ await runSigningListener({
188
+ account,
189
+ signal: controller.signal,
190
+ onSession: (session) => {
191
+ log(`Joining session ${session.code} (message: ${session.message})`, options);
192
+ },
193
+ });
194
+ log("Listener stopped.", options);
195
+ }));
196
+ // --- Pool operations (require createContext for UnlinkWallet) ---
197
+ multisig
198
+ .command("deposit")
199
+ .description("Deposit tokens into the privacy pool for a multisig account")
200
+ .requiredOption("-g, --group-id <id>", "Multisig group ID")
201
+ .requiredOption("-t, --token <address>", "Token contract address")
202
+ .requiredOption("-a, --amount <value>", "Amount (raw atomic units)")
203
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
204
+ const options = resolveOptions(cmd.optsWithGlobals());
205
+ requireGatewayUrl(options);
206
+ requireChainId(options);
207
+ requirePoolAddress(options);
208
+ requirePrivateKey(options);
209
+ const groupId = cmdOpts["groupId"];
210
+ const token = cmdOpts["token"];
211
+ const amount = BigInt(cmdOpts["amount"]);
212
+ const { accountView } = await loadAccountView(options, groupId);
213
+ const ctx = await createContext(options);
214
+ const ethRpcUrl = options.nodeUrl ?? ctx.gatewayUrl;
215
+ const provider = new ethers.JsonRpcProvider(ethRpcUrl, ctx.wallet.chainId, { staticNetwork: true });
216
+ const signer = new ethers.Wallet(options.privateKey, provider);
217
+ const depositor = await signer.getAddress();
218
+ const erc20 = new ethers.Contract(token, [
219
+ "function allowance(address,address) view returns (uint256)",
220
+ "function approve(address,uint256) returns (bool)",
221
+ ], signer);
222
+ const allowance = (await erc20.getFunction("allowance")(depositor, options.poolAddress));
223
+ if (allowance < amount) {
224
+ log("Approving token spend...", options);
225
+ const approveTx = (await erc20.getFunction("approve")(options.poolAddress, amount));
226
+ await approveTx.wait();
227
+ }
228
+ log("Preparing deposit...", options);
229
+ const relay = await ctx.wallet.deposit({
230
+ depositor,
231
+ deposits: [{ token, amount }],
232
+ account: accountView,
233
+ });
234
+ log(`Relay ID: ${relay.relayId}\nSubmitting transaction...`, options);
235
+ const nonce = await provider.getTransactionCount(depositor, "pending");
236
+ const tx = await signer.sendTransaction({
237
+ to: relay.to,
238
+ data: relay.calldata,
239
+ nonce,
240
+ });
241
+ log(`Tx hash: ${tx.hash}\nWaiting for confirmation...`, options);
242
+ await tx.wait();
243
+ log("Reconciling deposit...", options);
244
+ const result = await ctx.wallet.confirmDeposit(relay.relayId);
245
+ output({
246
+ relayId: relay.relayId,
247
+ txHash: tx.hash,
248
+ status: "confirmed",
249
+ commitments: result.commitments,
250
+ }, options);
251
+ }));
252
+ multisig
253
+ .command("sync")
254
+ .description("Sync local state with blockchain for a multisig account")
255
+ .requiredOption("-g, --group-id <id>", "Multisig group ID")
256
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
257
+ const options = resolveOptions(cmd.optsWithGlobals());
258
+ requireGatewayUrl(options);
259
+ requireChainId(options);
260
+ const groupId = cmdOpts["groupId"];
261
+ const { accountView } = await loadAccountView(options, groupId);
262
+ const ctx = await createContext(options);
263
+ log("Syncing...", options);
264
+ await ctx.wallet.sync({}, { account: accountView });
265
+ output(options.json
266
+ ? { synced: true, chainId: ctx.wallet.chainId }
267
+ : "Sync complete.", options);
268
+ }));
269
+ multisig
270
+ .command("balance")
271
+ .argument("[token]", "Token address (omit for all)")
272
+ .description("Show token balances for a multisig account")
273
+ .requiredOption("-g, --group-id <id>", "Multisig group ID")
274
+ .action(withErrorHandler(async (token, cmdOpts, cmd) => {
275
+ const options = resolveOptions(cmd.optsWithGlobals());
276
+ requireGatewayUrl(options);
277
+ requireChainId(options);
278
+ const groupId = cmdOpts["groupId"];
279
+ const { accountView } = await loadAccountView(options, groupId);
280
+ const ctx = await createContext(options);
281
+ if (token) {
282
+ const bal = await ctx.wallet.getBalance(token, {
283
+ account: accountView,
284
+ });
285
+ output(options.json
286
+ ? { token, balance: bal.toString() }
287
+ : `${token}: ${bal.toString()}`, options);
288
+ }
289
+ else {
290
+ const balances = await ctx.wallet.getBalances({
291
+ account: accountView,
292
+ });
293
+ const balMap = balances;
294
+ const entries = Object.entries(balMap).map(([t, b]) => ({
295
+ token: t,
296
+ balance: b.toString(),
297
+ }));
298
+ if (options.json) {
299
+ output({ balances: entries }, options);
300
+ }
301
+ else if (entries.length === 0) {
302
+ process.stdout.write("No balances. Run 'unlink multisig sync --group-id ...' first.\n");
303
+ }
304
+ else {
305
+ for (const { token: t, balance: b } of entries) {
306
+ process.stdout.write(`${t}: ${b}\n`);
307
+ }
308
+ }
309
+ }
310
+ }));
311
+ multisig
312
+ .command("transfer")
313
+ .description("Private transfer from a multisig account (FROST signing)")
314
+ .requiredOption("-g, --group-id <id>", "Multisig group ID")
315
+ .requiredOption("-r, --to <address>", "Recipient Unlink address (0zk1...)")
316
+ .requiredOption("-t, --token <address>", "Token contract address")
317
+ .requiredOption("-a, --amount <value>", "Amount (raw atomic units)")
318
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
319
+ const options = resolveOptions(cmd.optsWithGlobals());
320
+ requireGatewayUrl(options);
321
+ requireChainId(options);
322
+ requirePoolAddress(options);
323
+ const groupId = cmdOpts["groupId"];
324
+ const { account, accountView } = await loadAccountView(options, groupId);
325
+ const ctx = await createContext(options);
326
+ const participants = Array.from({ length: account.config.totalShares }, (_, i) => i + 1);
327
+ const msWallet = createMultisigWallet({
328
+ gatewayUrl: account.gatewayUrl,
329
+ });
330
+ const signer = msWallet.createSigner({ account, participants });
331
+ log("Planning and submitting transfer...", options);
332
+ const result = await ctx.wallet.transfer({
333
+ transfers: [
334
+ {
335
+ token: cmdOpts["token"],
336
+ recipient: cmdOpts["to"],
337
+ amount: BigInt(cmdOpts["amount"]),
338
+ },
339
+ ],
340
+ }, { signer, account: accountView });
341
+ output({ relayId: result.relayId, status: "submitted" }, options);
342
+ }));
343
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerNotesCommands(program: Command): void;
@@ -0,0 +1,28 @@
1
+ import { createContext } from "../lib/context.js";
2
+ import { withErrorHandler } from "../lib/errors.js";
3
+ import { mergeConfigDefaults, requireChainId, requireGatewayUrl, resolveOptions, } from "../lib/options.js";
4
+ import { output } from "../lib/output.js";
5
+ export function registerNotesCommands(program) {
6
+ program
7
+ .command("notes")
8
+ .description("List all notes (UTXO set)")
9
+ .action(withErrorHandler(async (_opts, cmd) => {
10
+ const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
11
+ requireGatewayUrl(options);
12
+ requireChainId(options);
13
+ const ctx = await createContext(options);
14
+ const notes = await ctx.wallet.getNotes();
15
+ if (options.json) {
16
+ output({ notes }, options);
17
+ }
18
+ else if (notes.length === 0) {
19
+ process.stdout.write("No notes. Run 'unlink sync' first.\n");
20
+ }
21
+ else {
22
+ for (const n of notes) {
23
+ const status = n.spentAt != null ? "spent" : "unspent";
24
+ process.stdout.write(`#${String(n.index).padStart(4)} ${status.padEnd(7)} ${n.token} ${n.value}\n`);
25
+ }
26
+ }
27
+ }));
28
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerSyncCommands(program: Command): void;
@@ -0,0 +1,51 @@
1
+ import { createContext } from "../lib/context.js";
2
+ import { withErrorHandler } from "../lib/errors.js";
3
+ import { mergeConfigDefaults, requireChainId, requireGatewayUrl, resolveOptions, } from "../lib/options.js";
4
+ import { log, output } from "../lib/output.js";
5
+ export function registerSyncCommands(program) {
6
+ program
7
+ .command("sync")
8
+ .option("--all", "Sync all accounts (not just active)")
9
+ .description("Sync local state with blockchain")
10
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
11
+ const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
12
+ requireGatewayUrl(options);
13
+ requireChainId(options);
14
+ const ctx = await createContext(options);
15
+ const syncAll = Boolean(cmdOpts["all"]);
16
+ if (syncAll) {
17
+ const accounts = await ctx.wallet.accounts.list();
18
+ if (accounts.length === 0) {
19
+ throw new Error("No accounts found. Create an account first.");
20
+ }
21
+ const originalActive = await ctx.wallet.accounts.getActiveIndex();
22
+ try {
23
+ for (const acct of accounts) {
24
+ await ctx.wallet.accounts.setActive(acct.index);
25
+ log(`Syncing account #${acct.index}...`, options);
26
+ await ctx.wallet.sync();
27
+ }
28
+ }
29
+ finally {
30
+ // Always restore original active account, even if sync fails.
31
+ if (originalActive !== null) {
32
+ await ctx.wallet.accounts.setActive(originalActive);
33
+ }
34
+ }
35
+ output(options.json
36
+ ? {
37
+ synced: true,
38
+ chainId: ctx.wallet.chainId,
39
+ accounts: accounts.length,
40
+ }
41
+ : `Sync complete (${accounts.length} accounts).`, options);
42
+ }
43
+ else {
44
+ log("Syncing...", options);
45
+ await ctx.wallet.sync();
46
+ output(options.json
47
+ ? { synced: true, chainId: ctx.wallet.chainId }
48
+ : "Sync complete.", options);
49
+ }
50
+ }));
51
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerTransferCommands(program: Command): void;
@@ -0,0 +1,47 @@
1
+ import { Option } from "commander";
2
+ import { createContext } from "../lib/context.js";
3
+ import { withErrorHandler } from "../lib/errors.js";
4
+ import { mergeConfigDefaults, requireChainId, requireGatewayUrl, requirePoolAddress, resolveOptions, } from "../lib/options.js";
5
+ import { log, output } from "../lib/output.js";
6
+ import { isRelaySuccess, pollRelayStatus } from "../lib/relay.js";
7
+ export function registerTransferCommands(program) {
8
+ program
9
+ .command("transfer")
10
+ .requiredOption("--to <address>", "Recipient Unlink address (0zk1...)")
11
+ .requiredOption("--token <address>", "Token contract address")
12
+ .requiredOption("--amount <value>", "Amount (raw atomic units)")
13
+ .addOption(new Option("--wait", "Wait for relay confirmation").default(true))
14
+ .addOption(new Option("--no-wait", "Fire-and-forget (do not wait for confirmation)"))
15
+ .description("Private transfer to another Unlink address")
16
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
17
+ const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
18
+ requireGatewayUrl(options);
19
+ requireChainId(options);
20
+ requirePoolAddress(options);
21
+ const ctx = await createContext(options);
22
+ const activeIdx = await ctx.wallet.accounts.getActiveIndex();
23
+ log(`Using account #${activeIdx}`, options);
24
+ log("Planning and submitting transfer...", options);
25
+ const result = await ctx.wallet.transfer({
26
+ transfers: [
27
+ {
28
+ token: cmdOpts["token"],
29
+ recipient: cmdOpts["to"],
30
+ amount: BigInt(cmdOpts["amount"]),
31
+ },
32
+ ],
33
+ });
34
+ log(`Relay ID: ${result.relayId}`, options);
35
+ const shouldWait = cmdOpts["wait"];
36
+ if (shouldWait) {
37
+ const final = await pollRelayStatus(ctx.wallet, result.relayId, options);
38
+ if (!isRelaySuccess(final)) {
39
+ throw new Error(`Transfer relay ${final.relayId} ended with status "${final.status}"${final.error ? `: ${final.error}` : ""}`);
40
+ }
41
+ output(final, options);
42
+ }
43
+ else {
44
+ output({ relayId: result.relayId, status: "submitted" }, options);
45
+ }
46
+ }));
47
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerTxStatusCommands(program: Command): void;
@@ -0,0 +1,31 @@
1
+ import { createContext } from "../lib/context.js";
2
+ import { withErrorHandler } from "../lib/errors.js";
3
+ import { mergeConfigDefaults, requireGatewayUrl, resolveOptions, } from "../lib/options.js";
4
+ import { output } from "../lib/output.js";
5
+ export function registerTxStatusCommands(program) {
6
+ program
7
+ .command("tx-status")
8
+ .argument("<id>", "Transaction/relay ID")
9
+ .description("Check transaction status")
10
+ .action(withErrorHandler(async (txId, _opts, cmd) => {
11
+ const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
12
+ requireGatewayUrl(options);
13
+ const ctx = await createContext(options, { local: true });
14
+ const status = await ctx.wallet.getTxStatus(txId);
15
+ if (options.json) {
16
+ output(status, options);
17
+ }
18
+ else {
19
+ process.stdout.write(`State: ${status.state}\n`);
20
+ if (status.txHash) {
21
+ process.stdout.write(`TxHash: ${status.txHash}\n`);
22
+ }
23
+ if (status.error) {
24
+ process.stdout.write(`Error: ${status.error}\n`);
25
+ }
26
+ if (status.receipt?.blockNumber != null) {
27
+ process.stdout.write(`Block: ${status.receipt.blockNumber}\n`);
28
+ }
29
+ }
30
+ }));
31
+ }
@@ -0,0 +1,2 @@
1
+ import type { Command } from "commander";
2
+ export declare function registerWalletCommands(program: Command): void;
@@ -0,0 +1,98 @@
1
+ import { createContext } from "../lib/context.js";
2
+ import { withErrorHandler } from "../lib/errors.js";
3
+ import { resolveOptions } from "../lib/options.js";
4
+ import { output } from "../lib/output.js";
5
+ function readStdin() {
6
+ return new Promise((resolve, reject) => {
7
+ if (process.stdin.isTTY) {
8
+ resolve("");
9
+ return;
10
+ }
11
+ let data = "";
12
+ process.stdin.setEncoding("utf8");
13
+ process.stdin.on("data", (chunk) => {
14
+ data += chunk;
15
+ });
16
+ process.stdin.on("end", () => resolve(data.trim()));
17
+ process.stdin.on("error", reject);
18
+ });
19
+ }
20
+ export function registerWalletCommands(program) {
21
+ const wallet = program.command("wallet").description("Wallet management");
22
+ wallet
23
+ .command("create")
24
+ .description("Create new wallet and display recovery mnemonic")
25
+ .action(withErrorHandler(async (_opts, cmd) => {
26
+ const options = resolveOptions(cmd.optsWithGlobals());
27
+ const ctx = await createContext(options, { local: true });
28
+ const { mnemonic } = await ctx.wallet.seed.create();
29
+ await ctx.wallet.accounts.create();
30
+ if (options.json) {
31
+ output({ mnemonic, accountIndex: 0 }, options);
32
+ }
33
+ else {
34
+ process.stdout.write("Wallet created successfully!\n");
35
+ process.stdout.write("\nRecovery mnemonic (write this down and store safely):\n\n");
36
+ process.stdout.write(` ${mnemonic}\n\n`);
37
+ process.stdout.write("First account (index 0) created and set as active.\n");
38
+ }
39
+ }));
40
+ wallet
41
+ .command("import")
42
+ .description("Import wallet from BIP-39 mnemonic")
43
+ .option("--mnemonic <words>", "Mnemonic phrase (or pipe via stdin)")
44
+ .option("--overwrite", "Overwrite existing wallet", false)
45
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
46
+ const options = resolveOptions(cmd.optsWithGlobals());
47
+ let mnemonic = cmdOpts["mnemonic"];
48
+ if (!mnemonic) {
49
+ mnemonic = await readStdin();
50
+ }
51
+ if (!mnemonic?.trim()) {
52
+ throw new Error("Mnemonic required. Use --mnemonic or pipe via stdin.");
53
+ }
54
+ const ctx = await createContext(options, { local: true });
55
+ await ctx.wallet.seed.importMnemonic(mnemonic.trim(), {
56
+ overwrite: Boolean(cmdOpts["overwrite"]),
57
+ });
58
+ await ctx.wallet.accounts.create();
59
+ output(options.json
60
+ ? { imported: true, accountIndex: 0 }
61
+ : "Wallet imported successfully. Account 0 created.", options);
62
+ }));
63
+ wallet
64
+ .command("export")
65
+ .description("Export wallet recovery mnemonic")
66
+ .action(withErrorHandler(async (_opts, cmd) => {
67
+ const options = resolveOptions(cmd.optsWithGlobals());
68
+ const ctx = await createContext(options, { local: true });
69
+ const mnemonic = await ctx.wallet.seed.exportMnemonic();
70
+ output(options.json ? { mnemonic } : mnemonic, options);
71
+ }));
72
+ wallet
73
+ .command("delete")
74
+ .description("Delete wallet (irreversible)")
75
+ .option("--yes", "Skip confirmation", false)
76
+ .action(withErrorHandler(async (cmdOpts, cmd) => {
77
+ const options = resolveOptions(cmd.optsWithGlobals());
78
+ if (!cmdOpts["yes"]) {
79
+ throw new Error("Delete wallet? This cannot be undone. Use --yes to confirm.");
80
+ }
81
+ const ctx = await createContext(options, { local: true });
82
+ await ctx.wallet.seed.delete();
83
+ output(options.json ? { deleted: true } : "Wallet deleted.", options);
84
+ }));
85
+ wallet
86
+ .command("status")
87
+ .description("Check if wallet exists")
88
+ .action(withErrorHandler(async (_opts, cmd) => {
89
+ const options = resolveOptions(cmd.optsWithGlobals());
90
+ const ctx = await createContext(options, { local: true });
91
+ const exists = await ctx.wallet.seed.exists();
92
+ output(options.json
93
+ ? { exists }
94
+ : exists
95
+ ? "Wallet exists"
96
+ : "No wallet found", options);
97
+ }));
98
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from "commander";
2
+ export declare function registerWithdrawCommands(program: Command): void;