@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/dist/cli.cjs ADDED
@@ -0,0 +1,3031 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ var commander = require('commander');
5
+ var fs = require('fs');
6
+ var V = require('viem');
7
+ var accounts = require('viem/accounts');
8
+ var connect = require('@connectrpc/connect');
9
+ var connectWeb = require('@connectrpc/connect-web');
10
+ var protobuf = require('@bufbuild/protobuf');
11
+ var codegenv2 = require('@bufbuild/protobuf/codegenv2');
12
+ var effect = require('effect');
13
+ var chains = require('viem/chains');
14
+
15
+ function _interopNamespace(e) {
16
+ if (e && e.__esModule) return e;
17
+ var n = Object.create(null);
18
+ if (e) {
19
+ Object.keys(e).forEach(function (k) {
20
+ if (k !== 'default') {
21
+ var d = Object.getOwnPropertyDescriptor(e, k);
22
+ Object.defineProperty(n, k, d.get ? d : {
23
+ enumerable: true,
24
+ get: function () { return e[k]; }
25
+ });
26
+ }
27
+ });
28
+ }
29
+ n.default = e;
30
+ return Object.freeze(n);
31
+ }
32
+
33
+ var V__namespace = /*#__PURE__*/_interopNamespace(V);
34
+
35
+ // src/poseidon2.ts
36
+ var PRIME = 0x30644e72e131a029b85045b68181585d2833e84879b9709143e1f593f0000001n;
37
+ var NULLIFIER_DOMAIN = 0xdeadn;
38
+ var ADDRESS_DOMAIN = 0xdeafn;
39
+ var RC_EXT_0 = [
40
+ [
41
+ 0x1da4d6adfb0d0b494584f763db50a81908580a5f5e295e168b9b8d31770fac4fn,
42
+ 0x0946129a2e33b4e819707a56a3b3790eab80d0a0c7a0c63451fab2a59acc074cn
43
+ ],
44
+ [
45
+ 0x2a39b9d5376afd35580abd6952986570867b87303b07140268794dad4b8f82ean,
46
+ 0x27605717d1245c20c546b3c7014e5fa3e4a70e66ecd6de5d32448eea5e3069b2n
47
+ ],
48
+ [
49
+ 0x24c896cb2594e17b973653193a470ed7e771ebb09a228d638e50809c03773632n,
50
+ 0x0911096c45dd9cda0d61d783957003db6d8701c7869a919ad8042e1b7d597a49n
51
+ ]
52
+ ];
53
+ var RC_INT = [
54
+ 0x26ff6166f1e4b99e27eee6d352a8ce26f1daba01aad28021f11f38a70603afdcn,
55
+ // 3
56
+ 0x008e2faedcf76d08ad6591ff90e50fea79bcb5e18cfb7d954d5b89437bf64b7en,
57
+ // 4
58
+ 0x19c9da2379b598ace3ad4d1872470830f6184a3cec71eeb632f98820eb353d78n,
59
+ // 5
60
+ 0x0f7c4eb15d8b0b62a8f6a090ec9610a2ab3dfcdb57e2539aa459a40583dfe96bn,
61
+ // 6
62
+ 0x18b99417dc26b5e079750eba282362d1d46900b47dd5ff809171588b08ac3983n,
63
+ // 7
64
+ 0x1ee044081160b3eee2d4493feab82141c73f1c054b76320a8848af08a8d91a26n,
65
+ // 8
66
+ 0x29bb95c8763efd3e0e87f5df12ee8a150455b6d7a14780d19122220366c258dcn,
67
+ // 9
68
+ 0x22c23eec9cb13ff8a3ee9a363d740653215e8991f7f9ec12067b4705e9a5c9fbn,
69
+ // 10
70
+ 0x23589e033a31a667680c8b18926c3be09115c7644c4f905cc7deefc1690b42dcn,
71
+ // 11
72
+ 0x304e99b887f2e1e92c9c0cde5f2bdd4764f60b98a219f1f0dd64ec938a6a247cn,
73
+ // 12
74
+ 0x22e817865236ad3a76fbe88bbdf31fcae792f326271d53a3a493b2de7f7d8b4cn,
75
+ // 13
76
+ 0x10c9efe573e86fa5b238a3f5c70a00bf02bc7dcfcc4878e026b77c881dc8b1c9n,
77
+ // 14
78
+ 0x0a94f16be920d85f4d6f80e745c6bddbc084f309edffef023ea283c72b89bbb6n,
79
+ // 15
80
+ 0x23ed72b4d01d14e3c7888fedb770494abc2c1ea81d6465b5df3da0ebcd69101an,
81
+ // 16
82
+ 0x17c5115640e4cebeed0e6cbb511ac38498df815a77cb162de8d8f1022eb6bb74n,
83
+ // 17
84
+ 0x2e507fcca290d0d9cf765245750eb04773e09e1fc813959fb99680a82772e4fcn,
85
+ // 18
86
+ 0x0d4a98999f5b39176af6cce65c8d12162433f055cb71d70dfc8803013292bbbfn,
87
+ // 19
88
+ 0x238d8022cc09c21ab3c01261a03dc125321d829e22a7a3b7a1bd3c335eccfa21n,
89
+ // 20
90
+ 0x010cd8e4c2b7051cb81dc8292e35d8e943ce365613d5b39770e454ed9f4ae165n,
91
+ // 21
92
+ 0x088027e54f2a3604b11178cf0ea3c6aa861173a90fb231238e03539590ecc027n,
93
+ // 22
94
+ 0x1b840f5311a2b1d4b4cd7aa7e5a9a6d161165468daa938108302b73e464819dbn,
95
+ // 23
96
+ 0x2bf51a5da1828a1cf9b764b1e16c15929a3a346e44198ea0cb366fcd8db78dc1n,
97
+ // 24
98
+ 0x206ad089d8d296ffe68a6a86767a7fe726b8872f9c7beef9d56a3a50f6f23827n,
99
+ // 25
100
+ 0x24d19193171494fa1a54e0a99ac16d05eaec4b6d617c7c199fc07ff88eac550cn,
101
+ // 26
102
+ 0x1dd654a2ca9d9f24f33d88246a40dfb32c40662278b9c0b995d9f9fbaf152138n,
103
+ // 27
104
+ 0x0d171025c925f6e259d20ecbd3a601108c85f05b0fe7793b8acf57f3789785e4n,
105
+ // 28
106
+ 0x055bef435a43aec245cd99ccb0f7c8791d9e8cf2b80d95dd98b9908fed877d55n,
107
+ // 29
108
+ 0x10d2ac8c61c8a2e88a2a3f42d9359a14be63d0ad4cfd9f203b75765d0e595f0en,
109
+ // 30
110
+ 0x103479710e70996982a84057ec3ba6b2254d7966ddc41e85296e3d0790dcfa56n,
111
+ // 31
112
+ 0x2a366f0448fda3c05914ffb48c765da8de96f9aa340db1638225f8372921807bn,
113
+ // 32
114
+ 0x16be0fb8ef62da17919b6e0378d00594153bb8899aeb686c04626b63285837a4n,
115
+ // 33
116
+ 0x0417038500e9d06c60abbc7f0d0d24c32dec8a0b2aa5a4d52cfd8c78a15bc370n,
117
+ // 34
118
+ 0x26a6873b43ffd2ccf66ec6f4493ff9b54f4d76480bc486a3e5a0308fdd013812n,
119
+ // 35
120
+ 0x0a3314a838f32630a96251914fe5ad262f3db9b2aa8aa9f7391922d36278c498n,
121
+ // 36
122
+ 0x0fde0c5429a6beb07f462d4821f48f86aeadb46a090b15a044f4b59399027da4n,
123
+ // 37
124
+ 0x0abc2d5049972a6b9b357e4163793b0bb517e1eb535a984df11a1c89cda2c8a9n,
125
+ // 38
126
+ 0x0dab51d6e3ebfa661d21722fb21e55051b427a5f173f7f17735205dbb77c464en,
127
+ // 39
128
+ 0x29c36622598b511d51af6cc37123652fb21be5c2d68fb8efe9b92e70a8c1ae03n,
129
+ // 40
130
+ 0x2c03ec80adac2a33ae846bc0b700d0bcc41c4096e53ac6990d6bbe7ea2fbc85cn,
131
+ // 41
132
+ 0x0918fdbe9cf3a59fbdb4c6852d065105317303116017d893b8b521e3cebe1e0dn,
133
+ // 42
134
+ 0x1f19ec22e69ca33f599dd13cd7e495a8176a79df4f7acf79a9d2135acabe2359n,
135
+ // 43
136
+ 0x1c4b037c8ae85ee1eb32b872eb7f76928c4c76b29ceb001346447b2911080704n,
137
+ // 44
138
+ 0x2b68900ed906616d6c826d0bde341766ba3131e04d420de5af0a69c534efd8dbn,
139
+ // 45
140
+ 0x20ca92aa222fcc69448f8dac653c8daaa180ff6dfb397cef723d4f0c782bc7f0n,
141
+ // 46
142
+ 0x10d22d05bdff6bb375276fc82057db337045a5ab7ac053941f6186289b66b2b6n,
143
+ // 47
144
+ 0x0b1ffdbb529367bb98f32ba45784cb435aa910b4a00636d1e5ca79e88bdd6cd9n,
145
+ // 48
146
+ 0x2da32b38e7984bc2ed757ec705eccf8282c7b4f82e5e515f6f89bcc33022ce9fn,
147
+ // 49
148
+ 0x042593ad87403f6d2674b8b55a886725b87eb33958031e94d292cecc6abed1bbn,
149
+ // 50
150
+ 0x181fa1b4d067783a19d7367bf49b3f01051faedab463a6de9308fbd6e7d419f1n,
151
+ // 51
152
+ 0x15aaa6cc9b7900b15683c95515c26028a8e35b00ed8a815c34927188c60de660n
153
+ // 52
154
+ ];
155
+ var RC_EXT_1 = [
156
+ [
157
+ 0x1bf28a93209084bbbc63234f057254c280b1a636f5a0eced6787320212f75a7an,
158
+ 0x1cdb8c8bee5426f02cd9e229776118f156273b312f805c8e6f8c9d81a620cb6fn
159
+ ],
160
+ [
161
+ 0x08299c0abf196d53162e0facb5f1876f516df2505cc387a0f8ea0e8760d5ca7en,
162
+ 0x221643d205fe82778a7b7b58cb65c4962d76c0072cabd1124117269d7c710b8an
163
+ ],
164
+ [
165
+ 0x2d036a95f81cf49bb7a0143a28c88767f6bd10c3f74b22db487920d43343dbffn,
166
+ 0x08a50897c06aafe6ea414fb1bceca2267cd4a39486729fbc6d5d1bb7a172ebd2n
167
+ ]
168
+ ];
169
+ function addMod(a, b) {
170
+ return ((a + b) % PRIME + PRIME) % PRIME;
171
+ }
172
+ function mulMod(a, b) {
173
+ return (a * b % PRIME + PRIME) % PRIME;
174
+ }
175
+ function sbox(x) {
176
+ const x2 = mulMod(x, x);
177
+ const x4 = mulMod(x2, x2);
178
+ return mulMod(x4, x);
179
+ }
180
+ function matMulExternal(s0, s1) {
181
+ const sum = addMod(s0, s1);
182
+ return [addMod(sum, s0), addMod(sum, s1)];
183
+ }
184
+ function matMulInternal(s0, s1) {
185
+ const sum = addMod(s0, s1);
186
+ return [addMod(s0, sum), addMod(addMod(s1, s1), sum)];
187
+ }
188
+ function permutation(state0, state1) {
189
+ let s0 = state0;
190
+ let s1 = state1;
191
+ [s0, s1] = matMulExternal(s0, s1);
192
+ for (let i = 0; i < 3; i++) {
193
+ s0 = addMod(s0, RC_EXT_0[i][0]);
194
+ s1 = addMod(s1, RC_EXT_0[i][1]);
195
+ s0 = sbox(s0);
196
+ s1 = sbox(s1);
197
+ [s0, s1] = matMulExternal(s0, s1);
198
+ }
199
+ for (let i = 0; i < 50; i++) {
200
+ s0 = addMod(s0, RC_INT[i]);
201
+ s0 = sbox(s0);
202
+ [s0, s1] = matMulInternal(s0, s1);
203
+ }
204
+ for (let i = 0; i < 3; i++) {
205
+ s0 = addMod(s0, RC_EXT_1[i][0]);
206
+ s1 = addMod(s1, RC_EXT_1[i][1]);
207
+ s0 = sbox(s0);
208
+ s1 = sbox(s1);
209
+ [s0, s1] = matMulExternal(s0, s1);
210
+ }
211
+ return [s0, s1];
212
+ }
213
+ function poseidon2Hash(inputs) {
214
+ let hashState = 0n;
215
+ for (const block of inputs) {
216
+ const [, permutedState1] = permutation(hashState, block);
217
+ hashState = addMod(block, permutedState1);
218
+ }
219
+ return hashState;
220
+ }
221
+ function hexToBigInt(hex) {
222
+ return BigInt(hex);
223
+ }
224
+ function validateSecret(secret) {
225
+ if (secret >= PRIME) {
226
+ throw new Error(
227
+ `Secret must be less than the BN254 scalar field prime (${PRIME.toString(16)})`
228
+ );
229
+ }
230
+ if (secret < 0n) {
231
+ throw new Error("Secret must be non-negative");
232
+ }
233
+ }
234
+ function addressToBigInt(address) {
235
+ return BigInt(address);
236
+ }
237
+ function computeNullifier(secret, dstChainId) {
238
+ const secretBigInt = hexToBigInt(secret);
239
+ validateSecret(secretBigInt);
240
+ return poseidon2Hash([NULLIFIER_DOMAIN, dstChainId, secretBigInt]);
241
+ }
242
+ function generatePaymentKey() {
243
+ while (true) {
244
+ const bytes = new Uint8Array(32);
245
+ crypto.getRandomValues(bytes);
246
+ const value = BigInt(
247
+ "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("")
248
+ );
249
+ if (value < PRIME) {
250
+ return "0x" + Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
251
+ }
252
+ }
253
+ }
254
+ function computeUnspendableAddress(secret, dstChainId, beneficiaries) {
255
+ const secretBigInt = hexToBigInt(secret);
256
+ validateSecret(secretBigInt);
257
+ const hash = poseidon2Hash([
258
+ ADDRESS_DOMAIN,
259
+ dstChainId,
260
+ secretBigInt,
261
+ addressToBigInt(beneficiaries[0]),
262
+ addressToBigInt(beneficiaries[1]),
263
+ addressToBigInt(beneficiaries[2]),
264
+ addressToBigInt(beneficiaries[3])
265
+ ]);
266
+ const addressMask = (1n << 160n) - 1n;
267
+ const addressBigInt = hash & addressMask;
268
+ return `0x${addressBigInt.toString(16).padStart(40, "0")}`;
269
+ }
270
+ var DEFAULT_CONFIG_PATH = "./private-payments.config.json";
271
+ function loadConfig(configPath) {
272
+ const path = configPath ?? DEFAULT_CONFIG_PATH;
273
+ if (!fs.existsSync(path)) {
274
+ throw new Error(`Config file not found: ${path}
275
+ Create a private-payments.config.json file or specify --config <path>`);
276
+ }
277
+ try {
278
+ const content = fs.readFileSync(path, "utf-8");
279
+ const config = JSON.parse(content);
280
+ const required = [
281
+ "proverUrl",
282
+ "sourceRpcUrl",
283
+ "destinationRpcUrl",
284
+ "srcZAssetAddress",
285
+ "dstZAssetAddress",
286
+ "sourceChainId",
287
+ "destinationChainId"
288
+ ];
289
+ for (const field of required) {
290
+ if (config[field] === void 0) {
291
+ throw new Error(`Missing required field in config: ${field}`);
292
+ }
293
+ }
294
+ return config;
295
+ } catch (e) {
296
+ if (e instanceof SyntaxError) {
297
+ throw new Error(`Invalid JSON in config file: ${path}`);
298
+ }
299
+ throw e;
300
+ }
301
+ }
302
+
303
+ // src/cli/utils.ts
304
+ var ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";
305
+ function parseBeneficiaries(input) {
306
+ if (!input || input.trim() === "") {
307
+ return [];
308
+ }
309
+ const addresses = input.split(",").map((s) => s.trim().toLowerCase());
310
+ if (addresses.length > 4) {
311
+ throw new Error("Maximum 4 beneficiaries allowed");
312
+ }
313
+ for (const addr of addresses) {
314
+ if (!isValidAddress(addr)) {
315
+ throw new Error(`Invalid address: ${addr}`);
316
+ }
317
+ }
318
+ return addresses;
319
+ }
320
+ function padBeneficiaries(beneficiaries) {
321
+ return [
322
+ beneficiaries[0] ?? ZERO_ADDRESS,
323
+ beneficiaries[1] ?? ZERO_ADDRESS,
324
+ beneficiaries[2] ?? ZERO_ADDRESS,
325
+ beneficiaries[3] ?? ZERO_ADDRESS
326
+ ];
327
+ }
328
+ function parseClientIds(input) {
329
+ const ids = input.split(",").map((s) => {
330
+ const id = parseInt(s.trim(), 10);
331
+ if (isNaN(id) || id < 0) {
332
+ throw new Error(`Invalid client ID: ${s}`);
333
+ }
334
+ return id;
335
+ });
336
+ if (ids.length === 0) {
337
+ throw new Error("At least one client ID is required");
338
+ }
339
+ return ids;
340
+ }
341
+ function validateSecret2(secret) {
342
+ if (!secret.startsWith("0x")) {
343
+ throw new Error("Secret must start with 0x");
344
+ }
345
+ if (secret.length !== 66) {
346
+ throw new Error("Secret must be 32 bytes (66 characters with 0x prefix)");
347
+ }
348
+ if (!/^0x[0-9a-fA-F]{64}$/.test(secret)) {
349
+ throw new Error("Secret must be a valid hex string");
350
+ }
351
+ return secret.toLowerCase();
352
+ }
353
+ function isValidAddress(address) {
354
+ return /^0x[0-9a-fA-F]{40}$/.test(address);
355
+ }
356
+ function validateAddress(address, name) {
357
+ if (!isValidAddress(address)) {
358
+ throw new Error(`Invalid ${name} address: ${address}`);
359
+ }
360
+ return address.toLowerCase();
361
+ }
362
+ function formatAmount(amount, decimals, symbol) {
363
+ const divisor = 10n ** BigInt(decimals);
364
+ const whole = amount / divisor;
365
+ const fraction = amount % divisor;
366
+ const fractionStr = fraction.toString().padStart(decimals, "0");
367
+ const trimmedFraction = fractionStr.replace(/0+$/, "");
368
+ const formatted = trimmedFraction ? `${whole}.${trimmedFraction}` : whole.toString();
369
+ return symbol ? `${formatted} ${symbol}` : formatted;
370
+ }
371
+ function getExplorerUrl(chainId, txHash) {
372
+ const explorers = {
373
+ 1: "https://etherscan.io",
374
+ 8453: "https://basescan.org",
375
+ 42161: "https://arbiscan.io",
376
+ 10: "https://optimistic.etherscan.io",
377
+ 137: "https://polygonscan.com"
378
+ };
379
+ const explorer = explorers[chainId];
380
+ if (explorer) {
381
+ return `${explorer}/tx/${txHash}`;
382
+ }
383
+ return txHash;
384
+ }
385
+
386
+ // src/cli/commands/generate.ts
387
+ var generateCommand = new commander.Command("generate").description(
388
+ "Generate a new unspendable address (generates random secret by default)"
389
+ ).option(
390
+ "--secret <hex>",
391
+ "Use existing 32-byte secret (0x-prefixed hex) instead of generating"
392
+ ).option(
393
+ "--beneficiaries <addresses>",
394
+ "Comma-separated beneficiary addresses (max 4, empty = unbounded)"
395
+ ).option(
396
+ "--config <path>",
397
+ "Path to config file",
398
+ "./private-payments.config.json"
399
+ ).action(async (options) => {
400
+ try {
401
+ const config = loadConfig(options.config);
402
+ const secret = options.secret ? validateSecret2(options.secret) : generatePaymentKey();
403
+ const isNewSecret = !options.secret;
404
+ const beneficiaries = parseBeneficiaries(options.beneficiaries);
405
+ const paddedBeneficiaries = padBeneficiaries(beneficiaries);
406
+ const depositAddress = computeUnspendableAddress(
407
+ secret,
408
+ BigInt(config.destinationChainId),
409
+ paddedBeneficiaries
410
+ );
411
+ const nullifier = computeNullifier(
412
+ secret,
413
+ BigInt(config.destinationChainId)
414
+ );
415
+ const isUnbounded = beneficiaries.length === 0;
416
+ if (isNewSecret) {
417
+ console.log("Secret:", secret);
418
+ console.log("");
419
+ console.log(
420
+ "WARNING: Save this secret! It is required to redeem funds."
421
+ );
422
+ console.log("");
423
+ }
424
+ console.log("Deposit Address:", depositAddress);
425
+ console.log(
426
+ "Nullifier:",
427
+ "0x" + nullifier.toString(16).padStart(64, "0")
428
+ );
429
+ console.log(
430
+ "Mode:",
431
+ isUnbounded ? "Unbounded (any beneficiary allowed)" : `Bounded (${beneficiaries.length} beneficiaries)`
432
+ );
433
+ if (!isUnbounded) {
434
+ console.log("Beneficiaries:");
435
+ for (const addr of beneficiaries) {
436
+ console.log(` - ${addr}`);
437
+ }
438
+ }
439
+ } catch (e) {
440
+ console.error("Error:", e.message);
441
+ process.exit(1);
442
+ }
443
+ });
444
+ var ERC20_ABI = [
445
+ {
446
+ inputs: [{ name: "account", type: "address" }],
447
+ name: "balanceOf",
448
+ outputs: [{ name: "", type: "uint256" }],
449
+ stateMutability: "view",
450
+ type: "function"
451
+ },
452
+ {
453
+ inputs: [],
454
+ name: "decimals",
455
+ outputs: [{ name: "", type: "uint8" }],
456
+ stateMutability: "view",
457
+ type: "function"
458
+ },
459
+ {
460
+ inputs: [],
461
+ name: "symbol",
462
+ outputs: [{ name: "", type: "string" }],
463
+ stateMutability: "view",
464
+ type: "function"
465
+ },
466
+ {
467
+ inputs: [
468
+ { name: "spender", type: "address" },
469
+ { name: "amount", type: "uint256" }
470
+ ],
471
+ name: "approve",
472
+ outputs: [{ name: "", type: "bool" }],
473
+ stateMutability: "nonpayable",
474
+ type: "function"
475
+ },
476
+ {
477
+ inputs: [
478
+ { name: "to", type: "address" },
479
+ { name: "amount", type: "uint256" }
480
+ ],
481
+ name: "transfer",
482
+ outputs: [{ name: "", type: "bool" }],
483
+ stateMutability: "nonpayable",
484
+ type: "function"
485
+ },
486
+ {
487
+ inputs: [
488
+ { name: "from", type: "address" },
489
+ { name: "to", type: "address" },
490
+ { name: "amount", type: "uint256" }
491
+ ],
492
+ name: "transferFrom",
493
+ outputs: [{ name: "", type: "bool" }],
494
+ stateMutability: "nonpayable",
495
+ type: "function"
496
+ }
497
+ ];
498
+ var IBC_STORE_ABI = [
499
+ {
500
+ inputs: [{ name: "clientId", type: "uint32" }],
501
+ name: "getClient",
502
+ outputs: [{ name: "", type: "address" }],
503
+ stateMutability: "view",
504
+ type: "function"
505
+ }
506
+ ];
507
+ var LIGHT_CLIENT_ABI = [
508
+ {
509
+ inputs: [{ name: "clientId", type: "uint32" }],
510
+ name: "getLatestHeight",
511
+ outputs: [{ name: "", type: "uint64" }],
512
+ stateMutability: "view",
513
+ type: "function"
514
+ },
515
+ {
516
+ inputs: [
517
+ { name: "clientId", type: "uint32" },
518
+ { name: "height", type: "uint64" }
519
+ ],
520
+ name: "getConsensusState",
521
+ outputs: [{ name: "", type: "bytes" }],
522
+ stateMutability: "view",
523
+ type: "function"
524
+ }
525
+ ];
526
+ var ZASSET_ABI = [
527
+ {
528
+ inputs: [{ name: "nullifier", type: "uint256" }],
529
+ name: "nullifierBalance",
530
+ outputs: [{ name: "", type: "uint256" }],
531
+ stateMutability: "view",
532
+ type: "function"
533
+ },
534
+ {
535
+ inputs: [{ name: "clientId", type: "uint32" }],
536
+ name: "counterparty",
537
+ outputs: [
538
+ {
539
+ components: [
540
+ { name: "tokenAddressKey", type: "bytes32" },
541
+ { name: "balanceSlot", type: "bytes32" }
542
+ ],
543
+ type: "tuple"
544
+ }
545
+ ],
546
+ stateMutability: "view",
547
+ type: "function"
548
+ },
549
+ {
550
+ inputs: [],
551
+ name: "ibcHandler",
552
+ outputs: [{ name: "", type: "address" }],
553
+ stateMutability: "view",
554
+ type: "function"
555
+ },
556
+ {
557
+ inputs: [{ name: "clientId", type: "uint32" }],
558
+ name: "stateRootIndex",
559
+ outputs: [{ name: "", type: "uint256" }],
560
+ stateMutability: "view",
561
+ type: "function"
562
+ },
563
+ {
564
+ inputs: [],
565
+ name: "underlying",
566
+ outputs: [{ name: "", type: "address" }],
567
+ stateMutability: "view",
568
+ type: "function"
569
+ },
570
+ {
571
+ inputs: [{ name: "amount", type: "uint256" }],
572
+ name: "deposit",
573
+ outputs: [],
574
+ stateMutability: "nonpayable",
575
+ type: "function"
576
+ },
577
+ {
578
+ inputs: [
579
+ { name: "to", type: "address" },
580
+ { name: "amount", type: "uint256" }
581
+ ],
582
+ name: "transfer",
583
+ outputs: [{ name: "", type: "bool" }],
584
+ stateMutability: "nonpayable",
585
+ type: "function"
586
+ },
587
+ {
588
+ inputs: [
589
+ { name: "proof", type: "uint256[8]" },
590
+ { name: "commitments", type: "uint256[2]" },
591
+ { name: "commitmentPok", type: "uint256[2]" },
592
+ {
593
+ name: "lightClients",
594
+ type: "tuple[]",
595
+ components: [
596
+ { name: "clientId", type: "uint32" },
597
+ { name: "height", type: "uint64" }
598
+ ]
599
+ },
600
+ { name: "nullifier", type: "uint256" },
601
+ { name: "value", type: "uint256" },
602
+ { name: "beneficiary", type: "address" },
603
+ { name: "attestedMessage", type: "bytes32" },
604
+ { name: "signature", type: "bytes" }
605
+ ],
606
+ name: "redeem",
607
+ outputs: [],
608
+ stateMutability: "nonpayable",
609
+ type: "function"
610
+ },
611
+ {
612
+ anonymous: false,
613
+ inputs: [
614
+ { indexed: true, name: "from", type: "address" },
615
+ { indexed: true, name: "to", type: "address" },
616
+ { indexed: false, name: "value", type: "uint256" }
617
+ ],
618
+ name: "Transfer",
619
+ type: "event"
620
+ },
621
+ {
622
+ anonymous: false,
623
+ inputs: [
624
+ { indexed: true, name: "nullifier", type: "uint256" },
625
+ { indexed: true, name: "redeemAmount", type: "uint256" },
626
+ { indexed: true, name: "beneficiary", type: "address" }
627
+ ],
628
+ name: "Redeemed",
629
+ type: "event"
630
+ }
631
+ ];
632
+ var IBC_HANDLER_ABI = [
633
+ {
634
+ inputs: [
635
+ {
636
+ components: [
637
+ { name: "clientId", type: "uint32" },
638
+ { name: "clientMessage", type: "bytes" },
639
+ { name: "relayer", type: "address" }
640
+ ],
641
+ name: "msg_",
642
+ type: "tuple"
643
+ }
644
+ ],
645
+ name: "updateClient",
646
+ outputs: [],
647
+ stateMutability: "nonpayable",
648
+ type: "function"
649
+ }
650
+ ];
651
+ var RpcClient = class {
652
+ client;
653
+ constructor(rpcUrl) {
654
+ this.client = V.createPublicClient({
655
+ transport: V.http(rpcUrl)
656
+ });
657
+ }
658
+ getClient() {
659
+ return this.client;
660
+ }
661
+ async getChainId() {
662
+ return BigInt(await this.client.getChainId());
663
+ }
664
+ async getLatestBlockNumber() {
665
+ return this.client.getBlockNumber();
666
+ }
667
+ async getBalance(tokenAddress, accountAddress, blockNumber) {
668
+ return this.client.readContract({
669
+ address: tokenAddress,
670
+ abi: ERC20_ABI,
671
+ functionName: "balanceOf",
672
+ args: [accountAddress],
673
+ blockNumber
674
+ });
675
+ }
676
+ async getDecimals(tokenAddress) {
677
+ return this.client.readContract({
678
+ address: tokenAddress,
679
+ abi: ERC20_ABI,
680
+ functionName: "decimals"
681
+ });
682
+ }
683
+ async getSymbol(tokenAddress) {
684
+ return this.client.readContract({
685
+ address: tokenAddress,
686
+ abi: ERC20_ABI,
687
+ functionName: "symbol"
688
+ });
689
+ }
690
+ async getLightClientAddress(ibcStoreAddress, clientId) {
691
+ return this.client.readContract({
692
+ address: ibcStoreAddress,
693
+ abi: IBC_STORE_ABI,
694
+ functionName: "getClient",
695
+ args: [clientId]
696
+ });
697
+ }
698
+ async getLatestHeight(lightClientAddress, clientId) {
699
+ return this.client.readContract({
700
+ address: lightClientAddress,
701
+ abi: LIGHT_CLIENT_ABI,
702
+ functionName: "getLatestHeight",
703
+ args: [clientId]
704
+ });
705
+ }
706
+ /**
707
+ * Get consensus state, extracting the state root at the given byte index
708
+ */
709
+ async getConsensusState(lightClientAddress, clientId, height, stateRootIndex) {
710
+ const consensusBytes = await this.client.readContract({
711
+ address: lightClientAddress,
712
+ abi: LIGHT_CLIENT_ABI,
713
+ functionName: "getConsensusState",
714
+ args: [clientId, height]
715
+ });
716
+ const bytes = V.hexToBytes(consensusBytes);
717
+ const idx = Number(stateRootIndex);
718
+ const stateRoot = V.bytesToHex(bytes.slice(idx, idx + 32));
719
+ return { stateRoot };
720
+ }
721
+ async getNullifierBalance(zassetAddress, nullifier) {
722
+ return this.client.readContract({
723
+ address: zassetAddress,
724
+ abi: ZASSET_ABI,
725
+ functionName: "nullifierBalance",
726
+ args: [nullifier]
727
+ });
728
+ }
729
+ async getCounterparty(zassetAddress, clientId) {
730
+ const result = await this.client.readContract({
731
+ address: zassetAddress,
732
+ abi: ZASSET_ABI,
733
+ functionName: "counterparty",
734
+ args: [clientId]
735
+ });
736
+ return {
737
+ tokenAddressKey: result.tokenAddressKey,
738
+ balanceSlot: result.balanceSlot
739
+ };
740
+ }
741
+ async getIbcHandlerAddress(zassetAddress) {
742
+ return this.client.readContract({
743
+ address: zassetAddress,
744
+ abi: ZASSET_ABI,
745
+ functionName: "ibcHandler"
746
+ });
747
+ }
748
+ async getStateRootIndex(zassetAddress, clientId) {
749
+ return this.client.readContract({
750
+ address: zassetAddress,
751
+ abi: ZASSET_ABI,
752
+ functionName: "stateRootIndex",
753
+ args: [clientId]
754
+ });
755
+ }
756
+ async getProof(address, storageKeys, blockNumber) {
757
+ return this.client.getProof({
758
+ address,
759
+ storageKeys,
760
+ blockNumber
761
+ });
762
+ }
763
+ async getBlock(blockNumber) {
764
+ return this.client.getBlock({ blockNumber });
765
+ }
766
+ /**
767
+ * Get redemption history for a nullifier by querying Redeemed events
768
+ */
769
+ async getRedemptionHistory(zassetAddress, nullifier, fromBlock, toBlock) {
770
+ const logs = await this.client.getLogs({
771
+ address: zassetAddress,
772
+ event: {
773
+ type: "event",
774
+ name: "Redeemed",
775
+ inputs: [
776
+ { indexed: true, name: "nullifier", type: "uint256" },
777
+ { indexed: true, name: "redeemAmount", type: "uint256" },
778
+ { indexed: true, name: "beneficiary", type: "address" }
779
+ ]
780
+ },
781
+ args: {
782
+ nullifier
783
+ },
784
+ fromBlock: fromBlock ?? "earliest",
785
+ toBlock: toBlock ?? "latest"
786
+ });
787
+ return logs.map((log) => ({
788
+ txHash: log.transactionHash,
789
+ blockNumber: log.blockNumber,
790
+ redeemAmount: log.args.redeemAmount,
791
+ beneficiary: log.args.beneficiary
792
+ }));
793
+ }
794
+ };
795
+ function computeStorageSlot(address, mappingSlot) {
796
+ const paddedAddress = V.padHex(address, { size: 32 });
797
+ const paddedSlot = V.padHex(V.toHex(mappingSlot), { size: 32 });
798
+ return V.keccak256(V.encodePacked(["bytes32", "bytes32"], [paddedAddress, paddedSlot]));
799
+ }
800
+ async function fetchLightClients(dstClient, zassetAddress, clientIds) {
801
+ const ibcHandlerAddress = await dstClient.getIbcHandlerAddress(zassetAddress);
802
+ const results = [];
803
+ for (const clientId of clientIds) {
804
+ try {
805
+ const lightClientAddress = await dstClient.getLightClientAddress(
806
+ ibcHandlerAddress,
807
+ clientId
808
+ );
809
+ const height = await dstClient.getLatestHeight(lightClientAddress, clientId);
810
+ const stateRootIndex = await dstClient.getStateRootIndex(zassetAddress, clientId);
811
+ const { stateRoot } = await dstClient.getConsensusState(
812
+ lightClientAddress,
813
+ clientId,
814
+ height,
815
+ stateRootIndex
816
+ );
817
+ results.push({ clientId, height, stateRoot });
818
+ } catch {
819
+ continue;
820
+ }
821
+ }
822
+ return results;
823
+ }
824
+ async function fetchMptProof(srcClient, tokenAddress, storageSlot, blockNumber) {
825
+ const proof = await srcClient.getProof(tokenAddress, [storageSlot], blockNumber);
826
+ let storageProof = [];
827
+ let storageValue = "0x0";
828
+ if (proof.storageProof.length > 0 && proof.storageProof[0]) {
829
+ storageProof = proof.storageProof[0].proof;
830
+ storageValue = V.toHex(proof.storageProof[0].value);
831
+ }
832
+ return {
833
+ accountProof: proof.accountProof,
834
+ storageProof,
835
+ storageValue,
836
+ storageRoot: proof.storageHash
837
+ };
838
+ }
839
+ function deterministicShuffleClients(clients, secret) {
840
+ if (clients.length <= 1) {
841
+ return [...clients];
842
+ }
843
+ const secretBytes = V.hexToBytes(secret);
844
+ const heightsBytes = new Uint8Array(clients.length * 8);
845
+ for (let i = 0; i < clients.length; i++) {
846
+ const view = new DataView(heightsBytes.buffer, i * 8, 8);
847
+ view.setBigUint64(0, clients[i].height, false);
848
+ }
849
+ const combined = new Uint8Array(secretBytes.length + heightsBytes.length);
850
+ combined.set(secretBytes, 0);
851
+ combined.set(heightsBytes, secretBytes.length);
852
+ const seedHex = V.sha256(combined);
853
+ const seed = V.hexToBytes(seedHex);
854
+ const shuffled = [...clients];
855
+ let seedIndex = 0;
856
+ for (let i = shuffled.length - 1; i > 0; i--) {
857
+ const randomByte = seed[seedIndex % seed.length];
858
+ seedIndex++;
859
+ const j = randomByte % (i + 1);
860
+ [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
861
+ }
862
+ return shuffled;
863
+ }
864
+ function parseProofJson(proofJsonBytes) {
865
+ const jsonStr = new TextDecoder().decode(proofJsonBytes);
866
+ const raw = JSON.parse(jsonStr);
867
+ const commitments = raw.commitments ?? ["0x0", "0x0"];
868
+ return {
869
+ proof: raw.proof,
870
+ commitments,
871
+ commitmentPok: raw.commitmentPok,
872
+ publicInputs: raw.publicInputs ?? []
873
+ };
874
+ }
875
+ function proofJsonToRedeemParams(proofJson, metadata) {
876
+ const parseBigInt = (s) => {
877
+ if (s.startsWith("0x")) {
878
+ return V.hexToBigInt(s);
879
+ }
880
+ return BigInt(s);
881
+ };
882
+ return {
883
+ proof: proofJson.proof.map(parseBigInt),
884
+ commitments: proofJson.commitments.map(parseBigInt),
885
+ commitmentPok: proofJson.commitmentPok.map(parseBigInt),
886
+ lightClients: metadata.lightClients.map((lc) => ({
887
+ clientId: lc.clientId,
888
+ height: lc.height
889
+ })),
890
+ nullifier: metadata.nullifier,
891
+ value: metadata.value,
892
+ beneficiary: metadata.beneficiary,
893
+ attestedMessage: metadata.attestedMessage,
894
+ signature: metadata.signature
895
+ };
896
+ }
897
+ async function submitRedeem(zassetAddress, params, walletClient) {
898
+ if (!walletClient.account) {
899
+ throw new Error("WalletClient must have an account");
900
+ }
901
+ if (!walletClient.chain) {
902
+ throw new Error("WalletClient must have a chain configured");
903
+ }
904
+ const hash = await walletClient.writeContractSync({
905
+ address: zassetAddress,
906
+ abi: ZASSET_ABI,
907
+ functionName: "redeem",
908
+ args: [
909
+ params.proof,
910
+ params.commitments,
911
+ params.commitmentPok,
912
+ params.lightClients,
913
+ params.nullifier,
914
+ params.value,
915
+ params.beneficiary,
916
+ params.attestedMessage,
917
+ params.signature
918
+ ],
919
+ chain: walletClient.chain,
920
+ account: walletClient.account
921
+ }).then((x) => x.transactionHash);
922
+ return hash;
923
+ }
924
+ function rlpEncodeBlockHeader(block) {
925
+ const toRlpHex = (value) => {
926
+ if (value === void 0 || value === null || value === 0n || value === 0) {
927
+ return "0x";
928
+ }
929
+ let hex = typeof value === "bigint" ? value.toString(16) : value.toString(16);
930
+ if (hex.length % 2 !== 0) {
931
+ hex = "0" + hex;
932
+ }
933
+ return `0x${hex}`;
934
+ };
935
+ const headerFields = [
936
+ block.parentHash,
937
+ block.sha3Uncles,
938
+ block.miner,
939
+ block.stateRoot,
940
+ block.transactionsRoot,
941
+ block.receiptsRoot,
942
+ block.logsBloom ?? "0x" + "00".repeat(256),
943
+ toRlpHex(block.difficulty),
944
+ toRlpHex(block.number),
945
+ toRlpHex(block.gasLimit),
946
+ toRlpHex(block.gasUsed),
947
+ toRlpHex(block.timestamp),
948
+ block.extraData,
949
+ block.mixHash ?? "0x0000000000000000000000000000000000000000000000000000000000000000",
950
+ block.nonce ?? "0x0000000000000000"
951
+ ];
952
+ if (block.baseFeePerGas !== void 0 && block.baseFeePerGas !== null) {
953
+ headerFields.push(toRlpHex(block.baseFeePerGas));
954
+ }
955
+ if (block.withdrawalsRoot) {
956
+ headerFields.push(block.withdrawalsRoot);
957
+ }
958
+ if (block.blobGasUsed !== void 0 && block.blobGasUsed !== null) {
959
+ headerFields.push(toRlpHex(block.blobGasUsed));
960
+ }
961
+ if (block.excessBlobGas !== void 0 && block.excessBlobGas !== null) {
962
+ headerFields.push(toRlpHex(block.excessBlobGas));
963
+ }
964
+ if (block.parentBeaconBlockRoot) {
965
+ headerFields.push(block.parentBeaconBlockRoot);
966
+ }
967
+ const blockAny = block;
968
+ if (blockAny.requestsHash) {
969
+ headerFields.push(blockAny.requestsHash);
970
+ }
971
+ const rlpEncoded = V.toRlp(headerFields);
972
+ const computedHash = V.keccak256(rlpEncoded);
973
+ if (computedHash !== block.hash) {
974
+ throw new Error(
975
+ `RLP encoding mismatch: computed hash ${computedHash} does not match block hash ${block.hash}. Block number: ${block.number}, fields count: ${headerFields.length}`
976
+ );
977
+ }
978
+ return rlpEncoded;
979
+ }
980
+ async function updateLoopbackClient(rpcUrl, ibcHandlerAddress, clientId, height, walletClient) {
981
+ if (!walletClient.account) {
982
+ throw new Error("WalletClient must have an account");
983
+ }
984
+ if (!walletClient.chain) {
985
+ throw new Error("WalletClient must have a chain configured");
986
+ }
987
+ const publicClient = V.createPublicClient({
988
+ transport: V.http(rpcUrl)
989
+ });
990
+ const chainId = await publicClient.getChainId();
991
+ const blockNumber = height === "latest" ? await publicClient.getBlockNumber() : height;
992
+ const block = await publicClient.getBlock({ blockNumber });
993
+ if (!block.number) {
994
+ throw new Error("Block number is null");
995
+ }
996
+ const rlpEncodedHeader = rlpEncodeBlockHeader(block);
997
+ const clientMessage = V.encodeAbiParameters(
998
+ [
999
+ { type: "uint64", name: "height" },
1000
+ { type: "bytes", name: "encodedHeader" }
1001
+ ],
1002
+ [block.number, rlpEncodedHeader]
1003
+ );
1004
+ let currentBlock = await publicClient.getBlockNumber();
1005
+ while (currentBlock <= blockNumber) {
1006
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
1007
+ currentBlock = await publicClient.getBlockNumber();
1008
+ }
1009
+ const txHash = await walletClient.writeContractSync({
1010
+ address: ibcHandlerAddress,
1011
+ abi: IBC_HANDLER_ABI,
1012
+ functionName: "updateClient",
1013
+ args: [
1014
+ {
1015
+ clientId,
1016
+ clientMessage,
1017
+ relayer: walletClient.account.address
1018
+ }
1019
+ ],
1020
+ chain: walletClient.chain,
1021
+ account: walletClient.account
1022
+ }).then((x) => x.transactionHash);
1023
+ return {
1024
+ txHash,
1025
+ blockNumber: block.number,
1026
+ stateRoot: block.stateRoot,
1027
+ chainId: BigInt(chainId)
1028
+ };
1029
+ }
1030
+ async function depositToZAsset(rpcUrl, zAssetAddress, depositAddress, amount, walletClient) {
1031
+ if (!walletClient.account) {
1032
+ throw new Error("WalletClient must have an account");
1033
+ }
1034
+ if (!walletClient.chain) {
1035
+ throw new Error("WalletClient must have a chain configured");
1036
+ }
1037
+ const publicClient = V.createPublicClient({
1038
+ transport: V.http(rpcUrl)
1039
+ });
1040
+ const chainId = await publicClient.getChainId();
1041
+ const underlyingToken = await publicClient.readContract({
1042
+ address: zAssetAddress,
1043
+ abi: ZASSET_ABI,
1044
+ functionName: "underlying"
1045
+ });
1046
+ if (underlyingToken === "0x0000000000000000000000000000000000000000") {
1047
+ throw new Error("ZAsset is not a wrapped token (underlying is zero address)");
1048
+ }
1049
+ const approveReceipt = await walletClient.writeContractSync({
1050
+ address: underlyingToken,
1051
+ abi: ERC20_ABI,
1052
+ functionName: "approve",
1053
+ args: [zAssetAddress, amount],
1054
+ chain: walletClient.chain,
1055
+ account: walletClient.account
1056
+ });
1057
+ if (approveReceipt.status === "reverted") {
1058
+ throw new Error(`Approve transaction reverted: ${approveReceipt.transactionHash}`);
1059
+ }
1060
+ const depositReceipt = await walletClient.writeContractSync({
1061
+ address: zAssetAddress,
1062
+ abi: ZASSET_ABI,
1063
+ functionName: "deposit",
1064
+ args: [amount],
1065
+ chain: walletClient.chain,
1066
+ account: walletClient.account
1067
+ });
1068
+ if (depositReceipt.status === "reverted") {
1069
+ throw new Error(`Deposit transaction reverted: ${depositReceipt.transactionHash}`);
1070
+ }
1071
+ const transferReceipt = await walletClient.writeContractSync({
1072
+ address: zAssetAddress,
1073
+ abi: ZASSET_ABI,
1074
+ functionName: "transfer",
1075
+ args: [depositAddress, amount],
1076
+ chain: walletClient.chain,
1077
+ account: walletClient.account
1078
+ });
1079
+ if (transferReceipt.status === "reverted") {
1080
+ throw new Error(`Transfer transaction reverted: ${transferReceipt.transactionHash}`);
1081
+ }
1082
+ return {
1083
+ txHash: transferReceipt.transactionHash,
1084
+ underlyingToken,
1085
+ chainId: BigInt(chainId)
1086
+ };
1087
+ }
1088
+ var file_prover = /* @__PURE__ */ codegenv2.fileDesc("Cgxwcm92ZXIucHJvdG8SBnByb3ZlciI0CgxQcm9vZlJlcXVlc3QSJAoHd2l0bmVzcxgBIAEoCzITLnByb3Zlci5XaXRuZXNzRGF0YSK1AgoLV2l0bmVzc0RhdGESDgoGc2VjcmV0GAEgASgJEhQKDGRzdF9jaGFpbl9pZBgCIAEoBBIVCg1iZW5lZmljaWFyaWVzGAMgAygJEhMKC2JlbmVmaWNpYXJ5GAQgASgJEhUKDXJlZGVlbV9hbW91bnQYBSABKAkSGAoQYWxyZWFkeV9yZWRlZW1lZBgGIAEoCRIuCg1saWdodF9jbGllbnRzGAcgAygLMhcucHJvdmVyLkxpZ2h0Q2xpZW50RGF0YRIdChVzZWxlY3RlZF9jbGllbnRfaW5kZXgYCCABKA0SJwoJbXB0X3Byb29mGAkgASgLMhQucHJvdmVyLk1QVFByb29mRGF0YRIVCg10b2tlbl9hZGRyZXNzGAogASgJEhQKDG1hcHBpbmdfc2xvdBgLIAEoCSJICg9MaWdodENsaWVudERhdGESEQoJY2xpZW50X2lkGAEgASgNEg4KBmhlaWdodBgCIAEoBBISCgpzdGF0ZV9yb290GAMgASgJImkKDE1QVFByb29mRGF0YRIVCg1hY2NvdW50X3Byb29mGAEgAygJEhUKDXN0b3JhZ2VfcHJvb2YYAiADKAkSFQoNc3RvcmFnZV92YWx1ZRgDIAEoCRIUCgxzdG9yYWdlX3Jvb3QYBCABKAkifAoMUG9sbFJlc3BvbnNlEiIKB3BlbmRpbmcYASABKAsyDy5wcm92ZXIuUGVuZGluZ0gAEhwKBGRvbmUYAiABKAsyDC5wcm92ZXIuRG9uZUgAEiAKBmZhaWxlZBgDIAEoCzIOLnByb3Zlci5GYWlsZWRIAEIICgZyZXN1bHQiCQoHUGVuZGluZyIuCgREb25lEhIKCnByb29mX2pzb24YASABKAwSEgoKY3JlYXRlZF9hdBgCIAEoAyIfCgZGYWlsZWQSFQoNZXJyb3JfbWVzc2FnZRgBIAEoCSIRCg9WZXJpZmllclJlcXVlc3QiLQoQVmVyaWZpZXJSZXNwb25zZRIZChF2ZXJpZmllcl9jb250cmFjdBgBIAEoCTKIAQoNUHJvdmVyU2VydmljZRIyCgRQb2xsEhQucHJvdmVyLlByb29mUmVxdWVzdBoULnByb3Zlci5Qb2xsUmVzcG9uc2USQwoORXhwb3J0VmVyaWZpZXISFy5wcm92ZXIuVmVyaWZpZXJSZXF1ZXN0GhgucHJvdmVyLlZlcmlmaWVyUmVzcG9uc2VCHFoacHJpdmF0ZS10cmFuc2Zlci9hcGkvcHJvdG9iBnByb3RvMw");
1089
+ var ProofRequestSchema = /* @__PURE__ */ codegenv2.messageDesc(file_prover, 0);
1090
+ var WitnessDataSchema = /* @__PURE__ */ codegenv2.messageDesc(file_prover, 1);
1091
+ var LightClientDataSchema = /* @__PURE__ */ codegenv2.messageDesc(file_prover, 2);
1092
+ var MPTProofDataSchema = /* @__PURE__ */ codegenv2.messageDesc(file_prover, 3);
1093
+ var ProverService = /* @__PURE__ */ codegenv2.serviceDesc(file_prover, 0);
1094
+
1095
+ // src/prover.ts
1096
+ var ProverClient = class {
1097
+ client;
1098
+ pollIntervalMs;
1099
+ maxPollAttempts;
1100
+ constructor(proverUrl, options) {
1101
+ const transport = connectWeb.createGrpcWebTransport({
1102
+ baseUrl: proverUrl
1103
+ });
1104
+ this.client = connect.createClient(ProverService, transport);
1105
+ this.pollIntervalMs = options?.pollIntervalMs ?? 2e3;
1106
+ this.maxPollAttempts = options?.maxPollAttempts ?? 300;
1107
+ }
1108
+ /**
1109
+ * Generate a proof by sending witness data to the prover server
1110
+ *
1111
+ * The server handles witness transformation internally, so we send
1112
+ * the structured WitnessData instead of pre-serialized circuit witness bytes.
1113
+ */
1114
+ async generateProof(witness) {
1115
+ const protoWitness = this.witnessToProto(witness);
1116
+ const request = protobuf.create(ProofRequestSchema, {
1117
+ witness: protoWitness
1118
+ });
1119
+ let attempts = 0;
1120
+ while (attempts < this.maxPollAttempts) {
1121
+ try {
1122
+ const response = await this.client.poll(request);
1123
+ switch (response.result.case) {
1124
+ case "done":
1125
+ return {
1126
+ success: true,
1127
+ proofJson: response.result.value.proofJson,
1128
+ createdAt: new Date(Number(response.result.value.createdAt) * 1e3)
1129
+ };
1130
+ case "failed":
1131
+ return {
1132
+ success: false,
1133
+ error: response.result.value.errorMessage
1134
+ };
1135
+ case "pending":
1136
+ await this.sleep(this.pollIntervalMs);
1137
+ attempts++;
1138
+ break;
1139
+ default:
1140
+ await this.sleep(this.pollIntervalMs);
1141
+ attempts++;
1142
+ }
1143
+ } catch (error) {
1144
+ return {
1145
+ success: false,
1146
+ error: `RPC error: ${error instanceof Error ? error.message : String(error)}`
1147
+ };
1148
+ }
1149
+ }
1150
+ return {
1151
+ success: false,
1152
+ error: "Proof generation timed out"
1153
+ };
1154
+ }
1155
+ /**
1156
+ * Export the verifier contract from the prover server
1157
+ */
1158
+ async exportVerifier() {
1159
+ const response = await this.client.exportVerifier({});
1160
+ return response.verifierContract;
1161
+ }
1162
+ witnessToProto(witness) {
1163
+ return protobuf.create(WitnessDataSchema, {
1164
+ secret: witness.secret,
1165
+ dstChainId: witness.dstChainId,
1166
+ beneficiaries: [...witness.beneficiaries],
1167
+ beneficiary: witness.beneficiary,
1168
+ redeemAmount: witness.redeemAmount.toString(),
1169
+ alreadyRedeemed: witness.alreadyRedeemed.toString(),
1170
+ lightClients: witness.lightClients.map(
1171
+ (lc) => protobuf.create(LightClientDataSchema, {
1172
+ clientId: lc.clientId,
1173
+ height: lc.height,
1174
+ stateRoot: lc.stateRoot
1175
+ })
1176
+ ),
1177
+ selectedClientIndex: witness.selectedClientIndex,
1178
+ mptProof: protobuf.create(MPTProofDataSchema, {
1179
+ accountProof: [...witness.mptProof.accountProof],
1180
+ storageProof: [...witness.mptProof.storageProof],
1181
+ storageValue: witness.mptProof.storageValue,
1182
+ storageRoot: witness.mptProof.storageRoot
1183
+ }),
1184
+ tokenAddress: witness.srcZAssetAddress,
1185
+ mappingSlot: witness.mappingSlot
1186
+ });
1187
+ }
1188
+ sleep(ms) {
1189
+ return new Promise((resolve) => setTimeout(resolve, ms));
1190
+ }
1191
+ };
1192
+
1193
+ // src/attestation.ts
1194
+ var AttestationClient = class {
1195
+ baseUrl;
1196
+ apiKey;
1197
+ constructor(baseUrl, apiKey) {
1198
+ this.baseUrl = baseUrl.replace(/\/$/, "");
1199
+ this.apiKey = apiKey;
1200
+ }
1201
+ async getAttestation(unspendableAddress, beneficiary) {
1202
+ const request = {
1203
+ unspendableAddress,
1204
+ beneficiary
1205
+ };
1206
+ const headers = {
1207
+ "Content-Type": "application/json"
1208
+ };
1209
+ headers["x-api-key"] = this.apiKey;
1210
+ const response = await fetch(this.baseUrl, {
1211
+ method: "POST",
1212
+ headers,
1213
+ body: JSON.stringify(request)
1214
+ });
1215
+ if (!response.ok) {
1216
+ const errorText = await response.text();
1217
+ throw new Error(`Attestation service error: ${response.status} ${errorText}`);
1218
+ }
1219
+ const data = await response.json();
1220
+ const r = data.signature.r.slice(2);
1221
+ const s = data.signature.s.slice(2);
1222
+ const v = data.signature.v.toString(16).padStart(2, "0");
1223
+ const signature = `0x${r}${s}${v}`;
1224
+ return {
1225
+ attestedMessage: data.hash,
1226
+ signature
1227
+ };
1228
+ }
1229
+ };
1230
+ var publicClientLayer = (tag) => (...options) => effect.Layer.effect(
1231
+ tag,
1232
+ effect.pipe(
1233
+ effect.Effect.try({
1234
+ try: () => V__namespace.createPublicClient(...options),
1235
+ catch: (err) => new CreatePublicClientError({
1236
+ cause: err
1237
+ })
1238
+ }),
1239
+ effect.Effect.map((client) => ({ client }))
1240
+ )
1241
+ );
1242
+ var walletClientLayer = (tag) => (options) => effect.Layer.effect(
1243
+ tag,
1244
+ effect.pipe(
1245
+ effect.Effect.try({
1246
+ try: () => V__namespace.createWalletClient(options),
1247
+ catch: (err) => new CreateWalletClientError({
1248
+ cause: err
1249
+ })
1250
+ }),
1251
+ effect.Effect.map((client) => ({ client, account: options.account, chain: options.chain }))
1252
+ )
1253
+ );
1254
+ var WaitForTransactionReceiptError = class extends effect.Data.TaggedError(
1255
+ "WaitForTransactionReceiptError"
1256
+ ) {
1257
+ };
1258
+ var waitForTransactionReceipt = effect.Effect.fn("waitForTransactionReceipt")(
1259
+ (hash) => effect.pipe(
1260
+ PublicClient,
1261
+ effect.Effect.andThen(
1262
+ ({ client }) => effect.Effect.tryPromise({
1263
+ try: () => client.waitForTransactionReceipt({ hash }),
1264
+ catch: (err) => new WaitForTransactionReceiptError({
1265
+ cause: err
1266
+ })
1267
+ })
1268
+ )
1269
+ )
1270
+ );
1271
+ var readContract = effect.Effect.fn("readContract")(
1272
+ (params) => effect.pipe(
1273
+ PublicClient,
1274
+ effect.Effect.andThen(
1275
+ ({ client }) => effect.Effect.tryPromise({
1276
+ try: () => client.readContract(params),
1277
+ catch: (error) => new ReadContractError({
1278
+ cause: error
1279
+ })
1280
+ })
1281
+ )
1282
+ )
1283
+ );
1284
+ var writeContract = effect.Effect.fn("writeContract")(
1285
+ (params) => effect.pipe(
1286
+ WalletClient,
1287
+ effect.Effect.andThen(
1288
+ ({ client }) => effect.Effect.tryPromise({
1289
+ try: () => client.writeContract(params),
1290
+ catch: (error) => new WriteContractError({
1291
+ cause: error
1292
+ })
1293
+ })
1294
+ )
1295
+ )
1296
+ );
1297
+ effect.Effect.fn("writeContract")(
1298
+ (params) => effect.pipe(
1299
+ WalletClient,
1300
+ effect.Effect.andThen(
1301
+ ({ client }) => effect.Effect.tryPromise({
1302
+ try: () => client.writeContract(params),
1303
+ catch: (error) => new WriteContractError({
1304
+ cause: error
1305
+ })
1306
+ })
1307
+ )
1308
+ )
1309
+ );
1310
+ (class _ChannelDestination extends effect.Context.Tag("@unionlabs/sdk/Evm/ChannelDestination")() {
1311
+ static Live = effect.flow(
1312
+ _ChannelDestination.of,
1313
+ effect.Layer.succeed(this)
1314
+ );
1315
+ });
1316
+ (class _ChannelSource extends effect.Context.Tag("@unionlabs/sdk/Evm/ChannelSource")() {
1317
+ static Live = effect.flow(
1318
+ _ChannelSource.of,
1319
+ effect.Layer.succeed(this)
1320
+ );
1321
+ });
1322
+ (class extends effect.Context.Tag("@unionlabs/sdk/Evm/PublicClientSource")() {
1323
+ static Live = publicClientLayer(this);
1324
+ });
1325
+ (class extends effect.Context.Tag("@unionlabs/sdk/Evm/PublicClientDestination")() {
1326
+ static Live = publicClientLayer(this);
1327
+ });
1328
+ var PublicClient = class extends effect.Context.Tag("@unionlabs/sdk-evm/Evm/PublicClient")() {
1329
+ static Live = publicClientLayer(this);
1330
+ };
1331
+ var WalletClient = class extends effect.Context.Tag("@unionlabs/sdk/Evm/WalletClient")() {
1332
+ static Live = walletClientLayer(this);
1333
+ };
1334
+ var ReadContractError = class extends effect.Data.TaggedError("@unionlabs/sdk/Evm/ReadContractError") {
1335
+ };
1336
+ var WriteContractError = class extends effect.Data.TaggedError("@unionlabs/sdk/Evm/WriteContractError") {
1337
+ };
1338
+ (class extends effect.Data.TaggedError("@unionlabs/sdk/Evm/SimulateContractError") {
1339
+ });
1340
+ var CreatePublicClientError = class extends effect.Data.TaggedError("@unionlabs/sdk/Evm/CreatePublicClientError") {
1341
+ };
1342
+ var CreateWalletClientError = class extends effect.Data.TaggedError("@unionlabs/sdk/Evm/CreateWalletClientError") {
1343
+ };
1344
+
1345
+ // src/client.ts
1346
+ var ZERO_ADDRESS2 = "0x0000000000000000000000000000000000000000";
1347
+ var commonRetry = {
1348
+ schedule: effect.Schedule.linear(effect.Duration.seconds(2)),
1349
+ times: 6
1350
+ };
1351
+ var UnionPrivatePayments = class {
1352
+ config;
1353
+ srcClient;
1354
+ dstClient;
1355
+ proverClient;
1356
+ constructor(config) {
1357
+ this.config = config;
1358
+ this.srcClient = new RpcClient(config.sourceRpcUrl);
1359
+ this.dstClient = new RpcClient(config.destinationRpcUrl);
1360
+ this.proverClient = new ProverClient(config.proverUrl);
1361
+ }
1362
+ /**
1363
+ * Get the deposit address for a given secret and beneficiaries
1364
+ *
1365
+ * The returned address is an "unspendable" address derived from the secret.
1366
+ * Tokens sent to this address can only be redeemed via ZK proof.
1367
+ *
1368
+ * @param secret - The 32-byte secret as a hex string
1369
+ * @param beneficiaries - Array of 1-4 beneficiary addresses (remaining slots zero-padded)
1370
+ * @returns The unspendable deposit address
1371
+ */
1372
+ getDepositAddress(secret, beneficiaries) {
1373
+ const paddedBeneficiaries = this.padBeneficiaries(beneficiaries);
1374
+ return computeUnspendableAddress(
1375
+ secret,
1376
+ this.config.destinationChainId,
1377
+ paddedBeneficiaries
1378
+ );
1379
+ }
1380
+ /**
1381
+ * Get the nullifier for a given paymentKey
1382
+ *
1383
+ * The nullifier is used to prevent double-spending. Each (paymentKey, chainId) pair
1384
+ * produces a unique nullifier that is recorded on-chain when funds are redeemed.
1385
+ *
1386
+ * @param paymentKey - The 32-byte paymentKey as a hex string
1387
+ * @returns The nullifier as a bigint
1388
+ */
1389
+ getNullifier(paymentKey) {
1390
+ return computeNullifier(paymentKey, this.config.destinationChainId);
1391
+ }
1392
+ /**
1393
+ * Get balance information
1394
+ *
1395
+ * Returns:
1396
+ * - confirmed: balance visible to light client (provable)
1397
+ * - redeemed: amount already redeemed via nullifier
1398
+ * - available: amount that can be redeemed now (confirmed - redeemed)
1399
+ * - pending: deposits not yet visible to light client
1400
+ *
1401
+ * @param options - Options for getting balance
1402
+ * @returns Balance information
1403
+ */
1404
+ async getBalance(options) {
1405
+ const { depositAddress, nullifier, clientId } = options;
1406
+ const ibcHandlerAddress = await this.dstClient.getIbcHandlerAddress(
1407
+ this.config.dstZAssetAddress
1408
+ );
1409
+ const lightClientAddress = await this.dstClient.getLightClientAddress(
1410
+ ibcHandlerAddress,
1411
+ clientId
1412
+ );
1413
+ const lightClientHeight = await this.dstClient.getLatestHeight(
1414
+ lightClientAddress,
1415
+ clientId
1416
+ );
1417
+ const balance = await this.getBalanceAtHeight(
1418
+ depositAddress,
1419
+ nullifier,
1420
+ lightClientHeight
1421
+ );
1422
+ return {
1423
+ ...balance,
1424
+ lightClientHeight
1425
+ };
1426
+ }
1427
+ /**
1428
+ * Get balance information at a specific block height
1429
+ *
1430
+ * @param depositAddress - The deposit address (unspendable address)
1431
+ * @param nullifier - The nullifier for this secret
1432
+ * @param height - The block height to query confirmed balance at
1433
+ * @returns Balance information at the given height
1434
+ */
1435
+ async getBalanceAtHeight(depositAddress, nullifier, height) {
1436
+ const latestHeight = await this.srcClient.getLatestBlockNumber();
1437
+ const [balanceAtTip, confirmed, redeemed] = await Promise.all([
1438
+ this.srcClient.getBalance(this.config.srcZAssetAddress, depositAddress),
1439
+ this.srcClient.getBalance(
1440
+ this.config.srcZAssetAddress,
1441
+ depositAddress,
1442
+ height
1443
+ ),
1444
+ this.dstClient.getNullifierBalance(
1445
+ this.config.dstZAssetAddress,
1446
+ nullifier
1447
+ )
1448
+ ]);
1449
+ const available = confirmed > redeemed ? confirmed - redeemed : 0n;
1450
+ const pending = balanceAtTip > confirmed ? balanceAtTip - confirmed : 0n;
1451
+ return {
1452
+ confirmed,
1453
+ redeemed,
1454
+ available,
1455
+ pending,
1456
+ latestHeight
1457
+ };
1458
+ }
1459
+ /**
1460
+ * Generate a proof for redeeming funds
1461
+ *
1462
+ * This method:
1463
+ * 1. Fetches light client data from the destination chain
1464
+ * 2. Deterministically shuffles clients for privacy
1465
+ * 3. Fetches MPT proof from the source chain
1466
+ * 4. Builds the witness data
1467
+ * 5. Sends the witness to the prover server
1468
+ * 6. Polls until the proof is ready
1469
+ *
1470
+ * @param secret - The 32-byte secret as a hex string
1471
+ * @param beneficiaries - Array of 0-4 beneficiary addresses (empty array = unbounded mode)
1472
+ * @param beneficiary - The beneficiary address to redeem to
1473
+ * @param amount - Amount to redeem
1474
+ * @param clientIds - Light client IDs to include in the proof (for anonymity set)
1475
+ * @param selectedClientId - The specific light client ID to use for the proof
1476
+ * @returns GenerateProofResult containing proof result and client-side metadata
1477
+ */
1478
+ async generateProof(secret, beneficiaries, beneficiary, amount, clientIds, selectedClientId) {
1479
+ if (!clientIds.includes(selectedClientId)) {
1480
+ return {
1481
+ proof: {
1482
+ success: false,
1483
+ error: `selectedClientId ${selectedClientId} not in clientIds`
1484
+ }
1485
+ };
1486
+ }
1487
+ if (beneficiary === ZERO_ADDRESS2) {
1488
+ return {
1489
+ proof: {
1490
+ success: false,
1491
+ error: "Beneficiary address cannot be zero"
1492
+ }
1493
+ };
1494
+ }
1495
+ const paddedBeneficiaries = this.padBeneficiaries(beneficiaries);
1496
+ const depositAddress = this.getDepositAddress(secret, beneficiaries);
1497
+ const lightClients = await fetchLightClients(
1498
+ this.dstClient,
1499
+ this.config.dstZAssetAddress,
1500
+ clientIds
1501
+ );
1502
+ if (lightClients.length === 0) {
1503
+ return {
1504
+ proof: {
1505
+ success: false,
1506
+ error: "No valid light clients found"
1507
+ }
1508
+ };
1509
+ }
1510
+ const shuffled = deterministicShuffleClients(lightClients, secret);
1511
+ const selectedClientIndex = shuffled.findIndex(
1512
+ (c) => c.clientId === selectedClientId
1513
+ );
1514
+ if (selectedClientIndex === -1) {
1515
+ return {
1516
+ proof: {
1517
+ success: false,
1518
+ error: `Client ${selectedClientId} not found after fetching`
1519
+ }
1520
+ };
1521
+ }
1522
+ const selectedClient = shuffled[selectedClientIndex];
1523
+ const { tokenAddressKey, balanceSlot } = await this.dstClient.getCounterparty(
1524
+ this.config.dstZAssetAddress,
1525
+ selectedClientId
1526
+ );
1527
+ const ZERO_BYTES32 = "0x0000000000000000000000000000000000000000000000000000000000000000";
1528
+ if (balanceSlot === ZERO_BYTES32 || tokenAddressKey === ZERO_BYTES32) {
1529
+ return {
1530
+ proof: {
1531
+ success: false,
1532
+ error: `Light client ${selectedClientId} is not configured as a counterparty on the destination ZAsset. Please call setCounterparty() on the ZAsset contract first.`
1533
+ }
1534
+ };
1535
+ }
1536
+ const mappingSlot = V.hexToBigInt(balanceSlot);
1537
+ const nullifier = this.getNullifier(secret);
1538
+ const balance = await this.getBalanceAtHeight(
1539
+ depositAddress,
1540
+ nullifier,
1541
+ selectedClient.height
1542
+ );
1543
+ if (amount > balance.available) {
1544
+ return {
1545
+ proof: {
1546
+ success: false,
1547
+ error: `Insufficient available balance. Requested: ${amount}, Available: ${balance.available} (Confirmed at height ${selectedClient.height}: ${balance.confirmed}, Already redeemed: ${balance.redeemed})`
1548
+ }
1549
+ };
1550
+ }
1551
+ const storageSlot = computeStorageSlot(depositAddress, mappingSlot);
1552
+ const mptProof = await fetchMptProof(
1553
+ this.srcClient,
1554
+ this.config.srcZAssetAddress,
1555
+ storageSlot,
1556
+ selectedClient.height
1557
+ );
1558
+ const witness = {
1559
+ secret,
1560
+ dstChainId: this.config.destinationChainId,
1561
+ beneficiaries: paddedBeneficiaries,
1562
+ beneficiary,
1563
+ redeemAmount: amount,
1564
+ alreadyRedeemed: balance.redeemed,
1565
+ lightClients: shuffled,
1566
+ selectedClientIndex,
1567
+ mptProof,
1568
+ srcZAssetAddress: this.config.srcZAssetAddress,
1569
+ mappingSlot: `0x${mappingSlot.toString(16)}`
1570
+ };
1571
+ const proofResult = await this.proverClient.generateProof(witness);
1572
+ if (proofResult.success) {
1573
+ return {
1574
+ proof: proofResult,
1575
+ metadata: {
1576
+ depositAddress,
1577
+ beneficiary,
1578
+ value: amount,
1579
+ lightClients: shuffled,
1580
+ nullifier
1581
+ }
1582
+ };
1583
+ }
1584
+ return { proof: proofResult };
1585
+ }
1586
+ /**
1587
+ * Export the verifier contract from the prover server
1588
+ *
1589
+ * The verifier contract is used to verify proofs on-chain.
1590
+ * It is circuit-specific and does not change between proofs.
1591
+ *
1592
+ * @returns The Solidity verifier contract source code
1593
+ */
1594
+ async exportVerifier() {
1595
+ return this.proverClient.exportVerifier();
1596
+ }
1597
+ /**
1598
+ * Get source ZAsset token information
1599
+ *
1600
+ * @returns Token symbol and decimals
1601
+ */
1602
+ async getSrcZAssetInfo() {
1603
+ const [symbol, decimals] = await Promise.all([
1604
+ this.srcClient.getSymbol(this.config.srcZAssetAddress),
1605
+ this.srcClient.getDecimals(this.config.srcZAssetAddress)
1606
+ ]);
1607
+ return { symbol, decimals };
1608
+ }
1609
+ /**
1610
+ * Get destination ZAsset token information
1611
+ *
1612
+ * @returns Token symbol and decimals
1613
+ */
1614
+ async getDstZAssetInfo() {
1615
+ const [symbol, decimals] = await Promise.all([
1616
+ this.dstClient.getSymbol(this.config.dstZAssetAddress),
1617
+ this.dstClient.getDecimals(this.config.dstZAssetAddress)
1618
+ ]);
1619
+ return { symbol, decimals };
1620
+ }
1621
+ /**
1622
+ * Deposit underlying tokens to ZAsset and transfer to deposit address
1623
+ *
1624
+ * Executes 3 transactions: approve → deposit → transfer.
1625
+ * The wallet client must be connected to the source chain.
1626
+ *
1627
+ * @param secret - The 32-byte secret as a hex string
1628
+ * @param beneficiaries - Array of 0-4 beneficiary addresses
1629
+ * @param amount - Amount to deposit (in underlying token's smallest unit)
1630
+ * @param walletClient - viem WalletClient with account and chain configured
1631
+ * @returns Transaction hash, deposit address, underlying token, and chain ID
1632
+ */
1633
+ async deposit(options) {
1634
+ const { paymentKey, beneficiaries, amount, walletClient } = options;
1635
+ if (!walletClient.account) {
1636
+ throw new Error("WalletClient must have an account");
1637
+ }
1638
+ if (!walletClient.chain) {
1639
+ throw new Error("WalletClient must have a chain configured");
1640
+ }
1641
+ return effect.Effect.gen(this, function* () {
1642
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
1643
+ const underlyingToken = yield* effect.pipe(
1644
+ readContract({
1645
+ address: this.config.srcZAssetAddress,
1646
+ abi: [
1647
+ {
1648
+ inputs: [],
1649
+ name: "underlying",
1650
+ outputs: [{ name: "", type: "address" }],
1651
+ stateMutability: "view",
1652
+ type: "function"
1653
+ }
1654
+ ],
1655
+ functionName: "underlying"
1656
+ }),
1657
+ effect.Effect.retry(commonRetry),
1658
+ effect.Effect.provideService(PublicClient, {
1659
+ client: this.srcClient.getClient()
1660
+ })
1661
+ );
1662
+ if (underlyingToken === ZERO_ADDRESS2) {
1663
+ return yield* effect.Effect.fail(
1664
+ Error("ZAsset is not a wrapped token (underlying is zero address)")
1665
+ );
1666
+ }
1667
+ const ERC20_ABI2 = [
1668
+ {
1669
+ inputs: [
1670
+ { name: "spender", type: "address" },
1671
+ { name: "amount", type: "uint256" }
1672
+ ],
1673
+ name: "approve",
1674
+ outputs: [{ name: "", type: "bool" }],
1675
+ stateMutability: "nonpayable",
1676
+ type: "function"
1677
+ }
1678
+ ];
1679
+ const ZASSET_ABI2 = [
1680
+ {
1681
+ inputs: [{ name: "amount", type: "uint256" }],
1682
+ name: "deposit",
1683
+ outputs: [],
1684
+ stateMutability: "nonpayable",
1685
+ type: "function"
1686
+ },
1687
+ {
1688
+ inputs: [
1689
+ { name: "to", type: "address" },
1690
+ { name: "amount", type: "uint256" }
1691
+ ],
1692
+ name: "transfer",
1693
+ outputs: [{ name: "", type: "bool" }],
1694
+ stateMutability: "nonpayable",
1695
+ type: "function"
1696
+ }
1697
+ ];
1698
+ const approveHash = yield* effect.pipe(
1699
+ writeContract({
1700
+ address: underlyingToken,
1701
+ abi: ERC20_ABI2,
1702
+ functionName: "approve",
1703
+ args: [this.config.srcZAssetAddress, amount],
1704
+ chain: walletClient.chain,
1705
+ account: walletClient.account
1706
+ }),
1707
+ effect.Effect.retry(commonRetry)
1708
+ );
1709
+ const approveReceipt = yield* effect.pipe(
1710
+ waitForTransactionReceipt(approveHash),
1711
+ effect.Effect.retry(commonRetry)
1712
+ );
1713
+ if (approveReceipt.status === "reverted") {
1714
+ return yield* effect.Effect.fail(
1715
+ Error(`Approve transaction reverted: ${approveHash}`)
1716
+ );
1717
+ }
1718
+ const depositHash = yield* effect.pipe(
1719
+ writeContract({
1720
+ address: this.config.srcZAssetAddress,
1721
+ abi: ZASSET_ABI2,
1722
+ functionName: "deposit",
1723
+ args: [amount],
1724
+ chain: walletClient.chain,
1725
+ account: walletClient.account
1726
+ }),
1727
+ effect.Effect.retry(commonRetry)
1728
+ );
1729
+ const depositReceipt = yield* waitForTransactionReceipt(depositHash).pipe(effect.Effect.retry(commonRetry));
1730
+ if (depositReceipt.status === "reverted") {
1731
+ throw new Error(`Deposit transaction reverted: ${depositHash}`);
1732
+ }
1733
+ const transferHash = yield* effect.pipe(
1734
+ writeContract({
1735
+ address: this.config.srcZAssetAddress,
1736
+ abi: ZASSET_ABI2,
1737
+ functionName: "transfer",
1738
+ args: [depositAddress, amount],
1739
+ chain: walletClient.chain,
1740
+ account: walletClient.account
1741
+ }),
1742
+ effect.Effect.retry(commonRetry)
1743
+ );
1744
+ const transferReceipt = yield* waitForTransactionReceipt(transferHash);
1745
+ if (transferReceipt.status === "reverted") {
1746
+ throw new Error(`Transfer transaction reverted: ${transferHash}`);
1747
+ }
1748
+ return {
1749
+ txHash: transferHash,
1750
+ depositAddress,
1751
+ underlyingToken,
1752
+ chainId: this.config.sourceChainId,
1753
+ height: transferReceipt.blockNumber
1754
+ };
1755
+ }).pipe(
1756
+ effect.Effect.provide(
1757
+ PublicClient.Live({
1758
+ chain: options.walletClient.chain,
1759
+ transport: V.http(this.config.sourceRpcUrl)
1760
+ })
1761
+ ),
1762
+ effect.Effect.provideService(WalletClient, {
1763
+ client: options.walletClient,
1764
+ account: options.walletClient.account,
1765
+ chain: options.walletClient.chain
1766
+ }),
1767
+ effect.Effect.runPromise
1768
+ );
1769
+ }
1770
+ /**
1771
+ * Deposit underlying tokens to ZAsset and transfer to deposit address
1772
+ *
1773
+ * Executes 3 transactions: approve → deposit → transfer.
1774
+ * The wallet client must be connected to the source chain.
1775
+ *
1776
+ * @param secret - The 32-byte secret as a hex string
1777
+ * @param beneficiaries - Array of 0-4 beneficiary addresses
1778
+ * @param amount - Amount to deposit (in underlying token's smallest unit)
1779
+ * @param walletClient - viem WalletClient with account and chain configured
1780
+ * @returns Transaction hash, deposit address, underlying token, and chain ID
1781
+ */
1782
+ async unsafeDeposit(options) {
1783
+ const { paymentKey, beneficiaries, amount, walletClient } = options;
1784
+ if (!walletClient.account) {
1785
+ throw new Error("WalletClient must have an account");
1786
+ }
1787
+ if (!walletClient.chain) {
1788
+ throw new Error("WalletClient must have a chain configured");
1789
+ }
1790
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
1791
+ const publicClient = V.createPublicClient({
1792
+ chain: walletClient.chain,
1793
+ transport: V.http(this.config.sourceRpcUrl)
1794
+ });
1795
+ const underlyingToken = await this.srcClient.getClient().readContract({
1796
+ address: this.config.srcZAssetAddress,
1797
+ abi: [
1798
+ {
1799
+ inputs: [],
1800
+ name: "underlying",
1801
+ outputs: [{ name: "", type: "address" }],
1802
+ stateMutability: "view",
1803
+ type: "function"
1804
+ }
1805
+ ],
1806
+ functionName: "underlying"
1807
+ });
1808
+ if (underlyingToken === ZERO_ADDRESS2) {
1809
+ throw new Error(
1810
+ "ZAsset is not a wrapped token (underlying is zero address)"
1811
+ );
1812
+ }
1813
+ const ERC20_ABI2 = [
1814
+ {
1815
+ inputs: [
1816
+ { name: "spender", type: "address" },
1817
+ { name: "amount", type: "uint256" }
1818
+ ],
1819
+ name: "approve",
1820
+ outputs: [{ name: "", type: "bool" }],
1821
+ stateMutability: "nonpayable",
1822
+ type: "function"
1823
+ }
1824
+ ];
1825
+ const ZASSET_ABI2 = [
1826
+ {
1827
+ inputs: [{ name: "amount", type: "uint256" }],
1828
+ name: "deposit",
1829
+ outputs: [],
1830
+ stateMutability: "nonpayable",
1831
+ type: "function"
1832
+ },
1833
+ {
1834
+ inputs: [
1835
+ { name: "to", type: "address" },
1836
+ { name: "amount", type: "uint256" }
1837
+ ],
1838
+ name: "transfer",
1839
+ outputs: [{ name: "", type: "bool" }],
1840
+ stateMutability: "nonpayable",
1841
+ type: "function"
1842
+ }
1843
+ ];
1844
+ const approveHash = await walletClient.writeContractSync({
1845
+ address: underlyingToken,
1846
+ abi: ERC20_ABI2,
1847
+ functionName: "approve",
1848
+ args: [this.config.srcZAssetAddress, amount],
1849
+ chain: walletClient.chain,
1850
+ account: walletClient.account
1851
+ }).then((x) => x.transactionHash);
1852
+ const approveReceipt = await publicClient.waitForTransactionReceipt({
1853
+ hash: approveHash
1854
+ });
1855
+ if (approveReceipt.status === "reverted") {
1856
+ throw new Error(`Approve transaction reverted: ${approveHash}`);
1857
+ }
1858
+ const depositHash = await walletClient.writeContractSync({
1859
+ address: this.config.srcZAssetAddress,
1860
+ abi: ZASSET_ABI2,
1861
+ functionName: "deposit",
1862
+ args: [amount],
1863
+ chain: walletClient.chain,
1864
+ account: walletClient.account
1865
+ }).then((x) => x.transactionHash);
1866
+ const depositReceipt = await publicClient.waitForTransactionReceipt({
1867
+ hash: depositHash
1868
+ });
1869
+ if (depositReceipt.status === "reverted") {
1870
+ throw new Error(`Deposit transaction reverted: ${depositHash}`);
1871
+ }
1872
+ const transferHash = await walletClient.writeContractSync({
1873
+ address: this.config.srcZAssetAddress,
1874
+ abi: ZASSET_ABI2,
1875
+ functionName: "transfer",
1876
+ args: [depositAddress, amount],
1877
+ chain: walletClient.chain,
1878
+ account: walletClient.account
1879
+ }).then((x) => x.transactionHash);
1880
+ const transferReceipt = await publicClient.waitForTransactionReceipt({
1881
+ hash: transferHash
1882
+ });
1883
+ if (transferReceipt.status === "reverted") {
1884
+ throw new Error(`Transfer transaction reverted: ${transferHash}`);
1885
+ }
1886
+ return {
1887
+ txHash: transferHash,
1888
+ depositAddress,
1889
+ underlyingToken,
1890
+ chainId: this.config.sourceChainId,
1891
+ height: transferReceipt.blockNumber
1892
+ };
1893
+ }
1894
+ /**
1895
+ * Update loopback light client to a specific block height
1896
+ *
1897
+ * Fetches the IBC handler address internally from the destination ZAsset.
1898
+ * The wallet client must be connected to the destination chain.
1899
+ *
1900
+ * @returns Transaction hash, block number, state root, and chain ID
1901
+ */
1902
+ async updateLightClient(options) {
1903
+ const { clientId, height, walletClient } = options;
1904
+ if (!walletClient.account) {
1905
+ throw new Error("WalletClient must have an account");
1906
+ }
1907
+ if (!walletClient.chain) {
1908
+ throw new Error("WalletClient must have a chain configured");
1909
+ }
1910
+ return effect.Effect.gen(this, function* () {
1911
+ const publicClient = V.createPublicClient({
1912
+ chain: walletClient.chain,
1913
+ transport: V.http(this.config.destinationRpcUrl)
1914
+ });
1915
+ const ibcHandlerAddress = yield* effect.Effect.tryPromise({
1916
+ try: () => this.dstClient.getIbcHandlerAddress(this.config.dstZAssetAddress),
1917
+ catch: (cause) => effect.Effect.fail(cause)
1918
+ });
1919
+ const blockNumber = height === "latest" ? yield* effect.pipe(
1920
+ effect.Effect.tryPromise(() => publicClient.getBlockNumber()),
1921
+ effect.Effect.retry(commonRetry)
1922
+ ) : height;
1923
+ const block = yield* effect.pipe(
1924
+ effect.Effect.tryPromise(() => publicClient.getBlock({ blockNumber })),
1925
+ effect.Effect.retry(commonRetry)
1926
+ );
1927
+ if (!block.number) {
1928
+ throw new Error("Block number is null");
1929
+ }
1930
+ const toRlpHex = (value) => {
1931
+ if (value === void 0 || value === null || value === 0n || value === 0) {
1932
+ return "0x";
1933
+ }
1934
+ let hex = typeof value === "bigint" ? value.toString(16) : value.toString(16);
1935
+ if (hex.length % 2 !== 0) {
1936
+ hex = "0" + hex;
1937
+ }
1938
+ return `0x${hex}`;
1939
+ };
1940
+ const headerFields = [
1941
+ block.parentHash,
1942
+ block.sha3Uncles,
1943
+ block.miner,
1944
+ block.stateRoot,
1945
+ block.transactionsRoot,
1946
+ block.receiptsRoot,
1947
+ block.logsBloom ?? "0x" + "00".repeat(256),
1948
+ toRlpHex(block.difficulty),
1949
+ toRlpHex(block.number),
1950
+ toRlpHex(block.gasLimit),
1951
+ toRlpHex(block.gasUsed),
1952
+ toRlpHex(block.timestamp),
1953
+ block.extraData,
1954
+ block.mixHash ?? "0x0000000000000000000000000000000000000000000000000000000000000000",
1955
+ block.nonce ?? "0x0000000000000000"
1956
+ ];
1957
+ if (block.baseFeePerGas !== void 0 && block.baseFeePerGas !== null) {
1958
+ headerFields.push(toRlpHex(block.baseFeePerGas));
1959
+ }
1960
+ if (block.withdrawalsRoot) {
1961
+ headerFields.push(block.withdrawalsRoot);
1962
+ }
1963
+ if (block.blobGasUsed !== void 0 && block.blobGasUsed !== null) {
1964
+ headerFields.push(toRlpHex(block.blobGasUsed));
1965
+ }
1966
+ if (block.excessBlobGas !== void 0 && block.excessBlobGas !== null) {
1967
+ headerFields.push(toRlpHex(block.excessBlobGas));
1968
+ }
1969
+ if (block.parentBeaconBlockRoot) {
1970
+ headerFields.push(block.parentBeaconBlockRoot);
1971
+ }
1972
+ const blockAny = block;
1973
+ if (blockAny.requestsHash) {
1974
+ headerFields.push(blockAny.requestsHash);
1975
+ }
1976
+ const rlpEncoded = V.toRlp(headerFields);
1977
+ const computedHash = V.keccak256(rlpEncoded);
1978
+ if (computedHash !== block.hash) {
1979
+ throw new Error(
1980
+ `RLP encoding mismatch: computed hash ${computedHash} does not match block hash ${block.hash}`
1981
+ );
1982
+ }
1983
+ const clientMessage = V.encodeAbiParameters(
1984
+ [
1985
+ { type: "uint64", name: "height" },
1986
+ { type: "bytes", name: "encodedHeader" }
1987
+ ],
1988
+ [block.number, rlpEncoded]
1989
+ );
1990
+ const LIGHTCLIENT_ABI = [
1991
+ {
1992
+ inputs: [
1993
+ { name: "caller", type: "address" },
1994
+ { name: "clientId", type: "uint32" },
1995
+ { name: "clientMessage", type: "bytes" },
1996
+ { name: "relayer", type: "address" }
1997
+ ],
1998
+ name: "updateClient",
1999
+ outputs: [],
2000
+ stateMutability: "nonpayable",
2001
+ type: "function"
2002
+ }
2003
+ ];
2004
+ let currentBlock = yield* effect.pipe(
2005
+ effect.Effect.tryPromise(() => publicClient.getBlockNumber()),
2006
+ effect.Effect.retry(commonRetry)
2007
+ );
2008
+ while (currentBlock <= blockNumber) {
2009
+ yield* effect.Effect.sleep("1 second");
2010
+ currentBlock = yield* effect.Effect.tryPromise(
2011
+ () => publicClient.getBlockNumber()
2012
+ );
2013
+ }
2014
+ const loopbackClient = yield* effect.pipe(
2015
+ readContract({
2016
+ address: ibcHandlerAddress,
2017
+ abi: IBC_STORE_ABI,
2018
+ functionName: "getClient",
2019
+ args: [clientId]
2020
+ }),
2021
+ effect.Effect.retry(commonRetry)
2022
+ );
2023
+ const txHash = yield* effect.pipe(
2024
+ writeContract({
2025
+ address: loopbackClient,
2026
+ abi: LIGHTCLIENT_ABI,
2027
+ functionName: "updateClient",
2028
+ args: [
2029
+ walletClient.account.address,
2030
+ clientId,
2031
+ clientMessage,
2032
+ walletClient.account.address
2033
+ ],
2034
+ chain: walletClient.chain,
2035
+ account: walletClient.account
2036
+ }),
2037
+ effect.Effect.retry(commonRetry)
2038
+ );
2039
+ const chainId = yield* effect.Effect.sync(() => this.config.destinationChainId);
2040
+ return {
2041
+ txHash,
2042
+ blockNumber: block.number,
2043
+ stateRoot: block.stateRoot,
2044
+ chainId
2045
+ };
2046
+ }).pipe(
2047
+ effect.Effect.provide(
2048
+ PublicClient.Live({
2049
+ chain: walletClient.chain,
2050
+ transport: V.http(this.config.destinationRpcUrl)
2051
+ })
2052
+ ),
2053
+ effect.Effect.provideService(WalletClient, {
2054
+ client: options.walletClient,
2055
+ account: options.walletClient.account,
2056
+ chain: options.walletClient.chain
2057
+ }),
2058
+ effect.Effect.runPromise
2059
+ );
2060
+ }
2061
+ /**
2062
+ * Update loopback light client to a specific block height
2063
+ *
2064
+ * Fetches the IBC handler address internally from the destination ZAsset.
2065
+ * The wallet client must be connected to the destination chain.
2066
+ *
2067
+ * @returns Transaction hash, block number, state root, and chain ID
2068
+ */
2069
+ async unsafeUpdateLightClient(options) {
2070
+ const { clientId, height, walletClient } = options;
2071
+ if (!walletClient.account) {
2072
+ throw new Error("WalletClient must have an account");
2073
+ }
2074
+ if (!walletClient.chain) {
2075
+ throw new Error("WalletClient must have a chain configured");
2076
+ }
2077
+ const publicClient = V.createPublicClient({
2078
+ chain: walletClient.chain,
2079
+ transport: V.http(this.config.destinationRpcUrl)
2080
+ });
2081
+ const ibcHandlerAddress = await this.dstClient.getIbcHandlerAddress(
2082
+ this.config.dstZAssetAddress
2083
+ );
2084
+ const blockNumber = height === "latest" ? await publicClient.getBlockNumber() : height;
2085
+ const block = await publicClient.getBlock({ blockNumber });
2086
+ if (!block.number) {
2087
+ throw new Error("Block number is null");
2088
+ }
2089
+ const toRlpHex = (value) => {
2090
+ if (value === void 0 || value === null || value === 0n || value === 0) {
2091
+ return "0x";
2092
+ }
2093
+ let hex = typeof value === "bigint" ? value.toString(16) : value.toString(16);
2094
+ if (hex.length % 2 !== 0) {
2095
+ hex = "0" + hex;
2096
+ }
2097
+ return `0x${hex}`;
2098
+ };
2099
+ const headerFields = [
2100
+ block.parentHash,
2101
+ block.sha3Uncles,
2102
+ block.miner,
2103
+ block.stateRoot,
2104
+ block.transactionsRoot,
2105
+ block.receiptsRoot,
2106
+ block.logsBloom ?? "0x" + "00".repeat(256),
2107
+ toRlpHex(block.difficulty),
2108
+ toRlpHex(block.number),
2109
+ toRlpHex(block.gasLimit),
2110
+ toRlpHex(block.gasUsed),
2111
+ toRlpHex(block.timestamp),
2112
+ block.extraData,
2113
+ block.mixHash ?? "0x0000000000000000000000000000000000000000000000000000000000000000",
2114
+ block.nonce ?? "0x0000000000000000"
2115
+ ];
2116
+ if (block.baseFeePerGas !== void 0 && block.baseFeePerGas !== null) {
2117
+ headerFields.push(toRlpHex(block.baseFeePerGas));
2118
+ }
2119
+ if (block.withdrawalsRoot) {
2120
+ headerFields.push(block.withdrawalsRoot);
2121
+ }
2122
+ if (block.blobGasUsed !== void 0 && block.blobGasUsed !== null) {
2123
+ headerFields.push(toRlpHex(block.blobGasUsed));
2124
+ }
2125
+ if (block.excessBlobGas !== void 0 && block.excessBlobGas !== null) {
2126
+ headerFields.push(toRlpHex(block.excessBlobGas));
2127
+ }
2128
+ if (block.parentBeaconBlockRoot) {
2129
+ headerFields.push(block.parentBeaconBlockRoot);
2130
+ }
2131
+ const blockAny = block;
2132
+ if (blockAny.requestsHash) {
2133
+ headerFields.push(blockAny.requestsHash);
2134
+ }
2135
+ const rlpEncoded = V.toRlp(headerFields);
2136
+ const computedHash = V.keccak256(rlpEncoded);
2137
+ if (computedHash !== block.hash) {
2138
+ throw new Error(
2139
+ `RLP encoding mismatch: computed hash ${computedHash} does not match block hash ${block.hash}`
2140
+ );
2141
+ }
2142
+ const clientMessage = V.encodeAbiParameters(
2143
+ [
2144
+ { type: "uint64", name: "height" },
2145
+ { type: "bytes", name: "encodedHeader" }
2146
+ ],
2147
+ [block.number, rlpEncoded]
2148
+ );
2149
+ const LIGHTCLIENT_ABI = [
2150
+ {
2151
+ inputs: [
2152
+ { name: "caller", type: "address" },
2153
+ { name: "clientId", type: "uint32" },
2154
+ { name: "clientMessage", type: "bytes" },
2155
+ { name: "relayer", type: "address" }
2156
+ ],
2157
+ name: "updateClient",
2158
+ outputs: [],
2159
+ stateMutability: "nonpayable",
2160
+ type: "function"
2161
+ }
2162
+ ];
2163
+ let currentBlock = await publicClient.getBlockNumber();
2164
+ while (currentBlock <= blockNumber) {
2165
+ await new Promise((resolve) => setTimeout(resolve, 1e3));
2166
+ currentBlock = await publicClient.getBlockNumber();
2167
+ }
2168
+ const loopbackClient = await publicClient.readContract({
2169
+ address: ibcHandlerAddress,
2170
+ abi: IBC_STORE_ABI,
2171
+ functionName: "getClient",
2172
+ args: [clientId]
2173
+ });
2174
+ const txHash = await walletClient.writeContractSync({
2175
+ address: loopbackClient,
2176
+ abi: LIGHTCLIENT_ABI,
2177
+ functionName: "updateClient",
2178
+ args: [
2179
+ walletClient.account.address,
2180
+ clientId,
2181
+ clientMessage,
2182
+ walletClient.account.address
2183
+ ],
2184
+ chain: walletClient.chain,
2185
+ account: walletClient.account
2186
+ }).then((x) => x.transactionHash);
2187
+ return {
2188
+ txHash,
2189
+ blockNumber: block.number,
2190
+ stateRoot: block.stateRoot,
2191
+ chainId: this.config.destinationChainId
2192
+ };
2193
+ }
2194
+ /**
2195
+ * Get redemption history for a secret
2196
+ *
2197
+ * Queries the Redeemed events filtered by the nullifier derived from the secret.
2198
+ * This is a read-only operation that doesn't require a wallet.
2199
+ *
2200
+ * @param secret - The 32-byte secret as a hex string
2201
+ * @param fromBlock - Start block number (default: earliest)
2202
+ * @param toBlock - End block number (default: latest)
2203
+ * @returns Array of redemption transactions
2204
+ */
2205
+ getRedemptionHistory(secret, fromBlock, toBlock) {
2206
+ return effect.Effect.gen(this, function* () {
2207
+ const nullifier = this.getNullifier(secret);
2208
+ return yield* effect.pipe(
2209
+ effect.Effect.tryPromise(
2210
+ () => this.dstClient.getRedemptionHistory(
2211
+ this.config.dstZAssetAddress,
2212
+ nullifier,
2213
+ fromBlock,
2214
+ toBlock
2215
+ )
2216
+ ),
2217
+ effect.Effect.retry(commonRetry)
2218
+ );
2219
+ }).pipe(effect.Effect.runPromise);
2220
+ }
2221
+ /**
2222
+ * Get redemption history for a secret
2223
+ *
2224
+ * Queries the Redeemed events filtered by the nullifier derived from the secret.
2225
+ * This is a read-only operation that doesn't require a wallet.
2226
+ *
2227
+ * @param secret - The 32-byte secret as a hex string
2228
+ * @param fromBlock - Start block number (default: earliest)
2229
+ * @param toBlock - End block number (default: latest)
2230
+ * @returns Array of redemption transactions
2231
+ */
2232
+ async unsafeGetRedemptionHistory(secret, fromBlock, toBlock) {
2233
+ const nullifier = this.getNullifier(secret);
2234
+ return this.dstClient.getRedemptionHistory(
2235
+ this.config.dstZAssetAddress,
2236
+ nullifier,
2237
+ fromBlock,
2238
+ toBlock
2239
+ );
2240
+ }
2241
+ /**
2242
+ * Full redeem flow: generate proof + get attestation + submit transaction
2243
+ *
2244
+ * This method orchestrates the entire redemption process:
2245
+ * 1. Generates a ZK proof via the prover server
2246
+ * 2. Gets attestation from the attestation service
2247
+ * 3. Submits the redeem transaction
2248
+ *
2249
+ * The wallet client must be connected to the destination chain.
2250
+ *
2251
+ * @returns Transaction hash, proof, and metadata
2252
+ */
2253
+ redeem(options) {
2254
+ const {
2255
+ paymentKey,
2256
+ beneficiaries,
2257
+ beneficiary,
2258
+ amount,
2259
+ clientIds,
2260
+ selectedClientId,
2261
+ walletClient,
2262
+ unwrap = true
2263
+ } = options;
2264
+ const attestationUrl = this.config.attestorUrl;
2265
+ const attestorApiKey = this.config.attestorApiKey;
2266
+ if (!attestationUrl) {
2267
+ throw Error(
2268
+ "Attestation URL must be provided either in redeem options or ClientOptions"
2269
+ );
2270
+ }
2271
+ if (!attestorApiKey) {
2272
+ throw Error(
2273
+ "Attestation API key must be provided either in redeem options or ClientOptions"
2274
+ );
2275
+ }
2276
+ if (!walletClient.account) {
2277
+ throw Error("WalletClient must have an account");
2278
+ }
2279
+ if (!walletClient.chain) {
2280
+ throw Error("WalletClient must have a chain configured");
2281
+ }
2282
+ return effect.Effect.gen(this, function* () {
2283
+ const proofResult = yield* effect.Effect.tryPromise({
2284
+ try: () => this.generateProof(
2285
+ paymentKey,
2286
+ beneficiaries,
2287
+ beneficiary,
2288
+ amount,
2289
+ clientIds,
2290
+ selectedClientId
2291
+ ),
2292
+ catch: (cause) => effect.Effect.fail(cause)
2293
+ }).pipe(effect.Effect.retry(commonRetry));
2294
+ if (!proofResult.proof.success || !proofResult.proof.proofJson || !proofResult.metadata) {
2295
+ return yield* effect.Effect.fail(
2296
+ Error(proofResult.proof.error ?? "Proof generation failed")
2297
+ );
2298
+ }
2299
+ const proofJson = parseProofJson(proofResult.proof.proofJson);
2300
+ const metadata = proofResult.metadata;
2301
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
2302
+ const attestationClient = new AttestationClient(
2303
+ attestationUrl,
2304
+ attestorApiKey
2305
+ );
2306
+ const attestationResponse = yield* effect.Effect.tryPromise({
2307
+ try: () => attestationClient.getAttestation(depositAddress, beneficiary),
2308
+ catch: (cause) => effect.Effect.fail(cause)
2309
+ }).pipe(effect.Effect.retry(commonRetry));
2310
+ const redeemParams = proofJsonToRedeemParams(proofJson, {
2311
+ lightClients: metadata.lightClients,
2312
+ nullifier: metadata.nullifier,
2313
+ value: metadata.value,
2314
+ beneficiary: metadata.beneficiary,
2315
+ attestedMessage: attestationResponse.attestedMessage,
2316
+ signature: attestationResponse.signature
2317
+ });
2318
+ const ZASSET_ABI2 = [
2319
+ {
2320
+ inputs: [
2321
+ { name: "proof", type: "uint256[8]" },
2322
+ { name: "commitments", type: "uint256[2]" },
2323
+ { name: "commitmentPok", type: "uint256[2]" },
2324
+ {
2325
+ name: "lightClients",
2326
+ type: "tuple[]",
2327
+ components: [
2328
+ { name: "clientId", type: "uint32" },
2329
+ { name: "height", type: "uint64" }
2330
+ ]
2331
+ },
2332
+ { name: "nullifier", type: "uint256" },
2333
+ { name: "value", type: "uint256" },
2334
+ { name: "beneficiary", type: "address" },
2335
+ { name: "attestedMessage", type: "bytes32" },
2336
+ { name: "signature", type: "bytes" },
2337
+ { name: "unwrap", type: "bool" }
2338
+ ],
2339
+ name: "redeem",
2340
+ outputs: [],
2341
+ stateMutability: "nonpayable",
2342
+ type: "function"
2343
+ }
2344
+ ];
2345
+ const txHash = yield* effect.pipe(
2346
+ writeContract({
2347
+ address: this.config.dstZAssetAddress,
2348
+ abi: ZASSET_ABI2,
2349
+ functionName: "redeem",
2350
+ args: [
2351
+ redeemParams.proof,
2352
+ redeemParams.commitments,
2353
+ redeemParams.commitmentPok,
2354
+ redeemParams.lightClients,
2355
+ redeemParams.nullifier,
2356
+ redeemParams.value,
2357
+ redeemParams.beneficiary,
2358
+ redeemParams.attestedMessage,
2359
+ redeemParams.signature,
2360
+ unwrap
2361
+ ],
2362
+ chain: walletClient.chain,
2363
+ account: walletClient.account
2364
+ }),
2365
+ effect.Effect.retry(commonRetry)
2366
+ );
2367
+ return {
2368
+ txHash,
2369
+ proof: proofJson,
2370
+ metadata
2371
+ };
2372
+ }).pipe(
2373
+ effect.Effect.provide(
2374
+ PublicClient.Live({
2375
+ chain: options.walletClient.chain,
2376
+ transport: V.http(this.config.destinationRpcUrl)
2377
+ })
2378
+ ),
2379
+ effect.Effect.provideService(WalletClient, {
2380
+ client: options.walletClient,
2381
+ account: options.walletClient.account,
2382
+ chain: options.walletClient.chain
2383
+ }),
2384
+ effect.Effect.runPromise
2385
+ );
2386
+ }
2387
+ /**
2388
+ * Full redeem flow: generate proof + get attestation + submit transaction
2389
+ *
2390
+ * This method orchestrates the entire redemption process:
2391
+ * 1. Generates a ZK proof via the prover server
2392
+ * 2. Gets attestation from the attestation service
2393
+ * 3. Submits the redeem transaction
2394
+ *
2395
+ * The wallet client must be connected to the destination chain.
2396
+ *
2397
+ * @returns Transaction hash, proof, and metadata
2398
+ */
2399
+ async unsafeRedeem(options) {
2400
+ const {
2401
+ paymentKey,
2402
+ beneficiaries,
2403
+ beneficiary,
2404
+ amount,
2405
+ clientIds,
2406
+ selectedClientId,
2407
+ walletClient,
2408
+ unwrap = true
2409
+ } = options;
2410
+ const attestationUrl = this.config.attestorUrl;
2411
+ const attestorApiKey = this.config.attestorApiKey;
2412
+ if (!attestationUrl) {
2413
+ throw new Error(
2414
+ "Attestation URL must be provided either in redeem options or ClientOptions"
2415
+ );
2416
+ }
2417
+ if (!attestorApiKey) {
2418
+ throw new Error(
2419
+ "Attestation API key must be provided either in redeem options or ClientOptions"
2420
+ );
2421
+ }
2422
+ if (!walletClient.account) {
2423
+ throw new Error("WalletClient must have an account");
2424
+ }
2425
+ if (!walletClient.chain) {
2426
+ throw new Error("WalletClient must have a chain configured");
2427
+ }
2428
+ const proofResult = await this.generateProof(
2429
+ paymentKey,
2430
+ beneficiaries,
2431
+ beneficiary,
2432
+ amount,
2433
+ clientIds,
2434
+ selectedClientId
2435
+ );
2436
+ if (!proofResult.proof.success || !proofResult.proof.proofJson || !proofResult.metadata) {
2437
+ throw new Error(proofResult.proof.error ?? "Proof generation failed");
2438
+ }
2439
+ const proofJson = parseProofJson(proofResult.proof.proofJson);
2440
+ const metadata = proofResult.metadata;
2441
+ const depositAddress = this.getDepositAddress(paymentKey, beneficiaries);
2442
+ const attestationClient = new AttestationClient(
2443
+ attestationUrl,
2444
+ attestorApiKey
2445
+ );
2446
+ const attestationResponse = await attestationClient.getAttestation(
2447
+ depositAddress,
2448
+ beneficiary
2449
+ );
2450
+ const redeemParams = proofJsonToRedeemParams(proofJson, {
2451
+ lightClients: metadata.lightClients,
2452
+ nullifier: metadata.nullifier,
2453
+ value: metadata.value,
2454
+ beneficiary: metadata.beneficiary,
2455
+ attestedMessage: attestationResponse.attestedMessage,
2456
+ signature: attestationResponse.signature
2457
+ });
2458
+ const publicClient = V.createPublicClient({
2459
+ chain: walletClient.chain,
2460
+ transport: V.http(this.config.destinationRpcUrl)
2461
+ });
2462
+ const ZASSET_ABI2 = [
2463
+ {
2464
+ inputs: [
2465
+ { name: "proof", type: "uint256[8]" },
2466
+ { name: "commitments", type: "uint256[2]" },
2467
+ { name: "commitmentPok", type: "uint256[2]" },
2468
+ {
2469
+ name: "lightClients",
2470
+ type: "tuple[]",
2471
+ components: [
2472
+ { name: "clientId", type: "uint32" },
2473
+ { name: "height", type: "uint64" }
2474
+ ]
2475
+ },
2476
+ { name: "nullifier", type: "uint256" },
2477
+ { name: "value", type: "uint256" },
2478
+ { name: "beneficiary", type: "address" },
2479
+ { name: "attestedMessage", type: "bytes32" },
2480
+ { name: "signature", type: "bytes" },
2481
+ { name: "unwrap", type: "bool" }
2482
+ ],
2483
+ name: "redeem",
2484
+ outputs: [],
2485
+ stateMutability: "nonpayable",
2486
+ type: "function"
2487
+ }
2488
+ ];
2489
+ const txHash = await walletClient.writeContractSync({
2490
+ address: this.config.dstZAssetAddress,
2491
+ abi: ZASSET_ABI2,
2492
+ functionName: "redeem",
2493
+ args: [
2494
+ redeemParams.proof,
2495
+ redeemParams.commitments,
2496
+ redeemParams.commitmentPok,
2497
+ redeemParams.lightClients,
2498
+ redeemParams.nullifier,
2499
+ redeemParams.value,
2500
+ redeemParams.beneficiary,
2501
+ redeemParams.attestedMessage,
2502
+ redeemParams.signature,
2503
+ unwrap
2504
+ ],
2505
+ chain: walletClient.chain,
2506
+ account: walletClient.account
2507
+ }).then((x) => x.transactionHash);
2508
+ const receipt = await publicClient.waitForTransactionReceipt({
2509
+ hash: txHash
2510
+ });
2511
+ if (receipt.status === "reverted") {
2512
+ throw new Error(`Redeem transaction reverted: ${txHash}`);
2513
+ }
2514
+ return {
2515
+ txHash,
2516
+ proof: proofJson,
2517
+ metadata
2518
+ };
2519
+ }
2520
+ /**
2521
+ * Pad beneficiaries array to exactly 4 addresses
2522
+ * Empty array = unbounded mode (all zeros, any beneficiary allowed)
2523
+ */
2524
+ padBeneficiaries(beneficiaries) {
2525
+ if (beneficiaries.length > 4) {
2526
+ throw new Error("Maximum 4 beneficiaries allowed");
2527
+ }
2528
+ const padded = [
2529
+ beneficiaries[0] ?? ZERO_ADDRESS2,
2530
+ beneficiaries[1] ?? ZERO_ADDRESS2,
2531
+ beneficiaries[2] ?? ZERO_ADDRESS2,
2532
+ beneficiaries[3] ?? ZERO_ADDRESS2
2533
+ ];
2534
+ return padded;
2535
+ }
2536
+ };
2537
+
2538
+ // src/cli/commands/balance.ts
2539
+ var balanceCommand = new commander.Command("balance").description("Query balance via light client").requiredOption("--secret <hex>", "32-byte secret (0x-prefixed hex)").option(
2540
+ "--beneficiaries <addresses>",
2541
+ "Comma-separated beneficiary addresses"
2542
+ ).requiredOption("--client-id <id>", "Light client ID to use").option(
2543
+ "--config <path>",
2544
+ "Path to config file",
2545
+ "./private-payments.config.json"
2546
+ ).action(async (options) => {
2547
+ try {
2548
+ const config = loadConfig(options.config);
2549
+ const secret = validateSecret2(options.secret);
2550
+ const beneficiaries = parseBeneficiaries(options.beneficiaries);
2551
+ const clientId = parseInt(options.clientId, 10);
2552
+ if (isNaN(clientId) || clientId < 0) {
2553
+ throw new Error("Invalid light client ID");
2554
+ }
2555
+ const client = new UnionPrivatePayments({
2556
+ proverUrl: config.proverUrl,
2557
+ sourceRpcUrl: config.sourceRpcUrl,
2558
+ destinationRpcUrl: config.destinationRpcUrl,
2559
+ srcZAssetAddress: config.srcZAssetAddress,
2560
+ dstZAssetAddress: config.dstZAssetAddress,
2561
+ sourceChainId: BigInt(config.sourceChainId),
2562
+ destinationChainId: BigInt(config.destinationChainId)
2563
+ });
2564
+ const depositAddress = client.getDepositAddress(
2565
+ secret,
2566
+ beneficiaries
2567
+ );
2568
+ const nullifier = client.getNullifier(secret);
2569
+ console.log("Querying balance...");
2570
+ console.log("Deposit Address:", depositAddress);
2571
+ console.log("Light Client ID:", clientId);
2572
+ const balance = await client.getBalance({
2573
+ depositAddress,
2574
+ nullifier,
2575
+ clientId
2576
+ });
2577
+ const tokenInfo = await client.getSrcZAssetInfo();
2578
+ console.log("");
2579
+ console.log(`Latest height: ${balance.latestHeight}`);
2580
+ console.log(`Light client height: ${balance.lightClientHeight}`);
2581
+ console.log(
2582
+ `Light client lag: ${balance.latestHeight - balance.lightClientHeight}`
2583
+ );
2584
+ console.log(`Balance:`);
2585
+ console.log(
2586
+ " Pending:",
2587
+ formatAmount(
2588
+ balance.pending,
2589
+ tokenInfo.decimals,
2590
+ tokenInfo.symbol
2591
+ )
2592
+ );
2593
+ console.log(
2594
+ " Confirmed:",
2595
+ formatAmount(
2596
+ balance.confirmed,
2597
+ tokenInfo.decimals,
2598
+ tokenInfo.symbol
2599
+ )
2600
+ );
2601
+ console.log(
2602
+ " Redeemed:",
2603
+ formatAmount(
2604
+ balance.redeemed,
2605
+ tokenInfo.decimals,
2606
+ tokenInfo.symbol
2607
+ )
2608
+ );
2609
+ console.log(
2610
+ " Available:",
2611
+ formatAmount(
2612
+ balance.available,
2613
+ tokenInfo.decimals,
2614
+ tokenInfo.symbol
2615
+ )
2616
+ );
2617
+ console.log("");
2618
+ console.log("Raw balance:");
2619
+ console.log(" Pending:", balance.pending.toString());
2620
+ console.log(" Confirmed:", balance.confirmed.toString());
2621
+ console.log(" Redeemed:", balance.redeemed.toString());
2622
+ console.log(" Available:", balance.available.toString());
2623
+ } catch (e) {
2624
+ console.error("Error:", e.message);
2625
+ process.exit(1);
2626
+ }
2627
+ });
2628
+ var proveCommand = new commander.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) => {
2629
+ try {
2630
+ const config = loadConfig(options.config);
2631
+ const secret = validateSecret2(options.secret);
2632
+ const beneficiaries = parseBeneficiaries(options.beneficiaries);
2633
+ const beneficiary = validateAddress(options.beneficiary, "beneficiary");
2634
+ const clientIds = parseClientIds(options.clientIds);
2635
+ const selectedClientId = parseInt(options.selectedClient, 10);
2636
+ if (isNaN(selectedClientId)) {
2637
+ throw new Error("Invalid selected client ID");
2638
+ }
2639
+ if (!clientIds.includes(selectedClientId)) {
2640
+ throw new Error(`Selected client ${selectedClientId} not in client IDs: ${clientIds.join(", ")}`);
2641
+ }
2642
+ const client = new UnionPrivatePayments({
2643
+ proverUrl: config.proverUrl,
2644
+ sourceRpcUrl: config.sourceRpcUrl,
2645
+ destinationRpcUrl: config.destinationRpcUrl,
2646
+ srcZAssetAddress: config.srcZAssetAddress,
2647
+ dstZAssetAddress: config.dstZAssetAddress,
2648
+ sourceChainId: BigInt(config.sourceChainId),
2649
+ destinationChainId: BigInt(config.destinationChainId)
2650
+ });
2651
+ const amount = BigInt(options.amount);
2652
+ console.error("Generating proof...");
2653
+ console.error(" Secret:", secret.slice(0, 10) + "...");
2654
+ console.error(" Beneficiary:", beneficiary);
2655
+ console.error(" Amount:", amount.toString());
2656
+ console.error(" Client IDs:", clientIds.join(", "));
2657
+ console.error(" Selected Client:", selectedClientId);
2658
+ const result = await client.generateProof(
2659
+ secret,
2660
+ beneficiaries,
2661
+ beneficiary,
2662
+ amount,
2663
+ clientIds,
2664
+ selectedClientId
2665
+ );
2666
+ if (!result.proof.success || !result.proof.proofJson || !result.metadata) {
2667
+ throw new Error(result.proof.error ?? "Proof generation failed");
2668
+ }
2669
+ console.error("Proof generated successfully!");
2670
+ const proofJson = parseProofJson(result.proof.proofJson);
2671
+ const output = {
2672
+ proof: proofJson,
2673
+ metadata: {
2674
+ depositAddress: result.metadata.depositAddress,
2675
+ beneficiary: result.metadata.beneficiary,
2676
+ value: result.metadata.value.toString(),
2677
+ nullifier: "0x" + result.metadata.nullifier.toString(16).padStart(64, "0"),
2678
+ lightClients: result.metadata.lightClients.map((lc) => ({
2679
+ clientId: lc.clientId,
2680
+ height: lc.height.toString(),
2681
+ stateRoot: lc.stateRoot
2682
+ }))
2683
+ }
2684
+ };
2685
+ const jsonOutput = JSON.stringify(output, null, 2);
2686
+ if (options.output) {
2687
+ fs.writeFileSync(options.output, jsonOutput);
2688
+ console.error(`Proof saved to: ${options.output}`);
2689
+ } else {
2690
+ console.log(jsonOutput);
2691
+ }
2692
+ console.error("");
2693
+ console.error("Metadata:");
2694
+ console.error(" Deposit Address:", result.metadata.depositAddress);
2695
+ console.error(" Beneficiary:", result.metadata.beneficiary);
2696
+ console.error(" Value:", result.metadata.value.toString());
2697
+ console.error(" Nullifier:", "0x" + result.metadata.nullifier.toString(16).padStart(64, "0"));
2698
+ console.error(" Light Clients:", result.metadata.lightClients.length);
2699
+ } catch (e) {
2700
+ console.error("Error:", e.message);
2701
+ process.exit(1);
2702
+ }
2703
+ });
2704
+ var CHAIN_MAP = {
2705
+ 1: chains.mainnet,
2706
+ 8453: chains.base,
2707
+ 42161: chains.arbitrum,
2708
+ 10: chains.optimism,
2709
+ 137: chains.polygon
2710
+ };
2711
+ var redeemCommand = new commander.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) => {
2712
+ try {
2713
+ const config = loadConfig(options.config);
2714
+ const privateKey = options.privateKey ?? process.env.SENDER_PRIVATE_KEY;
2715
+ if (!privateKey) {
2716
+ throw new Error("Private key required. Use --private-key or set SENDER_PRIVATE_KEY env var");
2717
+ }
2718
+ if (!privateKey.startsWith("0x")) {
2719
+ throw new Error("Private key must start with 0x");
2720
+ }
2721
+ if (!config.attestorUrl) {
2722
+ throw new Error("attestorUrl required in config for redemption");
2723
+ }
2724
+ if (!config.attestorApiKey) {
2725
+ throw new Error("attestorApiKey required in config for redemption");
2726
+ }
2727
+ let proofData;
2728
+ if (options.proof) {
2729
+ const proofContent = fs.readFileSync(options.proof, "utf-8");
2730
+ proofData = JSON.parse(proofContent);
2731
+ console.log("Loading proof from:", options.proof);
2732
+ } else {
2733
+ if (!options.secret || !options.beneficiary || !options.amount || !options.clientIds || !options.selectedClient) {
2734
+ throw new Error(
2735
+ "When not using --proof, you must provide: --secret, --beneficiary, --amount, --client-ids, --selected-client"
2736
+ );
2737
+ }
2738
+ const secret = validateSecret2(options.secret);
2739
+ const beneficiaries = parseBeneficiaries(options.beneficiaries);
2740
+ const beneficiary2 = validateAddress(options.beneficiary, "beneficiary");
2741
+ const clientIds = parseClientIds(options.clientIds);
2742
+ const selectedClientId = parseInt(options.selectedClient, 10);
2743
+ if (isNaN(selectedClientId)) {
2744
+ throw new Error("Invalid selected client ID");
2745
+ }
2746
+ if (!clientIds.includes(selectedClientId)) {
2747
+ throw new Error(`Selected client ${selectedClientId} not in client IDs: ${clientIds.join(", ")}`);
2748
+ }
2749
+ const client = new UnionPrivatePayments({
2750
+ proverUrl: config.proverUrl,
2751
+ sourceRpcUrl: config.sourceRpcUrl,
2752
+ destinationRpcUrl: config.destinationRpcUrl,
2753
+ srcZAssetAddress: config.srcZAssetAddress,
2754
+ dstZAssetAddress: config.dstZAssetAddress,
2755
+ sourceChainId: BigInt(config.sourceChainId),
2756
+ destinationChainId: BigInt(config.destinationChainId)
2757
+ });
2758
+ const amount = BigInt(options.amount);
2759
+ console.log("Generating proof...");
2760
+ console.log(" Secret:", secret.slice(0, 10) + "...");
2761
+ console.log(" Beneficiary:", beneficiary2);
2762
+ console.log(" Amount:", amount.toString());
2763
+ console.log(" Client IDs:", clientIds.join(", "));
2764
+ console.log(" Selected Client:", selectedClientId);
2765
+ const result = await client.generateProof(
2766
+ secret,
2767
+ beneficiaries,
2768
+ beneficiary2,
2769
+ amount,
2770
+ clientIds,
2771
+ selectedClientId
2772
+ );
2773
+ if (!result.proof.success || !result.proof.proofJson || !result.metadata) {
2774
+ throw new Error(result.proof.error ?? "Proof generation failed");
2775
+ }
2776
+ console.log("Proof generated successfully!");
2777
+ const proofJson = parseProofJson(result.proof.proofJson);
2778
+ proofData = {
2779
+ proof: proofJson,
2780
+ metadata: {
2781
+ depositAddress: result.metadata.depositAddress,
2782
+ beneficiary: result.metadata.beneficiary,
2783
+ value: result.metadata.value.toString(),
2784
+ nullifier: "0x" + result.metadata.nullifier.toString(16).padStart(64, "0"),
2785
+ lightClients: result.metadata.lightClients.map((lc) => ({
2786
+ clientId: lc.clientId,
2787
+ height: lc.height.toString(),
2788
+ stateRoot: lc.stateRoot
2789
+ }))
2790
+ }
2791
+ };
2792
+ }
2793
+ const lightClients = proofData.metadata.lightClients.map((lc) => ({
2794
+ clientId: lc.clientId,
2795
+ height: BigInt(lc.height),
2796
+ stateRoot: lc.stateRoot
2797
+ }));
2798
+ const nullifier = BigInt(proofData.metadata.nullifier);
2799
+ const value = BigInt(proofData.metadata.value);
2800
+ const beneficiary = proofData.metadata.beneficiary;
2801
+ const depositAddress = proofData.metadata.depositAddress;
2802
+ console.log("");
2803
+ console.log("Redemption details:");
2804
+ console.log(" Deposit Address:", depositAddress);
2805
+ console.log(" Beneficiary:", beneficiary);
2806
+ console.log(" Value:", value.toString());
2807
+ console.log(" Nullifier:", proofData.metadata.nullifier);
2808
+ console.log(" Light Clients:", lightClients.length);
2809
+ console.log("");
2810
+ console.log("Requesting attestation...");
2811
+ const attestationClient = new AttestationClient(
2812
+ config.attestorUrl,
2813
+ config.attestorApiKey
2814
+ );
2815
+ const attestation = await attestationClient.getAttestation(depositAddress, beneficiary);
2816
+ console.log(" Attested Message:", attestation.attestedMessage);
2817
+ console.log(" Signature:", attestation.signature.slice(0, 20) + "...");
2818
+ const redeemParams = proofJsonToRedeemParams(proofData.proof, {
2819
+ lightClients,
2820
+ nullifier,
2821
+ value,
2822
+ beneficiary,
2823
+ attestedMessage: attestation.attestedMessage,
2824
+ signature: attestation.signature
2825
+ });
2826
+ const chain = CHAIN_MAP[config.destinationChainId];
2827
+ if (!chain) {
2828
+ throw new Error(`Unsupported destination chain ID: ${config.destinationChainId}`);
2829
+ }
2830
+ const account = accounts.privateKeyToAccount(privateKey);
2831
+ const walletClient = V.createWalletClient({
2832
+ account,
2833
+ chain,
2834
+ transport: V.http(config.destinationRpcUrl)
2835
+ });
2836
+ console.log("");
2837
+ console.log("Submitting redeem transaction...");
2838
+ const txHash = await submitRedeem(
2839
+ config.dstZAssetAddress,
2840
+ redeemParams,
2841
+ walletClient
2842
+ );
2843
+ console.log("");
2844
+ console.log("Transaction submitted!");
2845
+ console.log("Hash:", txHash);
2846
+ console.log("Explorer:", getExplorerUrl(config.destinationChainId, txHash));
2847
+ } catch (e) {
2848
+ console.error("Error:", e.message);
2849
+ process.exit(1);
2850
+ }
2851
+ });
2852
+ var updateClientCommand = new commander.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) => {
2853
+ try {
2854
+ const privateKey = options.privateKey ?? process.env.SENDER_PRIVATE_KEY;
2855
+ if (!privateKey) {
2856
+ throw new Error("Private key required. Use --private-key or set SENDER_PRIVATE_KEY env var");
2857
+ }
2858
+ if (!privateKey.startsWith("0x")) {
2859
+ throw new Error("Private key must start with 0x");
2860
+ }
2861
+ const clientId = parseInt(options.clientId, 10);
2862
+ if (isNaN(clientId)) {
2863
+ throw new Error("Invalid client ID: must be a number");
2864
+ }
2865
+ const ibcHandlerAddress = options.ibcHandler;
2866
+ if (!ibcHandlerAddress.startsWith("0x") || ibcHandlerAddress.length !== 42) {
2867
+ throw new Error("Invalid IBC handler address");
2868
+ }
2869
+ const height = options.height === "latest" ? "latest" : BigInt(options.height);
2870
+ const publicClient = V.createPublicClient({
2871
+ transport: V.http(options.rpc)
2872
+ });
2873
+ const chainId = await publicClient.getChainId();
2874
+ const account = accounts.privateKeyToAccount(privateKey);
2875
+ const walletClient = V.createWalletClient({
2876
+ account,
2877
+ chain: { id: chainId },
2878
+ transport: V.http(options.rpc)
2879
+ });
2880
+ console.log("Updating loopback light client...");
2881
+ console.log(" RPC:", options.rpc);
2882
+ console.log(" IBC Handler:", ibcHandlerAddress);
2883
+ console.log(" Client ID:", clientId);
2884
+ console.log(" Height:", height === "latest" ? "latest" : height.toString());
2885
+ console.log("");
2886
+ const result = await updateLoopbackClient(
2887
+ options.rpc,
2888
+ ibcHandlerAddress,
2889
+ clientId,
2890
+ height,
2891
+ walletClient
2892
+ );
2893
+ console.log("Light client updated successfully!");
2894
+ console.log(" Chain ID:", result.chainId.toString());
2895
+ console.log(" Block Number:", result.blockNumber.toString());
2896
+ console.log(" State Root:", result.stateRoot);
2897
+ console.log(" Transaction:", result.txHash);
2898
+ console.log(" Explorer:", getExplorerUrl(Number(result.chainId), result.txHash));
2899
+ } catch (e) {
2900
+ console.error("Error:", e.message);
2901
+ process.exit(1);
2902
+ }
2903
+ });
2904
+ var depositCommand = new commander.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) => {
2905
+ try {
2906
+ const privateKey = options.privateKey ?? process.env.SENDER_PRIVATE_KEY;
2907
+ if (!privateKey) {
2908
+ throw new Error("Private key required. Use --private-key or set SENDER_PRIVATE_KEY env var");
2909
+ }
2910
+ if (!privateKey.startsWith("0x")) {
2911
+ throw new Error("Private key must start with 0x");
2912
+ }
2913
+ const config = loadConfig(options.config);
2914
+ const secret = validateSecret2(options.secret);
2915
+ const beneficiaries = parseBeneficiaries(options.beneficiaries);
2916
+ const paddedBeneficiaries = padBeneficiaries(beneficiaries);
2917
+ const rpcUrl = config.sourceRpcUrl;
2918
+ const zAssetAddress = config.srcZAssetAddress;
2919
+ const amount = BigInt(options.amount);
2920
+ const depositAddress = computeUnspendableAddress(
2921
+ secret,
2922
+ BigInt(config.destinationChainId),
2923
+ paddedBeneficiaries
2924
+ );
2925
+ const account = accounts.privateKeyToAccount(privateKey);
2926
+ const walletClient = V.createWalletClient({
2927
+ account,
2928
+ chain: { id: config.sourceChainId },
2929
+ transport: V.http(rpcUrl)
2930
+ });
2931
+ console.log("Depositing to ZAsset...");
2932
+ console.log(" ZAsset:", zAssetAddress);
2933
+ console.log(" Amount:", amount.toString());
2934
+ console.log(" Deposit Address:", depositAddress);
2935
+ console.log("");
2936
+ const result = await depositToZAsset(
2937
+ rpcUrl,
2938
+ zAssetAddress,
2939
+ depositAddress,
2940
+ amount,
2941
+ walletClient
2942
+ );
2943
+ console.log("");
2944
+ console.log("Deposit complete!");
2945
+ console.log(" Tx Hash:", result.txHash);
2946
+ console.log(" Explorer:", getExplorerUrl(Number(result.chainId), result.txHash));
2947
+ } catch (e) {
2948
+ console.error("Error:", e.message);
2949
+ process.exit(1);
2950
+ }
2951
+ });
2952
+ var historyCommand = new commander.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) => {
2953
+ try {
2954
+ const config = loadConfig(options.config);
2955
+ const secret = validateSecret2(options.secret);
2956
+ const client = new UnionPrivatePayments({
2957
+ proverUrl: config.proverUrl,
2958
+ sourceRpcUrl: config.sourceRpcUrl,
2959
+ destinationRpcUrl: config.destinationRpcUrl,
2960
+ srcZAssetAddress: config.srcZAssetAddress,
2961
+ dstZAssetAddress: config.dstZAssetAddress,
2962
+ sourceChainId: BigInt(config.sourceChainId),
2963
+ destinationChainId: BigInt(config.destinationChainId)
2964
+ });
2965
+ const nullifier = client.getNullifier(secret);
2966
+ const nullifierHex = "0x" + nullifier.toString(16).padStart(64, "0");
2967
+ console.log("Querying redemption history...");
2968
+ console.log("Nullifier:", nullifierHex);
2969
+ console.log("");
2970
+ const fromBlock = options.fromBlock ? BigInt(options.fromBlock) : void 0;
2971
+ const toBlock = options.toBlock ? BigInt(options.toBlock) : void 0;
2972
+ const dstRpcClient = new RpcClient(config.destinationRpcUrl);
2973
+ const history = await dstRpcClient.getRedemptionHistory(
2974
+ config.dstZAssetAddress,
2975
+ nullifier,
2976
+ fromBlock,
2977
+ toBlock
2978
+ );
2979
+ if (history.length === 0) {
2980
+ console.log("No redemptions found for this secret.");
2981
+ return;
2982
+ }
2983
+ const tokenInfo = await client.getDstZAssetInfo();
2984
+ console.log(`Found ${history.length} redemption${history.length > 1 ? "s" : ""}:`);
2985
+ console.log("");
2986
+ for (let i = 0; i < history.length; i++) {
2987
+ const entry = history[i];
2988
+ console.log(`#${i + 1} Block: ${entry.blockNumber}`);
2989
+ console.log(` Tx: ${entry.txHash}`);
2990
+ console.log(` Amount: ${formatAmount(entry.redeemAmount, tokenInfo.decimals, tokenInfo.symbol)}`);
2991
+ console.log(` Beneficiary: ${entry.beneficiary}`);
2992
+ console.log(` Explorer: ${getExplorerUrl(config.destinationChainId, entry.txHash)}`);
2993
+ console.log("");
2994
+ }
2995
+ const totalRedeemed = history.reduce((sum, h) => sum + h.redeemAmount, 0n);
2996
+ console.log(`Total redeemed: ${formatAmount(totalRedeemed, tokenInfo.decimals, tokenInfo.symbol)}`);
2997
+ } catch (e) {
2998
+ console.error("Error:", e.message);
2999
+ process.exit(1);
3000
+ }
3001
+ });
3002
+ var exportVerifierCommand = new commander.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) => {
3003
+ try {
3004
+ const proverClient = new ProverClient(options.proverUrl);
3005
+ console.error("Exporting verifier contract...");
3006
+ console.error(" Prover URL:", options.proverUrl);
3007
+ const verifierContract = await proverClient.exportVerifier();
3008
+ if (options.output) {
3009
+ fs.writeFileSync(options.output, verifierContract);
3010
+ console.error(`Verifier saved to: ${options.output}`);
3011
+ } else {
3012
+ console.log(verifierContract);
3013
+ }
3014
+ } catch (e) {
3015
+ console.error("Error:", e.message);
3016
+ process.exit(1);
3017
+ }
3018
+ });
3019
+
3020
+ // src/cli.ts
3021
+ var program = new commander.Command();
3022
+ program.name("payments").description("CLI for Union Private Payments - privacy-preserving transfers using ZK proofs").version("0.1.0");
3023
+ program.addCommand(generateCommand);
3024
+ program.addCommand(balanceCommand);
3025
+ program.addCommand(proveCommand);
3026
+ program.addCommand(redeemCommand);
3027
+ program.addCommand(updateClientCommand);
3028
+ program.addCommand(depositCommand);
3029
+ program.addCommand(historyCommand);
3030
+ program.addCommand(exportVerifierCommand);
3031
+ program.parse();