@unionlabs/payments 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -0
- package/dist/LICENSE +1 -0
- package/dist/chunk-37PNLRA6.js +2418 -0
- package/dist/cli.cjs +3031 -0
- package/dist/cli.js +675 -0
- package/dist/index.cjs +2451 -0
- package/dist/index.js +1 -0
- package/dist/package.json +18 -0
- package/dist/payments.d.ts +835 -0
- package/dist/tsdoc-metadata.json +11 -0
- package/package.json +60 -0
package/dist/cli.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();
|