@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.
- package/README.md +9 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +46 -0
- package/dist/commands/account.d.ts +2 -0
- package/dist/commands/account.js +83 -0
- package/dist/commands/balance.d.ts +2 -0
- package/dist/commands/balance.js +75 -0
- package/dist/commands/burner.d.ts +2 -0
- package/dist/commands/burner.js +114 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +140 -0
- package/dist/commands/deposit.d.ts +2 -0
- package/dist/commands/deposit.js +58 -0
- package/dist/commands/history.d.ts +2 -0
- package/dist/commands/history.js +30 -0
- package/dist/commands/multisig.d.ts +2 -0
- package/dist/commands/multisig.js +343 -0
- package/dist/commands/notes.d.ts +2 -0
- package/dist/commands/notes.js +28 -0
- package/dist/commands/sync.d.ts +2 -0
- package/dist/commands/sync.js +51 -0
- package/dist/commands/transfer.d.ts +2 -0
- package/dist/commands/transfer.js +47 -0
- package/dist/commands/tx-status.d.ts +2 -0
- package/dist/commands/tx-status.js +31 -0
- package/dist/commands/wallet.d.ts +2 -0
- package/dist/commands/wallet.js +98 -0
- package/dist/commands/withdraw.d.ts +2 -0
- package/dist/commands/withdraw.js +47 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +6 -0
- package/dist/lib/context.d.ts +14 -0
- package/dist/lib/context.js +42 -0
- package/dist/lib/errors.d.ts +1 -0
- package/dist/lib/errors.js +24 -0
- package/dist/lib/multisig-store.d.ts +8 -0
- package/dist/lib/multisig-store.js +121 -0
- package/dist/lib/options.d.ts +40 -0
- package/dist/lib/options.js +109 -0
- package/dist/lib/output.d.ts +6 -0
- package/dist/lib/output.js +34 -0
- package/dist/lib/relay.d.ts +18 -0
- package/dist/lib/relay.js +35 -0
- package/dist/lib/tokens.d.ts +14 -0
- package/dist/lib/tokens.js +142 -0
- package/dist/storage/sqlite.d.ts +1 -0
- package/dist/storage/sqlite.js +1 -0
- package/dist/test-utils.d.ts +56 -0
- package/dist/test-utils.js +73 -0
- 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,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,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,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,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,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
|
+
}
|