@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
package/README.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# @unlink-xyz/cli
|
|
2
|
+
|
|
3
|
+
Command-line interface for the Unlink privacy protocol.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
CLI tool for managing wallets, accounts, and performing private deposits, transfers, and withdrawals on EVM blockchains. Uses `@unlink-xyz/core` under the hood with SQLite for local state storage.
|
|
8
|
+
|
|
9
|
+
> **Documentation:** [CLI guide](../../docs/sdk/cli.mdx)
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { Command, Option } from "commander";
|
|
4
|
+
import { registerAccountCommands } from "./commands/account.js";
|
|
5
|
+
import { registerBalanceCommands } from "./commands/balance.js";
|
|
6
|
+
import { registerBurnerCommands } from "./commands/burner.js";
|
|
7
|
+
import { registerConfigCommands } from "./commands/config.js";
|
|
8
|
+
import { registerDepositCommands } from "./commands/deposit.js";
|
|
9
|
+
import { registerHistoryCommands } from "./commands/history.js";
|
|
10
|
+
import { registerMultisigCommands } from "./commands/multisig.js";
|
|
11
|
+
import { registerNotesCommands } from "./commands/notes.js";
|
|
12
|
+
import { registerSyncCommands } from "./commands/sync.js";
|
|
13
|
+
import { registerTransferCommands } from "./commands/transfer.js";
|
|
14
|
+
import { registerTxStatusCommands } from "./commands/tx-status.js";
|
|
15
|
+
import { registerWalletCommands } from "./commands/wallet.js";
|
|
16
|
+
import { registerWithdrawCommands } from "./commands/withdraw.js";
|
|
17
|
+
export function createProgram() {
|
|
18
|
+
const program = new Command("unlink")
|
|
19
|
+
.description("Unlink Protocol CLI — privacy-as-a-service for EVM blockchains")
|
|
20
|
+
.version("0.1.0");
|
|
21
|
+
program
|
|
22
|
+
.addOption(new Option("--gateway-url <url>", "Gateway endpoint URL").env("UNLINK_GATEWAY_URL"))
|
|
23
|
+
.addOption(new Option("--chain-id <id>", "Chain ID").env("UNLINK_CHAIN_ID"))
|
|
24
|
+
.addOption(new Option("--pool-address <addr>", "Pool contract address").env("UNLINK_POOL_ADDRESS"))
|
|
25
|
+
.addOption(new Option("--data-dir <path>", "Data directory")
|
|
26
|
+
.env("UNLINK_DATA_DIR")
|
|
27
|
+
.default(path.join(os.homedir(), ".unlink")))
|
|
28
|
+
.addOption(new Option("--node-url <url>", "Ethereum JSON-RPC URL for on-chain transactions (defaults to --gateway-url)").env("UNLINK_RPC_HTTP_URL"))
|
|
29
|
+
.addOption(new Option("--json", "Output JSON").default(false))
|
|
30
|
+
.addOption(new Option("--private-key <key>", "Private key for signing").env("UNLINK_PRIVATE_KEY"))
|
|
31
|
+
.addOption(new Option("--artifact-version <version>", "ZK artifact version for proof generation").env("UNLINK_ARTIFACT_VERSION"));
|
|
32
|
+
registerWalletCommands(program);
|
|
33
|
+
registerAccountCommands(program);
|
|
34
|
+
registerBalanceCommands(program);
|
|
35
|
+
registerBurnerCommands(program);
|
|
36
|
+
registerDepositCommands(program);
|
|
37
|
+
registerTransferCommands(program);
|
|
38
|
+
registerWithdrawCommands(program);
|
|
39
|
+
registerSyncCommands(program);
|
|
40
|
+
registerTxStatusCommands(program);
|
|
41
|
+
registerHistoryCommands(program);
|
|
42
|
+
registerNotesCommands(program);
|
|
43
|
+
registerConfigCommands(program);
|
|
44
|
+
registerMultisigCommands(program);
|
|
45
|
+
return program;
|
|
46
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createContext } from "../lib/context.js";
|
|
2
|
+
import { withErrorHandler } from "../lib/errors.js";
|
|
3
|
+
import { parseIndex, resolveOptions } from "../lib/options.js";
|
|
4
|
+
import { output } from "../lib/output.js";
|
|
5
|
+
export function registerAccountCommands(program) {
|
|
6
|
+
const account = program.command("account").description("Account management");
|
|
7
|
+
account
|
|
8
|
+
.command("list")
|
|
9
|
+
.description("List all derived accounts")
|
|
10
|
+
.action(withErrorHandler(async (_opts, cmd) => {
|
|
11
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
12
|
+
const ctx = await createContext(options, { local: true });
|
|
13
|
+
const accounts = await ctx.wallet.accounts.list();
|
|
14
|
+
const activeIndex = await ctx.wallet.accounts.getActiveIndex();
|
|
15
|
+
if (options.json) {
|
|
16
|
+
output({
|
|
17
|
+
accounts: accounts.map((a) => ({
|
|
18
|
+
index: a.index,
|
|
19
|
+
address: a.address,
|
|
20
|
+
masterPublicKey: a.masterPublicKey.toString(),
|
|
21
|
+
})),
|
|
22
|
+
activeIndex,
|
|
23
|
+
}, options);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
if (accounts.length === 0) {
|
|
27
|
+
process.stdout.write("No accounts. Create one with: unlink account create\n");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
for (const a of accounts) {
|
|
31
|
+
const marker = a.index === activeIndex ? " (active)" : "";
|
|
32
|
+
process.stdout.write(`#${a.index}${marker} ${a.address}\n`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}));
|
|
36
|
+
account
|
|
37
|
+
.command("create")
|
|
38
|
+
.description("Derive and persist a new account")
|
|
39
|
+
.action(withErrorHandler(async (_opts, cmd) => {
|
|
40
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
41
|
+
const ctx = await createContext(options, { local: true });
|
|
42
|
+
const acct = await ctx.wallet.accounts.create();
|
|
43
|
+
const list = await ctx.wallet.accounts.list();
|
|
44
|
+
const info = list.find((a) => a.masterPublicKey === acct.masterPublicKey);
|
|
45
|
+
output(options.json
|
|
46
|
+
? {
|
|
47
|
+
index: info?.index,
|
|
48
|
+
address: acct.address,
|
|
49
|
+
masterPublicKey: acct.masterPublicKey.toString(),
|
|
50
|
+
}
|
|
51
|
+
: `Account #${info?.index} created: ${acct.address}`, options);
|
|
52
|
+
}));
|
|
53
|
+
account
|
|
54
|
+
.command("active")
|
|
55
|
+
.description("Show active account")
|
|
56
|
+
.action(withErrorHandler(async (_opts, cmd) => {
|
|
57
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
58
|
+
const ctx = await createContext(options, { local: true });
|
|
59
|
+
const idx = await ctx.wallet.accounts.getActiveIndex();
|
|
60
|
+
if (idx === null) {
|
|
61
|
+
output(options.json ? { active: null } : "No active account", options);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const list = await ctx.wallet.accounts.list();
|
|
65
|
+
const active = list.find((a) => a.index === idx);
|
|
66
|
+
output(options.json
|
|
67
|
+
? { index: idx, address: active?.address }
|
|
68
|
+
: `#${idx} ${active?.address}`, options);
|
|
69
|
+
}));
|
|
70
|
+
account
|
|
71
|
+
.command("switch")
|
|
72
|
+
.argument("<index>", "Account index")
|
|
73
|
+
.description("Switch active account")
|
|
74
|
+
.action(withErrorHandler(async (indexStr, _opts, cmd) => {
|
|
75
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
76
|
+
const index = parseIndex(indexStr);
|
|
77
|
+
const ctx = await createContext(options, { local: true });
|
|
78
|
+
await ctx.wallet.accounts.setActive(index);
|
|
79
|
+
output(options.json
|
|
80
|
+
? { switched: true, index }
|
|
81
|
+
: `Switched to account #${index}`, options);
|
|
82
|
+
}));
|
|
83
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
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
|
+
import { formatTokenAmount, resolveTokenDecimals, resolveTokenSymbol, } from "../lib/tokens.js";
|
|
6
|
+
export function registerBalanceCommands(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("balance")
|
|
9
|
+
.argument("[token]", "Token address (omit for all)")
|
|
10
|
+
.description("Show token balances")
|
|
11
|
+
.action(withErrorHandler(async (token, _opts, cmd) => {
|
|
12
|
+
const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
|
|
13
|
+
requireGatewayUrl(options);
|
|
14
|
+
requireChainId(options);
|
|
15
|
+
const ctx = await createContext(options);
|
|
16
|
+
// Show active account header in human-readable mode
|
|
17
|
+
const activeIdx = await ctx.wallet.accounts.getActiveIndex();
|
|
18
|
+
const activeAcct = await ctx.wallet.accounts.getActive();
|
|
19
|
+
const addressTag = activeAcct
|
|
20
|
+
? ` (${truncateZkAddress(activeAcct.address)})`
|
|
21
|
+
: "";
|
|
22
|
+
if (token) {
|
|
23
|
+
const bal = await ctx.wallet.getBalance(token);
|
|
24
|
+
if (options.json) {
|
|
25
|
+
output({ token, balance: bal.toString() }, options);
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
const label = options.nodeUrl
|
|
29
|
+
? await resolveTokenSymbol(options.nodeUrl, token)
|
|
30
|
+
: token;
|
|
31
|
+
const decimals = options.nodeUrl
|
|
32
|
+
? await resolveTokenDecimals(options.nodeUrl, token)
|
|
33
|
+
: undefined;
|
|
34
|
+
process.stdout.write(`Account #${activeIdx}${addressTag}\n`);
|
|
35
|
+
process.stdout.write(`${label}: ${formatTokenAmount(bal, decimals)}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
const balances = await ctx.wallet.getBalances();
|
|
40
|
+
const balMap = balances;
|
|
41
|
+
const entries = Object.entries(balMap).map(([t, b]) => ({
|
|
42
|
+
token: t,
|
|
43
|
+
balance: b,
|
|
44
|
+
}));
|
|
45
|
+
if (options.json) {
|
|
46
|
+
output({
|
|
47
|
+
balances: entries.map((e) => ({
|
|
48
|
+
token: e.token,
|
|
49
|
+
balance: e.balance.toString(),
|
|
50
|
+
})),
|
|
51
|
+
}, options);
|
|
52
|
+
}
|
|
53
|
+
else if (entries.length === 0) {
|
|
54
|
+
process.stdout.write("No balances. Run 'unlink sync' first.\n");
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
process.stdout.write(`Account #${activeIdx}${addressTag}\n`);
|
|
58
|
+
for (const { token: t, balance: b } of entries) {
|
|
59
|
+
const label = options.nodeUrl
|
|
60
|
+
? await resolveTokenSymbol(options.nodeUrl, t)
|
|
61
|
+
: t;
|
|
62
|
+
const decimals = options.nodeUrl
|
|
63
|
+
? await resolveTokenDecimals(options.nodeUrl, t)
|
|
64
|
+
: undefined;
|
|
65
|
+
process.stdout.write(`${label}: ${formatTokenAmount(b, decimals)}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}));
|
|
70
|
+
}
|
|
71
|
+
function truncateZkAddress(addr) {
|
|
72
|
+
if (addr.length <= 16)
|
|
73
|
+
return addr;
|
|
74
|
+
return `${addr.slice(0, 8)}...${addr.slice(-4)}`;
|
|
75
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createContext } from "../lib/context.js";
|
|
2
|
+
import { withErrorHandler } from "../lib/errors.js";
|
|
3
|
+
import { mergeConfigDefaults, parseIndex, requireChainId, requireGatewayUrl, requirePoolAddress, resolveOptions, } from "../lib/options.js";
|
|
4
|
+
import { log, output } from "../lib/output.js";
|
|
5
|
+
export function registerBurnerCommands(program) {
|
|
6
|
+
const burner = program
|
|
7
|
+
.command("burner")
|
|
8
|
+
.description("Manage burner accounts for DeFi interactions");
|
|
9
|
+
burner
|
|
10
|
+
.command("address")
|
|
11
|
+
.argument("<index>", "BIP-44 derivation index")
|
|
12
|
+
.description("Derive burner address at a given index")
|
|
13
|
+
.action(withErrorHandler(async (indexArg, _opts, cmd) => {
|
|
14
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
15
|
+
const ctx = await createContext(options, { local: true });
|
|
16
|
+
const index = parseIndex(indexArg);
|
|
17
|
+
const account = await ctx.wallet.burner.addressOf(index);
|
|
18
|
+
output(account, options);
|
|
19
|
+
}));
|
|
20
|
+
burner
|
|
21
|
+
.command("fund")
|
|
22
|
+
.argument("<index>", "BIP-44 derivation index")
|
|
23
|
+
.requiredOption("--token <address>", "Token contract address")
|
|
24
|
+
.requiredOption("--amount <value>", "Amount (raw atomic units)")
|
|
25
|
+
.description("Withdraw from shielded pool to fund a burner")
|
|
26
|
+
.action(withErrorHandler(async (indexArg, cmdOpts, cmd) => {
|
|
27
|
+
const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
|
|
28
|
+
requireGatewayUrl(options);
|
|
29
|
+
requireChainId(options);
|
|
30
|
+
requirePoolAddress(options);
|
|
31
|
+
const ctx = await createContext(options);
|
|
32
|
+
const index = parseIndex(indexArg);
|
|
33
|
+
log("Funding burner from shielded pool...", options);
|
|
34
|
+
const result = await ctx.wallet.burner.fund(index, {
|
|
35
|
+
token: cmdOpts["token"],
|
|
36
|
+
amount: BigInt(cmdOpts["amount"]),
|
|
37
|
+
});
|
|
38
|
+
output({ relayId: result.relayId, status: "submitted" }, options);
|
|
39
|
+
}));
|
|
40
|
+
burner
|
|
41
|
+
.command("send")
|
|
42
|
+
.argument("<index>", "BIP-44 derivation index")
|
|
43
|
+
.requiredOption("--to <address>", "Destination address")
|
|
44
|
+
.option("--data <hex>", "Transaction calldata (hex)")
|
|
45
|
+
.option("--value <amount>", "ETH value in wei")
|
|
46
|
+
.option("--gas-limit <limit>", "Gas limit")
|
|
47
|
+
.description("Send a transaction from a burner account")
|
|
48
|
+
.action(withErrorHandler(async (indexArg, cmdOpts, cmd) => {
|
|
49
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
50
|
+
const ctx = await createContext(options, { local: true });
|
|
51
|
+
const index = parseIndex(indexArg);
|
|
52
|
+
log("Sending transaction from burner...", options);
|
|
53
|
+
const result = await ctx.wallet.burner.send(index, {
|
|
54
|
+
to: cmdOpts["to"],
|
|
55
|
+
data: cmdOpts["data"] ?? undefined,
|
|
56
|
+
value: cmdOpts["value"]
|
|
57
|
+
? BigInt(cmdOpts["value"])
|
|
58
|
+
: undefined,
|
|
59
|
+
gasLimit: cmdOpts["gasLimit"]
|
|
60
|
+
? BigInt(cmdOpts["gasLimit"])
|
|
61
|
+
: undefined,
|
|
62
|
+
});
|
|
63
|
+
output(result, options);
|
|
64
|
+
}));
|
|
65
|
+
burner
|
|
66
|
+
.command("sweep")
|
|
67
|
+
.argument("<index>", "BIP-44 derivation index")
|
|
68
|
+
.requiredOption("--token <address>", "Token contract address")
|
|
69
|
+
.option("--amount <value>", "Amount to sweep (omit to sweep full balance)")
|
|
70
|
+
.description("Sweep burner balance back to shielded pool")
|
|
71
|
+
.action(withErrorHandler(async (indexArg, cmdOpts, cmd) => {
|
|
72
|
+
const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
|
|
73
|
+
requireGatewayUrl(options);
|
|
74
|
+
requireChainId(options);
|
|
75
|
+
requirePoolAddress(options);
|
|
76
|
+
const ctx = await createContext(options);
|
|
77
|
+
const index = parseIndex(indexArg);
|
|
78
|
+
log("Sweeping burner balance to pool...", options);
|
|
79
|
+
const result = await ctx.wallet.burner.sweepToPool(index, {
|
|
80
|
+
token: cmdOpts["token"],
|
|
81
|
+
amount: cmdOpts["amount"]
|
|
82
|
+
? BigInt(cmdOpts["amount"])
|
|
83
|
+
: undefined,
|
|
84
|
+
});
|
|
85
|
+
output(result, options);
|
|
86
|
+
}));
|
|
87
|
+
burner
|
|
88
|
+
.command("export-key")
|
|
89
|
+
.argument("<index>", "BIP-44 derivation index")
|
|
90
|
+
.description("Export burner private key for wallet import")
|
|
91
|
+
.action(withErrorHandler(async (indexArg, _opts, cmd) => {
|
|
92
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
93
|
+
const ctx = await createContext(options, { local: true });
|
|
94
|
+
const index = parseIndex(indexArg);
|
|
95
|
+
const key = await ctx.wallet.burner.exportKey(index);
|
|
96
|
+
output(options.json ? { privateKey: key } : key, options);
|
|
97
|
+
}));
|
|
98
|
+
burner
|
|
99
|
+
.command("balance")
|
|
100
|
+
.argument("<address>", "Burner or EOA address")
|
|
101
|
+
.option("--token <address>", "ERC-20 token address (omit for native ETH)")
|
|
102
|
+
.description("Check burner ETH or token balance")
|
|
103
|
+
.action(withErrorHandler(async (address, cmdOpts, cmd) => {
|
|
104
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
105
|
+
const ctx = await createContext(options, { local: true });
|
|
106
|
+
const token = cmdOpts["token"];
|
|
107
|
+
const balance = token
|
|
108
|
+
? await ctx.wallet.burner.getTokenBalance(address, token)
|
|
109
|
+
: await ctx.wallet.burner.getBalance(address);
|
|
110
|
+
output(options.json
|
|
111
|
+
? { address, token: token ?? "ETH", balance: balance.toString() }
|
|
112
|
+
: `${token ?? "ETH"}: ${balance.toString()}`, options);
|
|
113
|
+
}));
|
|
114
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import readline from "node:readline/promises";
|
|
2
|
+
import { withErrorHandler } from "../lib/errors.js";
|
|
3
|
+
import { loadConfigFile, mergeConfigDefaults, resolveOptions, saveConfigFile, } from "../lib/options.js";
|
|
4
|
+
import { log, output } from "../lib/output.js";
|
|
5
|
+
const VALID_CONFIG_KEYS = [
|
|
6
|
+
"gatewayUrl",
|
|
7
|
+
"chainId",
|
|
8
|
+
"poolAddress",
|
|
9
|
+
"nodeUrl",
|
|
10
|
+
"artifactVersion",
|
|
11
|
+
];
|
|
12
|
+
export function registerConfigCommands(program) {
|
|
13
|
+
const config = program.command("config").description("Configuration");
|
|
14
|
+
config
|
|
15
|
+
.command("show")
|
|
16
|
+
.description("Show current configuration (with value sources)")
|
|
17
|
+
.action(withErrorHandler(async (_opts, cmd) => {
|
|
18
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
19
|
+
const fileConfig = loadConfigFile(options.dataDir);
|
|
20
|
+
const merged = mergeConfigDefaults(options);
|
|
21
|
+
function source(key, cliValue) {
|
|
22
|
+
if (cliValue !== undefined)
|
|
23
|
+
return "flag/env";
|
|
24
|
+
if (fileConfig[key] !== undefined)
|
|
25
|
+
return "config";
|
|
26
|
+
return "";
|
|
27
|
+
}
|
|
28
|
+
const rows = [
|
|
29
|
+
{
|
|
30
|
+
key: "Gateway URL",
|
|
31
|
+
value: merged.gatewayUrl ?? "(not set)",
|
|
32
|
+
src: source("gatewayUrl", options.gatewayUrl),
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: "Chain ID",
|
|
36
|
+
value: merged.chainId ?? "(not set)",
|
|
37
|
+
src: source("chainId", options.chainId),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
key: "Pool Address",
|
|
41
|
+
value: merged.poolAddress ?? "(not set)",
|
|
42
|
+
src: source("poolAddress", options.poolAddress),
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
key: "Node URL",
|
|
46
|
+
value: merged.nodeUrl ?? "(not set)",
|
|
47
|
+
src: source("nodeUrl", options.nodeUrl),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "Artifact Ver",
|
|
51
|
+
value: merged.artifactVersion ?? "(not set)",
|
|
52
|
+
src: source("artifactVersion", options.artifactVersion),
|
|
53
|
+
},
|
|
54
|
+
{ key: "Data Dir", value: merged.dataDir, src: "" },
|
|
55
|
+
];
|
|
56
|
+
if (options.json) {
|
|
57
|
+
output({
|
|
58
|
+
gatewayUrl: merged.gatewayUrl ?? null,
|
|
59
|
+
chainId: merged.chainId ?? null,
|
|
60
|
+
poolAddress: merged.poolAddress ?? null,
|
|
61
|
+
nodeUrl: merged.nodeUrl ?? null,
|
|
62
|
+
artifactVersion: merged.artifactVersion ?? null,
|
|
63
|
+
dataDir: merged.dataDir,
|
|
64
|
+
}, options);
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
for (const row of rows) {
|
|
68
|
+
const srcTag = row.src ? ` [${row.src}]` : "";
|
|
69
|
+
process.stdout.write(`${row.key.padEnd(14)} ${row.value}${srcTag}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}));
|
|
73
|
+
config
|
|
74
|
+
.command("init")
|
|
75
|
+
.description("Initialize connection config (interactive)")
|
|
76
|
+
.action(withErrorHandler(async (_opts, cmd) => {
|
|
77
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
78
|
+
const existing = loadConfigFile(options.dataDir);
|
|
79
|
+
const rl = readline.createInterface({
|
|
80
|
+
input: process.stdin,
|
|
81
|
+
output: process.stderr,
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const gatewayUrl = (await rl.question(`Gateway URL [${existing.gatewayUrl ?? ""}]: `)) || existing.gatewayUrl;
|
|
85
|
+
const chainIdStr = (await rl.question(`Chain ID [${existing.chainId ?? ""}]: `)) ||
|
|
86
|
+
String(existing.chainId ?? "");
|
|
87
|
+
const poolAddress = (await rl.question(`Pool Address [${existing.poolAddress ?? ""}]: `)) || existing.poolAddress;
|
|
88
|
+
const nodeUrl = (await rl.question(`Node URL [${existing.nodeUrl ?? ""}]: `)) ||
|
|
89
|
+
existing.nodeUrl;
|
|
90
|
+
const artifactVersion = (await rl.question(`Artifact Version [${existing.artifactVersion ?? ""}]: `)) || existing.artifactVersion;
|
|
91
|
+
const newConfig = {};
|
|
92
|
+
if (gatewayUrl)
|
|
93
|
+
newConfig.gatewayUrl = gatewayUrl;
|
|
94
|
+
if (chainIdStr)
|
|
95
|
+
newConfig.chainId = Number(chainIdStr);
|
|
96
|
+
if (poolAddress)
|
|
97
|
+
newConfig.poolAddress = poolAddress;
|
|
98
|
+
if (nodeUrl)
|
|
99
|
+
newConfig.nodeUrl = nodeUrl;
|
|
100
|
+
if (artifactVersion)
|
|
101
|
+
newConfig.artifactVersion = artifactVersion;
|
|
102
|
+
saveConfigFile(options.dataDir, newConfig);
|
|
103
|
+
log(`Config saved to ${options.dataDir}/config.json`, options);
|
|
104
|
+
if (options.json) {
|
|
105
|
+
output(newConfig, options);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
rl.close();
|
|
110
|
+
}
|
|
111
|
+
}));
|
|
112
|
+
config
|
|
113
|
+
.command("set")
|
|
114
|
+
.argument("<key>", `Config key (${VALID_CONFIG_KEYS.join(", ")})`)
|
|
115
|
+
.argument("<value>", "Config value")
|
|
116
|
+
.description("Set a single config value")
|
|
117
|
+
.action(withErrorHandler(async (key, value, _opts, cmd) => {
|
|
118
|
+
const options = resolveOptions(cmd.optsWithGlobals());
|
|
119
|
+
if (!VALID_CONFIG_KEYS.includes(key)) {
|
|
120
|
+
throw new Error(`Invalid config key "${key}". Valid keys: ${VALID_CONFIG_KEYS.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
const existing = loadConfigFile(options.dataDir);
|
|
123
|
+
const updated = { ...existing };
|
|
124
|
+
if (key === "chainId") {
|
|
125
|
+
const num = Number(value);
|
|
126
|
+
if (Number.isNaN(num) || !Number.isInteger(num) || num < 0) {
|
|
127
|
+
throw new Error("chainId must be a non-negative integer");
|
|
128
|
+
}
|
|
129
|
+
updated.chainId = num;
|
|
130
|
+
}
|
|
131
|
+
else {
|
|
132
|
+
updated[key] = value;
|
|
133
|
+
}
|
|
134
|
+
saveConfigFile(options.dataDir, updated);
|
|
135
|
+
log(`Set ${key} = ${value}`, options);
|
|
136
|
+
if (options.json) {
|
|
137
|
+
output(updated, options);
|
|
138
|
+
}
|
|
139
|
+
}));
|
|
140
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ethers } from "ethers";
|
|
2
|
+
import { createContext } from "../lib/context.js";
|
|
3
|
+
import { withErrorHandler } from "../lib/errors.js";
|
|
4
|
+
import { mergeConfigDefaults, requireChainId, requireGatewayUrl, requirePoolAddress, requirePrivateKey, resolveOptions, } from "../lib/options.js";
|
|
5
|
+
import { log, output } from "../lib/output.js";
|
|
6
|
+
export function registerDepositCommands(program) {
|
|
7
|
+
program
|
|
8
|
+
.command("deposit")
|
|
9
|
+
.requiredOption("--token <address>", "Token contract address")
|
|
10
|
+
.requiredOption("--amount <value>", "Amount (raw atomic units)")
|
|
11
|
+
.description("Deposit tokens into the privacy pool")
|
|
12
|
+
.action(withErrorHandler(async (cmdOpts, cmd) => {
|
|
13
|
+
const options = mergeConfigDefaults(resolveOptions(cmd.optsWithGlobals()));
|
|
14
|
+
requireGatewayUrl(options);
|
|
15
|
+
requireChainId(options);
|
|
16
|
+
requirePoolAddress(options);
|
|
17
|
+
requirePrivateKey(options);
|
|
18
|
+
const ctx = await createContext(options);
|
|
19
|
+
const amount = BigInt(cmdOpts["amount"]);
|
|
20
|
+
const token = cmdOpts["token"];
|
|
21
|
+
const ethRpcUrl = options.nodeUrl ?? ctx.gatewayUrl;
|
|
22
|
+
const provider = new ethers.JsonRpcProvider(ethRpcUrl, ctx.wallet.chainId, { staticNetwork: true });
|
|
23
|
+
const signer = new ethers.Wallet(options.privateKey, provider);
|
|
24
|
+
const depositor = await signer.getAddress();
|
|
25
|
+
const erc20 = new ethers.Contract(token, [
|
|
26
|
+
"function allowance(address,address) view returns (uint256)",
|
|
27
|
+
"function approve(address,uint256) returns (bool)",
|
|
28
|
+
], signer);
|
|
29
|
+
const allowance = (await erc20.getFunction("allowance")(depositor, options.poolAddress));
|
|
30
|
+
if (allowance < amount) {
|
|
31
|
+
log("Approving token spend...", options);
|
|
32
|
+
const approveTx = (await erc20.getFunction("approve")(options.poolAddress, amount));
|
|
33
|
+
await approveTx.wait();
|
|
34
|
+
}
|
|
35
|
+
log("Preparing deposit...", options);
|
|
36
|
+
const relay = await ctx.wallet.deposit({
|
|
37
|
+
depositor,
|
|
38
|
+
deposits: [{ token, amount }],
|
|
39
|
+
});
|
|
40
|
+
log(`Relay ID: ${relay.relayId}\nSubmitting transaction...`, options);
|
|
41
|
+
const nonce = await provider.getTransactionCount(depositor, "pending");
|
|
42
|
+
const tx = await signer.sendTransaction({
|
|
43
|
+
to: relay.to,
|
|
44
|
+
data: relay.calldata,
|
|
45
|
+
nonce,
|
|
46
|
+
});
|
|
47
|
+
log(`Tx hash: ${tx.hash}\nWaiting for confirmation...`, options);
|
|
48
|
+
await tx.wait();
|
|
49
|
+
log("Reconciling deposit...", options);
|
|
50
|
+
const result = await ctx.wallet.confirmDeposit(relay.relayId);
|
|
51
|
+
output({
|
|
52
|
+
relayId: relay.relayId,
|
|
53
|
+
txHash: tx.hash,
|
|
54
|
+
status: "confirmed",
|
|
55
|
+
commitments: result.commitments,
|
|
56
|
+
}, options);
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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 registerHistoryCommands(program) {
|
|
6
|
+
program
|
|
7
|
+
.command("history")
|
|
8
|
+
.description("Show transaction history")
|
|
9
|
+
.option("--include-self-sends", "Include self-sends", false)
|
|
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 entries = await ctx.wallet.getHistory({
|
|
16
|
+
includeSelfSends: Boolean(cmdOpts["includeSelfSends"]),
|
|
17
|
+
});
|
|
18
|
+
if (options.json) {
|
|
19
|
+
output({ entries }, options);
|
|
20
|
+
}
|
|
21
|
+
else if (entries.length === 0) {
|
|
22
|
+
process.stdout.write("No transaction history.\n");
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
for (const e of entries) {
|
|
26
|
+
process.stdout.write(`${e.kind} ${e.txHash ?? "pending"}\n`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}));
|
|
30
|
+
}
|