@unionlabs/payments 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/LICENSE +1 -0
- package/dist/LICENSE +1 -0
- package/dist/chunk-37PNLRA6.js +2418 -0
- package/dist/cli.cjs +3031 -0
- package/dist/cli.js +675 -0
- package/dist/index.cjs +2451 -0
- package/dist/index.js +1 -0
- package/dist/package.json +18 -0
- package/dist/payments.d.ts +835 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +60 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,675 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { generatePaymentKey, computeUnspendableAddress, computeNullifier, UnionPrivatePayments, parseProofJson, AttestationClient, proofJsonToRedeemParams, submitRedeem, updateLoopbackClient, depositToZAsset, RpcClient, ProverClient } from './chunk-37PNLRA6.js';
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { writeFileSync, readFileSync, existsSync } from 'fs';
|
|
5
|
+
import { createWalletClient, http, createPublicClient } from 'viem';
|
|
6
|
+
import { polygon, optimism, arbitrum, base, mainnet } from 'viem/chains';
|
|
7
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
8
|
+
|
|
9
|
+
var DEFAULT_CONFIG_PATH = "./private-payments.config.json";
|
|
10
|
+
function loadConfig(configPath) {
|
|
11
|
+
const path = configPath ?? DEFAULT_CONFIG_PATH;
|
|
12
|
+
if (!existsSync(path)) {
|
|
13
|
+
throw new Error(`Config file not found: ${path}
|
|
14
|
+
Create a private-payments.config.json file or specify --config <path>`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const content = readFileSync(path, "utf-8");
|
|
18
|
+
const config = JSON.parse(content);
|
|
19
|
+
const required = [
|
|
20
|
+
"proverUrl",
|
|
21
|
+
"sourceRpcUrl",
|
|
22
|
+
"destinationRpcUrl",
|
|
23
|
+
"srcZAssetAddress",
|
|
24
|
+
"dstZAssetAddress",
|
|
25
|
+
"sourceChainId",
|
|
26
|
+
"destinationChainId"
|
|
27
|
+
];
|
|
28
|
+
for (const field of required) {
|
|
29
|
+
if (config[field] === void 0) {
|
|
30
|
+
throw new Error(`Missing required field in config: ${field}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return config;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
if (e instanceof SyntaxError) {
|
|
36
|
+
throw new Error(`Invalid JSON in config file: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
throw e;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// src/cli/utils.ts
|
|
43
|
+
var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
|
|
44
|
+
function parseBeneficiaries(input) {
|
|
45
|
+
if (!input || input.trim() === "") {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const addresses = input.split(",").map((s) => s.trim().toLowerCase());
|
|
49
|
+
if (addresses.length > 4) {
|
|
50
|
+
throw new Error("Maximum 4 beneficiaries allowed");
|
|
51
|
+
}
|
|
52
|
+
for (const addr of addresses) {
|
|
53
|
+
if (!isValidAddress(addr)) {
|
|
54
|
+
throw new Error(`Invalid address: ${addr}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return addresses;
|
|
58
|
+
}
|
|
59
|
+
function padBeneficiaries(beneficiaries) {
|
|
60
|
+
return [
|
|
61
|
+
beneficiaries[0] ?? ZERO_ADDRESS,
|
|
62
|
+
beneficiaries[1] ?? ZERO_ADDRESS,
|
|
63
|
+
beneficiaries[2] ?? ZERO_ADDRESS,
|
|
64
|
+
beneficiaries[3] ?? ZERO_ADDRESS
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
function parseClientIds(input) {
|
|
68
|
+
const ids = input.split(",").map((s) => {
|
|
69
|
+
const id = parseInt(s.trim(), 10);
|
|
70
|
+
if (isNaN(id) || id < 0) {
|
|
71
|
+
throw new Error(`Invalid client ID: ${s}`);
|
|
72
|
+
}
|
|
73
|
+
return id;
|
|
74
|
+
});
|
|
75
|
+
if (ids.length === 0) {
|
|
76
|
+
throw new Error("At least one client ID is required");
|
|
77
|
+
}
|
|
78
|
+
return ids;
|
|
79
|
+
}
|
|
80
|
+
function validateSecret(secret) {
|
|
81
|
+
if (!secret.startsWith("0x")) {
|
|
82
|
+
throw new Error("Secret must start with 0x");
|
|
83
|
+
}
|
|
84
|
+
if (secret.length !== 66) {
|
|
85
|
+
throw new Error("Secret must be 32 bytes (66 characters with 0x prefix)");
|
|
86
|
+
}
|
|
87
|
+
if (!/^0x[0-9a-fA-F]{64}$/.test(secret)) {
|
|
88
|
+
throw new Error("Secret must be a valid hex string");
|
|
89
|
+
}
|
|
90
|
+
return secret.toLowerCase();
|
|
91
|
+
}
|
|
92
|
+
function isValidAddress(address) {
|
|
93
|
+
return /^0x[0-9a-fA-F]{40}$/.test(address);
|
|
94
|
+
}
|
|
95
|
+
function validateAddress(address, name) {
|
|
96
|
+
if (!isValidAddress(address)) {
|
|
97
|
+
throw new Error(`Invalid ${name} address: ${address}`);
|
|
98
|
+
}
|
|
99
|
+
return address.toLowerCase();
|
|
100
|
+
}
|
|
101
|
+
function formatAmount(amount, decimals, symbol) {
|
|
102
|
+
const divisor = 10n ** BigInt(decimals);
|
|
103
|
+
const whole = amount / divisor;
|
|
104
|
+
const fraction = amount % divisor;
|
|
105
|
+
const fractionStr = fraction.toString().padStart(decimals, "0");
|
|
106
|
+
const trimmedFraction = fractionStr.replace(/0+$/, "");
|
|
107
|
+
const formatted = trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString();
|
|
108
|
+
return symbol ? `${formatted} ${symbol}` : formatted;
|
|
109
|
+
}
|
|
110
|
+
function getExplorerUrl(chainId, txHash) {
|
|
111
|
+
const explorers = {
|
|
112
|
+
1: "https://etherscan.io",
|
|
113
|
+
8453: "https://basescan.org",
|
|
114
|
+
42161: "https://arbiscan.io",
|
|
115
|
+
10: "https://optimistic.etherscan.io",
|
|
116
|
+
137: "https://polygonscan.com"
|
|
117
|
+
};
|
|
118
|
+
const explorer = explorers[chainId];
|
|
119
|
+
if (explorer) {
|
|
120
|
+
return `${explorer}/tx/${txHash}`;
|
|
121
|
+
}
|
|
122
|
+
return txHash;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// src/cli/commands/generate.ts
|
|
126
|
+
var generateCommand = new Command("generate").description(
|
|
127
|
+
"Generate a new unspendable address (generates random secret by default)"
|
|
128
|
+
).option(
|
|
129
|
+
"--secret <hex>",
|
|
130
|
+
"Use existing 32-byte secret (0x-prefixed hex) instead of generating"
|
|
131
|
+
).option(
|
|
132
|
+
"--beneficiaries <addresses>",
|
|
133
|
+
"Comma-separated beneficiary addresses (max 4, empty = unbounded)"
|
|
134
|
+
).option(
|
|
135
|
+
"--config <path>",
|
|
136
|
+
"Path to config file",
|
|
137
|
+
"./private-payments.config.json"
|
|
138
|
+
).action(async (options) => {
|
|
139
|
+
try {
|
|
140
|
+
const config = loadConfig(options.config);
|
|
141
|
+
const secret = options.secret ? validateSecret(options.secret) : generatePaymentKey();
|
|
142
|
+
const isNewSecret = !options.secret;
|
|
143
|
+
const beneficiaries = parseBeneficiaries(options.beneficiaries);
|
|
144
|
+
const paddedBeneficiaries = padBeneficiaries(beneficiaries);
|
|
145
|
+
const depositAddress = computeUnspendableAddress(
|
|
146
|
+
secret,
|
|
147
|
+
BigInt(config.destinationChainId),
|
|
148
|
+
paddedBeneficiaries
|
|
149
|
+
);
|
|
150
|
+
const nullifier = computeNullifier(
|
|
151
|
+
secret,
|
|
152
|
+
BigInt(config.destinationChainId)
|
|
153
|
+
);
|
|
154
|
+
const isUnbounded = beneficiaries.length === 0;
|
|
155
|
+
if (isNewSecret) {
|
|
156
|
+
console.log("Secret:", secret);
|
|
157
|
+
console.log("");
|
|
158
|
+
console.log(
|
|
159
|
+
"WARNING: Save this secret! It is required to redeem funds."
|
|
160
|
+
);
|
|
161
|
+
console.log("");
|
|
162
|
+
}
|
|
163
|
+
console.log("Deposit Address:", depositAddress);
|
|
164
|
+
console.log(
|
|
165
|
+
"Nullifier:",
|
|
166
|
+
"0x" + nullifier.toString(16).padStart(64, "0")
|
|
167
|
+
);
|
|
168
|
+
console.log(
|
|
169
|
+
"Mode:",
|
|
170
|
+
isUnbounded ? "Unbounded (any beneficiary allowed)" : `Bounded (${beneficiaries.length} beneficiaries)`
|
|
171
|
+
);
|
|
172
|
+
if (!isUnbounded) {
|
|
173
|
+
console.log("Beneficiaries:");
|
|
174
|
+
for (const addr of beneficiaries) {
|
|
175
|
+
console.log(` - ${addr}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
} catch (e) {
|
|
179
|
+
console.error("Error:", e.message);
|
|
180
|
+
process.exit(1);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
var balanceCommand = new Command("balance").description("Query balance via light client").requiredOption("--secret <hex>", "32-byte secret (0x-prefixed hex)").option(
|
|
184
|
+
"--beneficiaries <addresses>",
|
|
185
|
+
"Comma-separated beneficiary addresses"
|
|
186
|
+
).requiredOption("--client-id <id>", "Light client ID to use").option(
|
|
187
|
+
"--config <path>",
|
|
188
|
+
"Path to config file",
|
|
189
|
+
"./private-payments.config.json"
|
|
190
|
+
).action(async (options) => {
|
|
191
|
+
try {
|
|
192
|
+
const config = loadConfig(options.config);
|
|
193
|
+
const secret = validateSecret(options.secret);
|
|
194
|
+
const beneficiaries = parseBeneficiaries(options.beneficiaries);
|
|
195
|
+
const clientId = parseInt(options.clientId, 10);
|
|
196
|
+
if (isNaN(clientId) || clientId < 0) {
|
|
197
|
+
throw new Error("Invalid light client ID");
|
|
198
|
+
}
|
|
199
|
+
const client = new UnionPrivatePayments({
|
|
200
|
+
proverUrl: config.proverUrl,
|
|
201
|
+
sourceRpcUrl: config.sourceRpcUrl,
|
|
202
|
+
destinationRpcUrl: config.destinationRpcUrl,
|
|
203
|
+
srcZAssetAddress: config.srcZAssetAddress,
|
|
204
|
+
dstZAssetAddress: config.dstZAssetAddress,
|
|
205
|
+
sourceChainId: BigInt(config.sourceChainId),
|
|
206
|
+
destinationChainId: BigInt(config.destinationChainId)
|
|
207
|
+
});
|
|
208
|
+
const depositAddress = client.getDepositAddress(
|
|
209
|
+
secret,
|
|
210
|
+
beneficiaries
|
|
211
|
+
);
|
|
212
|
+
const nullifier = client.getNullifier(secret);
|
|
213
|
+
console.log("Querying balance...");
|
|
214
|
+
console.log("Deposit Address:", depositAddress);
|
|
215
|
+
console.log("Light Client ID:", clientId);
|
|
216
|
+
const balance = await client.getBalance({
|
|
217
|
+
depositAddress,
|
|
218
|
+
nullifier,
|
|
219
|
+
clientId
|
|
220
|
+
});
|
|
221
|
+
const tokenInfo = await client.getSrcZAssetInfo();
|
|
222
|
+
console.log("");
|
|
223
|
+
console.log(`Latest height: ${balance.latestHeight}`);
|
|
224
|
+
console.log(`Light client height: ${balance.lightClientHeight}`);
|
|
225
|
+
console.log(
|
|
226
|
+
`Light client lag: ${balance.latestHeight - balance.lightClientHeight}`
|
|
227
|
+
);
|
|
228
|
+
console.log(`Balance:`);
|
|
229
|
+
console.log(
|
|
230
|
+
" Pending:",
|
|
231
|
+
formatAmount(
|
|
232
|
+
balance.pending,
|
|
233
|
+
tokenInfo.decimals,
|
|
234
|
+
tokenInfo.symbol
|
|
235
|
+
)
|
|
236
|
+
);
|
|
237
|
+
console.log(
|
|
238
|
+
" Confirmed:",
|
|
239
|
+
formatAmount(
|
|
240
|
+
balance.confirmed,
|
|
241
|
+
tokenInfo.decimals,
|
|
242
|
+
tokenInfo.symbol
|
|
243
|
+
)
|
|
244
|
+
);
|
|
245
|
+
console.log(
|
|
246
|
+
" Redeemed:",
|
|
247
|
+
formatAmount(
|
|
248
|
+
balance.redeemed,
|
|
249
|
+
tokenInfo.decimals,
|
|
250
|
+
tokenInfo.symbol
|
|
251
|
+
)
|
|
252
|
+
);
|
|
253
|
+
console.log(
|
|
254
|
+
" Available:",
|
|
255
|
+
formatAmount(
|
|
256
|
+
balance.available,
|
|
257
|
+
tokenInfo.decimals,
|
|
258
|
+
tokenInfo.symbol
|
|
259
|
+
)
|
|
260
|
+
);
|
|
261
|
+
console.log("");
|
|
262
|
+
console.log("Raw balance:");
|
|
263
|
+
console.log(" Pending:", balance.pending.toString());
|
|
264
|
+
console.log(" Confirmed:", balance.confirmed.toString());
|
|
265
|
+
console.log(" Redeemed:", balance.redeemed.toString());
|
|
266
|
+
console.log(" Available:", balance.available.toString());
|
|
267
|
+
} catch (e) {
|
|
268
|
+
console.error("Error:", e.message);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
var proveCommand = new Command("prove").description("Generate ZK proof for redemption").requiredOption("--secret <hex>", "32-byte secret (0x-prefixed hex)").option("--beneficiaries <addresses>", "Comma-separated beneficiary addresses for address derivation").requiredOption("--beneficiary <address>", "Address to redeem to").requiredOption("--amount <value>", 'Amount to redeem in smallest units (e.g., "2000000" for 2 USDC)').requiredOption("--client-ids <ids>", "Comma-separated light client IDs for anonymity set").requiredOption("--selected-client <id>", "Which client ID to use for proof").option("--output <file>", "Save proof JSON to file (default: stdout)").option("--config <path>", "Path to config file", "./private-payments.config.json").action(async (options) => {
|
|
273
|
+
try {
|
|
274
|
+
const config = loadConfig(options.config);
|
|
275
|
+
const secret = validateSecret(options.secret);
|
|
276
|
+
const beneficiaries = parseBeneficiaries(options.beneficiaries);
|
|
277
|
+
const beneficiary = validateAddress(options.beneficiary, "beneficiary");
|
|
278
|
+
const clientIds = parseClientIds(options.clientIds);
|
|
279
|
+
const selectedClientId = parseInt(options.selectedClient, 10);
|
|
280
|
+
if (isNaN(selectedClientId)) {
|
|
281
|
+
throw new Error("Invalid selected client ID");
|
|
282
|
+
}
|
|
283
|
+
if (!clientIds.includes(selectedClientId)) {
|
|
284
|
+
throw new Error(`Selected client ${selectedClientId} not in client IDs: ${clientIds.join(", ")}`);
|
|
285
|
+
}
|
|
286
|
+
const client = new UnionPrivatePayments({
|
|
287
|
+
proverUrl: config.proverUrl,
|
|
288
|
+
sourceRpcUrl: config.sourceRpcUrl,
|
|
289
|
+
destinationRpcUrl: config.destinationRpcUrl,
|
|
290
|
+
srcZAssetAddress: config.srcZAssetAddress,
|
|
291
|
+
dstZAssetAddress: config.dstZAssetAddress,
|
|
292
|
+
sourceChainId: BigInt(config.sourceChainId),
|
|
293
|
+
destinationChainId: BigInt(config.destinationChainId)
|
|
294
|
+
});
|
|
295
|
+
const amount = BigInt(options.amount);
|
|
296
|
+
console.error("Generating proof...");
|
|
297
|
+
console.error(" Secret:", secret.slice(0, 10) + "...");
|
|
298
|
+
console.error(" Beneficiary:", beneficiary);
|
|
299
|
+
console.error(" Amount:", amount.toString());
|
|
300
|
+
console.error(" Client IDs:", clientIds.join(", "));
|
|
301
|
+
console.error(" Selected Client:", selectedClientId);
|
|
302
|
+
const result = await client.generateProof(
|
|
303
|
+
secret,
|
|
304
|
+
beneficiaries,
|
|
305
|
+
beneficiary,
|
|
306
|
+
amount,
|
|
307
|
+
clientIds,
|
|
308
|
+
selectedClientId
|
|
309
|
+
);
|
|
310
|
+
if (!result.proof.success || !result.proof.proofJson || !result.metadata) {
|
|
311
|
+
throw new Error(result.proof.error ?? "Proof generation failed");
|
|
312
|
+
}
|
|
313
|
+
console.error("Proof generated successfully!");
|
|
314
|
+
const proofJson = parseProofJson(result.proof.proofJson);
|
|
315
|
+
const output = {
|
|
316
|
+
proof: proofJson,
|
|
317
|
+
metadata: {
|
|
318
|
+
depositAddress: result.metadata.depositAddress,
|
|
319
|
+
beneficiary: result.metadata.beneficiary,
|
|
320
|
+
value: result.metadata.value.toString(),
|
|
321
|
+
nullifier: "0x" + result.metadata.nullifier.toString(16).padStart(64, "0"),
|
|
322
|
+
lightClients: result.metadata.lightClients.map((lc) => ({
|
|
323
|
+
clientId: lc.clientId,
|
|
324
|
+
height: lc.height.toString(),
|
|
325
|
+
stateRoot: lc.stateRoot
|
|
326
|
+
}))
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const jsonOutput = JSON.stringify(output, null, 2);
|
|
330
|
+
if (options.output) {
|
|
331
|
+
writeFileSync(options.output, jsonOutput);
|
|
332
|
+
console.error(`Proof saved to: ${options.output}`);
|
|
333
|
+
} else {
|
|
334
|
+
console.log(jsonOutput);
|
|
335
|
+
}
|
|
336
|
+
console.error("");
|
|
337
|
+
console.error("Metadata:");
|
|
338
|
+
console.error(" Deposit Address:", result.metadata.depositAddress);
|
|
339
|
+
console.error(" Beneficiary:", result.metadata.beneficiary);
|
|
340
|
+
console.error(" Value:", result.metadata.value.toString());
|
|
341
|
+
console.error(" Nullifier:", "0x" + result.metadata.nullifier.toString(16).padStart(64, "0"));
|
|
342
|
+
console.error(" Light Clients:", result.metadata.lightClients.length);
|
|
343
|
+
} catch (e) {
|
|
344
|
+
console.error("Error:", e.message);
|
|
345
|
+
process.exit(1);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
var CHAIN_MAP = {
|
|
349
|
+
1: mainnet,
|
|
350
|
+
8453: base,
|
|
351
|
+
42161: arbitrum,
|
|
352
|
+
10: optimism,
|
|
353
|
+
137: polygon
|
|
354
|
+
};
|
|
355
|
+
var redeemCommand = new Command("redeem").description("Submit redemption transaction (with existing proof or generate on the fly)").option("--proof <file>", "Path to proof JSON file (from prove command)").option("--secret <hex>", "32-byte secret (0x-prefixed hex) - required if no --proof").option("--beneficiaries <addresses>", "Comma-separated beneficiary addresses for address derivation").option("--beneficiary <address>", "Address to redeem to - required if no --proof").option("--amount <value>", "Amount to redeem in smallest units - required if no --proof").option("--client-ids <ids>", "Comma-separated light client IDs for anonymity set - required if no --proof").option("--selected-client <id>", "Which client ID to use for proof - required if no --proof").option("--private-key <key>", "Private key to send transaction (or env SENDER_PRIVATE_KEY)").option("--config <path>", "Path to config file", "./private-payments.config.json").action(async (options) => {
|
|
356
|
+
try {
|
|
357
|
+
const config = loadConfig(options.config);
|
|
358
|
+
const privateKey = options.privateKey ?? process.env.SENDER_PRIVATE_KEY;
|
|
359
|
+
if (!privateKey) {
|
|
360
|
+
throw new Error("Private key required. Use --private-key or set SENDER_PRIVATE_KEY env var");
|
|
361
|
+
}
|
|
362
|
+
if (!privateKey.startsWith("0x")) {
|
|
363
|
+
throw new Error("Private key must start with 0x");
|
|
364
|
+
}
|
|
365
|
+
if (!config.attestorUrl) {
|
|
366
|
+
throw new Error("attestorUrl required in config for redemption");
|
|
367
|
+
}
|
|
368
|
+
if (!config.attestorApiKey) {
|
|
369
|
+
throw new Error("attestorApiKey required in config for redemption");
|
|
370
|
+
}
|
|
371
|
+
let proofData;
|
|
372
|
+
if (options.proof) {
|
|
373
|
+
const proofContent = readFileSync(options.proof, "utf-8");
|
|
374
|
+
proofData = JSON.parse(proofContent);
|
|
375
|
+
console.log("Loading proof from:", options.proof);
|
|
376
|
+
} else {
|
|
377
|
+
if (!options.secret || !options.beneficiary || !options.amount || !options.clientIds || !options.selectedClient) {
|
|
378
|
+
throw new Error(
|
|
379
|
+
"When not using --proof, you must provide: --secret, --beneficiary, --amount, --client-ids, --selected-client"
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
const secret = validateSecret(options.secret);
|
|
383
|
+
const beneficiaries = parseBeneficiaries(options.beneficiaries);
|
|
384
|
+
const beneficiary2 = validateAddress(options.beneficiary, "beneficiary");
|
|
385
|
+
const clientIds = parseClientIds(options.clientIds);
|
|
386
|
+
const selectedClientId = parseInt(options.selectedClient, 10);
|
|
387
|
+
if (isNaN(selectedClientId)) {
|
|
388
|
+
throw new Error("Invalid selected client ID");
|
|
389
|
+
}
|
|
390
|
+
if (!clientIds.includes(selectedClientId)) {
|
|
391
|
+
throw new Error(`Selected client ${selectedClientId} not in client IDs: ${clientIds.join(", ")}`);
|
|
392
|
+
}
|
|
393
|
+
const client = new UnionPrivatePayments({
|
|
394
|
+
proverUrl: config.proverUrl,
|
|
395
|
+
sourceRpcUrl: config.sourceRpcUrl,
|
|
396
|
+
destinationRpcUrl: config.destinationRpcUrl,
|
|
397
|
+
srcZAssetAddress: config.srcZAssetAddress,
|
|
398
|
+
dstZAssetAddress: config.dstZAssetAddress,
|
|
399
|
+
sourceChainId: BigInt(config.sourceChainId),
|
|
400
|
+
destinationChainId: BigInt(config.destinationChainId)
|
|
401
|
+
});
|
|
402
|
+
const amount = BigInt(options.amount);
|
|
403
|
+
console.log("Generating proof...");
|
|
404
|
+
console.log(" Secret:", secret.slice(0, 10) + "...");
|
|
405
|
+
console.log(" Beneficiary:", beneficiary2);
|
|
406
|
+
console.log(" Amount:", amount.toString());
|
|
407
|
+
console.log(" Client IDs:", clientIds.join(", "));
|
|
408
|
+
console.log(" Selected Client:", selectedClientId);
|
|
409
|
+
const result = await client.generateProof(
|
|
410
|
+
secret,
|
|
411
|
+
beneficiaries,
|
|
412
|
+
beneficiary2,
|
|
413
|
+
amount,
|
|
414
|
+
clientIds,
|
|
415
|
+
selectedClientId
|
|
416
|
+
);
|
|
417
|
+
if (!result.proof.success || !result.proof.proofJson || !result.metadata) {
|
|
418
|
+
throw new Error(result.proof.error ?? "Proof generation failed");
|
|
419
|
+
}
|
|
420
|
+
console.log("Proof generated successfully!");
|
|
421
|
+
const proofJson = parseProofJson(result.proof.proofJson);
|
|
422
|
+
proofData = {
|
|
423
|
+
proof: proofJson,
|
|
424
|
+
metadata: {
|
|
425
|
+
depositAddress: result.metadata.depositAddress,
|
|
426
|
+
beneficiary: result.metadata.beneficiary,
|
|
427
|
+
value: result.metadata.value.toString(),
|
|
428
|
+
nullifier: "0x" + result.metadata.nullifier.toString(16).padStart(64, "0"),
|
|
429
|
+
lightClients: result.metadata.lightClients.map((lc) => ({
|
|
430
|
+
clientId: lc.clientId,
|
|
431
|
+
height: lc.height.toString(),
|
|
432
|
+
stateRoot: lc.stateRoot
|
|
433
|
+
}))
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
const lightClients = proofData.metadata.lightClients.map((lc) => ({
|
|
438
|
+
clientId: lc.clientId,
|
|
439
|
+
height: BigInt(lc.height),
|
|
440
|
+
stateRoot: lc.stateRoot
|
|
441
|
+
}));
|
|
442
|
+
const nullifier = BigInt(proofData.metadata.nullifier);
|
|
443
|
+
const value = BigInt(proofData.metadata.value);
|
|
444
|
+
const beneficiary = proofData.metadata.beneficiary;
|
|
445
|
+
const depositAddress = proofData.metadata.depositAddress;
|
|
446
|
+
console.log("");
|
|
447
|
+
console.log("Redemption details:");
|
|
448
|
+
console.log(" Deposit Address:", depositAddress);
|
|
449
|
+
console.log(" Beneficiary:", beneficiary);
|
|
450
|
+
console.log(" Value:", value.toString());
|
|
451
|
+
console.log(" Nullifier:", proofData.metadata.nullifier);
|
|
452
|
+
console.log(" Light Clients:", lightClients.length);
|
|
453
|
+
console.log("");
|
|
454
|
+
console.log("Requesting attestation...");
|
|
455
|
+
const attestationClient = new AttestationClient(
|
|
456
|
+
config.attestorUrl,
|
|
457
|
+
config.attestorApiKey
|
|
458
|
+
);
|
|
459
|
+
const attestation = await attestationClient.getAttestation(depositAddress, beneficiary);
|
|
460
|
+
console.log(" Attested Message:", attestation.attestedMessage);
|
|
461
|
+
console.log(" Signature:", attestation.signature.slice(0, 20) + "...");
|
|
462
|
+
const redeemParams = proofJsonToRedeemParams(proofData.proof, {
|
|
463
|
+
lightClients,
|
|
464
|
+
nullifier,
|
|
465
|
+
value,
|
|
466
|
+
beneficiary,
|
|
467
|
+
attestedMessage: attestation.attestedMessage,
|
|
468
|
+
signature: attestation.signature
|
|
469
|
+
});
|
|
470
|
+
const chain = CHAIN_MAP[config.destinationChainId];
|
|
471
|
+
if (!chain) {
|
|
472
|
+
throw new Error(`Unsupported destination chain ID: ${config.destinationChainId}`);
|
|
473
|
+
}
|
|
474
|
+
const account = privateKeyToAccount(privateKey);
|
|
475
|
+
const walletClient = createWalletClient({
|
|
476
|
+
account,
|
|
477
|
+
chain,
|
|
478
|
+
transport: http(config.destinationRpcUrl)
|
|
479
|
+
});
|
|
480
|
+
console.log("");
|
|
481
|
+
console.log("Submitting redeem transaction...");
|
|
482
|
+
const txHash = await submitRedeem(
|
|
483
|
+
config.dstZAssetAddress,
|
|
484
|
+
redeemParams,
|
|
485
|
+
walletClient
|
|
486
|
+
);
|
|
487
|
+
console.log("");
|
|
488
|
+
console.log("Transaction submitted!");
|
|
489
|
+
console.log("Hash:", txHash);
|
|
490
|
+
console.log("Explorer:", getExplorerUrl(config.destinationChainId, txHash));
|
|
491
|
+
} catch (e) {
|
|
492
|
+
console.error("Error:", e.message);
|
|
493
|
+
process.exit(1);
|
|
494
|
+
}
|
|
495
|
+
});
|
|
496
|
+
var updateClientCommand = new Command("update-client").description("Update loopback light client to a specific block height").requiredOption("--rpc <url>", "RPC URL for the chain").requiredOption("--client-id <id>", "Light client ID to update").option("--height <height>", "Block height to update to (default: latest)", "latest").requiredOption("--ibc-handler <address>", "IBCHandler contract address").option("--private-key <key>", "Private key to send transaction (or env SENDER_PRIVATE_KEY)").action(async (options) => {
|
|
497
|
+
try {
|
|
498
|
+
const privateKey = options.privateKey ?? process.env.SENDER_PRIVATE_KEY;
|
|
499
|
+
if (!privateKey) {
|
|
500
|
+
throw new Error("Private key required. Use --private-key or set SENDER_PRIVATE_KEY env var");
|
|
501
|
+
}
|
|
502
|
+
if (!privateKey.startsWith("0x")) {
|
|
503
|
+
throw new Error("Private key must start with 0x");
|
|
504
|
+
}
|
|
505
|
+
const clientId = parseInt(options.clientId, 10);
|
|
506
|
+
if (isNaN(clientId)) {
|
|
507
|
+
throw new Error("Invalid client ID: must be a number");
|
|
508
|
+
}
|
|
509
|
+
const ibcHandlerAddress = options.ibcHandler;
|
|
510
|
+
if (!ibcHandlerAddress.startsWith("0x") || ibcHandlerAddress.length !== 42) {
|
|
511
|
+
throw new Error("Invalid IBC handler address");
|
|
512
|
+
}
|
|
513
|
+
const height = options.height === "latest" ? "latest" : BigInt(options.height);
|
|
514
|
+
const publicClient = createPublicClient({
|
|
515
|
+
transport: http(options.rpc)
|
|
516
|
+
});
|
|
517
|
+
const chainId = await publicClient.getChainId();
|
|
518
|
+
const account = privateKeyToAccount(privateKey);
|
|
519
|
+
const walletClient = createWalletClient({
|
|
520
|
+
account,
|
|
521
|
+
chain: { id: chainId },
|
|
522
|
+
transport: http(options.rpc)
|
|
523
|
+
});
|
|
524
|
+
console.log("Updating loopback light client...");
|
|
525
|
+
console.log(" RPC:", options.rpc);
|
|
526
|
+
console.log(" IBC Handler:", ibcHandlerAddress);
|
|
527
|
+
console.log(" Client ID:", clientId);
|
|
528
|
+
console.log(" Height:", height === "latest" ? "latest" : height.toString());
|
|
529
|
+
console.log("");
|
|
530
|
+
const result = await updateLoopbackClient(
|
|
531
|
+
options.rpc,
|
|
532
|
+
ibcHandlerAddress,
|
|
533
|
+
clientId,
|
|
534
|
+
height,
|
|
535
|
+
walletClient
|
|
536
|
+
);
|
|
537
|
+
console.log("Light client updated successfully!");
|
|
538
|
+
console.log(" Chain ID:", result.chainId.toString());
|
|
539
|
+
console.log(" Block Number:", result.blockNumber.toString());
|
|
540
|
+
console.log(" State Root:", result.stateRoot);
|
|
541
|
+
console.log(" Transaction:", result.txHash);
|
|
542
|
+
console.log(" Explorer:", getExplorerUrl(Number(result.chainId), result.txHash));
|
|
543
|
+
} catch (e) {
|
|
544
|
+
console.error("Error:", e.message);
|
|
545
|
+
process.exit(1);
|
|
546
|
+
}
|
|
547
|
+
});
|
|
548
|
+
var depositCommand = new Command("deposit").description("Wrap underlying tokens and deposit to ZAsset").requiredOption("--amount <amount>", 'Amount to deposit in smallest units (e.g., "50000000" for 50 USDC)').requiredOption("--secret <hex>", "32-byte secret to derive deposit address (0x-prefixed hex)").option("--beneficiaries <addresses>", "Comma-separated beneficiary addresses (max 4, empty = unbounded)").option("--private-key <key>", "Private key to sign transaction (or env SENDER_PRIVATE_KEY)").option("--config <path>", "Path to config file", "./private-payments.config.json").action(async (options) => {
|
|
549
|
+
try {
|
|
550
|
+
const privateKey = options.privateKey ?? process.env.SENDER_PRIVATE_KEY;
|
|
551
|
+
if (!privateKey) {
|
|
552
|
+
throw new Error("Private key required. Use --private-key or set SENDER_PRIVATE_KEY env var");
|
|
553
|
+
}
|
|
554
|
+
if (!privateKey.startsWith("0x")) {
|
|
555
|
+
throw new Error("Private key must start with 0x");
|
|
556
|
+
}
|
|
557
|
+
const config = loadConfig(options.config);
|
|
558
|
+
const secret = validateSecret(options.secret);
|
|
559
|
+
const beneficiaries = parseBeneficiaries(options.beneficiaries);
|
|
560
|
+
const paddedBeneficiaries = padBeneficiaries(beneficiaries);
|
|
561
|
+
const rpcUrl = config.sourceRpcUrl;
|
|
562
|
+
const zAssetAddress = config.srcZAssetAddress;
|
|
563
|
+
const amount = BigInt(options.amount);
|
|
564
|
+
const depositAddress = computeUnspendableAddress(
|
|
565
|
+
secret,
|
|
566
|
+
BigInt(config.destinationChainId),
|
|
567
|
+
paddedBeneficiaries
|
|
568
|
+
);
|
|
569
|
+
const account = privateKeyToAccount(privateKey);
|
|
570
|
+
const walletClient = createWalletClient({
|
|
571
|
+
account,
|
|
572
|
+
chain: { id: config.sourceChainId },
|
|
573
|
+
transport: http(rpcUrl)
|
|
574
|
+
});
|
|
575
|
+
console.log("Depositing to ZAsset...");
|
|
576
|
+
console.log(" ZAsset:", zAssetAddress);
|
|
577
|
+
console.log(" Amount:", amount.toString());
|
|
578
|
+
console.log(" Deposit Address:", depositAddress);
|
|
579
|
+
console.log("");
|
|
580
|
+
const result = await depositToZAsset(
|
|
581
|
+
rpcUrl,
|
|
582
|
+
zAssetAddress,
|
|
583
|
+
depositAddress,
|
|
584
|
+
amount,
|
|
585
|
+
walletClient
|
|
586
|
+
);
|
|
587
|
+
console.log("");
|
|
588
|
+
console.log("Deposit complete!");
|
|
589
|
+
console.log(" Tx Hash:", result.txHash);
|
|
590
|
+
console.log(" Explorer:", getExplorerUrl(Number(result.chainId), result.txHash));
|
|
591
|
+
} catch (e) {
|
|
592
|
+
console.error("Error:", e.message);
|
|
593
|
+
process.exit(1);
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
var historyCommand = new Command("history").description("List redemption transactions for a secret").requiredOption("--secret <hex>", "32-byte secret (0x-prefixed hex)").option("--from-block <number>", "Start block number (default: earliest)").option("--to-block <number>", "End block number (default: latest)").option("--config <path>", "Path to config file", "./private-payments.config.json").action(async (options) => {
|
|
597
|
+
try {
|
|
598
|
+
const config = loadConfig(options.config);
|
|
599
|
+
const secret = validateSecret(options.secret);
|
|
600
|
+
const client = new UnionPrivatePayments({
|
|
601
|
+
proverUrl: config.proverUrl,
|
|
602
|
+
sourceRpcUrl: config.sourceRpcUrl,
|
|
603
|
+
destinationRpcUrl: config.destinationRpcUrl,
|
|
604
|
+
srcZAssetAddress: config.srcZAssetAddress,
|
|
605
|
+
dstZAssetAddress: config.dstZAssetAddress,
|
|
606
|
+
sourceChainId: BigInt(config.sourceChainId),
|
|
607
|
+
destinationChainId: BigInt(config.destinationChainId)
|
|
608
|
+
});
|
|
609
|
+
const nullifier = client.getNullifier(secret);
|
|
610
|
+
const nullifierHex = "0x" + nullifier.toString(16).padStart(64, "0");
|
|
611
|
+
console.log("Querying redemption history...");
|
|
612
|
+
console.log("Nullifier:", nullifierHex);
|
|
613
|
+
console.log("");
|
|
614
|
+
const fromBlock = options.fromBlock ? BigInt(options.fromBlock) : void 0;
|
|
615
|
+
const toBlock = options.toBlock ? BigInt(options.toBlock) : void 0;
|
|
616
|
+
const dstRpcClient = new RpcClient(config.destinationRpcUrl);
|
|
617
|
+
const history = await dstRpcClient.getRedemptionHistory(
|
|
618
|
+
config.dstZAssetAddress,
|
|
619
|
+
nullifier,
|
|
620
|
+
fromBlock,
|
|
621
|
+
toBlock
|
|
622
|
+
);
|
|
623
|
+
if (history.length === 0) {
|
|
624
|
+
console.log("No redemptions found for this secret.");
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
const tokenInfo = await client.getDstZAssetInfo();
|
|
628
|
+
console.log(`Found ${history.length} redemption${history.length > 1 ? "s" : ""}:`);
|
|
629
|
+
console.log("");
|
|
630
|
+
for (let i = 0; i < history.length; i++) {
|
|
631
|
+
const entry = history[i];
|
|
632
|
+
console.log(`#${i + 1} Block: ${entry.blockNumber}`);
|
|
633
|
+
console.log(` Tx: ${entry.txHash}`);
|
|
634
|
+
console.log(` Amount: ${formatAmount(entry.redeemAmount, tokenInfo.decimals, tokenInfo.symbol)}`);
|
|
635
|
+
console.log(` Beneficiary: ${entry.beneficiary}`);
|
|
636
|
+
console.log(` Explorer: ${getExplorerUrl(config.destinationChainId, entry.txHash)}`);
|
|
637
|
+
console.log("");
|
|
638
|
+
}
|
|
639
|
+
const totalRedeemed = history.reduce((sum, h) => sum + h.redeemAmount, 0n);
|
|
640
|
+
console.log(`Total redeemed: ${formatAmount(totalRedeemed, tokenInfo.decimals, tokenInfo.symbol)}`);
|
|
641
|
+
} catch (e) {
|
|
642
|
+
console.error("Error:", e.message);
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
var exportVerifierCommand = new Command("export-verifier").description("Export the Solidity verifier contract from the prover server").option("--prover-url <url>", "Prover gRPC-web URL", "http://localhost:8080").option("--output <file>", "Save verifier to file (default: stdout)").action(async (options) => {
|
|
647
|
+
try {
|
|
648
|
+
const proverClient = new ProverClient(options.proverUrl);
|
|
649
|
+
console.error("Exporting verifier contract...");
|
|
650
|
+
console.error(" Prover URL:", options.proverUrl);
|
|
651
|
+
const verifierContract = await proverClient.exportVerifier();
|
|
652
|
+
if (options.output) {
|
|
653
|
+
writeFileSync(options.output, verifierContract);
|
|
654
|
+
console.error(`Verifier saved to: ${options.output}`);
|
|
655
|
+
} else {
|
|
656
|
+
console.log(verifierContract);
|
|
657
|
+
}
|
|
658
|
+
} catch (e) {
|
|
659
|
+
console.error("Error:", e.message);
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// src/cli.ts
|
|
665
|
+
var program = new Command();
|
|
666
|
+
program.name("payments").description("CLI for Union Private Payments - privacy-preserving transfers using ZK proofs").version("0.1.0");
|
|
667
|
+
program.addCommand(generateCommand);
|
|
668
|
+
program.addCommand(balanceCommand);
|
|
669
|
+
program.addCommand(proveCommand);
|
|
670
|
+
program.addCommand(redeemCommand);
|
|
671
|
+
program.addCommand(updateClientCommand);
|
|
672
|
+
program.addCommand(depositCommand);
|
|
673
|
+
program.addCommand(historyCommand);
|
|
674
|
+
program.addCommand(exportVerifierCommand);
|
|
675
|
+
program.parse();
|