@unlink-xyz/cli 0.1.4 → 0.1.5
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/dist/commands/deposit.js +2 -3
- package/dist/commands/multisig.d.ts +1 -1
- package/dist/commands/multisig.js +162 -21
- package/package.json +4 -4
package/dist/commands/deposit.js
CHANGED
|
@@ -20,7 +20,8 @@ export function registerDepositCommands(program) {
|
|
|
20
20
|
const token = cmdOpts["token"];
|
|
21
21
|
const ethRpcUrl = options.nodeUrl ?? ctx.gatewayUrl;
|
|
22
22
|
const provider = new ethers.JsonRpcProvider(ethRpcUrl, ctx.wallet.chainId, { staticNetwork: true });
|
|
23
|
-
const
|
|
23
|
+
const wallet = new ethers.Wallet(options.privateKey, provider);
|
|
24
|
+
const signer = new ethers.NonceManager(wallet);
|
|
24
25
|
const depositor = await signer.getAddress();
|
|
25
26
|
const erc20 = new ethers.Contract(token, [
|
|
26
27
|
"function allowance(address,address) view returns (uint256)",
|
|
@@ -38,11 +39,9 @@ export function registerDepositCommands(program) {
|
|
|
38
39
|
deposits: [{ token, amount }],
|
|
39
40
|
});
|
|
40
41
|
log(`Relay ID: ${relay.relayId}\nSubmitting transaction...`, options);
|
|
41
|
-
const nonce = await provider.getTransactionCount(depositor, "pending");
|
|
42
42
|
const tx = await signer.sendTransaction({
|
|
43
43
|
to: relay.to,
|
|
44
44
|
data: relay.calldata,
|
|
45
|
-
nonce,
|
|
46
45
|
});
|
|
47
46
|
log(`Tx hash: ${tx.hash}\nWaiting for confirmation...`, options);
|
|
48
47
|
await tx.wait();
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Command } from "commander";
|
|
2
2
|
export declare function registerMultisigCommands(program: Command): void;
|
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import * as readline from "node:readline";
|
|
1
2
|
import { createMultisigWallet, runSigningListener, signMultisig, } from "@unlink-xyz/multisig";
|
|
3
|
+
import { Option } from "commander";
|
|
2
4
|
import { ethers } from "ethers";
|
|
3
5
|
import { createContext } from "../lib/context.js";
|
|
4
6
|
import { withErrorHandler } from "../lib/errors.js";
|
|
5
7
|
import { listMultisigAccounts, loadMultisigAccount, saveMultisigAccount, } from "../lib/multisig-store.js";
|
|
6
8
|
import { requireChainId, requireGatewayUrl, requirePoolAddress, requirePrivateKey, resolveOptions, } from "../lib/options.js";
|
|
7
9
|
import { log, output } from "../lib/output.js";
|
|
10
|
+
import { isRelaySuccess, pollRelayStatus } from "../lib/relay.js";
|
|
8
11
|
async function loadAccountView(options, groupId) {
|
|
9
12
|
const account = await loadMultisigAccount(options.dataDir, groupId);
|
|
10
13
|
const msWallet = createMultisigWallet({
|
|
@@ -22,6 +25,49 @@ function parseOptionalParticipantIndex(cmdOpts) {
|
|
|
22
25
|
}
|
|
23
26
|
return participantIndex;
|
|
24
27
|
}
|
|
28
|
+
function parseParticipants(raw, config) {
|
|
29
|
+
if (raw === undefined) {
|
|
30
|
+
return Array.from({ length: config.threshold }, (_, i) => i + 1);
|
|
31
|
+
}
|
|
32
|
+
const indices = raw.split(",").map(Number);
|
|
33
|
+
for (const idx of indices) {
|
|
34
|
+
if (!Number.isInteger(idx) || idx < 1 || idx > config.totalShares) {
|
|
35
|
+
throw new Error(`Invalid participant index ${idx}: must be between 1 and ${config.totalShares}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const unique = new Set(indices);
|
|
39
|
+
if (unique.size !== indices.length) {
|
|
40
|
+
throw new Error("Duplicate participant indices are not allowed");
|
|
41
|
+
}
|
|
42
|
+
if (indices.length < config.threshold) {
|
|
43
|
+
throw new Error(`Need at least ${config.threshold} participants (threshold), got ${indices.length}`);
|
|
44
|
+
}
|
|
45
|
+
return indices;
|
|
46
|
+
}
|
|
47
|
+
async function promptApproval(signal) {
|
|
48
|
+
if (signal.aborted)
|
|
49
|
+
return false;
|
|
50
|
+
const rl = readline.createInterface({
|
|
51
|
+
input: process.stdin,
|
|
52
|
+
output: process.stderr,
|
|
53
|
+
});
|
|
54
|
+
return new Promise((resolve) => {
|
|
55
|
+
let settled = false;
|
|
56
|
+
const finish = (approved) => {
|
|
57
|
+
if (settled)
|
|
58
|
+
return;
|
|
59
|
+
settled = true;
|
|
60
|
+
signal.removeEventListener("abort", onAbort);
|
|
61
|
+
rl.close();
|
|
62
|
+
resolve(approved);
|
|
63
|
+
};
|
|
64
|
+
const onAbort = () => finish(false);
|
|
65
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
66
|
+
rl.question("Approve? [y/N] ", (answer) => {
|
|
67
|
+
finish(answer.toLowerCase() === "y");
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
25
71
|
export function registerMultisigCommands(program) {
|
|
26
72
|
const multisig = program
|
|
27
73
|
.command("multisig")
|
|
@@ -172,25 +218,50 @@ export function registerMultisigCommands(program) {
|
|
|
172
218
|
}));
|
|
173
219
|
multisig
|
|
174
220
|
.command("listen")
|
|
175
|
-
.description("Listen for signing sessions and
|
|
221
|
+
.description("Listen for signing sessions and co-sign after approval")
|
|
176
222
|
.requiredOption("-g, --group-id <id>", "Multisig group ID")
|
|
177
223
|
.option("-p, --participant-index <n>", "Participant index override when multiple local participants exist for a group")
|
|
224
|
+
.option("--auto-approve", "Skip approval prompt (auto-sign, for dev/testing)")
|
|
178
225
|
.action(withErrorHandler(async (cmdOpts, cmd) => {
|
|
179
226
|
const options = resolveOptions(cmd.optsWithGlobals());
|
|
180
227
|
const groupId = cmdOpts["groupId"];
|
|
181
228
|
const participantIndex = parseOptionalParticipantIndex(cmdOpts);
|
|
229
|
+
const autoApprove = cmdOpts["autoApprove"] === true;
|
|
182
230
|
const account = await loadMultisigAccount(options.dataDir, groupId, participantIndex);
|
|
183
231
|
log(`Listening for signing sessions on group ${groupId} (participant ${account.participantIndex})...`, options);
|
|
232
|
+
if (autoApprove) {
|
|
233
|
+
log("Auto-approve enabled — signing all sessions automatically.", options);
|
|
234
|
+
}
|
|
184
235
|
log("Press Ctrl+C to stop.", options);
|
|
185
236
|
const controller = new AbortController();
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
const onSigint = () => controller.abort();
|
|
238
|
+
process.on("SIGINT", onSigint);
|
|
239
|
+
try {
|
|
240
|
+
await runSigningListener({
|
|
241
|
+
account,
|
|
242
|
+
signal: controller.signal,
|
|
243
|
+
onSession: (session) => {
|
|
244
|
+
log(`\nSession ${session.code} (message: ${session.message})`, options);
|
|
245
|
+
if (session.metadata) {
|
|
246
|
+
const m = session.metadata;
|
|
247
|
+
if (m["type"])
|
|
248
|
+
log(` Type: ${m["type"]}`, options);
|
|
249
|
+
if (m["recipient"])
|
|
250
|
+
log(` To: ${m["recipient"]}`, options);
|
|
251
|
+
if (m["token"])
|
|
252
|
+
log(` Token: ${m["token"]}`, options);
|
|
253
|
+
if (m["amount"])
|
|
254
|
+
log(` Amount: ${m["amount"]}`, options);
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
approve: autoApprove
|
|
258
|
+
? undefined
|
|
259
|
+
: () => promptApproval(controller.signal),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
process.off("SIGINT", onSigint);
|
|
264
|
+
}
|
|
194
265
|
log("Listener stopped.", options);
|
|
195
266
|
}));
|
|
196
267
|
// --- Pool operations (require createContext for UnlinkWallet) ---
|
|
@@ -213,7 +284,8 @@ export function registerMultisigCommands(program) {
|
|
|
213
284
|
const ctx = await createContext(options);
|
|
214
285
|
const ethRpcUrl = options.nodeUrl ?? ctx.gatewayUrl;
|
|
215
286
|
const provider = new ethers.JsonRpcProvider(ethRpcUrl, ctx.wallet.chainId, { staticNetwork: true });
|
|
216
|
-
const
|
|
287
|
+
const wallet = new ethers.Wallet(options.privateKey, provider);
|
|
288
|
+
const signer = new ethers.NonceManager(wallet);
|
|
217
289
|
const depositor = await signer.getAddress();
|
|
218
290
|
const erc20 = new ethers.Contract(token, [
|
|
219
291
|
"function allowance(address,address) view returns (uint256)",
|
|
@@ -232,11 +304,9 @@ export function registerMultisigCommands(program) {
|
|
|
232
304
|
account: accountView,
|
|
233
305
|
});
|
|
234
306
|
log(`Relay ID: ${relay.relayId}\nSubmitting transaction...`, options);
|
|
235
|
-
const nonce = await provider.getTransactionCount(depositor, "pending");
|
|
236
307
|
const tx = await signer.sendTransaction({
|
|
237
308
|
to: relay.to,
|
|
238
309
|
data: relay.calldata,
|
|
239
|
-
nonce,
|
|
240
310
|
});
|
|
241
311
|
log(`Tx hash: ${tx.hash}\nWaiting for confirmation...`, options);
|
|
242
312
|
await tx.wait();
|
|
@@ -315,6 +385,7 @@ export function registerMultisigCommands(program) {
|
|
|
315
385
|
.requiredOption("-r, --to <address>", "Recipient Unlink address (0zk1...)")
|
|
316
386
|
.requiredOption("-t, --token <address>", "Token contract address")
|
|
317
387
|
.requiredOption("-a, --amount <value>", "Amount (raw atomic units)")
|
|
388
|
+
.option("-P, --participants <indices>", "Comma-separated participant indices for threshold signing (default: first t)")
|
|
318
389
|
.action(withErrorHandler(async (cmdOpts, cmd) => {
|
|
319
390
|
const options = resolveOptions(cmd.optsWithGlobals());
|
|
320
391
|
requireGatewayUrl(options);
|
|
@@ -323,21 +394,91 @@ export function registerMultisigCommands(program) {
|
|
|
323
394
|
const groupId = cmdOpts["groupId"];
|
|
324
395
|
const { account, accountView } = await loadAccountView(options, groupId);
|
|
325
396
|
const ctx = await createContext(options);
|
|
326
|
-
const participants =
|
|
397
|
+
const participants = parseParticipants(cmdOpts["participants"], account.config);
|
|
398
|
+
const token = cmdOpts["token"];
|
|
399
|
+
const recipient = cmdOpts["to"];
|
|
400
|
+
const amount = BigInt(cmdOpts["amount"]);
|
|
327
401
|
const msWallet = createMultisigWallet({
|
|
328
402
|
gatewayUrl: account.gatewayUrl,
|
|
329
403
|
});
|
|
330
|
-
const signer = msWallet.createSigner({
|
|
404
|
+
const signer = msWallet.createSigner({
|
|
405
|
+
account,
|
|
406
|
+
participants,
|
|
407
|
+
metadata: {
|
|
408
|
+
type: "transfer",
|
|
409
|
+
recipient,
|
|
410
|
+
token,
|
|
411
|
+
amount: amount.toString(),
|
|
412
|
+
},
|
|
413
|
+
onSessionCreated: (code) => {
|
|
414
|
+
log(`Signing session created: ${code}`, options);
|
|
415
|
+
const cliName = cmd.parent?.parent?.name() ?? "unlink";
|
|
416
|
+
log(`Co-signers can run:\n ${cliName} multisig sign -g ${groupId} -s ${code} -m <message>`, options);
|
|
417
|
+
log("Waiting for co-signers...", options);
|
|
418
|
+
},
|
|
419
|
+
});
|
|
331
420
|
log("Planning and submitting transfer...", options);
|
|
332
421
|
const result = await ctx.wallet.transfer({
|
|
333
|
-
transfers: [
|
|
334
|
-
{
|
|
335
|
-
token: cmdOpts["token"],
|
|
336
|
-
recipient: cmdOpts["to"],
|
|
337
|
-
amount: BigInt(cmdOpts["amount"]),
|
|
338
|
-
},
|
|
339
|
-
],
|
|
422
|
+
transfers: [{ token, recipient, amount }],
|
|
340
423
|
}, { signer, account: accountView });
|
|
341
424
|
output({ relayId: result.relayId, status: "submitted" }, options);
|
|
342
425
|
}));
|
|
426
|
+
multisig
|
|
427
|
+
.command("withdraw")
|
|
428
|
+
.description("Withdraw from a multisig account to an EOA (FROST signing)")
|
|
429
|
+
.requiredOption("-g, --group-id <id>", "Multisig group ID")
|
|
430
|
+
.requiredOption("--to <address>", "Recipient EOA address (0x...)")
|
|
431
|
+
.requiredOption("-t, --token <address>", "Token contract address")
|
|
432
|
+
.requiredOption("-a, --amount <value>", "Amount (raw atomic units)")
|
|
433
|
+
.option("-P, --participants <indices>", "Comma-separated participant indices for threshold signing (default: first t)")
|
|
434
|
+
.addOption(new Option("--wait", "Wait for relay confirmation").default(true))
|
|
435
|
+
.addOption(new Option("--no-wait", "Fire-and-forget (do not wait for confirmation)"))
|
|
436
|
+
.action(withErrorHandler(async (cmdOpts, cmd) => {
|
|
437
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
438
|
+
requireGatewayUrl(options);
|
|
439
|
+
requireChainId(options);
|
|
440
|
+
requirePoolAddress(options);
|
|
441
|
+
const groupId = cmdOpts["groupId"];
|
|
442
|
+
const { account, accountView } = await loadAccountView(options, groupId);
|
|
443
|
+
const ctx = await createContext(options);
|
|
444
|
+
const participants = parseParticipants(cmdOpts["participants"], account.config);
|
|
445
|
+
const token = cmdOpts["token"];
|
|
446
|
+
const recipient = cmdOpts["to"];
|
|
447
|
+
const amount = BigInt(cmdOpts["amount"]);
|
|
448
|
+
const msWallet = createMultisigWallet({
|
|
449
|
+
gatewayUrl: account.gatewayUrl,
|
|
450
|
+
});
|
|
451
|
+
const signer = msWallet.createSigner({
|
|
452
|
+
account,
|
|
453
|
+
participants,
|
|
454
|
+
metadata: {
|
|
455
|
+
type: "withdraw",
|
|
456
|
+
recipient,
|
|
457
|
+
token,
|
|
458
|
+
amount: amount.toString(),
|
|
459
|
+
},
|
|
460
|
+
onSessionCreated: (code) => {
|
|
461
|
+
log(`Signing session created: ${code}`, options);
|
|
462
|
+
const cliName = cmd.parent?.parent?.name() ?? "unlink";
|
|
463
|
+
log(`Co-signers can run:\n ${cliName} multisig sign -g ${groupId} -s ${code} -m <message>`, options);
|
|
464
|
+
log("Waiting for co-signers...", options);
|
|
465
|
+
},
|
|
466
|
+
});
|
|
467
|
+
log("Planning and submitting withdrawal...", options);
|
|
468
|
+
const result = await ctx.wallet.withdraw({
|
|
469
|
+
withdrawals: [{ token, recipient, amount }],
|
|
470
|
+
}, { signer, account: accountView });
|
|
471
|
+
log(`Relay ID: ${result.relayId}`, options);
|
|
472
|
+
const shouldWait = cmdOpts["wait"];
|
|
473
|
+
if (shouldWait) {
|
|
474
|
+
const final = await pollRelayStatus(ctx.wallet, result.relayId, options);
|
|
475
|
+
if (!isRelaySuccess(final)) {
|
|
476
|
+
throw new Error(`Withdrawal relay ${final.relayId} ended with status "${final.status}"${final.error ? `: ${final.error}` : ""}`);
|
|
477
|
+
}
|
|
478
|
+
output(final, options);
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
output({ relayId: result.relayId, status: "submitted" }, options);
|
|
482
|
+
}
|
|
483
|
+
}));
|
|
343
484
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlink-xyz/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"unlink-cli": "./dist/index.js"
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
"format:check": "prettier . --check"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@unlink-xyz/core": "0.1.
|
|
30
|
-
"@unlink-xyz/multisig": "0.1.
|
|
31
|
-
"@unlink-xyz/node": "0.1.
|
|
29
|
+
"@unlink-xyz/core": "0.1.5",
|
|
30
|
+
"@unlink-xyz/multisig": "0.1.5",
|
|
31
|
+
"@unlink-xyz/node": "0.1.5",
|
|
32
32
|
"commander": "^13.0.0",
|
|
33
33
|
"dotenv": "^16.0.0",
|
|
34
34
|
"ethers": "6.15.0"
|