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