mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-11 23:07:58 -05:00
Compare commits
578 Commits
v0.3.0
...
auditIssue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdecab26ee | ||
|
|
e2b6e4f1ee | ||
|
|
7c2901f243 | ||
|
|
4a7331c4a7 | ||
|
|
1fb4a557ab | ||
|
|
7862b5278a | ||
|
|
e2ef1ed62e | ||
|
|
96bfb32e5b | ||
|
|
2f9a3442f8 | ||
|
|
8833c5f0af | ||
|
|
9505ed425b | ||
|
|
7435d9976e | ||
|
|
2dae355817 | ||
|
|
e734209df0 | ||
|
|
fedf05cc7e | ||
|
|
7dc7bb4a5e | ||
|
|
d2c6cff629 | ||
|
|
25469d50e4 | ||
|
|
548301d32d | ||
|
|
ac7cd956a8 | ||
|
|
5423f65503 | ||
|
|
3c0f36f444 | ||
|
|
32c6b13e7d | ||
|
|
e3bbd393d8 | ||
|
|
b203072196 | ||
|
|
ee17357e03 | ||
|
|
f6770cf2d4 | ||
|
|
cd8065be16 | ||
|
|
94d6bc8280 | ||
|
|
24a871814e | ||
|
|
3567167ccb | ||
|
|
b4e6be0f98 | ||
|
|
e86db6f72e | ||
|
|
3a8446fda1 | ||
|
|
c1c518e922 | ||
|
|
bcc16373f9 | ||
|
|
0c65c78d7e | ||
|
|
fbb0fe3a1a | ||
|
|
2f8df2fe3a | ||
|
|
1452ef5f4a | ||
|
|
c963d622a1 | ||
|
|
ca307ef52c | ||
|
|
597920944a | ||
|
|
aa21c603fe | ||
|
|
af44452199 | ||
|
|
c9c03a699d | ||
|
|
cbbf32225a | ||
|
|
5dfa46f126 | ||
|
|
f5f6f139a3 | ||
|
|
46987d1002 | ||
|
|
334816adda | ||
|
|
d3d4541b55 | ||
|
|
0927798ea0 | ||
|
|
d00f59173f | ||
|
|
7fc77523cc | ||
|
|
0aa0f4efa2 | ||
|
|
8db993a76f | ||
|
|
a1892cb9a1 | ||
|
|
f2c428871f | ||
|
|
d621f982d6 | ||
|
|
db968c34fd | ||
|
|
f535662407 | ||
|
|
e5a426a991 | ||
|
|
d5422da19d | ||
|
|
12286389b1 | ||
|
|
6968940f0c | ||
|
|
ea12f8fc42 | ||
|
|
c72a843c85 | ||
|
|
15f28dfe76 | ||
|
|
0e8588e819 | ||
|
|
080568cb93 | ||
|
|
6b4f6ccd96 | ||
|
|
3bf2d78215 | ||
|
|
76ea1cabcd | ||
|
|
f4e1c9b250 | ||
|
|
5d34594bfd | ||
|
|
b6e62ed303 | ||
|
|
771335002b | ||
|
|
c9d88ce73b | ||
|
|
39e346f057 | ||
|
|
5769d08a0e | ||
|
|
99ed78bf49 | ||
|
|
ef97000344 | ||
|
|
55ad789c3a | ||
|
|
2a59616b2f | ||
|
|
54f3fbcf41 | ||
|
|
a31e53871a | ||
|
|
ceaa69adeb | ||
|
|
ba617b0a75 | ||
|
|
aa16947628 | ||
|
|
80efc04784 | ||
|
|
7dd04ee62e | ||
|
|
3f7db13dba | ||
|
|
37ead8f0c9 | ||
|
|
98ae7ec6c7 | ||
|
|
131f84b7fc | ||
|
|
c8f454fd90 | ||
|
|
58dff2f0ce | ||
|
|
8ef89face7 | ||
|
|
ad0a019ca1 | ||
|
|
c7b245fed9 | ||
|
|
b715a7f491 | ||
|
|
c620f3cd7e | ||
|
|
7640415ab6 | ||
|
|
d01df873e6 | ||
|
|
639cf4c26d | ||
|
|
f964136a6e | ||
|
|
506d376b36 | ||
|
|
9835886fb1 | ||
|
|
54f2c7d1fe | ||
|
|
4a3d9615a9 | ||
|
|
fcd629c20d | ||
|
|
4eba0baabf | ||
|
|
387e478249 | ||
|
|
b61af4acd2 | ||
|
|
ff4ff05c83 | ||
|
|
dfe73e4684 | ||
|
|
746038be5b | ||
|
|
99f08967ab | ||
|
|
916b7fbb5f | ||
|
|
f1d16bbb88 | ||
|
|
6af93bde8c | ||
|
|
d7f21c96a5 | ||
|
|
5934cf19bd | ||
|
|
7c242ad985 | ||
|
|
016c1e59d8 | ||
|
|
fe9eacd061 | ||
|
|
d39c16bfdb | ||
|
|
4f9ddac4b8 | ||
|
|
fee9d200c0 | ||
|
|
e214c6ef35 | ||
|
|
6dbee9fad2 | ||
|
|
2fb5cefa5b | ||
|
|
a92a39481b | ||
|
|
b6fc2164a3 | ||
|
|
772e44dd60 | ||
|
|
009e3762f7 | ||
|
|
3b79d553b9 | ||
|
|
f48979b4da | ||
|
|
b49964287a | ||
|
|
5e29d4e4e7 | ||
|
|
e5e8fa187a | ||
|
|
92bc376ba6 | ||
|
|
13ff3155df | ||
|
|
91b88cd603 | ||
|
|
264491a5b0 | ||
|
|
8b5bfcafba | ||
|
|
850bde5f46 | ||
|
|
c95de82ee3 | ||
|
|
5f5f039a1c | ||
|
|
d740fdc594 | ||
|
|
b37555fcb4 | ||
|
|
4d6b836961 | ||
|
|
9e386dd784 | ||
|
|
ebfa843bf8 | ||
|
|
283bc365d6 | ||
|
|
26617c96d3 | ||
|
|
27bc22d755 | ||
|
|
95bf1dfba3 | ||
|
|
212daabf85 | ||
|
|
3b0d3536e7 | ||
|
|
636f60af2f | ||
|
|
4945f48a43 | ||
|
|
c96c835966 | ||
|
|
7747e353cc | ||
|
|
c46e063f0f | ||
|
|
824b9a90ac | ||
|
|
533c81e5c6 | ||
|
|
6a5ea24873 | ||
|
|
dc3d4c0edb | ||
|
|
dd513f8261 | ||
|
|
369df16cae | ||
|
|
f6eb73c43a | ||
|
|
147ff4aaad | ||
|
|
7b7777bf20 | ||
|
|
85dcdc10e5 | ||
|
|
f34ccaceed | ||
|
|
9451501f20 | ||
|
|
92c69de7ef | ||
|
|
1b95f66431 | ||
|
|
4facaba47f | ||
|
|
7111fa621b | ||
|
|
8c884186a6 | ||
|
|
4a61371e0a | ||
|
|
b698c7f3a9 | ||
|
|
bcc460bf34 | ||
|
|
b0635d9f6a | ||
|
|
0eeddb376d | ||
|
|
bcc74926dd | ||
|
|
cffd316c89 | ||
|
|
472ac28d80 | ||
|
|
1d35f4e80a | ||
|
|
7b5ca4e88d | ||
|
|
140b606152 | ||
|
|
61c5c43572 | ||
|
|
32d508fb6f | ||
|
|
e77f9bd887 | ||
|
|
319f2d1a95 | ||
|
|
a4d90f109c | ||
|
|
d8fca1bf0c | ||
|
|
8246c53d29 | ||
|
|
2448bdea4d | ||
|
|
25813f118b | ||
|
|
1aa00a8848 | ||
|
|
c208296f8c | ||
|
|
763ede3cb4 | ||
|
|
671cec495c | ||
|
|
2e2b4f3cf2 | ||
|
|
7423b28f89 | ||
|
|
44fa3102b9 | ||
|
|
852b676f87 | ||
|
|
9f4064eef9 | ||
|
|
2f4d91e336 | ||
|
|
ee5f044028 | ||
|
|
1b95731106 | ||
|
|
9c552dc3c9 | ||
|
|
f66fa370e9 | ||
|
|
8180d365f2 | ||
|
|
2b8f3c2ead | ||
|
|
99847b4ab2 | ||
|
|
8fa6b50b61 | ||
|
|
5911285f8d | ||
|
|
e1e985a82c | ||
|
|
add135161f | ||
|
|
a3a7cc6fe7 | ||
|
|
9078f6189a | ||
|
|
46dfe7b08d | ||
|
|
ce5d8b3a01 | ||
|
|
19f6cbdc6f | ||
|
|
758c7c4fe2 | ||
|
|
68dbaa25c8 | ||
|
|
cc3da2bc5d | ||
|
|
1995bbcba0 | ||
|
|
da03830e04 | ||
|
|
9a6612bda2 | ||
|
|
a266bb2432 | ||
|
|
29240dd288 | ||
|
|
7d8d72e285 | ||
|
|
4d5c8d9ecc | ||
|
|
71c9e63ae6 | ||
|
|
00b668a87c | ||
|
|
4f5b7c75d9 | ||
|
|
1a13d8dcb8 | ||
|
|
aa8a969857 | ||
|
|
e5897786d7 | ||
|
|
d5f977b657 | ||
|
|
0969715701 | ||
|
|
d95404ec6b | ||
|
|
ac03d4d165 | ||
|
|
969dd2b605 | ||
|
|
bc18c8be1a | ||
|
|
30fc05cbd5 | ||
|
|
29566d50f0 | ||
|
|
8064eb04d0 | ||
|
|
ef1b175f8b | ||
|
|
27d939556a | ||
|
|
f2d3aada78 | ||
|
|
f0f02f6921 | ||
|
|
0bfaf98dfa | ||
|
|
e0250ca502 | ||
|
|
27b936863b | ||
|
|
e20b2eea52 | ||
|
|
a8e9f89e8f | ||
|
|
1cf7159003 | ||
|
|
ab1209afbd | ||
|
|
231bf59184 | ||
|
|
294b5d13a6 | ||
|
|
4f6172a9a0 | ||
|
|
542c33d983 | ||
|
|
0d1f5c20d7 | ||
|
|
41ef0fe974 | ||
|
|
e8aaf4512f | ||
|
|
08d490eef4 | ||
|
|
1e100cbf33 | ||
|
|
6aee4db636 | ||
|
|
efa2e062cc | ||
|
|
8f182bc7ba | ||
|
|
e4c9c8dc16 | ||
|
|
270abf90e4 | ||
|
|
e243212e67 | ||
|
|
916ba7285a | ||
|
|
56f33ecf4e | ||
|
|
035891aa37 | ||
|
|
69a1a19edb | ||
|
|
d476556588 | ||
|
|
5286429503 | ||
|
|
72b546c7a7 | ||
|
|
51ee33f753 | ||
|
|
3de61b7a83 | ||
|
|
b62d1709dd | ||
|
|
0855462220 | ||
|
|
bb2ebed49d | ||
|
|
bd0ff0da7b | ||
|
|
44fec67e6c | ||
|
|
c163638cc2 | ||
|
|
ec3c05d822 | ||
|
|
ebbe072320 | ||
|
|
b40d29c82d | ||
|
|
5c6d96f7e5 | ||
|
|
ccc8a59f2f | ||
|
|
57e7ec3d24 | ||
|
|
0a00d8606f | ||
|
|
940dd11de9 | ||
|
|
02298b8029 | ||
|
|
08c059acb4 | ||
|
|
54e752f503 | ||
|
|
bc3d1463f1 | ||
|
|
bc37324932 | ||
|
|
e613af7b1e | ||
|
|
63f230c742 | ||
|
|
a4a0e951a8 | ||
|
|
f3da0977b8 | ||
|
|
b8cefff40e | ||
|
|
59e1a65242 | ||
|
|
dc4b421bbe | ||
|
|
dca0eb3ff6 | ||
|
|
771de1fbab | ||
|
|
2b5ad82d5c | ||
|
|
c508211f18 | ||
|
|
6e00912981 | ||
|
|
d1adf2572a | ||
|
|
4888426a4e | ||
|
|
cd103faf84 | ||
|
|
20fc590cd7 | ||
|
|
99dbaadda9 | ||
|
|
02ffad7f85 | ||
|
|
84dd0a09d8 | ||
|
|
e4ab6b53db | ||
|
|
097174b243 | ||
|
|
93c7935dc6 | ||
|
|
b06435e3d7 | ||
|
|
bb8bd2458e | ||
|
|
6379006133 | ||
|
|
4bc4701faf | ||
|
|
5b7231b980 | ||
|
|
3cfabaa4b1 | ||
|
|
b8ae335449 | ||
|
|
221b7b94e0 | ||
|
|
8f17aa983d | ||
|
|
6a3df55a38 | ||
|
|
200718814d | ||
|
|
0904d8169d | ||
|
|
d476a53d0d | ||
|
|
5b771c0e8c | ||
|
|
2b16c7367d | ||
|
|
0c956188a3 | ||
|
|
6ec971ecac | ||
|
|
80e56c1bb5 | ||
|
|
05660dd468 | ||
|
|
aa0ba1ad47 | ||
|
|
feaf497c22 | ||
|
|
06a3dd025a | ||
|
|
cac08ec5a6 | ||
|
|
7c93ccea89 | ||
|
|
647c1bbf4d | ||
|
|
c2bf4bbc16 | ||
|
|
2d28b7b17a | ||
|
|
92b9b136a7 | ||
|
|
c3e79a7c8e | ||
|
|
163af9d49c | ||
|
|
f2a490aaf2 | ||
|
|
1481c69dc0 | ||
|
|
17052cdaa1 | ||
|
|
def1eaf208 | ||
|
|
5a33e43945 | ||
|
|
998ee6ba30 | ||
|
|
f0a3b56a44 | ||
|
|
4147726d2a | ||
|
|
192db97b01 | ||
|
|
e2ee017824 | ||
|
|
a485d84364 | ||
|
|
ae5e1adcc2 | ||
|
|
1ee24813ed | ||
|
|
4dfa0bdd4a | ||
|
|
de5c288d07 | ||
|
|
db0cf3acc8 | ||
|
|
035f1872fe | ||
|
|
60ed167b2d | ||
|
|
1860ecbd18 | ||
|
|
e830f49714 | ||
|
|
8753978719 | ||
|
|
fec0f12e45 | ||
|
|
dcfd13d8b3 | ||
|
|
665ea530fc | ||
|
|
b547361d86 | ||
|
|
b513d9feab | ||
|
|
8d4257aac2 | ||
|
|
e4ad6d317c | ||
|
|
080513ec12 | ||
|
|
3c9490b5a6 | ||
|
|
e7decded4c | ||
|
|
427d05ce82 | ||
|
|
a6e5d6d3d8 | ||
|
|
bcbab0d08a | ||
|
|
1303ffea9b | ||
|
|
356b0674e7 | ||
|
|
90499370b5 | ||
|
|
0007a8491d | ||
|
|
eeb7f506ab | ||
|
|
8a7187c041 | ||
|
|
68547ca88f | ||
|
|
576802a79a | ||
|
|
ce8620c978 | ||
|
|
ce9e654815 | ||
|
|
19bdc19e8d | ||
|
|
b05525a1b8 | ||
|
|
53e2f518d5 | ||
|
|
9d22092b2c | ||
|
|
3936b59e22 | ||
|
|
a7d8209664 | ||
|
|
6ed993d951 | ||
|
|
23613b42b8 | ||
|
|
92195d1f3e | ||
|
|
5b2168e262 | ||
|
|
8d5b0867fe | ||
|
|
03ced09fc2 | ||
|
|
b267fcd3f3 | ||
|
|
297fba186b | ||
|
|
ebb90e2f9a | ||
|
|
6608add701 | ||
|
|
6d07b754dc | ||
|
|
3ac37a0e43 | ||
|
|
b1bac6fa79 | ||
|
|
87eb0b53fb | ||
|
|
17b7293057 | ||
|
|
11414de511 | ||
|
|
6c6972ce5c | ||
|
|
a8b5e4c9d7 | ||
|
|
07c59cd0bd | ||
|
|
3512982eff | ||
|
|
7a37dc89d0 | ||
|
|
17654c5042 | ||
|
|
2f62a6da8f | ||
|
|
ee2a533c72 | ||
|
|
6f45f7942f | ||
|
|
df6daffcce | ||
|
|
287c1c64ee | ||
|
|
c13e9800a4 | ||
|
|
4548bf0a77 | ||
|
|
e64b637440 | ||
|
|
fd6f60d39c | ||
|
|
3a519cd6e4 | ||
|
|
56bdf8a1df | ||
|
|
600d898fc0 | ||
|
|
8f5eff131a | ||
|
|
eebff6d205 | ||
|
|
bcf6aef290 | ||
|
|
ed7bbf5397 | ||
|
|
0aacf46234 | ||
|
|
723740348f | ||
|
|
63d9470cad | ||
|
|
c285e8dfaf | ||
|
|
cf46dbf3ff | ||
|
|
427ce33267 | ||
|
|
7a7cc0a9d4 | ||
|
|
6d143fcbc8 | ||
|
|
8c8e840dc6 | ||
|
|
d30c692263 | ||
|
|
014871ee8d | ||
|
|
20d46b4900 | ||
|
|
268ffe41f8 | ||
|
|
a0a2ee878b | ||
|
|
3a9c6ce4cd | ||
|
|
3106e1dd0a | ||
|
|
8745c185b9 | ||
|
|
668362304b | ||
|
|
e89a9151e6 | ||
|
|
c435fe0471 | ||
|
|
956607a479 | ||
|
|
04af3ae83a | ||
|
|
27b0864c79 | ||
|
|
76a7daa0bd | ||
|
|
b38e216585 | ||
|
|
5da15cb16f | ||
|
|
44b89fd8f3 | ||
|
|
b8122ced5c | ||
|
|
7613237f96 | ||
|
|
368aa43085 | ||
|
|
68b5446a6f | ||
|
|
a0f858dbbb | ||
|
|
83e30afea1 | ||
|
|
d9616333a4 | ||
|
|
0570216fb2 | ||
|
|
0515caacfc | ||
|
|
767eafe338 | ||
|
|
d76fb1c227 | ||
|
|
ec2ae3eb2d | ||
|
|
964b4c7098 | ||
|
|
4259ee5bce | ||
|
|
06a1b7318e | ||
|
|
3c9cbe6c3e | ||
|
|
6b86b95f3c | ||
|
|
911ffa4865 | ||
|
|
c5f460969f | ||
|
|
a2fb8f7039 | ||
|
|
87696c3251 | ||
|
|
a4dbeb00e9 | ||
|
|
2a24a15460 | ||
|
|
9040e90457 | ||
|
|
3d483d6b76 | ||
|
|
7d6aa698ec | ||
|
|
7d240daaa5 | ||
|
|
5f866991dc | ||
|
|
0959db5c4c | ||
|
|
c6c37ffc47 | ||
|
|
1a80eb4a4a | ||
|
|
a7c5de25aa | ||
|
|
a2bd4165b3 | ||
|
|
d4bf0b38c4 | ||
|
|
64b312e646 | ||
|
|
1abdcd46df | ||
|
|
64db0b7eba | ||
|
|
b809a22cee | ||
|
|
94ae095acf | ||
|
|
d52d598573 | ||
|
|
8a4c6e52f1 | ||
|
|
f6473e72af | ||
|
|
0f82acf931 | ||
|
|
bfada65260 | ||
|
|
79423ec79f | ||
|
|
5225cb1883 | ||
|
|
f4f5703eb6 | ||
|
|
36825dbbe6 | ||
|
|
54e90045fb | ||
|
|
89c8b28d20 | ||
|
|
aa5c2fea6b | ||
|
|
66c257cacd | ||
|
|
501afd7d64 | ||
|
|
676471b3fe | ||
|
|
26686cdfe3 | ||
|
|
7cbd6a7617 | ||
|
|
1c3b1c76b3 | ||
|
|
ed9ab2a103 | ||
|
|
b38547126e | ||
|
|
e1aa4de7fb | ||
|
|
c0fa1cdb91 | ||
|
|
7f5e5e2f27 | ||
|
|
8761d5e7f6 | ||
|
|
c62dceb6a9 | ||
|
|
d8dd145ae8 | ||
|
|
e43ddbd0fe | ||
|
|
ce73b22a50 | ||
|
|
449f5b439a | ||
|
|
3fcddfe2f7 | ||
|
|
0eee5e34b4 | ||
|
|
7fb3a62655 | ||
|
|
7c15195aa6 | ||
|
|
b15c81ed2e | ||
|
|
a2756603bc | ||
|
|
7fc2b45419 | ||
|
|
ba83438cf0 | ||
|
|
e0d47566db | ||
|
|
39d02cbea0 | ||
|
|
b8ae6e9370 | ||
|
|
1b403b9300 | ||
|
|
6ce54b404d | ||
|
|
dbdd56795d | ||
|
|
1358430dbb | ||
|
|
b200502594 | ||
|
|
5f506f5f14 | ||
|
|
c982fe7a69 | ||
|
|
718be15cac | ||
|
|
ae6db5b3f7 | ||
|
|
d4cfe0e4f6 | ||
|
|
aad71f738f | ||
|
|
1e88e2711f | ||
|
|
b6e057bed8 | ||
|
|
5c1388e95c | ||
|
|
c3169e93b4 | ||
|
|
847b02d31e | ||
|
|
b5c30bedb2 | ||
|
|
350a31b66f | ||
|
|
36396a74d9 | ||
|
|
78465e133e | ||
|
|
363a518acd | ||
|
|
565e767d12 | ||
|
|
49a3661e52 | ||
|
|
9cc3db2fce |
@@ -27,21 +27,21 @@ runs:
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: |
|
||||
cp .env.release .env
|
||||
envsubst < config.release.json > config.json
|
||||
yarn install --frozen-lockfile
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: |
|
||||
CRYPTO_COMPARE_API_KEY=${CRYPTO_COMPARE_API_KEY} \
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-testnet.json \
|
||||
NETWORK_CONFIGS_DIR=../contracts/networks \
|
||||
yarn build:${{ inputs.browser }}
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: mv ./extension/${{ inputs.file-name }} ./extension/quill-${{ inputs.file-name }}
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
tag_name: ${{ inputs.tag-name }}
|
||||
tag: ${{ inputs.tag-name }}
|
||||
# Note: This path is from repo root
|
||||
# working-directory is not applied
|
||||
files: ./extension/extension/quill-${{ inputs.file-name }}
|
||||
# working-directory is not applied
|
||||
file: ./extension/extension/quill-${{ inputs.file-name }}
|
||||
overwrite: true
|
||||
|
||||
13
.github/actions/local-aggregator-deploy/action.yml
vendored
Normal file
13
.github/actions/local-aggregator-deploy/action.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
name: Local Aggregator Deploy
|
||||
description: Runs an aggregator instance
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- working-directory: ./aggregator
|
||||
shell: bash
|
||||
run: cp .env.test .env
|
||||
|
||||
- working-directory: ./aggregator
|
||||
shell: bash
|
||||
run: deno run --allow-read --allow-write --allow-env --allow-net ./programs/aggregator.ts 2>&1 | tee -a aggregatorLogs.txt &
|
||||
9
.github/actions/local-contract-deploy-geth/action.yml
vendored
Normal file
9
.github/actions/local-contract-deploy-geth/action.yml
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
name: Local Contract Deploy
|
||||
description: Runs a Hardhat node & deploys contracts
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn start &
|
||||
17
.github/actions/local-contract-deploy-hardhat/action.yml
vendored
Normal file
17
.github/actions/local-contract-deploy-hardhat/action.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Local Contract Deploy
|
||||
description: Runs a Hardhat node & deploys contracts
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat node &
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat fundDeployer --network gethDev
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat run scripts/deploy_all.ts --network gethDev
|
||||
29
.github/workflows/aggregator-dockerhub.yml
vendored
Normal file
29
.github/workflows/aggregator-dockerhub.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
name: aggregator-dockerhub
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
- '.github/workflows/aggregator-dockerhub.yml'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./aggregator
|
||||
|
||||
env:
|
||||
DENO_VERSION: 1.x
|
||||
|
||||
jobs:
|
||||
push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: ${{ env.DENO_VERSION }}
|
||||
- run: git show HEAD
|
||||
- run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login --username blswalletghactions --password-stdin
|
||||
- run: ./programs/build.ts --image-name blswallet/aggregator --image-only --also-tag-latest --push
|
||||
2
.github/workflows/aggregator-proxy.yml
vendored
2
.github/workflows/aggregator-proxy.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
paths:
|
||||
- 'aggregator-proxy/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator-proxy/**'
|
||||
|
||||
|
||||
32
.github/workflows/aggregator.yml
vendored
32
.github/workflows/aggregator.yml
vendored
@@ -6,11 +6,19 @@ on:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
# Check for breaking changes from contracts
|
||||
- 'contracts/**'
|
||||
- '.github/workflows/aggregator.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
# Check for breaking changes from contracts
|
||||
- 'contracts/**'
|
||||
- '.github/workflows/aggregator.yml'
|
||||
branches-ignore:
|
||||
# Changes targeting this branch should be tested+fixed when being merged
|
||||
# into main
|
||||
- contract-updates
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -60,19 +68,13 @@ jobs:
|
||||
deno-version: ${{ env.DENO_VERSION }}
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
|
||||
# Setup contracts
|
||||
# Setup node & contracts
|
||||
- working-directory: ./contracts
|
||||
run: yarn hardhat node &
|
||||
run: yarn start &
|
||||
- working-directory: ./contracts
|
||||
run: yarn hardhat fundDeployer --network gethDev
|
||||
- working-directory: ./contracts
|
||||
run: yarn hardhat run scripts/deploy_all.ts --network gethDev
|
||||
run: ./scripts/wait-for-rpc.sh
|
||||
|
||||
- working-directory: ./
|
||||
run: docker-compose up -d postgres
|
||||
- run: cp .env.example .env
|
||||
- run: deno test --allow-net --allow-env --allow-read --unstable
|
||||
|
||||
# Cleanup
|
||||
- working-directory: ./
|
||||
run: docker-compose down
|
||||
- run: cp .env.local.example .env
|
||||
- run: deno test --allow-net --allow-env --allow-read
|
||||
- uses: mxschmitt/action-tmate@v3
|
||||
- run: sleep 3600
|
||||
|
||||
8
.github/workflows/clients.yml
vendored
8
.github/workflows/clients.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
paths:
|
||||
- 'contracts/clients/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'contracts/clients/**'
|
||||
|
||||
@@ -16,8 +14,12 @@ defaults:
|
||||
run:
|
||||
working-directory: ./contracts/clients
|
||||
|
||||
env:
|
||||
DENO_VERSION: 1.x
|
||||
|
||||
jobs:
|
||||
test:
|
||||
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
||||
13
.github/workflows/contracts.yml
vendored
13
.github/workflows/contracts.yml
vendored
@@ -8,8 +8,6 @@ on:
|
||||
- 'contracts/**'
|
||||
- '!contracts/clients/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'contracts/**'
|
||||
- '!contracts/clients/**'
|
||||
@@ -34,3 +32,14 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
- run: yarn test
|
||||
|
||||
# ensure gas measurement script runs
|
||||
test-gas-measurements:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
- uses: ./.github/actions/local-contract-deploy-hardhat
|
||||
|
||||
- run: yarn hardhat run ./scripts/measure_gas/run.ts --network gethDev
|
||||
|
||||
7
.github/workflows/extension.yml
vendored
7
.github/workflows/extension.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
paths:
|
||||
- 'extension/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'extension/**'
|
||||
|
||||
@@ -43,10 +41,7 @@ jobs:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
cache: yarn
|
||||
cache-dependency-path: extension/yarn.lock
|
||||
# Valid network config not needed to test build.
|
||||
- working-directory: ./contracts/networks
|
||||
run: echo "{}" > "local.json"
|
||||
- run: cp .env.example .env
|
||||
- run: cp config.example.json config.json
|
||||
- run: yarn install --frozen-lockfile
|
||||
# For now, just check that chrome builds
|
||||
- run: yarn build:chrome
|
||||
|
||||
69
.github/workflows/integration.yml
vendored
Normal file
69
.github/workflows/integration.yml
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
name: integration
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
# Check for breaking changes from contracts
|
||||
- 'contracts/**'
|
||||
- '.github/workflows/integration.yml'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
# Check for breaking changes from contracts
|
||||
- 'contracts/**'
|
||||
- '.github/workflows/integration.yml'
|
||||
branches-ignore:
|
||||
# Changes targeting this branch should be tested+fixed when being merged
|
||||
# into main
|
||||
- contract-updates
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./contracts/clients
|
||||
|
||||
env:
|
||||
DENO_VERSION: 1.x
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
- working-directory: ./contracts/clients
|
||||
run: yarn build
|
||||
|
||||
test-integration:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: ${{ env.DENO_VERSION }}
|
||||
|
||||
# - name: run geth node and deploy contracts
|
||||
- uses: ./.github/actions/local-contract-deploy-geth
|
||||
|
||||
- working-directory: ./contracts
|
||||
run: ./scripts/wait-for-contract-deploy.sh
|
||||
|
||||
# - name: run aggregator
|
||||
- uses: ./.github/actions/local-aggregator-deploy
|
||||
|
||||
# - name: integration tests
|
||||
- working-directory: ./contracts
|
||||
run: yarn test-integration
|
||||
|
||||
# - name: upload artifacts
|
||||
- uses: actions/upload-artifact@v3
|
||||
if: always()
|
||||
with:
|
||||
name: aggregator-logs
|
||||
path: ./aggregator/aggregatorLogs.txt
|
||||
retention-days: 5
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.data
|
||||
.DS_Store
|
||||
.idea
|
||||
@@ -1,6 +1,6 @@
|
||||
# Contribute to BLS Wallet
|
||||
|
||||
Thank for taking the time to contribute to BLS Wallet!
|
||||
Thanks for taking the time to contribute to BLS Wallet!
|
||||
|
||||
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
|
||||
|
||||
@@ -18,7 +18,7 @@ First search for an [existing issue](https://github.com/web3well/bls-wallet/issu
|
||||
|
||||
### Solve an issue
|
||||
|
||||
Search for a [existing issue](https://github.com/github/docs/issues) that is unassigned and interests you. If this is your first time contrbuting, you may want to choose a [good first issue](https://github.com/web3well/bls-wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||
Search for an [existing issue](https://github.com/github/docs/issues) that is unassigned and interests you. If this is your first time contributing, you may want to choose a [good first issue](https://github.com/web3well/bls-wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
|
||||
|
||||
## Make Changes
|
||||
|
||||
@@ -34,7 +34,7 @@ Search for a [existing issue](https://github.com/github/docs/issues) that is una
|
||||
|
||||
## Commit your update
|
||||
|
||||
Commit your changes over one or more commits. It is recommend your format your commit messages as follows:
|
||||
Commit your changes over one or more commits. It is recommended to format your commit messages as follows:
|
||||
|
||||
```
|
||||
A short summary of what you did
|
||||
@@ -47,13 +47,14 @@ A list or paragraph of more specific details
|
||||
Create a pull request (PR) from your fork's branch to `main`, filling in the descriptions template including [linking to the issue you are resolving](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). Feel free to open a draft PR while you are actively working.
|
||||
|
||||
Once ready, a BLS Wallet team member will review the PR.
|
||||
- When run, all Github Actions workflows should succeed.
|
||||
- All TODO/FIXME comments in code should be resolved, unless marked `merge-ok` with a description/issue link describing how they can be resolved in future work.
|
||||
- The author of a comment may mark it as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations) when they are satisified with a requested change or answer to a question. You are not required to resolve all comments as some may provide good historical information.
|
||||
|
||||
- When run, all Github Actions workflows should succeed.
|
||||
- All TODO/FIXME comments in code should be resolved, unless marked `merge-ok` with a description/issue link describing how they can be resolved in future work.
|
||||
- The author of a comment may mark it as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations) when they are satisfied with a requested change or answer to a question. You are not required to resolve all comments as some may provide good historical information.
|
||||
|
||||
## Your PR is merged!
|
||||
|
||||
Thanks for your hard work! Accept our heartfelt graditiude and revel in your masterful coding and/or documentational skills.
|
||||
Thanks for your hard work! Accept our heartfelt gratitude and revel in your masterful coding and/or documentational skills.
|
||||
|
||||
### Thanks
|
||||
|
||||
|
||||
27
README.md
27
README.md
@@ -1,16 +1,22 @@
|
||||

|
||||
|
||||
An Ethereum Layer 2 smart contract wallet that uses [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature) and aggregated transactions to reduce gas costs.
|
||||
## What is BLS Wallet?
|
||||
|
||||
You can watch a full end-to-end demo of the project [here](https://www.youtube.com/watch?v=MOQ3sCLP56g)
|
||||
A set of components to bring lower gas costs to EVM rollups via aggregated [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature). Our smart contract wallet supports recovery, atomic multi-action operations, sponsored transactions and user-controlled upgradability.
|
||||
|
||||
You can watch a full end-to-end demo of the project [here](https://www.youtube.com/watch?v=MOQ3sCLP56g).
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [See an overview of BLS Wallet & how the components work together](./docs/system_overview.md)
|
||||
- [Use BLS Wallet in a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md)
|
||||
- Setup the BLS Wallet components for:
|
||||
- [Local develeopment](./docs/local_development.md)
|
||||
- [Remote development](./docs/remote_development.md)
|
||||
- See an [overview](./docs/system_overview.md) of BLS Wallet & how the components work together.
|
||||
- Use BLS Wallet in [a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md).
|
||||
- Use BLS Wallet in [your L2 dApp](./docs/use_bls_wallet_dapp.md) for cheaper, multi action transactions.
|
||||
- Use BLS Wallet components and features with an [ethers.js provider and signer](./use_bls_provider.md)
|
||||
|
||||
### Setup your development environment
|
||||
|
||||
- [Local development](./docs/local_development.md)
|
||||
- [Remote development](./docs/remote_development.md)
|
||||
|
||||
## Components
|
||||
|
||||
@@ -34,6 +40,13 @@ npm package which provides easy to use constructs to interact with the contracts
|
||||
|
||||
Prototype browser extension used to manage BLS Wallets and sign transactions.
|
||||
|
||||
## Contract Deployments
|
||||
|
||||
See the [networks directory](./contracts/networks/) for a list of all contract deployment (network) manifests. Have an L2/rollup testnet you'd like BLS Wallet deployed on? [Open an issue](https://github.com/web3well/bls-wallet/issues/new) or [Deploy it yourself](./docs/remote_development.md)
|
||||
|
||||
- [Arbitrum Goerli](./contracts/networks/arbitrum-goerli.json)
|
||||
- [Optimism Goerli](./contracts/networks/optimism-goerli.json)
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
- [Work on an open issue](https://github.com/web3well/bls-wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
|
||||
@@ -20,20 +20,32 @@ import {
|
||||
// AggregatorProxyCallback,
|
||||
// ^ Alternatively, for manual control, import AggregatorProxyCallback to
|
||||
// just generate the req,res callback for use with http.createServer
|
||||
} from 'bls-wallet-aggregator-proxy';
|
||||
} from "bls-wallet-aggregator-proxy";
|
||||
|
||||
runAggregatorProxy(
|
||||
'https://arbitrum-testnet.blswallet.org',
|
||||
async bundle => {
|
||||
console.log('proxying bundle', JSON.stringify(bundle, null, 2));
|
||||
"https://arbitrum-goerli.blswallet.org",
|
||||
async (bundle) => {
|
||||
console.log("proxying bundle", JSON.stringify(bundle, null, 2));
|
||||
|
||||
// Return a different/augmented bundle to send to the upstream aggregator
|
||||
return bundle;
|
||||
},
|
||||
8080,
|
||||
'0.0.0.0',
|
||||
"0.0.0.0",
|
||||
() => {
|
||||
console.log('Proxying aggregator on port 8080');
|
||||
},
|
||||
console.log("Proxying aggregator on port 8080");
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
## Instant wallet example without dapp-sponsored transaction
|
||||
|
||||

|
||||
|
||||
## Instant wallet example with dapp-sponsored transaction
|
||||
|
||||

|
||||
|
||||
## Example dApp using a proxy aggregator
|
||||
|
||||
- https://github.com/JohnGuilding/single-pool-dex
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { runAggregatorProxy } from "../src";
|
||||
|
||||
runAggregatorProxy(
|
||||
'https://arbitrum-testnet.blswallet.org',
|
||||
'https://arbitrum-goerli.blswallet.org',
|
||||
async b => {
|
||||
console.log('proxying bundle', JSON.stringify(b, null, 2));
|
||||
return b;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"bls-wallet-clients": "^0.6.0",
|
||||
"bls-wallet-clients": "0.8.2-1fb4a55",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -2,22 +2,7 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@ethersproject/abi@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613"
|
||||
integrity sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w==
|
||||
dependencies:
|
||||
"@ethersproject/address" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/constants" "^5.5.0"
|
||||
"@ethersproject/hash" "^5.5.0"
|
||||
"@ethersproject/keccak256" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
|
||||
"@ethersproject/abi@5.6.1", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.0":
|
||||
"@ethersproject/abi@5.6.1", "@ethersproject/abi@^5.6.0":
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.1.tgz#f7de888edeb56b0a657b672bdd1b3a1135cd14f7"
|
||||
integrity sha512-0cqssYh6FXjlwKWBmLm3+zH2BNARoS5u/hxbz+LpQmcDB3w0W553h2btWui1/uZp2GBM/SI3KniTuMcYyHpA5w==
|
||||
@@ -32,20 +17,22 @@
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
"@ethersproject/strings" "^5.6.0"
|
||||
|
||||
"@ethersproject/abstract-provider@5.5.1":
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5"
|
||||
integrity sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg==
|
||||
"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449"
|
||||
integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==
|
||||
dependencies:
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/networks" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/transactions" "^5.5.0"
|
||||
"@ethersproject/web" "^5.5.0"
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/hash" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
|
||||
"@ethersproject/abstract-provider@5.6.0", "@ethersproject/abstract-provider@^5.5.0", "@ethersproject/abstract-provider@^5.6.0":
|
||||
"@ethersproject/abstract-provider@5.6.0", "@ethersproject/abstract-provider@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.0.tgz#0c4ac7054650dbd9c476cf5907f588bbb6ef3061"
|
||||
integrity sha512-oPMFlKLN+g+y7a79cLK3WiLcjWFnZQtXWgnLAbHZcN3s7L4v90UHpTOrLk+m3yr0gt+/h9STTM6zrr7PM8uoRw==
|
||||
@@ -58,18 +45,20 @@
|
||||
"@ethersproject/transactions" "^5.6.0"
|
||||
"@ethersproject/web" "^5.6.0"
|
||||
|
||||
"@ethersproject/abstract-signer@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d"
|
||||
integrity sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA==
|
||||
"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef"
|
||||
integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==
|
||||
dependencies:
|
||||
"@ethersproject/abstract-provider" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/networks" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/transactions" "^5.7.0"
|
||||
"@ethersproject/web" "^5.7.0"
|
||||
|
||||
"@ethersproject/abstract-signer@5.6.0", "@ethersproject/abstract-signer@^5.5.0", "@ethersproject/abstract-signer@^5.6.0":
|
||||
"@ethersproject/abstract-signer@5.6.0", "@ethersproject/abstract-signer@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.0.tgz#9cd7ae9211c2b123a3b29bf47aab17d4d016e3e7"
|
||||
integrity sha512-WOqnG0NJKtI8n0wWZPReHtaLkDByPL67tn4nBaDAhmVq8sjHTPbCdz4DRhVu/cfTOvfy9w3iq5QZ7BX7zw56BQ==
|
||||
@@ -80,18 +69,18 @@
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
|
||||
"@ethersproject/address@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f"
|
||||
integrity sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw==
|
||||
"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2"
|
||||
integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==
|
||||
dependencies:
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/keccak256" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/rlp" "^5.5.0"
|
||||
"@ethersproject/abstract-provider" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
|
||||
"@ethersproject/address@5.6.0", "@ethersproject/address@^5.5.0", "@ethersproject/address@^5.6.0":
|
||||
"@ethersproject/address@5.6.0", "@ethersproject/address@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.0.tgz#13c49836d73e7885fc148ad633afad729da25012"
|
||||
integrity sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ==
|
||||
@@ -102,29 +91,32 @@
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
"@ethersproject/rlp" "^5.6.0"
|
||||
|
||||
"@ethersproject/base64@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090"
|
||||
integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA==
|
||||
"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37"
|
||||
integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/rlp" "^5.7.0"
|
||||
|
||||
"@ethersproject/base64@5.6.0", "@ethersproject/base64@^5.5.0", "@ethersproject/base64@^5.6.0":
|
||||
"@ethersproject/base64@5.6.0", "@ethersproject/base64@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.0.tgz#a12c4da2a6fb86d88563216b0282308fc15907c9"
|
||||
integrity sha512-2Neq8wxJ9xHxCF9TUgmKeSh9BXJ6OAxWfeGWvbauPh8FuHEjamgHilllx8KkSd5ErxyHIX7Xv3Fkcud2kY9ezw==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.6.0"
|
||||
|
||||
"@ethersproject/basex@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.5.0.tgz#e40a53ae6d6b09ab4d977bd037010d4bed21b4d3"
|
||||
integrity sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ==
|
||||
"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c"
|
||||
integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
|
||||
"@ethersproject/basex@5.6.0", "@ethersproject/basex@^5.5.0", "@ethersproject/basex@^5.6.0":
|
||||
"@ethersproject/basex@5.6.0", "@ethersproject/basex@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.0.tgz#9ea7209bf0a1c3ddc2a90f180c3a7f0d7d2e8a69"
|
||||
integrity sha512-qN4T+hQd/Md32MoJpc69rOwLYRUXwjTlhHDIeUkUmiN/JyWkkLLMoG0TqvSQKNqZOMgN5stbUYN6ILC+eD7MEQ==
|
||||
@@ -132,16 +124,15 @@
|
||||
"@ethersproject/bytes" "^5.6.0"
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
|
||||
"@ethersproject/bignumber@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.5.0.tgz#875b143f04a216f4f8b96245bde942d42d279527"
|
||||
integrity sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg==
|
||||
"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b"
|
||||
integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
bn.js "^4.11.9"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
|
||||
"@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@^5.5.0", "@ethersproject/bignumber@^5.6.0":
|
||||
"@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.0.tgz#116c81b075c57fa765a8f3822648cf718a8a0e26"
|
||||
integrity sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA==
|
||||
@@ -150,49 +141,42 @@
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
bn.js "^4.11.9"
|
||||
|
||||
"@ethersproject/bytes@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c"
|
||||
integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==
|
||||
"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2"
|
||||
integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
bn.js "^5.2.1"
|
||||
|
||||
"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.5.0", "@ethersproject/bytes@^5.6.0":
|
||||
"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.6.0":
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7"
|
||||
integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/constants@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e"
|
||||
integrity sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ==
|
||||
"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d"
|
||||
integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==
|
||||
dependencies:
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
|
||||
"@ethersproject/constants@5.6.0", "@ethersproject/constants@^5.5.0", "@ethersproject/constants@^5.6.0":
|
||||
"@ethersproject/constants@5.6.0", "@ethersproject/constants@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.0.tgz#55e3eb0918584d3acc0688e9958b0cedef297088"
|
||||
integrity sha512-SrdaJx2bK0WQl23nSpV/b1aq293Lh0sUaZT/yYKPDKn4tlAbkH96SPJwIhwSwTsoQQZxuh1jnqsKwyymoiBdWA==
|
||||
dependencies:
|
||||
"@ethersproject/bignumber" "^5.6.0"
|
||||
|
||||
"@ethersproject/contracts@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.5.0.tgz#b735260d4bd61283a670a82d5275e2a38892c197"
|
||||
integrity sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg==
|
||||
"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e"
|
||||
integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==
|
||||
dependencies:
|
||||
"@ethersproject/abi" "^5.5.0"
|
||||
"@ethersproject/abstract-provider" "^5.5.0"
|
||||
"@ethersproject/abstract-signer" "^5.5.0"
|
||||
"@ethersproject/address" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/constants" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/transactions" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
|
||||
"@ethersproject/contracts@5.6.0":
|
||||
version "5.6.0"
|
||||
@@ -210,21 +194,23 @@
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
"@ethersproject/transactions" "^5.6.0"
|
||||
|
||||
"@ethersproject/hash@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9"
|
||||
integrity sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg==
|
||||
"@ethersproject/contracts@5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e"
|
||||
integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==
|
||||
dependencies:
|
||||
"@ethersproject/abstract-signer" "^5.5.0"
|
||||
"@ethersproject/address" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/keccak256" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
"@ethersproject/abi" "^5.7.0"
|
||||
"@ethersproject/abstract-provider" "^5.7.0"
|
||||
"@ethersproject/abstract-signer" "^5.7.0"
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/transactions" "^5.7.0"
|
||||
|
||||
"@ethersproject/hash@5.6.0", "@ethersproject/hash@^5.5.0", "@ethersproject/hash@^5.6.0":
|
||||
"@ethersproject/hash@5.6.0", "@ethersproject/hash@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.0.tgz#d24446a5263e02492f9808baa99b6e2b4c3429a2"
|
||||
integrity sha512-fFd+k9gtczqlr0/BruWLAu7UAOas1uRRJvOR84uDf4lNZ+bTkGl366qvniUZHKtlqxBRU65MkOobkmvmpHU+jA==
|
||||
@@ -238,25 +224,22 @@
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
"@ethersproject/strings" "^5.6.0"
|
||||
|
||||
"@ethersproject/hdnode@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.5.0.tgz#4a04e28f41c546f7c978528ea1575206a200ddf6"
|
||||
integrity sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q==
|
||||
"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7"
|
||||
integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==
|
||||
dependencies:
|
||||
"@ethersproject/abstract-signer" "^5.5.0"
|
||||
"@ethersproject/basex" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/pbkdf2" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/sha2" "^5.5.0"
|
||||
"@ethersproject/signing-key" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
"@ethersproject/transactions" "^5.5.0"
|
||||
"@ethersproject/wordlists" "^5.5.0"
|
||||
"@ethersproject/abstract-signer" "^5.7.0"
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/base64" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
|
||||
"@ethersproject/hdnode@5.6.0", "@ethersproject/hdnode@^5.5.0", "@ethersproject/hdnode@^5.6.0":
|
||||
"@ethersproject/hdnode@5.6.0", "@ethersproject/hdnode@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.6.0.tgz#9dcbe8d629bbbcf144f2cae476337fe92d320998"
|
||||
integrity sha512-61g3Jp3nwDqJcL/p4nugSyLrpl/+ChXIOtCEM8UDmWeB3JCAt5FoLdOMXQc3WWkc0oM2C0aAn6GFqqMcS/mHTw==
|
||||
@@ -274,26 +257,25 @@
|
||||
"@ethersproject/transactions" "^5.6.0"
|
||||
"@ethersproject/wordlists" "^5.6.0"
|
||||
|
||||
"@ethersproject/json-wallets@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz#dd522d4297e15bccc8e1427d247ec8376b60e325"
|
||||
integrity sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ==
|
||||
"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf"
|
||||
integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==
|
||||
dependencies:
|
||||
"@ethersproject/abstract-signer" "^5.5.0"
|
||||
"@ethersproject/address" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/hdnode" "^5.5.0"
|
||||
"@ethersproject/keccak256" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/pbkdf2" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/random" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
"@ethersproject/transactions" "^5.5.0"
|
||||
aes-js "3.0.0"
|
||||
scrypt-js "3.0.1"
|
||||
"@ethersproject/abstract-signer" "^5.7.0"
|
||||
"@ethersproject/basex" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/pbkdf2" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/sha2" "^5.7.0"
|
||||
"@ethersproject/signing-key" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
"@ethersproject/transactions" "^5.7.0"
|
||||
"@ethersproject/wordlists" "^5.7.0"
|
||||
|
||||
"@ethersproject/json-wallets@5.6.0", "@ethersproject/json-wallets@^5.5.0", "@ethersproject/json-wallets@^5.6.0":
|
||||
"@ethersproject/json-wallets@5.6.0", "@ethersproject/json-wallets@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.6.0.tgz#4c2fc27f17e36c583e7a252fb938bc46f98891e5"
|
||||
integrity sha512-fmh86jViB9r0ibWXTQipxpAGMiuxoqUf78oqJDlCAJXgnJF024hOOX7qVgqsjtbeoxmcLwpPsXNU0WEe/16qPQ==
|
||||
@@ -312,15 +294,26 @@
|
||||
aes-js "3.0.0"
|
||||
scrypt-js "3.0.1"
|
||||
|
||||
"@ethersproject/keccak256@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492"
|
||||
integrity sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg==
|
||||
"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360"
|
||||
integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
js-sha3 "0.8.0"
|
||||
"@ethersproject/abstract-signer" "^5.7.0"
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/hdnode" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/pbkdf2" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/random" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
"@ethersproject/transactions" "^5.7.0"
|
||||
aes-js "3.0.0"
|
||||
scrypt-js "3.0.1"
|
||||
|
||||
"@ethersproject/keccak256@5.6.0", "@ethersproject/keccak256@^5.5.0", "@ethersproject/keccak256@^5.6.0":
|
||||
"@ethersproject/keccak256@5.6.0", "@ethersproject/keccak256@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.0.tgz#fea4bb47dbf8f131c2e1774a1cecbfeb9d606459"
|
||||
integrity sha512-tk56BJ96mdj/ksi7HWZVWGjCq0WVl/QvfhFQNeL8fxhBlGoP+L80uDCiQcpJPd+2XxkivS3lwRm3E0CXTfol0w==
|
||||
@@ -328,39 +321,39 @@
|
||||
"@ethersproject/bytes" "^5.6.0"
|
||||
js-sha3 "0.8.0"
|
||||
|
||||
"@ethersproject/logger@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d"
|
||||
integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==
|
||||
"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a"
|
||||
integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
js-sha3 "0.8.0"
|
||||
|
||||
"@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.5.0", "@ethersproject/logger@^5.6.0":
|
||||
"@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a"
|
||||
integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg==
|
||||
|
||||
"@ethersproject/networks@5.5.2":
|
||||
version "5.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.2.tgz#784c8b1283cd2a931114ab428dae1bd00c07630b"
|
||||
integrity sha512-NEqPxbGBfy6O3x4ZTISb90SjEDkWYDUbEeIFhJly0F7sZjoQMnj5KYzMSkMkLKZ+1fGpx00EDpHQCy6PrDupkQ==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892"
|
||||
integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==
|
||||
|
||||
"@ethersproject/networks@5.6.2", "@ethersproject/networks@^5.5.0", "@ethersproject/networks@^5.6.0":
|
||||
"@ethersproject/networks@5.6.2", "@ethersproject/networks@^5.6.0":
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.2.tgz#2bacda62102c0b1fcee408315f2bed4f6fbdf336"
|
||||
integrity sha512-9uEzaJY7j5wpYGTojGp8U89mSsgQLc40PCMJLMCnFXTs7nhBveZ0t7dbqWUNrepWTszDbFkYD6WlL8DKx5huHA==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/pbkdf2@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz#e25032cdf02f31505d47afbf9c3e000d95c4a050"
|
||||
integrity sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg==
|
||||
"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0":
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6"
|
||||
integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/sha2" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
|
||||
"@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.5.0", "@ethersproject/pbkdf2@^5.6.0":
|
||||
"@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz#04fcc2d7c6bff88393f5b4237d906a192426685a"
|
||||
integrity sha512-Wu1AxTgJo3T3H6MIu/eejLFok9TYoSdgwRr5oGY1LTLfmGesDoSx05pemsbrPT2gG4cQME+baTSCp5sEo2erZQ==
|
||||
@@ -368,44 +361,27 @@
|
||||
"@ethersproject/bytes" "^5.6.0"
|
||||
"@ethersproject/sha2" "^5.6.0"
|
||||
|
||||
"@ethersproject/properties@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995"
|
||||
integrity sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA==
|
||||
"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102"
|
||||
integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/sha2" "^5.7.0"
|
||||
|
||||
"@ethersproject/properties@5.6.0", "@ethersproject/properties@^5.5.0", "@ethersproject/properties@^5.6.0":
|
||||
"@ethersproject/properties@5.6.0", "@ethersproject/properties@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.6.0.tgz#38904651713bc6bdd5bdd1b0a4287ecda920fa04"
|
||||
integrity sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg==
|
||||
dependencies:
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/providers@5.5.3":
|
||||
version "5.5.3"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.3.tgz#56c2b070542ac44eb5de2ed3cf6784acd60a3130"
|
||||
integrity sha512-ZHXxXXXWHuwCQKrgdpIkbzMNJMvs+9YWemanwp1fA7XZEv7QlilseysPvQe0D7Q7DlkJX/w/bGA1MdgK2TbGvA==
|
||||
"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30"
|
||||
integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==
|
||||
dependencies:
|
||||
"@ethersproject/abstract-provider" "^5.5.0"
|
||||
"@ethersproject/abstract-signer" "^5.5.0"
|
||||
"@ethersproject/address" "^5.5.0"
|
||||
"@ethersproject/basex" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/constants" "^5.5.0"
|
||||
"@ethersproject/hash" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/networks" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/random" "^5.5.0"
|
||||
"@ethersproject/rlp" "^5.5.0"
|
||||
"@ethersproject/sha2" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
"@ethersproject/transactions" "^5.5.0"
|
||||
"@ethersproject/web" "^5.5.0"
|
||||
bech32 "1.1.4"
|
||||
ws "7.4.6"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
|
||||
"@ethersproject/providers@5.6.4":
|
||||
version "5.6.4"
|
||||
@@ -432,15 +408,33 @@
|
||||
bech32 "1.1.4"
|
||||
ws "7.4.6"
|
||||
|
||||
"@ethersproject/random@5.5.1":
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.1.tgz#7cdf38ea93dc0b1ed1d8e480ccdaf3535c555415"
|
||||
integrity sha512-YaU2dQ7DuhL5Au7KbcQLHxcRHfgyNgvFV4sQOo0HrtW3Zkrc9ctWNz8wXQ4uCSfSDsqX2vcjhroxU5RQRV0nqA==
|
||||
"@ethersproject/providers@5.7.2":
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb"
|
||||
integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/abstract-provider" "^5.7.0"
|
||||
"@ethersproject/abstract-signer" "^5.7.0"
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/base64" "^5.7.0"
|
||||
"@ethersproject/basex" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/hash" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/networks" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/random" "^5.7.0"
|
||||
"@ethersproject/rlp" "^5.7.0"
|
||||
"@ethersproject/sha2" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
"@ethersproject/transactions" "^5.7.0"
|
||||
"@ethersproject/web" "^5.7.0"
|
||||
bech32 "1.1.4"
|
||||
ws "7.4.6"
|
||||
|
||||
"@ethersproject/random@5.6.0", "@ethersproject/random@^5.5.0", "@ethersproject/random@^5.6.0":
|
||||
"@ethersproject/random@5.6.0", "@ethersproject/random@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.0.tgz#1505d1ab6a250e0ee92f436850fa3314b2cb5ae6"
|
||||
integrity sha512-si0PLcLjq+NG/XHSZz90asNf+YfKEqJGVdxoEkSukzbnBgC8rydbgbUgBbBGLeHN4kAJwUFEKsu3sCXT93YMsw==
|
||||
@@ -448,15 +442,15 @@
|
||||
"@ethersproject/bytes" "^5.6.0"
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/rlp@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0"
|
||||
integrity sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA==
|
||||
"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c"
|
||||
integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
|
||||
"@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.5.0", "@ethersproject/rlp@^5.6.0":
|
||||
"@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.0.tgz#55a7be01c6f5e64d6e6e7edb6061aa120962a717"
|
||||
integrity sha512-dz9WR1xpcTL+9DtOT/aDO+YyxSSdO8YIS0jyZwHHSlAmnxA6cKU3TrTd4Xc/bHayctxTgGLYNuVVoiXE4tTq1g==
|
||||
@@ -464,16 +458,15 @@
|
||||
"@ethersproject/bytes" "^5.6.0"
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/sha2@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7"
|
||||
integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==
|
||||
"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304"
|
||||
integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
hash.js "1.1.7"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
|
||||
"@ethersproject/sha2@5.6.0", "@ethersproject/sha2@^5.5.0", "@ethersproject/sha2@^5.6.0":
|
||||
"@ethersproject/sha2@5.6.0", "@ethersproject/sha2@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.0.tgz#364c4c11cc753bda36f31f001628706ebadb64d9"
|
||||
integrity sha512-1tNWCPFLu1n3JM9t4/kytz35DkuF9MxqkGGEHNauEbaARdm2fafnOyw1s0tIQDPKF/7bkP1u3dbrmjpn5CelyA==
|
||||
@@ -482,19 +475,16 @@
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
hash.js "1.1.7"
|
||||
|
||||
"@ethersproject/signing-key@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0"
|
||||
integrity sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng==
|
||||
"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb"
|
||||
integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
bn.js "^4.11.9"
|
||||
elliptic "6.5.4"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
hash.js "1.1.7"
|
||||
|
||||
"@ethersproject/signing-key@5.6.0", "@ethersproject/signing-key@^5.5.0", "@ethersproject/signing-key@^5.6.0":
|
||||
"@ethersproject/signing-key@5.6.0", "@ethersproject/signing-key@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.0.tgz#4f02e3fb09e22b71e2e1d6dc4bcb5dafa69ce042"
|
||||
integrity sha512-S+njkhowmLeUu/r7ir8n78OUKx63kBdMCPssePS89So1TH4hZqnWFsThEd/GiXYp9qMxVrydf7KdM9MTGPFukA==
|
||||
@@ -506,17 +496,17 @@
|
||||
elliptic "6.5.4"
|
||||
hash.js "1.1.7"
|
||||
|
||||
"@ethersproject/solidity@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.5.0.tgz#2662eb3e5da471b85a20531e420054278362f93f"
|
||||
integrity sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw==
|
||||
"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3"
|
||||
integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==
|
||||
dependencies:
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/keccak256" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/sha2" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
bn.js "^5.2.1"
|
||||
elliptic "6.5.4"
|
||||
hash.js "1.1.7"
|
||||
|
||||
"@ethersproject/solidity@5.6.0":
|
||||
version "5.6.0"
|
||||
@@ -530,16 +520,19 @@
|
||||
"@ethersproject/sha2" "^5.6.0"
|
||||
"@ethersproject/strings" "^5.6.0"
|
||||
|
||||
"@ethersproject/strings@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549"
|
||||
integrity sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ==
|
||||
"@ethersproject/solidity@5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8"
|
||||
integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/constants" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/sha2" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
|
||||
"@ethersproject/strings@5.6.0", "@ethersproject/strings@^5.5.0", "@ethersproject/strings@^5.6.0":
|
||||
"@ethersproject/strings@5.6.0", "@ethersproject/strings@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.0.tgz#9891b26709153d996bf1303d39a7f4bc047878fd"
|
||||
integrity sha512-uv10vTtLTZqrJuqBZR862ZQjTIa724wGPWQqZrofaPI/kUsf53TBG0I0D+hQ1qyNtllbNzaW+PDPHHUI6/65Mg==
|
||||
@@ -548,22 +541,16 @@
|
||||
"@ethersproject/constants" "^5.6.0"
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/transactions@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908"
|
||||
integrity sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA==
|
||||
"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2"
|
||||
integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==
|
||||
dependencies:
|
||||
"@ethersproject/address" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/constants" "^5.5.0"
|
||||
"@ethersproject/keccak256" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/rlp" "^5.5.0"
|
||||
"@ethersproject/signing-key" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
|
||||
"@ethersproject/transactions@5.6.0", "@ethersproject/transactions@^5.5.0", "@ethersproject/transactions@^5.6.0":
|
||||
"@ethersproject/transactions@5.6.0", "@ethersproject/transactions@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.0.tgz#4b594d73a868ef6e1529a2f8f94a785e6791ae4e"
|
||||
integrity sha512-4HX+VOhNjXHZyGzER6E/LVI2i6lf9ejYeWD6l4g50AdmimyuStKc39kvKf1bXWQMg7QNVh+uC7dYwtaZ02IXeg==
|
||||
@@ -578,14 +565,20 @@
|
||||
"@ethersproject/rlp" "^5.6.0"
|
||||
"@ethersproject/signing-key" "^5.6.0"
|
||||
|
||||
"@ethersproject/units@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.5.0.tgz#104d02db5b5dc42cc672cc4587bafb87a95ee45e"
|
||||
integrity sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag==
|
||||
"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
|
||||
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
|
||||
dependencies:
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/constants" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/rlp" "^5.7.0"
|
||||
"@ethersproject/signing-key" "^5.7.0"
|
||||
|
||||
"@ethersproject/units@5.6.0":
|
||||
version "5.6.0"
|
||||
@@ -596,26 +589,14 @@
|
||||
"@ethersproject/constants" "^5.6.0"
|
||||
"@ethersproject/logger" "^5.6.0"
|
||||
|
||||
"@ethersproject/wallet@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.5.0.tgz#322a10527a440ece593980dca6182f17d54eae75"
|
||||
integrity sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q==
|
||||
"@ethersproject/units@5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1"
|
||||
integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==
|
||||
dependencies:
|
||||
"@ethersproject/abstract-provider" "^5.5.0"
|
||||
"@ethersproject/abstract-signer" "^5.5.0"
|
||||
"@ethersproject/address" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/hash" "^5.5.0"
|
||||
"@ethersproject/hdnode" "^5.5.0"
|
||||
"@ethersproject/json-wallets" "^5.5.0"
|
||||
"@ethersproject/keccak256" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/random" "^5.5.0"
|
||||
"@ethersproject/signing-key" "^5.5.0"
|
||||
"@ethersproject/transactions" "^5.5.0"
|
||||
"@ethersproject/wordlists" "^5.5.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
|
||||
"@ethersproject/wallet@5.6.0":
|
||||
version "5.6.0"
|
||||
@@ -638,18 +619,28 @@
|
||||
"@ethersproject/transactions" "^5.6.0"
|
||||
"@ethersproject/wordlists" "^5.6.0"
|
||||
|
||||
"@ethersproject/web@5.5.1":
|
||||
version "5.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316"
|
||||
integrity sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg==
|
||||
"@ethersproject/wallet@5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d"
|
||||
integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==
|
||||
dependencies:
|
||||
"@ethersproject/base64" "^5.5.0"
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
"@ethersproject/abstract-provider" "^5.7.0"
|
||||
"@ethersproject/abstract-signer" "^5.7.0"
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/hash" "^5.7.0"
|
||||
"@ethersproject/hdnode" "^5.7.0"
|
||||
"@ethersproject/json-wallets" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/random" "^5.7.0"
|
||||
"@ethersproject/signing-key" "^5.7.0"
|
||||
"@ethersproject/transactions" "^5.7.0"
|
||||
"@ethersproject/wordlists" "^5.7.0"
|
||||
|
||||
"@ethersproject/web@5.6.0", "@ethersproject/web@^5.5.0", "@ethersproject/web@^5.6.0":
|
||||
"@ethersproject/web@5.6.0", "@ethersproject/web@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.0.tgz#4bf8b3cbc17055027e1a5dd3c357e37474eaaeb8"
|
||||
integrity sha512-G/XHj0hV1FxI2teHRfCGvfBUHFmU+YOSbCxlAMqJklxSa7QMiHFQfAxvwY2PFqgvdkxEKwRNr/eCjfAPEm2Ctg==
|
||||
@@ -660,18 +651,18 @@
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
"@ethersproject/strings" "^5.6.0"
|
||||
|
||||
"@ethersproject/wordlists@5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.5.0.tgz#aac74963aa43e643638e5172353d931b347d584f"
|
||||
integrity sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q==
|
||||
"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0":
|
||||
version "5.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae"
|
||||
integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/hash" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
"@ethersproject/properties" "^5.5.0"
|
||||
"@ethersproject/strings" "^5.5.0"
|
||||
"@ethersproject/base64" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
|
||||
"@ethersproject/wordlists@5.6.0", "@ethersproject/wordlists@^5.5.0", "@ethersproject/wordlists@^5.6.0":
|
||||
"@ethersproject/wordlists@5.6.0", "@ethersproject/wordlists@^5.6.0":
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.6.0.tgz#79e62c5276e091d8575f6930ba01a29218ded032"
|
||||
integrity sha512-q0bxNBfIX3fUuAo9OmjlEYxP40IB8ABgb7HjEZCL5IKubzV3j30CWi2rqQbjTS2HfoyQbfINoKcTVWP4ejwR7Q==
|
||||
@@ -682,6 +673,17 @@
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
"@ethersproject/strings" "^5.6.0"
|
||||
|
||||
"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0":
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5"
|
||||
integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/hash" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/strings" "^5.7.0"
|
||||
|
||||
"@koa/cors@^3.3.0":
|
||||
version "3.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.3.0.tgz#b4c1c7ee303b7c968c8727f2a638a74675b50bb2"
|
||||
@@ -885,19 +887,25 @@ bech32@1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bls-wallet-clients@^0.6.0:
|
||||
version "0.6.0"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.6.0.tgz#9d9b1add69420bbaf807c1442151e487f4ee87a5"
|
||||
integrity sha512-6EivjMe2uRGIt6Aq5IampqlmsECavLqHGPm6Ki2l3+c+FnwfOQUzNelctVN/vRVxDbDpTX4iAfTIrYYpr1S/vw==
|
||||
bls-wallet-clients@0.8.2-1fb4a55:
|
||||
version "0.8.2-1fb4a55"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1fb4a55.tgz#bab40801ee1e60ffbc9c0bc924943c6f90605e7c"
|
||||
integrity sha512-2tlwOSUGzsOiam0G7GBmsN3W5cHjUwTmHR/DvGRH584zLkCC/8TFdAn2/laSF2baTYvegtIHwQNl1zX5DWivEQ==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "5.5.4"
|
||||
ethers "^5.7.2"
|
||||
node-fetch "2.6.7"
|
||||
|
||||
bn.js@^4.11.9:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
|
||||
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
|
||||
|
||||
bn.js@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
|
||||
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
|
||||
|
||||
brorand@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
|
||||
@@ -1036,42 +1044,6 @@ escape-html@^1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
|
||||
|
||||
ethers@5.5.4:
|
||||
version "5.5.4"
|
||||
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.4.tgz#e1155b73376a2f5da448e4a33351b57a885f4352"
|
||||
integrity sha512-N9IAXsF8iKhgHIC6pquzRgPBJEzc9auw3JoRkaKe+y4Wl/LFBtDDunNe7YmdomontECAcC5APaAgWZBiu1kirw==
|
||||
dependencies:
|
||||
"@ethersproject/abi" "5.5.0"
|
||||
"@ethersproject/abstract-provider" "5.5.1"
|
||||
"@ethersproject/abstract-signer" "5.5.0"
|
||||
"@ethersproject/address" "5.5.0"
|
||||
"@ethersproject/base64" "5.5.0"
|
||||
"@ethersproject/basex" "5.5.0"
|
||||
"@ethersproject/bignumber" "5.5.0"
|
||||
"@ethersproject/bytes" "5.5.0"
|
||||
"@ethersproject/constants" "5.5.0"
|
||||
"@ethersproject/contracts" "5.5.0"
|
||||
"@ethersproject/hash" "5.5.0"
|
||||
"@ethersproject/hdnode" "5.5.0"
|
||||
"@ethersproject/json-wallets" "5.5.0"
|
||||
"@ethersproject/keccak256" "5.5.0"
|
||||
"@ethersproject/logger" "5.5.0"
|
||||
"@ethersproject/networks" "5.5.2"
|
||||
"@ethersproject/pbkdf2" "5.5.0"
|
||||
"@ethersproject/properties" "5.5.0"
|
||||
"@ethersproject/providers" "5.5.3"
|
||||
"@ethersproject/random" "5.5.1"
|
||||
"@ethersproject/rlp" "5.5.0"
|
||||
"@ethersproject/sha2" "5.5.0"
|
||||
"@ethersproject/signing-key" "5.5.0"
|
||||
"@ethersproject/solidity" "5.5.0"
|
||||
"@ethersproject/strings" "5.5.0"
|
||||
"@ethersproject/transactions" "5.5.0"
|
||||
"@ethersproject/units" "5.5.0"
|
||||
"@ethersproject/wallet" "5.5.0"
|
||||
"@ethersproject/web" "5.5.1"
|
||||
"@ethersproject/wordlists" "5.5.0"
|
||||
|
||||
ethers@^5.5.3:
|
||||
version "5.6.4"
|
||||
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.4.tgz#23629e9a7d4bc5802dfb53d4da420d738744b53c"
|
||||
@@ -1108,6 +1080,42 @@ ethers@^5.5.3:
|
||||
"@ethersproject/web" "5.6.0"
|
||||
"@ethersproject/wordlists" "5.6.0"
|
||||
|
||||
ethers@^5.7.2:
|
||||
version "5.7.2"
|
||||
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e"
|
||||
integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==
|
||||
dependencies:
|
||||
"@ethersproject/abi" "5.7.0"
|
||||
"@ethersproject/abstract-provider" "5.7.0"
|
||||
"@ethersproject/abstract-signer" "5.7.0"
|
||||
"@ethersproject/address" "5.7.0"
|
||||
"@ethersproject/base64" "5.7.0"
|
||||
"@ethersproject/basex" "5.7.0"
|
||||
"@ethersproject/bignumber" "5.7.0"
|
||||
"@ethersproject/bytes" "5.7.0"
|
||||
"@ethersproject/constants" "5.7.0"
|
||||
"@ethersproject/contracts" "5.7.0"
|
||||
"@ethersproject/hash" "5.7.0"
|
||||
"@ethersproject/hdnode" "5.7.0"
|
||||
"@ethersproject/json-wallets" "5.7.0"
|
||||
"@ethersproject/keccak256" "5.7.0"
|
||||
"@ethersproject/logger" "5.7.0"
|
||||
"@ethersproject/networks" "5.7.1"
|
||||
"@ethersproject/pbkdf2" "5.7.0"
|
||||
"@ethersproject/properties" "5.7.0"
|
||||
"@ethersproject/providers" "5.7.2"
|
||||
"@ethersproject/random" "5.7.0"
|
||||
"@ethersproject/rlp" "5.7.0"
|
||||
"@ethersproject/sha2" "5.7.0"
|
||||
"@ethersproject/signing-key" "5.7.0"
|
||||
"@ethersproject/solidity" "5.7.0"
|
||||
"@ethersproject/strings" "5.7.0"
|
||||
"@ethersproject/transactions" "5.7.0"
|
||||
"@ethersproject/units" "5.7.0"
|
||||
"@ethersproject/wallet" "5.7.0"
|
||||
"@ethersproject/web" "5.7.1"
|
||||
"@ethersproject/wordlists" "5.7.0"
|
||||
|
||||
form-data@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
|
||||
@@ -1352,7 +1360,7 @@ negotiator@0.6.3:
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
node-fetch@2:
|
||||
node-fetch@2, node-fetch@2.6.7:
|
||||
version "2.6.7"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
|
||||
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==
|
||||
|
||||
@@ -1,32 +1,42 @@
|
||||
RPC_URL=http://localhost:8545
|
||||
RPC_URL=https://goerli-rollup.arbitrum.io/rpc
|
||||
RPC_POLLING_INTERVAL=4000
|
||||
|
||||
USE_TEST_NET=false
|
||||
|
||||
ORIGIN=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/local.json
|
||||
PRIVATE_KEY_AGG=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-goerli.json
|
||||
PRIVATE_KEY_AGG=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
|
||||
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
|
||||
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
|
||||
|
||||
PG_HOST=localhost
|
||||
PG_PORT=5432
|
||||
PG_USER=bls
|
||||
PG_PASSWORD=generate-a-strong-password
|
||||
PG_DB_NAME=bls_aggregator
|
||||
DB_PATH=aggregator.sqlite
|
||||
|
||||
BUNDLE_TABLE_NAME=bundles
|
||||
BUNDLE_QUERY_LIMIT=100
|
||||
MAX_ELIGIBILITY_DELAY=300
|
||||
|
||||
MAX_AGGREGATION_SIZE=12
|
||||
MAX_GAS_PER_BUNDLE=2000000
|
||||
MAX_AGGREGATION_DELAY_MILLIS=5000
|
||||
MAX_UNCONFIRMED_AGGREGATIONS=3
|
||||
|
||||
LOG_QUERIES=false
|
||||
TEST_LOGGING=false
|
||||
|
||||
REQUIRE_FEES=true
|
||||
BREAKEVEN_OPERATION_COUNT=4.5
|
||||
ALLOW_LOSSES=true
|
||||
FEE_TYPE=ether
|
||||
FEE_PER_GAS=0
|
||||
FEE_PER_BYTE=0
|
||||
|
||||
# Set this to false in production to avoid an unexpected transaction on startup.
|
||||
# Use ./programs/createInternalBlsWallet.ts beforehand instead.
|
||||
AUTO_CREATE_INTERNAL_BLS_WALLET=true
|
||||
|
||||
# Arbitrum doesn't seem to use/need priority fees
|
||||
PRIORITY_FEE_PER_GAS=0
|
||||
|
||||
# Arbitrum doesn't change its base fee much, in fact it's usually locked at
|
||||
# 0.1gwei. They use changes in gasLimit to account for L1 base fee changes.
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=2
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
|
||||
40
aggregator/.env.local.example
Normal file
40
aggregator/.env.local.example
Normal file
@@ -0,0 +1,40 @@
|
||||
RPC_URL=http://localhost:8545
|
||||
RPC_POLLING_INTERVAL=500
|
||||
|
||||
USE_TEST_NET=false
|
||||
|
||||
ORIGIN=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/local.json
|
||||
PRIVATE_KEY_AGG=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
|
||||
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
|
||||
|
||||
DB_PATH=aggregator.sqlite
|
||||
|
||||
BUNDLE_QUERY_LIMIT=100
|
||||
MAX_ELIGIBILITY_DELAY=300
|
||||
|
||||
MAX_GAS_PER_BUNDLE=2000000
|
||||
MAX_AGGREGATION_DELAY_MILLIS=5000
|
||||
MAX_UNCONFIRMED_AGGREGATIONS=3
|
||||
|
||||
LOG_QUERIES=false
|
||||
TEST_LOGGING=false
|
||||
|
||||
REQUIRE_FEES=true
|
||||
BREAKEVEN_OPERATION_COUNT=2.5
|
||||
ALLOW_LOSSES=true
|
||||
FEE_TYPE=ether
|
||||
|
||||
# Set this to false in production to avoid an unexpected transaction on startup.
|
||||
# Use ./programs/createInternalBlsWallet.ts beforehand instead.
|
||||
AUTO_CREATE_INTERNAL_BLS_WALLET=true
|
||||
|
||||
# 0.5 gwei
|
||||
PRIORITY_FEE_PER_GAS=500000000
|
||||
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
37
aggregator/.env.test
Normal file
37
aggregator/.env.test
Normal file
@@ -0,0 +1,37 @@
|
||||
RPC_URL=http://localhost:8545
|
||||
RPC_POLLING_INTERVAL=500
|
||||
|
||||
USE_TEST_NET=false
|
||||
|
||||
ORIGIN=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/local.json
|
||||
PRIVATE_KEY_AGG=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
|
||||
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
|
||||
|
||||
DB_PATH=aggregator.sqlite
|
||||
|
||||
BUNDLE_QUERY_LIMIT=100
|
||||
MAX_ELIGIBILITY_DELAY=300
|
||||
|
||||
MAX_GAS_PER_BUNDLE=2000000
|
||||
MAX_AGGREGATION_DELAY_MILLIS=5000
|
||||
MAX_UNCONFIRMED_AGGREGATIONS=3
|
||||
|
||||
LOG_QUERIES=true
|
||||
TEST_LOGGING=true
|
||||
|
||||
REQUIRE_FEES=true
|
||||
BREAKEVEN_OPERATION_COUNT=2.5
|
||||
ALLOW_LOSSES=true
|
||||
FEE_TYPE=ether
|
||||
|
||||
AUTO_CREATE_INTERNAL_BLS_WALLET=true
|
||||
|
||||
PRIORITY_FEE_PER_GAS=500000000
|
||||
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
4
aggregator/.gitignore
vendored
4
aggregator/.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
.env*
|
||||
!.env.example
|
||||
!.env*.example
|
||||
!.env*.test
|
||||
cov_profile*
|
||||
/build
|
||||
/aggregator.sqlite
|
||||
|
||||
1
aggregator/.vscode/launch.json
vendored
1
aggregator/.vscode/launch.json
vendored
@@ -13,7 +13,6 @@
|
||||
"runtimeExecutable": "deno",
|
||||
"runtimeArgs": [
|
||||
"run",
|
||||
"--unstable",
|
||||
"--inspect",
|
||||
"--allow-all"
|
||||
],
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
FROM denoland/deno:1.20.6
|
||||
FROM denoland/deno:1.30.1
|
||||
|
||||
ADD build /app
|
||||
WORKDIR /app
|
||||
|
||||
RUN deno cache --unstable ts/programs/aggregator.ts
|
||||
RUN deno cache ts/programs/aggregator.ts
|
||||
|
||||
ENV IS_DOCKER="true"
|
||||
|
||||
CMD [ \
|
||||
"deno", \
|
||||
"run", \
|
||||
"--unstable", \
|
||||
"-A", \
|
||||
"ts/programs/aggregator.ts" \
|
||||
]
|
||||
|
||||
@@ -6,6 +6,63 @@ Accepts transaction bundles (including bundles that contain a single
|
||||
transaction) and submits aggregations of these bundles to the configured
|
||||
Verification Gateway.
|
||||
|
||||
## Docker Usage
|
||||
|
||||
Docker images of the aggregator are
|
||||
[available on DockerHub](https://hub.docker.com/r/blswallet/aggregator).
|
||||
|
||||
If you're targeting a network that
|
||||
[already has a deployment of the BLSWallet contracts](../contracts/networks),
|
||||
you can use these images standalone (without this repository) as follows:
|
||||
|
||||
```sh
|
||||
mkdir aggregator
|
||||
cd aggregator
|
||||
|
||||
curl https://raw.githubusercontent.com/web3well/bls-wallet/main/aggregator/.env.example >.env
|
||||
|
||||
# Replace CHOSEN_NETWORK below
|
||||
curl https://raw.githubusercontent.com/web3well/bls-wallet/main/contracts/networks/CHOSEN_NETWORK.json >networkConfig.json
|
||||
```
|
||||
|
||||
In `.env`:
|
||||
|
||||
- Change `RPC_URL`
|
||||
- (If using `localhost`, you probably want `host.docker.internal`)
|
||||
- Change `PRIVATE_KEY_AGG`
|
||||
- Ignore `NETWORK_CONFIG_PATH` (it's not used inside docker)
|
||||
- See [Configuration](#configuration) for more detail and other options
|
||||
|
||||
If you're running in production, you might want to set
|
||||
`AUTO_CREATE_INTERNAL_BLS_WALLET` to `false`. The internal BLS wallet is needed
|
||||
for user fee estimation. Creating it is a one-time setup that will use
|
||||
`PRIVATE_KEY_AGG` to pay for gas. You can create it explicitly like this:
|
||||
|
||||
```sh
|
||||
docker run \
|
||||
--rm \
|
||||
-it \
|
||||
--mount type=bind,source="$PWD/.env",target=/app/.env \
|
||||
--mount type=bind,source="$PWD/networkConfig.json",target=/app/networkConfig.json \
|
||||
blswallet/aggregator \
|
||||
./ts/programs/createInternalBlsWallet.ts
|
||||
```
|
||||
|
||||
Finally, start the aggregator:
|
||||
|
||||
```sh
|
||||
docker run \
|
||||
--name choose-container-name \ # Optional
|
||||
-d \ # Optional
|
||||
-p3000:3000 \ # If you chose a different PORT in .env, change it here too
|
||||
--restart=unless-stopped \ # Optional
|
||||
--mount type=bind,source="$PWD/.env",target=/app/.env \
|
||||
--mount type=bind,source="$PWD/networkConfig.json",target=/app/networkConfig.json \
|
||||
blswallet/aggregator # Tags of the form :git-$VERSION are also available
|
||||
```
|
||||
|
||||
(You may need to remove the comments before pasting into your terminal.)
|
||||
|
||||
## Installation
|
||||
|
||||
Install [Deno](deno.land)
|
||||
@@ -23,74 +80,41 @@ you might have:
|
||||
|
||||
```
|
||||
.env.local
|
||||
.env.optimistic-kovan
|
||||
.env.arbitrum-goerli
|
||||
.env.optimism-goerli
|
||||
```
|
||||
|
||||
If you don't have a `.env`, you will need to append `--env <name>` to all
|
||||
commands.
|
||||
|
||||
### PostgreSQL
|
||||
#### Environment Variables
|
||||
|
||||
#### With docker-compose
|
||||
|
||||
```sh
|
||||
cd .. # root of repo
|
||||
docker-compose up -d postgres
|
||||
```
|
||||
|
||||
#### Local Install
|
||||
|
||||
Install, e.g.:
|
||||
|
||||
```sh
|
||||
sudo apt update
|
||||
sudo apt install postgresql postgresql-contrib
|
||||
```
|
||||
|
||||
Create a user called `bls`:
|
||||
|
||||
```
|
||||
$ sudo -u postgres createuser --interactive
|
||||
Enter name of role to add: bls
|
||||
Shall the new role be a superuser? (y/n) n
|
||||
Shall the new role be allowed to create databases? (y/n) n
|
||||
Shall the new role be allowed to create more new roles? (y/n) n
|
||||
```
|
||||
|
||||
Set the user's password:
|
||||
|
||||
```
|
||||
$ sudo -u postgres psql
|
||||
psql (12.6 (Ubuntu 12.6-0ubuntu0.20.04.1))
|
||||
Type "help" for help.
|
||||
|
||||
postgres=# ALTER USER bls WITH PASSWORD 'generate-a-strong-password';
|
||||
```
|
||||
|
||||
Create a table called `bls_aggregator`:
|
||||
|
||||
```sh
|
||||
sudo -u postgres createdb bls_aggregator
|
||||
```
|
||||
|
||||
On Ubuntu (and probably elsewhere), postgres is configured to offer SSL
|
||||
connections but with an invalid certificate. However, the deno driver for
|
||||
postgres doesn't support this.
|
||||
|
||||
There are two options here:
|
||||
|
||||
1. Set up SSL with a valid certificate
|
||||
([guide](https://www.postgresql.org/docs/current/ssl-tcp.html)).
|
||||
2. Turn off SSL in postgres (only for development or if you can ensure the
|
||||
connection isn't vulnerable to attack).
|
||||
1. View the config location with
|
||||
`sudo -u postgres psql -c 'SHOW config_file'`.
|
||||
2. Turn off ssl in that config.
|
||||
```diff
|
||||
-ssl = on
|
||||
+ssl = off
|
||||
```
|
||||
3. Restart postgres `sudo systemctl restart postgresql`.
|
||||
| Name | Example Value | Description |
|
||||
| ---------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
|
||||
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
|
||||
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
|
||||
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
|
||||
| PORT | 3000 | The port to bind the aggregator to |
|
||||
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
|
||||
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregator’s onchain address |
|
||||
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
|
||||
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
|
||||
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
|
||||
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
|
||||
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
|
||||
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
|
||||
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
|
||||
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
|
||||
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
|
||||
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
|
||||
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
|
||||
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
|
||||
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
|
||||
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
|
||||
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
|
||||
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
|
||||
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
|
||||
|
||||
## Running
|
||||
|
||||
@@ -102,6 +126,20 @@ Can be run locally or hosted.
|
||||
# ./programs/aggregator.ts --env <name>
|
||||
```
|
||||
|
||||
**Note**: It's also possible to run the aggregator directly from github:
|
||||
|
||||
```sh
|
||||
deno run \
|
||||
--allow-net \
|
||||
--allow-env \
|
||||
--allow-read=. \
|
||||
--allow-write=. \
|
||||
https://raw.githubusercontent.com/web3well/bls-wallet/main/aggregator/programs/aggregator.ts
|
||||
```
|
||||
|
||||
(This can be done without a clone of the repository, but you'll still need to
|
||||
set up `.env` and your network config.)
|
||||
|
||||
## Testing
|
||||
|
||||
- launch optimism
|
||||
@@ -110,6 +148,85 @@ Can be run locally or hosted.
|
||||
|
||||
NB each test must use unique address(es). (+ init code)
|
||||
|
||||
## Fees
|
||||
|
||||
### User Guide
|
||||
|
||||
User bundles must pay fees to compensate the aggregator (except in testing
|
||||
situations where the aggregator may be configured to accept bundles which don't
|
||||
pay fees (see `REQUIRE_FEES`)). The aggregator simply detects fees have been
|
||||
paid by observing the effect of a user bundle on its balance. This allows
|
||||
bundles to pay the aggregator using any mechanism of their choosing, and is why
|
||||
bundles do not have fields for paying fees explicitly.
|
||||
|
||||
The simplest way to do this is to include an extra action to pay `tx.origin`.
|
||||
|
||||
Use the `POST /estimateFee` API to determine the fee required for a bundle. The
|
||||
body of this request is the bundle. Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"feeType": "(See FEE_TYPE enviroment variable)",
|
||||
"feeDetected": "(The fee that has been detected for the provided bundle)",
|
||||
"feeRequired": "(Required fee)",
|
||||
"successes": [
|
||||
/* Array of bools indicating success of each action */
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Note that if you want to pay the aggregator using an additional action, you
|
||||
should include this additional action with a payment of zero when estimating,
|
||||
otherwise the additional action will increase the fee that needs to be paid. You
|
||||
can also use the [aggregator-proxy](../aggregator-proxy/) package as a proxy in
|
||||
place of an aggregator. This is useful to run more advanced logic such as
|
||||
inspecting bundles and potentially paying for them, before the proxy aggregator
|
||||
then sends the bundles to an underlying aggregator.
|
||||
|
||||
Also, `feeRequired` is the absolute minimum necessary fee to process the bundle
|
||||
at the time of estimation, so paying extra is advisable to increase the chance
|
||||
that the fee is sufficient during submission.
|
||||
|
||||
In the case of a malicious aggregator, or if the chosen aggregator service goes
|
||||
down, an end user can always execute actions themselves, by submitting a bundle
|
||||
on chain via `VerificationGatewaty.processBundle`.
|
||||
|
||||
### Technical Detail
|
||||
|
||||
The fees required by the aggregator are designed to prevent it from losing
|
||||
money. There are two main ways that losses can still happen:
|
||||
|
||||
1. Bundles that don't simulate accurately
|
||||
2. Bundles that make losses are allowed in config (`ALLOW_LOSSES`)
|
||||
|
||||
When calculating the required fee, the aggregator needs to account for two
|
||||
things:
|
||||
|
||||
1. The marginal cost of including the user bundle
|
||||
2. A contribution to the overhead of submitting the aggregate bundle
|
||||
|
||||
Remember that the whole point of aggregation is to save on fees using a single
|
||||
aggregate signature. This means that measuring the fee required to process the
|
||||
user bundle in isolation won't reflect that saving.
|
||||
|
||||
Instead, we measure the overhead using hypothetical operations that contain zero
|
||||
actions. We make a bundle with one of these, and another with two of these, and
|
||||
extrapolate backwards to a bundle containing zero operations (see
|
||||
`measureBundleOverheadGas`).
|
||||
|
||||
We can then subtract that overhead from the user's bundle to obtain its marginal
|
||||
cost.
|
||||
|
||||
The user's share of the overhead is then added by multiplying it by
|
||||
`operationCount / BREAKEVEN_OPERATION_COUNT`. User bundles usually have an
|
||||
`operationCount` of 1, so if `BREAKEVEN_OPERATION_COUNT` is 4.5, then the bundle
|
||||
will be required to pay 22% of the overhead.
|
||||
|
||||
From the aggregator's perspective, aggregate bundles with fewer operations than
|
||||
`BREAKEVEN_OPERATION_COUNT` should make a loss, and larger bundles should make a
|
||||
profit. If `ALLOW_LOSSES` is `false`, bundles which are predicted to make a loss
|
||||
will not be submitted.
|
||||
|
||||
## Development
|
||||
|
||||
### Environment
|
||||
@@ -133,7 +250,7 @@ Tests are defined in `test`. Running them directly is a bit verbose because of
|
||||
the deno flags you need:
|
||||
|
||||
```sh
|
||||
deno test --allow-net --allow-env --allow-read --unstable
|
||||
deno test --allow-net --allow-env --allow-read
|
||||
```
|
||||
|
||||
Instead, `./programs/premerge.ts` may be more useful for you. It'll make sure
|
||||
@@ -165,15 +282,22 @@ TS2300 [ERROR]: Duplicate identifier 'TypedArray'.
|
||||
You need to reload modules (`-r`):
|
||||
|
||||
```sh
|
||||
deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregator.ts
|
||||
deno run -r --allow-net --allow-env --allow-read ./programs/aggregator.ts
|
||||
```
|
||||
|
||||
#### Transaction reverted: function call to a non-contract account
|
||||
|
||||
- Is `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s `COST_ESTIMATOR_ADDRESS` set to the right precompile cost estimator's contract address?
|
||||
- Is `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s
|
||||
`COST_ESTIMATOR_ADDRESS` set to the right precompile cost estimator's contract
|
||||
address?
|
||||
- Are the BLS Wallet contracts deployed on the correct network?
|
||||
- Is `NETWORK_CONFIG_PATH` in `.env` set to the right config?
|
||||
|
||||
#### Deno version
|
||||
|
||||
Make sure your Deno version is
|
||||
[up to date.](https://deno.land/manual/getting_started/installation#updating)
|
||||
|
||||
### Notable Components
|
||||
|
||||
- **src/chain**: Should contain all of the contract interactions, exposing more
|
||||
@@ -190,10 +314,9 @@ deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregato
|
||||
- **`BundleService`**: Keeps track of all stored transactions, as well as
|
||||
accepting (or rejecting) them and submitting aggregated bundles to
|
||||
`EthereumService`.
|
||||
- **`BundleTable`**: Abstraction layer over postgres bundle tables, exposing
|
||||
typed functions instead of queries. Handles conversions to and from the field
|
||||
types supported by postgres so that other code can has a uniform js-friendly
|
||||
interface
|
||||
- **`BundleTable`**: Abstraction layer over sqlite bundle tables, exposing typed
|
||||
functions instead of queries. Handles conversions to and from the field types
|
||||
supported by sqlite so that other code can has a uniform js-friendly interface
|
||||
([`TransactionData`](https://github.com/jzaki/bls-wallet-signer/blob/673e2ae/src/types.ts#L12)).
|
||||
- **`Client`**: Provides an abstraction over the external HTTP interface so that
|
||||
programs talking to the aggregator can do so via regular js functions with
|
||||
@@ -208,16 +331,9 @@ deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregato
|
||||
## Hosting Guide
|
||||
|
||||
1. Configure your server to allow TCP on ports 80 and 443
|
||||
2. Follow the [Installation](#Installation) instructions
|
||||
3. Install docker and nginx:
|
||||
2. Install docker and nginx:
|
||||
`sudo apt update && sudo apt install docker.io nginx`
|
||||
|
||||
4. Run `./programs/build.ts`
|
||||
|
||||
- If you're using a named environment, add `--env <name>`
|
||||
- If `docker` requires `sudo`, add `--sudo-docker`
|
||||
|
||||
5. Configure log rotation in docker by setting `/etc/docker/daemon.json` to
|
||||
3. Configure log rotation in docker by setting `/etc/docker/daemon.json` to
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -231,19 +347,9 @@ deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregato
|
||||
|
||||
and restart docker `sudo systemctl restart docker`
|
||||
|
||||
6. Load the docker image: `sudo docker load <docker-image.tar.gz`
|
||||
7. Run the aggregator:
|
||||
|
||||
```sh
|
||||
sudo docker run \
|
||||
--name aggregator \
|
||||
-d \
|
||||
--net=host \
|
||||
--restart=unless-stopped \
|
||||
aggregator:latest
|
||||
```
|
||||
|
||||
8. Create `/etc/nginx/sites-available/aggregator`
|
||||
4. Follow the [Docker Usage](#docker-usage) instructions (just use port 3000,
|
||||
external requests are handled by nginx)
|
||||
5. Create `/etc/nginx/sites-available/aggregator`
|
||||
|
||||
```nginx
|
||||
server {
|
||||
@@ -266,7 +372,7 @@ This allows you to add some static content at `/home/aggregator/static-content`.
|
||||
Adding static content is optional; requests that don't match static content will
|
||||
be passed to the aggregator.
|
||||
|
||||
9. Create a symlink in sites-enabled
|
||||
6. Create a symlink in sites-enabled
|
||||
|
||||
```sh
|
||||
ln -s /etc/nginx/sites-available/aggregator /etc/nginx/sites-enabled/aggregator
|
||||
@@ -274,5 +380,5 @@ ln -s /etc/nginx/sites-available/aggregator /etc/nginx/sites-enabled/aggregator
|
||||
|
||||
Reload nginx for config to take effect: `sudo nginx -s reload`
|
||||
|
||||
10. Set up https for your domain by following the instructions at
|
||||
https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.
|
||||
7. Set up https for your domain by following the instructions at
|
||||
https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.
|
||||
|
||||
@@ -27,16 +27,19 @@ export {
|
||||
Contract,
|
||||
ethers,
|
||||
Wallet,
|
||||
} from "https://esm.sh/ethers@5.5.4";
|
||||
} from "https://esm.sh/ethers@5.7.2";
|
||||
|
||||
import { ethers } from "https://esm.sh/ethers@5.5.4";
|
||||
import { ethers } from "https://esm.sh/ethers@5.7.2";
|
||||
export type {
|
||||
BaseContract,
|
||||
BigNumberish,
|
||||
BytesLike,
|
||||
} from "https://esm.sh/ethers@5.5.4";
|
||||
} from "https://esm.sh/ethers@5.7.2";
|
||||
export const keccak256 = ethers.utils.keccak256;
|
||||
|
||||
// Adding more accurate type information here (ethers uses Array<any>)
|
||||
export const shuffled: <T>(array: T[]) => T[] = ethers.utils.shuffled;
|
||||
|
||||
export type {
|
||||
AggregatorUtilities,
|
||||
BlsWalletSigner,
|
||||
@@ -46,39 +49,27 @@ export type {
|
||||
MockERC20,
|
||||
NetworkConfig,
|
||||
Operation,
|
||||
OperationResultError,
|
||||
PublicKey,
|
||||
Signature,
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.6.0";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
BlsWalletWrapper,
|
||||
decodeError,
|
||||
ERC20__factory,
|
||||
getConfig,
|
||||
MockERC20__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.6.0";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
|
||||
|
||||
// Workaround for esbuild's export-star bug
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.6.0";
|
||||
const {
|
||||
bundleFromDto,
|
||||
bundleToDto,
|
||||
initBlsWalletSigner,
|
||||
} = blsWalletClients;
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
|
||||
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
|
||||
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
|
||||
|
||||
// Database dependencies
|
||||
export {
|
||||
Constraint,
|
||||
CreateTableMode,
|
||||
DataType,
|
||||
OrderByType,
|
||||
QueryClient,
|
||||
QueryTable,
|
||||
unsketchify,
|
||||
} from "https://deno.land/x/postquery@v0.1.1/mod.ts";
|
||||
|
||||
export type { TableOptions } from "https://deno.land/x/postquery@v0.1.1/mod.ts";
|
||||
export * as sqlite from "https://deno.land/x/sqlite@v3.7.0/mod.ts";
|
||||
export { Semaphore } from "https://deno.land/x/semaphore@v1.1.2/mod.ts";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient, BigNumber, Bundle } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
|
||||
import * as env from "../src/env.ts";
|
||||
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
|
||||
const [wallet] = await TestBlsWallets(
|
||||
const wallet = await TestBlsWallet(
|
||||
new ethers.providers.JsonRpcProvider(env.RPC_URL),
|
||||
1,
|
||||
);
|
||||
|
||||
console.log({
|
||||
privateKey: wallet.privateKey,
|
||||
privateKey: wallet.blsWalletSigner.privateKey,
|
||||
address: wallet.walletContract.address,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import {
|
||||
AggregatorClient,
|
||||
|
||||
33
aggregator/manualTests/estimateTransferFee.ts
Executable file
33
aggregator/manualTests/estimateTransferFee.ts
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient, ethers } from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
|
||||
import * as env from "../test/env.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 1,
|
||||
contractAddress: adminWallet.address,
|
||||
encodedFunction: "0x",
|
||||
}],
|
||||
});
|
||||
|
||||
const feeEstimation = await client.estimateFee(bundle);
|
||||
|
||||
console.log({ feeEstimation });
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
|
||||
|
||||
@@ -2,28 +2,23 @@ import { BlsWalletWrapper, ethers } from "../../deps.ts";
|
||||
|
||||
import * as env from "../../test/env.ts";
|
||||
import AdminWallet from "../../src/chain/AdminWallet.ts";
|
||||
import Range from "../../src/helpers/Range.ts";
|
||||
import Rng from "../../src/helpers/Rng.ts";
|
||||
import getNetworkConfig from "../../src/helpers/getNetworkConfig.ts";
|
||||
|
||||
export default async function TestBlsWallets(
|
||||
export default async function TestBlsWallet(
|
||||
provider: ethers.providers.Provider,
|
||||
count: number,
|
||||
index?: number,
|
||||
) {
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const parent = AdminWallet(provider);
|
||||
const rng = Rng.root.seed(env.PRIVATE_KEY_ADMIN, env.TEST_BLS_WALLETS_SECRET);
|
||||
|
||||
const wallets = await Promise.all(
|
||||
Range(count).map(async (i) => {
|
||||
const secret = rng.seed(`${i}`).address();
|
||||
return await BlsWalletWrapper.connect(
|
||||
secret,
|
||||
addresses.verificationGateway,
|
||||
parent.provider,
|
||||
);
|
||||
}),
|
||||
const secret = rng.seed(`${index}`).address();
|
||||
|
||||
return await BlsWalletWrapper.connect(
|
||||
secret,
|
||||
addresses.verificationGateway,
|
||||
parent.provider,
|
||||
);
|
||||
return wallets;
|
||||
}
|
||||
@@ -1,15 +1,23 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
BigNumber,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
|
||||
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
|
||||
const [walletIndexStr = "0"] = Deno.args;
|
||||
const walletIndex = Number(walletIndexStr);
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
@@ -17,26 +25,77 @@ const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const [wallet] = await TestBlsWallets(provider, 1);
|
||||
const wallet = await TestBlsWallet(provider, walletIndex);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
console.log("Funding wallet");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const startBalance = await testErc20.balanceOf(wallet.address);
|
||||
|
||||
const mintAction: ActionData = {
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.address,
|
||||
encodedFunction: testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
};
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
const feeEstimation = await client.estimateFee(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
console.log({ feeEstimation });
|
||||
|
||||
assert(feeEstimation.feeType === "ether");
|
||||
|
||||
const feeRequired = BigNumber.from(feeEstimation.feeRequired);
|
||||
|
||||
// Add 10% safety margin
|
||||
const fee = feeRequired.add(feeRequired.div(10));
|
||||
|
||||
const balance = await provider.getBalance(wallet.address);
|
||||
|
||||
// Ensure wallet can pay the fee
|
||||
if (balance.lt(fee)) {
|
||||
console.log("Funding wallet");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: fee.sub(balance),
|
||||
})).wait();
|
||||
}
|
||||
|
||||
const feeAction: ActionData = {
|
||||
ethValue: fee,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.address,
|
||||
encodedFunction: testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
}],
|
||||
actions: [mintAction, feeAction],
|
||||
});
|
||||
|
||||
// console.log("Calling estimateFee");
|
||||
|
||||
// const feeEstimation = await client.estimateFee(bundle);
|
||||
// console.log({ feeEstimation });
|
||||
|
||||
console.log("Sending mint bundle to aggregator");
|
||||
|
||||
const res = await client.add(bundle);
|
||||
@@ -47,7 +106,7 @@ if ("failures" in res) {
|
||||
console.log("Success response from aggregator", res.hash);
|
||||
|
||||
while (true) {
|
||||
const balance = (await testErc20.balanceOf(wallet.address));
|
||||
const balance = await testErc20.balanceOf(wallet.address);
|
||||
|
||||
console.log({
|
||||
startBalance: startBalance.toString(),
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { delay, ethers, MockERC20__factory } from "../deps.ts";
|
||||
|
||||
import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
@@ -20,7 +20,7 @@ const ethereumService = await EthereumService.create(
|
||||
);
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const [wallet] = await TestBlsWallets(provider, 1);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
const startBalance = await testErc20.balanceOf(wallet.address);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
@@ -47,7 +47,7 @@ console.log("Sending via ethereumService");
|
||||
})();
|
||||
|
||||
while (true) {
|
||||
const balance = (await testErc20.balanceOf(wallet.address));
|
||||
const balance = await testErc20.balanceOf(wallet.address);
|
||||
|
||||
console.log({
|
||||
startBalance: startBalance.toString(),
|
||||
|
||||
140
aggregator/manualTests/mintNViaAggregator.ts
Executable file
140
aggregator/manualTests/mintNViaAggregator.ts
Executable file
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
BigNumber,
|
||||
Bundle,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
|
||||
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
|
||||
import Range from "../src/helpers/Range.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
|
||||
const [walletNStr] = Deno.args;
|
||||
const walletN = Number(walletNStr);
|
||||
|
||||
if (!Number.isFinite(walletN)) {
|
||||
console.error("Usage: ./manualTests/mintNViaAggregator.ts <N>");
|
||||
Deno.exit(1);
|
||||
}
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const wallets = await Promise.all(
|
||||
Range(walletN).map((i) => TestBlsWallet(provider, i)),
|
||||
);
|
||||
|
||||
const firstWallet = wallets[0];
|
||||
|
||||
const mintAction: ActionData = {
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.address,
|
||||
encodedFunction: testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallets[0].address, 1],
|
||||
),
|
||||
};
|
||||
|
||||
const startBalance = await testErc20.balanceOf(firstWallet.address);
|
||||
|
||||
const bundles: Bundle[] = [];
|
||||
|
||||
for (const [i, wallet] of wallets.entries()) {
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
console.log("Funding wallet", i);
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const feeEstimation = await client.estimateFee(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
assert(feeEstimation.feeType === "ether");
|
||||
|
||||
const feeRequired = BigNumber.from(feeEstimation.feeRequired);
|
||||
|
||||
// Add 10% safety margin
|
||||
const fee = feeRequired.add(feeRequired.div(10));
|
||||
|
||||
const balance = await provider.getBalance(wallet.address);
|
||||
|
||||
// Ensure wallet can pay the fee
|
||||
if (balance.lt(fee)) {
|
||||
console.log("Funding wallet");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: fee.sub(balance),
|
||||
})).wait();
|
||||
}
|
||||
|
||||
const feeAction: ActionData = {
|
||||
ethValue: fee,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
bundles.push(wallet.sign({
|
||||
nonce,
|
||||
actions: [mintAction, feeAction],
|
||||
}));
|
||||
}
|
||||
|
||||
console.log("Sending mint bundles to aggregator");
|
||||
|
||||
await Promise.all(bundles.map(async (bundle) => {
|
||||
const res = await client.add(bundle);
|
||||
|
||||
if ("failures" in res) {
|
||||
throw new Error(res.failures.map((f) => f.description).join(", "));
|
||||
}
|
||||
|
||||
console.log("Success response from aggregator", res.hash);
|
||||
}));
|
||||
|
||||
while (true) {
|
||||
const balance = await testErc20.balanceOf(firstWallet.address);
|
||||
|
||||
console.log({
|
||||
startBalance: startBalance.toString(),
|
||||
balance: balance.toString(),
|
||||
});
|
||||
|
||||
if (balance.sub(startBalance).gte(walletN)) {
|
||||
console.log("done");
|
||||
break;
|
||||
}
|
||||
|
||||
console.log("Mints not completed, waiting 500ms");
|
||||
await delay(500);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-net --allow-env --allow-read
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import {
|
||||
AggregatorClient,
|
||||
@@ -11,8 +11,9 @@ import {
|
||||
|
||||
import * as env from "../test/env.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
|
||||
import Range from "../src/helpers/Range.ts";
|
||||
|
||||
const logStartTime = Date.now();
|
||||
|
||||
@@ -26,7 +27,14 @@ function log(...args: unknown[]) {
|
||||
console.log(RelativeTimestamp(), ...args);
|
||||
}
|
||||
|
||||
const leadTarget = env.MAX_AGGREGATION_SIZE * env.MAX_UNCONFIRMED_AGGREGATIONS;
|
||||
// Note: This value is a guess and may require some experimentation for optimal
|
||||
// throughput. The size of a full aggregation used to be hardcoded in config,
|
||||
// but now that we use gas to limit the bundle size we don't know this value
|
||||
// upfront anymore.
|
||||
const fullAggregationSize = 100;
|
||||
|
||||
const leadTarget = fullAggregationSize * env.MAX_UNCONFIRMED_AGGREGATIONS;
|
||||
|
||||
const pollingInterval = 400;
|
||||
const sendWalletCount = 50;
|
||||
|
||||
@@ -41,9 +49,8 @@ const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
log("Connecting/creating test wallets...");
|
||||
|
||||
const [recvWallet, ...sendWallets] = await TestBlsWallets(
|
||||
provider,
|
||||
sendWalletCount + 1,
|
||||
const [recvWallet, ...sendWallets] = await Promise.all(
|
||||
Range(sendWalletCount + 1).map((i) => TestBlsWallet(provider, i)),
|
||||
);
|
||||
|
||||
log("Checking/minting test tokens...");
|
||||
|
||||
58
aggregator/manualTests/zeroAddressError.ts
Normal file
58
aggregator/manualTests/zeroAddressError.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient, ethers, MockERC20__factory } from "../deps.ts";
|
||||
|
||||
// import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
// const ethereumService = await EthereumService.create(
|
||||
// (evt) => {
|
||||
// console.log(evt);
|
||||
// },
|
||||
// addresses.verificationGateway,
|
||||
// addresses.utilities,
|
||||
// env.PRIVATE_KEY_AGG,
|
||||
// );
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.address,
|
||||
encodedFunction: testErc20.interface.encodeFunctionData(
|
||||
"transferFrom",
|
||||
[
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
wallet.address,
|
||||
ethers.BigNumber.from(
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
),
|
||||
],
|
||||
),
|
||||
}],
|
||||
});
|
||||
|
||||
console.log("Sending via ethereumService or agg");
|
||||
|
||||
(async () => {
|
||||
try {
|
||||
// Test directly with ethereum service
|
||||
// await ethereumService.submitBundle(bundle);
|
||||
|
||||
// test by submitting request to the agg
|
||||
const res = await client.add(bundle);
|
||||
console.log(res);
|
||||
} catch (error) {
|
||||
console.error(error.stack);
|
||||
Deno.exit(1);
|
||||
}
|
||||
})();
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import app from "../src/app/app.ts";
|
||||
import AppEvent from "../src/app/AppEvent.ts";
|
||||
|
||||
@@ -1,24 +1,42 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write
|
||||
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env
|
||||
|
||||
import { dirname, parseArgs } from "../deps.ts";
|
||||
|
||||
import * as shell from "./helpers/shell.ts";
|
||||
import repoDir from "../src/helpers/repoDir.ts";
|
||||
import dotEnvPath, { envName } from "../src/helpers/dotEnvPath.ts";
|
||||
import nil from "../src/helpers/nil.ts";
|
||||
|
||||
const args = parseArgs(Deno.args);
|
||||
const parseArgsResult = parseArgs(Deno.args);
|
||||
|
||||
const args = {
|
||||
/** Whether to push the image to dockerhub. */
|
||||
push: parseArgsResult["push"],
|
||||
|
||||
/** Override the image name. Default: aggregator. */
|
||||
imageName: parseArgsResult["image-name"],
|
||||
|
||||
/** Only build the image, ie - don't also serialize the image to disk. */
|
||||
imageOnly: parseArgsResult["image-only"],
|
||||
|
||||
/** Prefix all docker commands with sudo. */
|
||||
sudoDocker: parseArgsResult["sudo-docker"],
|
||||
|
||||
/** Tag the image with latest as well as the default git-${sha}. */
|
||||
alsoTagLatest: parseArgsResult["also-tag-latest"],
|
||||
};
|
||||
|
||||
Deno.chdir(repoDir);
|
||||
const buildDir = `${repoDir}/build`;
|
||||
|
||||
await ensureFreshBuildDir();
|
||||
await buildEnvironment();
|
||||
await copyTypescriptFiles();
|
||||
await buildDockerImage();
|
||||
await tarballTypescriptFiles();
|
||||
|
||||
console.log("Aggregator build complete");
|
||||
if (args.push) {
|
||||
await pushDockerImage();
|
||||
}
|
||||
|
||||
console.log("\nAggregator build complete");
|
||||
|
||||
async function allFiles() {
|
||||
return [
|
||||
@@ -32,13 +50,7 @@ async function allFiles() {
|
||||
];
|
||||
}
|
||||
|
||||
async function shortContentHash(filePath: string) {
|
||||
const contentHash = (await shell.Line("shasum", "-a", "256", filePath));
|
||||
|
||||
return contentHash.slice(0, 7);
|
||||
}
|
||||
|
||||
async function BuildName() {
|
||||
async function Tag() {
|
||||
const commitShort = (await shell.Line("git", "rev-parse", "HEAD")).slice(
|
||||
0,
|
||||
7,
|
||||
@@ -47,15 +59,10 @@ async function BuildName() {
|
||||
const isDirty =
|
||||
(await shell.Lines("git", "status", "--porcelain")).length > 0;
|
||||
|
||||
const envHashShort = await shortContentHash(`${buildDir}/.env`);
|
||||
|
||||
return [
|
||||
"git",
|
||||
commitShort,
|
||||
...(isDirty ? ["dirty"] : []),
|
||||
"env",
|
||||
envName,
|
||||
envHashShort,
|
||||
].join("-");
|
||||
}
|
||||
|
||||
@@ -74,45 +81,6 @@ async function ensureFreshBuildDir() {
|
||||
await Deno.mkdir(buildDir);
|
||||
}
|
||||
|
||||
async function buildEnvironment() {
|
||||
const repoDotEnv = await Deno.readTextFile(dotEnvPath);
|
||||
|
||||
let networkConfigPaths: { repo: string; build: string } | nil = nil;
|
||||
const buildDotEnvLines: string[] = [];
|
||||
|
||||
for (const line of repoDotEnv.split("\n")) {
|
||||
let buildLine = line;
|
||||
|
||||
if (line.startsWith("NETWORK_CONFIG_PATH=")) {
|
||||
const repoNetworkConfigPath = line.slice(
|
||||
"NETWORK_CONFIG_PATH=".length,
|
||||
);
|
||||
|
||||
const networkConfigHash = await shortContentHash(repoNetworkConfigPath);
|
||||
|
||||
networkConfigPaths = {
|
||||
repo: repoNetworkConfigPath,
|
||||
build: `networkConfig-${networkConfigHash}.json`,
|
||||
};
|
||||
|
||||
// Need to replace this value with a build location because otherwise
|
||||
// this file might not be included in the docker image
|
||||
buildLine = `NETWORK_CONFIG_PATH=${networkConfigPaths.build}`;
|
||||
}
|
||||
|
||||
buildDotEnvLines.push(buildLine);
|
||||
}
|
||||
|
||||
if (networkConfigPaths !== nil) {
|
||||
await Deno.copyFile(
|
||||
networkConfigPaths.repo,
|
||||
`${buildDir}/${networkConfigPaths.build}`,
|
||||
);
|
||||
}
|
||||
|
||||
await Deno.writeTextFile(`${buildDir}/.env`, buildDotEnvLines.join("\n"));
|
||||
}
|
||||
|
||||
async function copyTypescriptFiles() {
|
||||
for (const f of await allFiles()) {
|
||||
if (!f.endsWith(".ts")) {
|
||||
@@ -138,9 +106,11 @@ async function tarballTypescriptFiles() {
|
||||
}
|
||||
|
||||
async function buildDockerImage() {
|
||||
const buildName = await BuildName();
|
||||
const tag = await Tag();
|
||||
const imageName = args.imageName ?? "aggregator";
|
||||
const imageNameAndTag = `${imageName}:${tag}`;
|
||||
|
||||
const sudoDockerArg = args["sudo-docker"] === true ? ["sudo"] : [];
|
||||
const sudoDockerArg = args.sudoDocker ? ["sudo"] : [];
|
||||
|
||||
await shell.run(
|
||||
...sudoDockerArg,
|
||||
@@ -148,18 +118,35 @@ async function buildDockerImage() {
|
||||
"build",
|
||||
repoDir,
|
||||
"-t",
|
||||
`aggregator:${buildName}`,
|
||||
imageNameAndTag,
|
||||
);
|
||||
|
||||
const dockerImageName = `aggregator-${buildName}-docker-image`;
|
||||
if (args.alsoTagLatest) {
|
||||
await shell.run(
|
||||
...sudoDockerArg,
|
||||
"docker",
|
||||
"tag",
|
||||
`${imageName}:${tag}`,
|
||||
`${imageName}:latest`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("\nDocker image created:", imageNameAndTag);
|
||||
|
||||
if (args.imageName) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dockerImageFileName = `${imageName}-${tag}-docker-image`;
|
||||
const tarFilePath = `${repoDir}/build/${dockerImageFileName}.tar`;
|
||||
|
||||
await shell.run(
|
||||
...sudoDockerArg,
|
||||
"docker",
|
||||
"save",
|
||||
"--output",
|
||||
`${repoDir}/build/${dockerImageName}.tar`,
|
||||
`aggregator:${buildName}`,
|
||||
tarFilePath,
|
||||
imageNameAndTag,
|
||||
);
|
||||
|
||||
if (sudoDockerArg.length > 0) {
|
||||
@@ -170,12 +157,23 @@ async function buildDockerImage() {
|
||||
"sudo",
|
||||
"chown",
|
||||
username,
|
||||
`${repoDir}/build/${dockerImageName}.tar`,
|
||||
tarFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
await shell.run(
|
||||
"gzip",
|
||||
`${repoDir}/build/${dockerImageName}.tar`,
|
||||
);
|
||||
await shell.run("gzip", tarFilePath);
|
||||
|
||||
console.log(`Docker image saved: ${tarFilePath}.gz`);
|
||||
}
|
||||
|
||||
async function pushDockerImage() {
|
||||
const tag = await Tag();
|
||||
const imageName = args.imageName ?? "aggregator";
|
||||
const imageNameAndTag = `${imageName}:${tag}`;
|
||||
|
||||
await shell.run("docker", "push", imageNameAndTag);
|
||||
|
||||
if (args.alsoTagLatest) {
|
||||
await shell.run("docker", "push", `${imageName}:latest`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write --allow-env
|
||||
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env
|
||||
|
||||
import { checkTs } from "./helpers/typescript.ts";
|
||||
|
||||
|
||||
@@ -16,10 +16,9 @@ deno test \
|
||||
--allow-env \
|
||||
--allow-read \
|
||||
--coverage=cov_profile \
|
||||
--unstable \
|
||||
test/*.test.ts
|
||||
|
||||
deno coverage cov_profile --unstable --lcov >cov_profile/profile.lcov
|
||||
deno coverage cov_profile --lcov >cov_profile/profile.lcov
|
||||
|
||||
genhtml -o cov_profile/html cov_profile/profile.lcov
|
||||
|
||||
|
||||
41
aggregator/programs/createInternalBlsWallet.ts
Executable file
41
aggregator/programs/createInternalBlsWallet.ts
Executable file
@@ -0,0 +1,41 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
VerificationGateway__factory,
|
||||
Wallet,
|
||||
} from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const wallet = new Wallet(env.PRIVATE_KEY_AGG, provider);
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const vg = VerificationGateway__factory.connect(
|
||||
addresses.verificationGateway,
|
||||
wallet,
|
||||
);
|
||||
|
||||
const internalBlsWallet = await BlsWalletWrapper.connect(
|
||||
env.PRIVATE_KEY_AGG,
|
||||
addresses.verificationGateway,
|
||||
provider,
|
||||
);
|
||||
|
||||
console.log("Connected internal wallet:", internalBlsWallet.address);
|
||||
|
||||
const nonce = await internalBlsWallet.Nonce();
|
||||
|
||||
if (!nonce.eq(0)) {
|
||||
console.log("Already exists with nonce", nonce.toNumber());
|
||||
} else {
|
||||
await (await vg.processBundle(internalBlsWallet.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
|
||||
console.log("Created successfully");
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-net --allow-read --allow-env
|
||||
|
||||
// Useful for when breaking database changes are made.
|
||||
|
||||
import createQueryClient from "../src/app/createQueryClient.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import BundleTable from "../src/app/BundleTable.ts";
|
||||
|
||||
const queryClient = createQueryClient(() => {});
|
||||
|
||||
for (const tableName of [env.BUNDLE_TABLE_NAME]) {
|
||||
const table = await BundleTable.create(queryClient, tableName);
|
||||
await table.drop();
|
||||
console.log(`dropped table ${tableName}`);
|
||||
}
|
||||
@@ -16,7 +16,7 @@ export async function checkTs(): Promise<void> {
|
||||
tsFiles.map((f) => `import "${repoDir}/${f}";`).join("\n"),
|
||||
);
|
||||
|
||||
await shell.run("deno", "cache", "--unstable", testFilePath);
|
||||
await shell.run("deno", "check", testFilePath);
|
||||
} finally {
|
||||
if (testFilePath !== nil) {
|
||||
await Deno.remove(testFilePath);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write --allow-env
|
||||
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-env
|
||||
|
||||
// TODO (merge-ok) Consider turning this into a standard eslint rule
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write --allow-env
|
||||
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env
|
||||
|
||||
import { lintTodosFixmes } from "./helpers/lint.ts"; // merge-ok
|
||||
import { checkTs } from "./helpers/typescript.ts";
|
||||
@@ -28,7 +28,7 @@ function Checks(): Check[] {
|
||||
["lint", async () => {
|
||||
await shell.run("deno", "lint", ".");
|
||||
}],
|
||||
["todos and fixmes", lintTodosFixmes], // merge-ok
|
||||
["todos and fixmes", lintTodosFixmes], // merge-ok
|
||||
["typescript", checkTs],
|
||||
["test", async () => {
|
||||
await shell.run(
|
||||
@@ -42,7 +42,6 @@ function Checks(): Check[] {
|
||||
"--allow-net",
|
||||
"--allow-env",
|
||||
"--allow-read",
|
||||
"--unstable",
|
||||
"--",
|
||||
"--env",
|
||||
envName,
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-net --allow-read --unstable
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-read --allow-write --allow-env
|
||||
|
||||
import { BigNumber } from "../deps.ts";
|
||||
import createQueryClient from "../src/app/createQueryClient.ts";
|
||||
import { BigNumber, sqlite } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import BundleTable from "../src/app/BundleTable.ts";
|
||||
|
||||
const queryClient = createQueryClient(() => {});
|
||||
const table = new BundleTable(new sqlite.DB(env.DB_PATH));
|
||||
|
||||
for (const tableName of [env.BUNDLE_TABLE_NAME]) {
|
||||
const table = await BundleTable.create(queryClient, tableName);
|
||||
console.log(tableName, await table.count());
|
||||
console.log(tableName, (await table.all()).map((bun) => bun.id));
|
||||
console.log(
|
||||
tableName,
|
||||
"findEligible",
|
||||
(await table.findEligible(BigNumber.from(0), 1000)).map((bun) => bun.id),
|
||||
);
|
||||
}
|
||||
console.log(table.count());
|
||||
console.log(table.all().map((bun) => bun.id));
|
||||
console.log(
|
||||
"findEligible",
|
||||
table.findEligible(BigNumber.from(0), 1000).map((bun) => bun.id)
|
||||
);
|
||||
|
||||
43
aggregator/programs/start-docker.sh
Executable file
43
aggregator/programs/start-docker.sh
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z ${VERSION+x} ]; then
|
||||
>&2 echo "Missing VERSION. Needs to match the first 7 characters of the git sha used to build the docker image."
|
||||
>&2 echo "Usage: VERSION=abc1234 start-docker.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ENV_PATH="${ENV_PATH:=.env}"
|
||||
|
||||
# Normalize ENV_PATH to an absolute path
|
||||
if [[ $(echo $ENV_PATH | head -c1) != "/" ]]; then
|
||||
ENV_PATH="$(cd $(dirname $ENV_PATH) && pwd)/$(basename $ENV_PATH)"
|
||||
fi
|
||||
|
||||
echo "Using env" $ENV_PATH
|
||||
|
||||
PORT=$(cat $ENV_PATH | grep '^PORT=' | tail -n1 | sed 's/^PORT=//')
|
||||
NETWORK_CONFIG_PATH=$(cat $ENV_PATH | grep '^NETWORK_CONFIG_PATH=' | tail -n1 | sed 's/^NETWORK_CONFIG_PATH=//')
|
||||
|
||||
# Normalize NETWORK_CONFIG_PATH to an absolute path
|
||||
if [[ $(echo $NETWORK_CONFIG_PATH | head -c1) != "/" ]]; then
|
||||
NETWORK_CONFIG_PATH="$(cd $(dirname $ENV_PATH) && cd $(dirname $NETWORK_CONFIG_PATH) && pwd)/$(basename $NETWORK_CONFIG_PATH)"
|
||||
fi
|
||||
|
||||
echo "Using network config" $NETWORK_CONFIG_PATH
|
||||
|
||||
NETWORK=$(basename $NETWORK_CONFIG_PATH .json)
|
||||
CONTAINER_NAME="aggregator-$VERSION-$NETWORK"
|
||||
IMAGE_NAME="aggregator:git-$VERSION"
|
||||
|
||||
echo "Creating $CONTAINER_NAME using $IMAGE_NAME"
|
||||
|
||||
docker run \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-d \
|
||||
--net=host \
|
||||
--restart=unless-stopped \
|
||||
--mount type=bind,source="$ENV_PATH",target=/app/.env \
|
||||
--mount type=bind,source="$NETWORK_CONFIG_PATH",target=/app/networkConfig.json \
|
||||
"$IMAGE_NAME"
|
||||
@@ -5,15 +5,15 @@ import AdminService from "./AdminService.ts";
|
||||
export default function AdminRouter(adminService: AdminService) {
|
||||
const router = new Router({ prefix: "/admin/" });
|
||||
|
||||
router.get("countTxs", async (ctx) => {
|
||||
const c = await adminService.bundleCount();
|
||||
router.get("countTxs", (ctx) => {
|
||||
const c = adminService.bundleCount();
|
||||
console.log(`Returning count ${c}\n`);
|
||||
ctx.response.headers.set("Content-Type", "application/json");
|
||||
ctx.response.body = c;
|
||||
});
|
||||
|
||||
router.get("resetTxs", async (ctx) => {
|
||||
await adminService.resetBundles();
|
||||
router.get("resetTxs", (ctx) => {
|
||||
adminService.resetBundles();
|
||||
ctx.response.body = "Transactions reset";
|
||||
});
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ export default class AdminService {
|
||||
private bundleTable: BundleTable,
|
||||
) {}
|
||||
|
||||
async resetBundles() {
|
||||
await this.bundleTable.clear();
|
||||
resetBundles() {
|
||||
this.bundleTable.clear();
|
||||
}
|
||||
|
||||
async bundleCount(): Promise<bigint> {
|
||||
return await this.bundleTable.count();
|
||||
bundleCount(): number {
|
||||
return this.bundleTable.count();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,211 +2,447 @@ import {
|
||||
BigNumber,
|
||||
BlsWalletSigner,
|
||||
Bundle,
|
||||
decodeError,
|
||||
ERC20,
|
||||
ERC20__factory,
|
||||
ethers,
|
||||
OperationResultError,
|
||||
Semaphore,
|
||||
shuffled,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import nil from "../helpers/nil.ts";
|
||||
import Range from "../helpers/Range.ts";
|
||||
import assert from "../helpers/assert.ts";
|
||||
import bigSum from "./helpers/bigSum.ts";
|
||||
import * as env from "../env.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
import { BundleRow } from "./BundleTable.ts";
|
||||
import countActions from "./helpers/countActions.ts";
|
||||
import ClientReportableError from "./helpers/ClientReportableError.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import bigSum from "./helpers/bigSum.ts";
|
||||
|
||||
type FeeConfig =
|
||||
| {
|
||||
type: "ether";
|
||||
allowLosses: boolean;
|
||||
breakevenOperationCount: number;
|
||||
}
|
||||
| {
|
||||
type: "token";
|
||||
address: string;
|
||||
ethValueInTokens: number;
|
||||
allowLosses: boolean;
|
||||
breakevenOperationCount: number;
|
||||
}
|
||||
| nil;
|
||||
|
||||
const envFeeConfig = ((): FeeConfig => {
|
||||
if (!env.REQUIRE_FEES) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
if (env.FEE_TYPE === "ether") {
|
||||
return {
|
||||
type: "ether",
|
||||
allowLosses: env.ALLOW_LOSSES,
|
||||
breakevenOperationCount: env.BREAKEVEN_OPERATION_COUNT,
|
||||
};
|
||||
}
|
||||
|
||||
const feeTypeParts = env.FEE_TYPE.split(":");
|
||||
assert(feeTypeParts.length === 2);
|
||||
assert(feeTypeParts[0] === "token");
|
||||
|
||||
const address = feeTypeParts[1];
|
||||
assert(/^0x[0-9a-fA-F]*$/.test(address));
|
||||
|
||||
assert(env.ETH_VALUE_IN_TOKENS !== nil);
|
||||
|
||||
return {
|
||||
type: "token",
|
||||
address,
|
||||
ethValueInTokens: env.ETH_VALUE_IN_TOKENS,
|
||||
allowLosses: env.ALLOW_LOSSES,
|
||||
breakevenOperationCount: env.BREAKEVEN_OPERATION_COUNT,
|
||||
};
|
||||
})();
|
||||
|
||||
export type AggregationStrategyResult = {
|
||||
aggregateBundle: Bundle | nil;
|
||||
includedRows: BundleRow[];
|
||||
bundleOverheadCost: BigNumber;
|
||||
expectedFee: BigNumber;
|
||||
expectedMaxCost: BigNumber;
|
||||
failedRows: BundleRow[];
|
||||
};
|
||||
|
||||
export type AggregationStrategyConfig =
|
||||
typeof AggregationStrategy["defaultConfig"];
|
||||
|
||||
export default class AggregationStrategy {
|
||||
static defaultConfig = {
|
||||
maxAggregationSize: env.MAX_AGGREGATION_SIZE,
|
||||
fees: {
|
||||
type: env.FEE_TYPE,
|
||||
perGas: env.FEE_PER_GAS,
|
||||
perByte: env.FEE_PER_BYTE,
|
||||
},
|
||||
maxGasPerBundle: env.MAX_GAS_PER_BUNDLE,
|
||||
fees: envFeeConfig,
|
||||
bundleCheckingConcurrency: env.BUNDLE_CHECKING_CONCURRENCY,
|
||||
};
|
||||
|
||||
#tokenDecimals?: number;
|
||||
|
||||
// The concurrency of #checkBundle is limited by this semaphore because it can
|
||||
// be called on many bundles in parallel
|
||||
#checkBundleSemaphore: Semaphore;
|
||||
|
||||
constructor(
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
public ethereumService: EthereumService,
|
||||
public config = AggregationStrategy.defaultConfig,
|
||||
) {}
|
||||
|
||||
async run(eligibleRows: BundleRow[]): (
|
||||
Promise<{
|
||||
aggregateBundle: Bundle | nil;
|
||||
includedRows: BundleRow[];
|
||||
failedRows: BundleRow[];
|
||||
}>
|
||||
public emit: (event: AppEvent) => void = () => {},
|
||||
) {
|
||||
this.#checkBundleSemaphore = new Semaphore(
|
||||
this.config.bundleCheckingConcurrency,
|
||||
);
|
||||
}
|
||||
|
||||
async run(eligibleRows: BundleRow[]): Promise<AggregationStrategyResult> {
|
||||
eligibleRows = await this.#filterRows(eligibleRows);
|
||||
|
||||
const bundleOverheadGas = await this.measureBundleOverheadGas();
|
||||
|
||||
let aggregateBundle = this.blsWalletSigner.aggregate([]);
|
||||
let aggregateGas = bundleOverheadGas;
|
||||
const includedRows: BundleRow[] = [];
|
||||
const failedRows: BundleRow[] = [];
|
||||
let expectedFee = BigNumber.from(0);
|
||||
|
||||
while (eligibleRows.length > 0) {
|
||||
while (true) {
|
||||
const {
|
||||
aggregateBundle: newAggregateBundle,
|
||||
aggregateGas: newAggregateGas,
|
||||
includedRows: newIncludedRows,
|
||||
expectedFees,
|
||||
failedRows: newFailedRows,
|
||||
remainingEligibleRows,
|
||||
} = await this.#augmentAggregateBundle(
|
||||
aggregateBundle,
|
||||
aggregateGas,
|
||||
eligibleRows,
|
||||
bundleOverheadGas,
|
||||
);
|
||||
|
||||
aggregateBundle = newAggregateBundle;
|
||||
aggregateGas = newAggregateGas;
|
||||
includedRows.push(...newIncludedRows);
|
||||
expectedFee = expectedFee.add(bigSum(expectedFees));
|
||||
failedRows.push(...newFailedRows);
|
||||
eligibleRows = remainingEligibleRows;
|
||||
}
|
||||
|
||||
return {
|
||||
aggregateBundle: aggregateBundle.operations.length > 0
|
||||
? aggregateBundle
|
||||
: nil,
|
||||
includedRows,
|
||||
failedRows,
|
||||
};
|
||||
}
|
||||
|
||||
async estimateFee(bundle: Bundle) {
|
||||
const es = this.ethereumService;
|
||||
const feeToken = this.#FeeToken();
|
||||
|
||||
const balanceCall = feeToken
|
||||
? es.Call(feeToken, "balanceOf", [es.wallet.address])
|
||||
: es.Call(es.utilities, "ethBalanceOf", [es.wallet.address]);
|
||||
|
||||
const [
|
||||
balanceResultBefore,
|
||||
bundleResult,
|
||||
balanceResultAfter,
|
||||
] = await es.callStaticSequence(
|
||||
balanceCall,
|
||||
es.Call(
|
||||
es.verificationGateway,
|
||||
"processBundle",
|
||||
[bundle],
|
||||
),
|
||||
balanceCall,
|
||||
);
|
||||
|
||||
if (
|
||||
balanceResultBefore.returnValue === undefined ||
|
||||
balanceResultAfter.returnValue === undefined
|
||||
) {
|
||||
throw new ClientReportableError("Failed to get balance");
|
||||
}
|
||||
|
||||
const balanceBefore = balanceResultBefore.returnValue[0];
|
||||
const balanceAfter = balanceResultAfter.returnValue[0];
|
||||
|
||||
const feeDetected = balanceAfter.sub(balanceBefore);
|
||||
|
||||
if (bundleResult.returnValue === undefined) {
|
||||
throw new ClientReportableError("Failed to statically process bundle");
|
||||
}
|
||||
|
||||
const feeRequired = await this.#measureRequiredFee(bundle);
|
||||
|
||||
const successes = bundleResult.returnValue.successes;
|
||||
|
||||
return {
|
||||
feeDetected,
|
||||
feeRequired,
|
||||
successes,
|
||||
};
|
||||
}
|
||||
|
||||
async #augmentAggregateBundle(
|
||||
previousAggregateBundle: Bundle,
|
||||
eligibleRows: BundleRow[],
|
||||
): (
|
||||
Promise<{
|
||||
aggregateBundle: Bundle;
|
||||
includedRows: BundleRow[];
|
||||
failedRows: BundleRow[];
|
||||
remainingEligibleRows: BundleRow[];
|
||||
}>
|
||||
) {
|
||||
let aggregateBundle: Bundle | nil = nil;
|
||||
let includedRows: BundleRow[] = [];
|
||||
const failedRows: BundleRow[] = [];
|
||||
// TODO (merge-ok): Count gas instead, have idea
|
||||
// or way to query max gas per txn (submission).
|
||||
let actionCount = countActions(previousAggregateBundle);
|
||||
|
||||
for (const row of eligibleRows) {
|
||||
const rowActionCount = countActions(row.bundle);
|
||||
|
||||
if (actionCount + rowActionCount > this.config.maxAggregationSize) {
|
||||
if (newIncludedRows.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
includedRows.push(row);
|
||||
actionCount += rowActionCount;
|
||||
}
|
||||
|
||||
if (includedRows.length === 0) {
|
||||
return {
|
||||
aggregateBundle: previousAggregateBundle,
|
||||
includedRows,
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
|
||||
// If we're not able to include anything more, don't consider any rows
|
||||
// eligible anymore.
|
||||
remainingEligibleRows: [],
|
||||
};
|
||||
}
|
||||
|
||||
const [previousFee, ...fees] = (await this.#measureFees([
|
||||
previousAggregateBundle,
|
||||
...includedRows.map((r) => r.bundle),
|
||||
]));
|
||||
|
||||
const firstFailureIndex = await this.#findFirstFailureIndex(
|
||||
previousAggregateBundle,
|
||||
previousFee,
|
||||
includedRows.map((r) => r.bundle),
|
||||
fees,
|
||||
const aggregateBundleCheck = await this.#checkBundle(
|
||||
aggregateBundle,
|
||||
BigNumber.from(0),
|
||||
);
|
||||
|
||||
let remainingEligibleRows: BundleRow[];
|
||||
|
||||
if (firstFailureIndex !== nil) {
|
||||
const failedRow = includedRows[firstFailureIndex];
|
||||
failedRows.push(failedRow);
|
||||
|
||||
includedRows = includedRows.slice(
|
||||
0,
|
||||
firstFailureIndex,
|
||||
);
|
||||
|
||||
const eligibleRowIndex = eligibleRows.indexOf(failedRow);
|
||||
assert(eligibleRowIndex !== -1);
|
||||
|
||||
remainingEligibleRows = eligibleRows.slice(includedRows.length + 1);
|
||||
} else {
|
||||
remainingEligibleRows = eligibleRows.slice(includedRows.length);
|
||||
}
|
||||
|
||||
aggregateBundle = this.blsWalletSigner.aggregate([
|
||||
previousAggregateBundle,
|
||||
...includedRows.map((r) => r.bundle),
|
||||
]);
|
||||
|
||||
return {
|
||||
let result: AggregationStrategyResult = {
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
expectedFee,
|
||||
expectedMaxCost: aggregateBundleCheck.expectedMaxCost,
|
||||
failedRows,
|
||||
remainingEligibleRows,
|
||||
};
|
||||
|
||||
if (this.config.fees?.allowLosses === false) {
|
||||
result = this.#preventLosses(
|
||||
result,
|
||||
aggregateBundleCheck.errorReason,
|
||||
this.config.fees.breakevenOperationCount,
|
||||
);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* This is not guaranteed to prevent losses. We cannot 100% know what is going
|
||||
* to happen until the bundle is actually submitted on chain.
|
||||
*/
|
||||
#preventLosses(
|
||||
result: AggregationStrategyResult,
|
||||
errorReason: OperationResultError | nil,
|
||||
breakevenOperationCount: number,
|
||||
): AggregationStrategyResult {
|
||||
if (result.aggregateBundle === nil) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const {
|
||||
aggregateBundle,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
includedRows,
|
||||
failedRows,
|
||||
} = result;
|
||||
|
||||
if (expectedFee.gte(expectedMaxCost)) {
|
||||
return result;
|
||||
}
|
||||
|
||||
this.emit({
|
||||
type: "aggregate-bundle-unprofitable",
|
||||
data: {
|
||||
reason: errorReason?.message,
|
||||
},
|
||||
});
|
||||
|
||||
if (aggregateBundle.operations.length < breakevenOperationCount) {
|
||||
return {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
};
|
||||
}
|
||||
|
||||
this.emit({ type: "unprofitable-despite-breakeven-operations" });
|
||||
|
||||
// This is unexpected: We have enough operations to breakeven, but the
|
||||
// bundle is unprofitable instead.
|
||||
//
|
||||
// This could happen due to small variations on a bundle that is
|
||||
// borderline, but it could also happen due to an intentional attack.
|
||||
// In the simplest case, an attacker submits two bundles that both pay
|
||||
// when simulated in isolation, but the first sets state that prevents
|
||||
// payment on the second bundle. We need to do something about this
|
||||
// because it could otherwise put us into a state that might never
|
||||
// resolve - next time we form a bundle, we'll run into the same issue
|
||||
// because the bundles will be considered again in the same order.
|
||||
//
|
||||
// To fix this, we simply mark half the bundles as failed. We could
|
||||
// isolate the issue by doing a lot of in-order reprocessing, but
|
||||
// having this defense in place should prevent the attack in the first
|
||||
// place, so false-positives here are a minor concern (keeping in mind
|
||||
// these bundles will still get retried later).
|
||||
|
||||
let failureSample = shuffled(includedRows);
|
||||
failureSample = failureSample.slice(0, failureSample.length / 2);
|
||||
|
||||
for (const row of failureSample) {
|
||||
row.submitError = "Included in failure sample for unprofitable bundle";
|
||||
}
|
||||
|
||||
failedRows.push(...failureSample);
|
||||
|
||||
return {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
};
|
||||
}
|
||||
|
||||
async estimateFee(bundle: Bundle, bundleOverheadGas?: BigNumber) {
|
||||
const [{ operationStatuses: successes, fee: feeDetected, errorReason }] =
|
||||
await this
|
||||
.#measureFees([
|
||||
bundle,
|
||||
]);
|
||||
|
||||
const feeInfo = await this.#measureFeeInfo(
|
||||
bundle,
|
||||
bundleOverheadGas,
|
||||
);
|
||||
|
||||
return {
|
||||
feeDetected,
|
||||
feeRequired: feeInfo?.requiredFee ?? BigNumber.from(0),
|
||||
successes,
|
||||
errorReason,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes rows that conflict with each other because they contain an
|
||||
* operation for the same wallet and the same nonce.
|
||||
*/
|
||||
async #filterRows(rows: BundleRow[]) {
|
||||
const rowsByKeyAndNonce = new Map<string, BundleRow[]>();
|
||||
|
||||
for (const row of rows) {
|
||||
for (let i = 0; i < row.bundle.operations.length; i++) {
|
||||
const publicKey = row.bundle.senderPublicKeys[i];
|
||||
const operation = row.bundle.operations[i];
|
||||
|
||||
const keyAndNonce = ethers.utils.solidityPack([
|
||||
"uint256",
|
||||
"uint256",
|
||||
"uint256",
|
||||
"uint256",
|
||||
"uint256",
|
||||
], [...publicKey, operation.nonce]);
|
||||
|
||||
const entry = rowsByKeyAndNonce.get(keyAndNonce) ?? [];
|
||||
entry.push(row);
|
||||
rowsByKeyAndNonce.set(keyAndNonce, entry);
|
||||
}
|
||||
}
|
||||
|
||||
let filteredRows = rows;
|
||||
|
||||
for (let rowGroup of rowsByKeyAndNonce.values()) {
|
||||
rowGroup = rowGroup.filter((r) => filteredRows.includes(r));
|
||||
|
||||
if (rowGroup.length <= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const bestRow = await this.#pickBest(rowGroup);
|
||||
const conflictingRows = rowGroup.filter((r) => r !== bestRow);
|
||||
|
||||
filteredRows = filteredRows.filter((r) => !conflictingRows.includes(r));
|
||||
}
|
||||
|
||||
return filteredRows;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the row which pays the highest 'excess fee'.
|
||||
*
|
||||
* Excess fee is the amount the fee exceeds its requirement.
|
||||
*/
|
||||
async #pickBest(rows: BundleRow[]) {
|
||||
assert(rows.length > 0);
|
||||
|
||||
const results = await Promise.all(
|
||||
rows.map((r) => this.#checkBundle(r.bundle)),
|
||||
);
|
||||
|
||||
const excessFees = results.map((res) =>
|
||||
res.expectedFee.sub(res.requiredFee)
|
||||
);
|
||||
|
||||
let bestExcessFee = excessFees[0];
|
||||
let bestExcessFeeIndex = 0;
|
||||
|
||||
for (let i = 1; i < excessFees.length; i++) {
|
||||
if (excessFees[i].gt(bestExcessFee)) {
|
||||
bestExcessFee = excessFees[i];
|
||||
bestExcessFeeIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return rows[bestExcessFeeIndex];
|
||||
}
|
||||
|
||||
/**
|
||||
* Augment the aggregate bundle with more eligible rows.
|
||||
*
|
||||
* This is part of an iterative process where the aggregate bundle is
|
||||
* initially empty and this method is used to accumulate more user bundles
|
||||
* until we can't add any more.
|
||||
*/
|
||||
async #augmentAggregateBundle(
|
||||
previousAggregateBundle: Bundle,
|
||||
previousAggregateGas: BigNumber,
|
||||
eligibleRows: BundleRow[],
|
||||
bundleOverheadGas: BigNumber,
|
||||
): Promise<{
|
||||
aggregateBundle: Bundle;
|
||||
aggregateGas: BigNumber;
|
||||
includedRows: BundleRow[];
|
||||
expectedFees: BigNumber[];
|
||||
failedRows: BundleRow[];
|
||||
remainingEligibleRows: BundleRow[];
|
||||
}> {
|
||||
const candidateRows = eligibleRows.splice(
|
||||
0,
|
||||
this.config.bundleCheckingConcurrency,
|
||||
);
|
||||
|
||||
let aggregateGas = previousAggregateGas;
|
||||
|
||||
// Checking in parallel here. We limit the number of candidate rows on each
|
||||
// round to limit this, and it's also protected by a semaphore.
|
||||
const rowChecks = await Promise.all(
|
||||
candidateRows.map((r) => this.#checkBundle(r.bundle, bundleOverheadGas)),
|
||||
);
|
||||
|
||||
const includedRows: BundleRow[] = [];
|
||||
const expectedFees: BigNumber[] = [];
|
||||
const failedRows: BundleRow[] = [];
|
||||
|
||||
for (
|
||||
const [
|
||||
i,
|
||||
{ success, gasEstimate, expectedFee, errorReason },
|
||||
] of rowChecks.entries()
|
||||
) {
|
||||
const row = candidateRows[i];
|
||||
|
||||
if (!success) {
|
||||
if (errorReason) {
|
||||
row.submitError = errorReason.message;
|
||||
}
|
||||
|
||||
failedRows.push(row);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
const newAggregateGas = (aggregateGas
|
||||
.add(gasEstimate)
|
||||
.sub(bundleOverheadGas));
|
||||
|
||||
if (newAggregateGas.gt(this.config.maxGasPerBundle)) {
|
||||
// Bundle would cause us to exceed maxGasPerBundle, so don't include it,
|
||||
// but also don't mark it as failed.
|
||||
continue;
|
||||
}
|
||||
|
||||
aggregateGas = newAggregateGas;
|
||||
includedRows.push(row);
|
||||
expectedFees.push(expectedFee);
|
||||
}
|
||||
|
||||
return {
|
||||
aggregateBundle: this.blsWalletSigner.aggregate([
|
||||
previousAggregateBundle,
|
||||
...includedRows.map((r) => r.bundle),
|
||||
]),
|
||||
aggregateGas,
|
||||
includedRows,
|
||||
expectedFees,
|
||||
failedRows,
|
||||
remainingEligibleRows: eligibleRows,
|
||||
};
|
||||
}
|
||||
|
||||
async #measureFees(bundles: Bundle[]): Promise<{
|
||||
success: boolean;
|
||||
operationStatuses: boolean[];
|
||||
fee: BigNumber;
|
||||
errorReason: OperationResultError | nil;
|
||||
}[]> {
|
||||
const es = this.ethereumService;
|
||||
const feeToken = this.#FeeToken();
|
||||
@@ -231,216 +467,208 @@ export default class AggregationStrategy {
|
||||
assert(after.success);
|
||||
|
||||
const bundleResult = processBundleResults[i];
|
||||
|
||||
let success: boolean;
|
||||
|
||||
if (bundleResult.success) {
|
||||
const [operationResults] = bundleResult.returnValue;
|
||||
|
||||
// We require that at least one operation succeeds, even though
|
||||
// processBundle doesn't revert in this case.
|
||||
success = operationResults.some((opSuccess) => opSuccess === true);
|
||||
} else {
|
||||
success = false;
|
||||
const fee = after.returnValue[0].sub(before.returnValue[0]);
|
||||
if (!bundleResult.success) {
|
||||
const errorReason: OperationResultError = {
|
||||
message: "Unknown error reason",
|
||||
};
|
||||
return {
|
||||
success: false,
|
||||
operationStatuses: bundles[i].operations.map(() => false),
|
||||
fee,
|
||||
errorReason,
|
||||
};
|
||||
}
|
||||
|
||||
const fee = after.returnValue[0].sub(before.returnValue[0]);
|
||||
const [operationStatuses, results] = bundleResult.returnValue;
|
||||
|
||||
return { success, fee };
|
||||
let errorReason: OperationResultError | nil;
|
||||
// We require that at least one operation succeeds, even though
|
||||
// processBundle doesn't revert in this case.
|
||||
const success = operationStatuses.some((opSuccess: boolean) =>
|
||||
opSuccess === true
|
||||
);
|
||||
|
||||
// If operation is not successful, attempt to decode an error message
|
||||
if (!success) {
|
||||
const error = results.map((result: string[]) => {
|
||||
try {
|
||||
if (result[0]) {
|
||||
return decodeError(result[0]);
|
||||
}
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return;
|
||||
}
|
||||
});
|
||||
errorReason = error[0];
|
||||
}
|
||||
|
||||
return { success, operationStatuses, fee, errorReason };
|
||||
});
|
||||
}
|
||||
|
||||
#FeeToken(): ERC20 | nil {
|
||||
const feeType = this.config.fees.type;
|
||||
|
||||
if (feeType === "ether") {
|
||||
if (this.config.fees?.type !== "token") {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return ERC20__factory.connect(
|
||||
feeType.slice("token:".length),
|
||||
this.config.fees.address,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
}
|
||||
|
||||
async #measureRequiredFee(bundle: Bundle) {
|
||||
const gasEstimate = await this.ethereumService.verificationGateway
|
||||
.estimateGas
|
||||
.processBundle(bundle);
|
||||
|
||||
const callDataSize = ethers.utils.hexDataLength(
|
||||
this.ethereumService.verificationGateway.interface
|
||||
.encodeFunctionData("processBundle", [bundle]),
|
||||
);
|
||||
|
||||
return (
|
||||
gasEstimate.mul(this.config.fees.perGas).add(
|
||||
this.config.fees.perByte.mul(callDataSize),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a lower bound for the fee that is required for processing the
|
||||
* bundle.
|
||||
*
|
||||
* This exists because it's a very good lower bound and it's very fast.
|
||||
* Therefore, when there's an insufficient fee bundle:
|
||||
* - This lower bound is usually enough to find it
|
||||
* - Finding it this way is much more efficient
|
||||
*/
|
||||
#measureRequiredFeeLowerBound(bundle: Bundle) {
|
||||
const callDataEmptyBundleSize = ethers.utils.hexDataLength(
|
||||
this.ethereumService.verificationGateway.interface
|
||||
.encodeFunctionData("processBundle", [
|
||||
this.blsWalletSigner.aggregate([]),
|
||||
]),
|
||||
);
|
||||
|
||||
const callDataSize = ethers.utils.hexDataLength(
|
||||
this.ethereumService.verificationGateway.interface
|
||||
.encodeFunctionData("processBundle", [bundle]),
|
||||
);
|
||||
|
||||
// We subtract the size of an empty bundle because it represents the number
|
||||
// of *additional* bytes added when aggregating. The bundle doesn't
|
||||
// necessarily have to pay the initial overhead to be viable.
|
||||
const callDataMarginalSize = callDataSize - callDataEmptyBundleSize;
|
||||
|
||||
return this.config.fees.perByte.mul(callDataMarginalSize);
|
||||
}
|
||||
|
||||
async #findFirstFailureIndex(
|
||||
previousAggregateBundle: Bundle,
|
||||
previousFee: { success: boolean; fee: BigNumber },
|
||||
bundles: Bundle[],
|
||||
fees: { success: boolean; fee: BigNumber }[],
|
||||
): Promise<number | nil> {
|
||||
if (bundles.length === 0) {
|
||||
async #measureFeeInfo(bundle: Bundle, bundleOverheadGas?: BigNumber) {
|
||||
if (this.config.fees === nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const len = bundles.length;
|
||||
assert(fees.length === len);
|
||||
bundleOverheadGas ??= await this.measureBundleOverheadGas();
|
||||
|
||||
const checkFirstN = async (n: number): Promise<{
|
||||
const gasEstimate = await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
|
||||
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
|
||||
const bundleOverheadGasContribution = BigNumber.from(
|
||||
Math.ceil(
|
||||
bundleOverheadGas.toNumber() /
|
||||
this.config.fees.breakevenOperationCount * bundle.operations.length,
|
||||
),
|
||||
);
|
||||
|
||||
const requiredGas = marginalGasEstimate.add(bundleOverheadGasContribution);
|
||||
|
||||
const { maxFeePerGas } = await this.ethereumService.GasConfig();
|
||||
|
||||
const ethWeiFee = requiredGas.mul(maxFeePerGas);
|
||||
|
||||
const token = this.#FeeToken();
|
||||
|
||||
if (!token) {
|
||||
return {
|
||||
requiredFee: ethWeiFee,
|
||||
expectedMaxCost: gasEstimate.mul(maxFeePerGas),
|
||||
gasEstimate,
|
||||
bundleOverheadGas,
|
||||
};
|
||||
}
|
||||
|
||||
const decimals = await this.#TokenDecimals();
|
||||
const decimalAdj = 10 ** (decimals - 18);
|
||||
|
||||
assert(this.config.fees?.type === "token");
|
||||
const ethWeiOverTokenWei = decimalAdj * this.config.fees.ethValueInTokens;
|
||||
|
||||
// Note the use of .toString below. Without it, BigNumber recognizes that
|
||||
// the float64 number cannot accurately represent integers in this range
|
||||
// and throws an overflow. However, this number is ultimately an estimation
|
||||
// with a margin of error, and the rounding caused by float64 is acceptable.
|
||||
const tokenWeiFee = BigNumber.from(
|
||||
Math.ceil(ethWeiFee.toNumber() * ethWeiOverTokenWei).toString(),
|
||||
);
|
||||
|
||||
return {
|
||||
requiredFee: tokenWeiFee,
|
||||
expectedMaxCost: gasEstimate.mul(maxFeePerGas),
|
||||
gasEstimate,
|
||||
bundleOverheadGas,
|
||||
};
|
||||
}
|
||||
|
||||
async #checkBundle(
|
||||
bundle: Bundle,
|
||||
bundleOverheadGas?: BigNumber,
|
||||
): Promise<
|
||||
{
|
||||
success: boolean;
|
||||
fee: BigNumber;
|
||||
gasEstimate: BigNumber;
|
||||
bundleOverheadGas?: BigNumber;
|
||||
expectedFee: BigNumber;
|
||||
requiredFee: BigNumber;
|
||||
}> => {
|
||||
if (n === 0) {
|
||||
expectedMaxCost: BigNumber;
|
||||
errorReason?: OperationResultError;
|
||||
}
|
||||
> {
|
||||
return await this.#checkBundleSemaphore.use(async () => {
|
||||
const [
|
||||
feeInfo,
|
||||
[{ success, fee, errorReason }],
|
||||
] = await Promise.all([
|
||||
this.#measureFeeInfo(
|
||||
bundle,
|
||||
bundleOverheadGas,
|
||||
),
|
||||
this.#measureFees([bundle]),
|
||||
]);
|
||||
|
||||
if (success && feeInfo && fee.lt(feeInfo.requiredFee)) {
|
||||
return {
|
||||
success: true,
|
||||
fee: BigNumber.from(0),
|
||||
requiredFee: BigNumber.from(0),
|
||||
success: false,
|
||||
gasEstimate: feeInfo.gasEstimate,
|
||||
bundleOverheadGas: feeInfo.bundleOverheadGas,
|
||||
expectedFee: fee,
|
||||
requiredFee: feeInfo.requiredFee,
|
||||
expectedMaxCost: feeInfo.expectedMaxCost,
|
||||
errorReason: { message: "Insufficient fee" },
|
||||
};
|
||||
}
|
||||
|
||||
const fee = bigSum([
|
||||
previousFee.fee,
|
||||
...fees.slice(0, n).map((r) => r.fee),
|
||||
]);
|
||||
const gasEstimate = feeInfo?.gasEstimate ??
|
||||
await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
|
||||
const requiredFee = await this.#measureRequiredFee(
|
||||
this.blsWalletSigner.aggregate([
|
||||
previousAggregateBundle,
|
||||
...bundles.slice(0, n),
|
||||
]),
|
||||
);
|
||||
return {
|
||||
success,
|
||||
gasEstimate,
|
||||
expectedFee: fee,
|
||||
requiredFee: feeInfo?.requiredFee ?? BigNumber.from(0),
|
||||
expectedMaxCost: feeInfo?.expectedMaxCost ?? BigNumber.from(0),
|
||||
errorReason,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const success = fee.gte(requiredFee);
|
||||
async measureBundleOverheadGas() {
|
||||
// The simple way to do this would be to estimate the gas of an empty
|
||||
// bundle. However, an empty bundle is a bit of a special case, in
|
||||
// particular the on-chain BLS library outright refuses to validate it. So
|
||||
// instead we estimate one operation and two operations and extrapolate
|
||||
// backwards to zero operations.
|
||||
|
||||
return { success, fee, requiredFee };
|
||||
};
|
||||
const es = this.ethereumService;
|
||||
const wallet = es.blsWalletWrapper;
|
||||
|
||||
// This calculation is entirely local and cheap. It can find a failing
|
||||
// bundle, but it might not be the *first* failing bundle.
|
||||
const fastFailureIndex = (() => {
|
||||
for (let i = 0; i < len; i++) {
|
||||
// If the actual call failed then we consider it a failure, even if the
|
||||
// fee is somehow met (e.g. if zero fee is required).
|
||||
if (fees[i].success === false) {
|
||||
return i;
|
||||
}
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
// Because the required fee mostly comes from the calldata size, this
|
||||
// should find the first insufficient fee most of the time.
|
||||
const lowerBound = this.#measureRequiredFeeLowerBound(bundles[i]);
|
||||
// It's a requirement that this wallet has already been created. Otherwise,
|
||||
// wallet creation would be included in the bundle overhead.
|
||||
assert(nonce.gt(0));
|
||||
|
||||
if (fees[i].fee.lt(lowerBound)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
})();
|
||||
const bundle1 = wallet.sign({ nonce, actions: [] });
|
||||
const bundle2 = wallet.sign({ nonce: nonce.add(1), actions: [] });
|
||||
|
||||
let left = 0;
|
||||
let leftRequiredFee = BigNumber.from(0);
|
||||
let right: number;
|
||||
let rightRequiredFee: BigNumber;
|
||||
const [oneOpGasEstimate, twoOpGasEstimate] = await Promise.all([
|
||||
es.verificationGateway.estimateGas.processBundle(bundle1),
|
||||
es.verificationGateway.estimateGas.processBundle(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
if (fastFailureIndex !== nil) {
|
||||
// Having a fast failure index is not enough because it might not be the
|
||||
// first. To establish that it really is the first, we need to ensure that
|
||||
// all bundles up to that index are ok (indeed, this is the assumption
|
||||
// that is relied upon outside - that the subset before the first failing
|
||||
// index can proceed without further checking).
|
||||
const opMarginalGasEstimate = twoOpGasEstimate.sub(oneOpGasEstimate);
|
||||
|
||||
const { success, requiredFee } = await checkFirstN(fastFailureIndex);
|
||||
return oneOpGasEstimate.sub(opMarginalGasEstimate);
|
||||
}
|
||||
|
||||
if (success) {
|
||||
return fastFailureIndex;
|
||||
}
|
||||
|
||||
// In case of failure, we now know there as a failing index in a more
|
||||
// narrow range, so we can at least restrict the bisect to this smaller
|
||||
// range.
|
||||
right = fastFailureIndex;
|
||||
rightRequiredFee = requiredFee;
|
||||
} else {
|
||||
// If we don't have a failing index, we still need to establish that there
|
||||
// is a failing index to be found. This is because it's a requirement of
|
||||
// the upcoming bisect logic that there is a failing bundle in
|
||||
// `bundles.slice(left, right)`.
|
||||
|
||||
const { success, requiredFee } = await checkFirstN(bundles.length);
|
||||
|
||||
if (success) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
right = bundles.length;
|
||||
rightRequiredFee = requiredFee;
|
||||
async #TokenDecimals(): Promise<number> {
|
||||
if (this.#tokenDecimals === nil) {
|
||||
const token = this.#FeeToken();
|
||||
assert(token !== nil);
|
||||
this.#tokenDecimals = await token.decimals();
|
||||
}
|
||||
|
||||
// Do a bisect to narrow in on the (first) culprit.
|
||||
while (right - left > 1) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
|
||||
const { success, requiredFee } = await checkFirstN(mid);
|
||||
|
||||
if (success) {
|
||||
left = mid;
|
||||
leftRequiredFee = requiredFee;
|
||||
} else {
|
||||
right = mid;
|
||||
rightRequiredFee = requiredFee;
|
||||
}
|
||||
}
|
||||
|
||||
assert(right - left === 1, "bisect should identify a single result");
|
||||
|
||||
// The bisect procedure maintains that the culprit is a member of
|
||||
// `bundles.slice(left, right)`. That's now equivalent to `[bundles[left]]`,
|
||||
// so `left` is our culprit index.
|
||||
|
||||
const bundleFee = fees[left].fee;
|
||||
const bundleRequiredFee = rightRequiredFee.sub(leftRequiredFee);
|
||||
|
||||
// Tracking the fees so that we can include this assertion isn't strictly
|
||||
// necessary. But the cost is negligible and should help troubleshooting a
|
||||
// lot if something goes wrong.
|
||||
assert(bundleFee.lt(bundleRequiredFee));
|
||||
|
||||
return left;
|
||||
return this.#tokenDecimals;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ import BundleHandler from "./helpers/BundleHandler.ts";
|
||||
import AggregationStrategy from "./AggregationStrategy.ts";
|
||||
import AsyncReturnType from "../helpers/AsyncReturnType.ts";
|
||||
import ClientReportableError from "./helpers/ClientReportableError.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
import never from "./helpers/never.ts";
|
||||
|
||||
export default function AggregationStrategyRouter(
|
||||
aggregationStrategy: AggregationStrategy,
|
||||
@@ -28,7 +30,19 @@ export default function AggregationStrategyRouter(
|
||||
}
|
||||
|
||||
ctx.response.body = {
|
||||
feeType: aggregationStrategy.config.fees.type,
|
||||
feeType: (() => {
|
||||
const feesConfig = aggregationStrategy.config.fees;
|
||||
|
||||
if (feesConfig === nil || feesConfig.type === "ether") {
|
||||
return "ether";
|
||||
}
|
||||
|
||||
if (feesConfig.type === "token") {
|
||||
return `token:${feesConfig.address}`;
|
||||
}
|
||||
|
||||
never(feesConfig);
|
||||
})(),
|
||||
feeDetected: result.feeDetected.toString(),
|
||||
feeRequired: result.feeRequired.toString(),
|
||||
successes: result.successes,
|
||||
|
||||
@@ -1,9 +1,38 @@
|
||||
import { HTTPMethods } from "../../deps.ts";
|
||||
|
||||
type AppEvent = (
|
||||
type AppEvent =
|
||||
| { type: "listening"; data: { port: number } }
|
||||
| { type: "db-query"; data: { sql: string; params: unknown[] } }
|
||||
| { type: "db-query"; data: { sql: string; params: unknown } }
|
||||
| { type: "waiting-unconfirmed-space" }
|
||||
| {
|
||||
type: "running-strategy";
|
||||
data: {
|
||||
eligibleRows: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "completed-strategy";
|
||||
data: {
|
||||
includedRows: number;
|
||||
bundleOverheadCost: string;
|
||||
expectedFee: string;
|
||||
expectedMaxCost: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "failed-row";
|
||||
data: {
|
||||
publicKeyShorts: string[];
|
||||
submitError?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "aggregate-bundle-unprofitable";
|
||||
data: {
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
| { type: "unprofitable-despite-breakeven-operations" }
|
||||
| {
|
||||
type: "submission-attempt";
|
||||
data: { publicKeyShorts: string[]; attemptNumber: number };
|
||||
@@ -19,7 +48,16 @@ type AppEvent = (
|
||||
| { type: "submission-sent"; data: { hash: string } }
|
||||
| {
|
||||
type: "submission-confirmed";
|
||||
data: { hash: string; bundleHashes: string[], blockNumber: number };
|
||||
data: {
|
||||
hash: string;
|
||||
bundleHashes: string[];
|
||||
blockNumber: number;
|
||||
profit: string;
|
||||
cost: string;
|
||||
expectedMaxCost: string;
|
||||
actualFee: string;
|
||||
expectedFee: string;
|
||||
};
|
||||
}
|
||||
| { type: "warning"; data: string }
|
||||
| {
|
||||
@@ -48,7 +86,6 @@ type AppEvent = (
|
||||
status: number;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AppEvent;
|
||||
|
||||
@@ -23,15 +23,20 @@ export default function BundleRouter(bundleService: BundleService) {
|
||||
|
||||
router.get(
|
||||
"bundleReceipt/:hash",
|
||||
async (ctx) => {
|
||||
const receipt = await bundleService.lookupReceipt(ctx.params.hash!);
|
||||
(ctx) => {
|
||||
const bundleRow = bundleService.lookupBundle(ctx.params.hash!);
|
||||
|
||||
if (receipt === nil) {
|
||||
if (bundleRow?.receipt === nil) {
|
||||
ctx.response.status = 404;
|
||||
|
||||
ctx.response.body = {
|
||||
submitError: bundleRow?.submitError,
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = receipt;
|
||||
ctx.response.body = bundleService.receiptFromBundle(bundleRow);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import {
|
||||
BigNumber,
|
||||
BlsWalletSigner,
|
||||
BlsWalletWrapper,
|
||||
Bundle,
|
||||
delay,
|
||||
ethers,
|
||||
QueryClient,
|
||||
Semaphore,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import { IClock } from "../helpers/Clock.ts";
|
||||
@@ -18,17 +19,18 @@ import runQueryGroup from "./runQueryGroup.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import BundleTable, { BundleRow, makeHash } from "./BundleTable.ts";
|
||||
import countActions from "./helpers/countActions.ts";
|
||||
import plus from "./helpers/plus.ts";
|
||||
import AggregationStrategy from "./AggregationStrategy.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
|
||||
export type AddBundleResponse = { hash: string } | { failures: TransactionFailure[] };
|
||||
export type AddBundleResponse = { hash: string } | {
|
||||
failures: TransactionFailure[];
|
||||
};
|
||||
|
||||
export default class BundleService {
|
||||
static defaultConfig = {
|
||||
bundleQueryLimit: env.BUNDLE_QUERY_LIMIT,
|
||||
maxAggregationSize: env.MAX_AGGREGATION_SIZE,
|
||||
breakevenOperationCount: env.BREAKEVEN_OPERATION_COUNT,
|
||||
maxAggregationDelayMillis: env.MAX_AGGREGATION_DELAY_MILLIS,
|
||||
maxUnconfirmedAggregations: env.MAX_UNCONFIRMED_AGGREGATIONS,
|
||||
maxEligibilityDelay: env.MAX_ELIGIBILITY_DELAY,
|
||||
@@ -38,12 +40,7 @@ export default class BundleService {
|
||||
unconfirmedActionCount = 0;
|
||||
unconfirmedRowIds = new Set<number>();
|
||||
|
||||
// TODO (merge-ok) use database table in the future to persist
|
||||
confirmedBundles = new Map<string, {
|
||||
bundle: Bundle,
|
||||
receipt: ethers.ContractReceipt,
|
||||
}>();
|
||||
|
||||
submissionSemaphore: Semaphore;
|
||||
submissionTimer: SubmissionTimer;
|
||||
submissionsInProgress = 0;
|
||||
|
||||
@@ -54,7 +51,6 @@ export default class BundleService {
|
||||
constructor(
|
||||
public emit: (evt: AppEvent) => void,
|
||||
public clock: IClock,
|
||||
public queryClient: QueryClient,
|
||||
public bundleTableMutex: Mutex,
|
||||
public bundleTable: BundleTable,
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
@@ -62,25 +58,24 @@ export default class BundleService {
|
||||
public aggregationStrategy: AggregationStrategy,
|
||||
public config = BundleService.defaultConfig,
|
||||
) {
|
||||
this.submissionSemaphore = new Semaphore(config.maxUnconfirmedAggregations);
|
||||
|
||||
this.submissionTimer = new SubmissionTimer(
|
||||
clock,
|
||||
config.maxAggregationDelayMillis,
|
||||
() => this.runSubmission(),
|
||||
);
|
||||
|
||||
(async () => {
|
||||
await delay(100);
|
||||
|
||||
while (!this.stopping) {
|
||||
this.tryAggregating();
|
||||
// TODO (merge-ok): Stop if there aren't any bundles?
|
||||
await this.ethereumService.waitForNextBlock();
|
||||
}
|
||||
})();
|
||||
this.ethereumService.provider.on("block", this.handleBlock);
|
||||
}
|
||||
|
||||
handleBlock = () => {
|
||||
this.addTask(() => this.tryAggregating());
|
||||
};
|
||||
|
||||
async stop() {
|
||||
this.stopping = true;
|
||||
this.ethereumService.provider.off("block", this.handleBlock);
|
||||
await Promise.all(Array.from(this.pendingTaskPromises));
|
||||
this.stopped = true;
|
||||
}
|
||||
@@ -108,19 +103,19 @@ export default class BundleService {
|
||||
return;
|
||||
}
|
||||
|
||||
const eligibleRows = await this.bundleTable.findEligible(
|
||||
const eligibleRows = this.bundleTable.findEligible(
|
||||
await this.ethereumService.BlockNumber(),
|
||||
this.config.bundleQueryLimit,
|
||||
);
|
||||
|
||||
const actionCount = eligibleRows
|
||||
const opCount = eligibleRows
|
||||
.filter((r) => !this.unconfirmedRowIds.has(r.id))
|
||||
.map((r) => countActions(r.bundle))
|
||||
.map((r) => r.bundle.operations.length)
|
||||
.reduce(plus, 0);
|
||||
|
||||
if (actionCount >= this.config.maxAggregationSize) {
|
||||
if (opCount >= this.config.breakevenOperationCount) {
|
||||
this.submissionTimer.trigger();
|
||||
} else if (actionCount > 0) {
|
||||
} else if (opCount > 0) {
|
||||
this.submissionTimer.notifyActive();
|
||||
} else {
|
||||
this.submissionTimer.clear();
|
||||
@@ -130,8 +125,8 @@ export default class BundleService {
|
||||
runQueryGroup<T>(body: () => Promise<T>): Promise<T> {
|
||||
return runQueryGroup(
|
||||
this.emit,
|
||||
(sql) => this.bundleTable.dbQuery(sql),
|
||||
this.bundleTableMutex,
|
||||
this.queryClient,
|
||||
body,
|
||||
);
|
||||
}
|
||||
@@ -151,15 +146,24 @@ export default class BundleService {
|
||||
};
|
||||
}
|
||||
|
||||
const signedCorrectly = this.blsWalletSigner.verify(bundle);
|
||||
const walletAddresses = await Promise.all(bundle.senderPublicKeys.map(
|
||||
(pubKey) =>
|
||||
BlsWalletWrapper.AddressFromPublicKey(
|
||||
pubKey,
|
||||
this.ethereumService.verificationGateway,
|
||||
),
|
||||
));
|
||||
|
||||
const failures: TransactionFailure[] = [];
|
||||
|
||||
if (signedCorrectly === false) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: "invalid signature",
|
||||
});
|
||||
for (const walletAddr of walletAddresses) {
|
||||
const signedCorrectly = this.blsWalletSigner.verify(bundle, walletAddr);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: `invalid signature for wallet address ${walletAddr}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
failures.push(...await this.ethereumService.checkNonces(bundle));
|
||||
@@ -171,7 +175,8 @@ export default class BundleService {
|
||||
return await this.runQueryGroup(async () => {
|
||||
const hash = makeHash();
|
||||
|
||||
await this.bundleTable.add({
|
||||
this.bundleTable.add({
|
||||
status: "pending",
|
||||
hash,
|
||||
bundle,
|
||||
eligibleAfter: await this.ethereumService.BlockNumber(),
|
||||
@@ -192,31 +197,46 @@ export default class BundleService {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO (merge-ok) Remove lint ignore when this hits db
|
||||
// deno-lint-ignore require-await
|
||||
async lookupReceipt(hash: string) {
|
||||
const confirmation = this.confirmedBundles.get(hash);
|
||||
lookupBundle(hash: string) {
|
||||
return this.bundleTable.findBundle(hash);
|
||||
}
|
||||
|
||||
if (!confirmation) {
|
||||
receiptFromBundle(bundle: BundleRow) {
|
||||
if (!bundle.receipt) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const receipt = confirmation.receipt;
|
||||
const { receipt, hash } = bundle;
|
||||
|
||||
return {
|
||||
bundleHash: hash,
|
||||
to: receipt.to,
|
||||
from: receipt.from,
|
||||
contractAddress: receipt.contractAddress,
|
||||
transactionIndex: receipt.transactionIndex,
|
||||
root: receipt.root,
|
||||
gasUsed: receipt.gasUsed,
|
||||
logsBloom: receipt.logsBloom,
|
||||
blockHash: receipt.blockHash,
|
||||
transactionHash: receipt.transactionHash,
|
||||
logs: receipt.logs,
|
||||
blockNumber: receipt.blockNumber,
|
||||
confirmations: receipt.confirmations,
|
||||
cumulativeGasUsed: receipt.cumulativeGasUsed,
|
||||
effectiveGasPrice: receipt.effectiveGasPrice,
|
||||
byzantium: receipt.byzantium,
|
||||
type: receipt.type,
|
||||
status: receipt.status,
|
||||
};
|
||||
}
|
||||
|
||||
async runSubmission() {
|
||||
this.submissionsInProgress++;
|
||||
|
||||
const submissionResult = await this.runQueryGroup(async () => {
|
||||
const bundleSubmitted = await this.runQueryGroup(async () => {
|
||||
const currentBlockNumber = await this.ethereumService.BlockNumber();
|
||||
|
||||
let eligibleRows = await this.bundleTable.findEligible(
|
||||
let eligibleRows = this.bundleTable.findEligible(
|
||||
currentBlockNumber,
|
||||
this.config.bundleQueryLimit,
|
||||
);
|
||||
@@ -226,38 +246,80 @@ export default class BundleService {
|
||||
(row) => !this.unconfirmedRowIds.has(row.id),
|
||||
);
|
||||
|
||||
const { aggregateBundle, includedRows, failedRows } = await this
|
||||
this.emit({
|
||||
type: "running-strategy",
|
||||
data: {
|
||||
eligibleRows: eligibleRows.length,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
bundleOverheadCost,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
failedRows,
|
||||
} = await this
|
||||
.aggregationStrategy.run(eligibleRows);
|
||||
|
||||
this.emit({
|
||||
type: "completed-strategy",
|
||||
data: {
|
||||
includedRows: includedRows.length,
|
||||
bundleOverheadCost: ethers.utils.formatEther(bundleOverheadCost),
|
||||
expectedFee: ethers.utils.formatEther(expectedFee),
|
||||
expectedMaxCost: ethers.utils.formatEther(expectedMaxCost),
|
||||
},
|
||||
});
|
||||
|
||||
for (const failedRow of failedRows) {
|
||||
await this.handleFailedRow(failedRow, currentBlockNumber);
|
||||
this.emit({
|
||||
type: "failed-row",
|
||||
data: {
|
||||
publicKeyShorts: failedRow.bundle.senderPublicKeys.map(
|
||||
toShortPublicKey,
|
||||
),
|
||||
submitError: failedRow.submitError,
|
||||
},
|
||||
});
|
||||
|
||||
this.handleFailedRow(failedRow, currentBlockNumber);
|
||||
}
|
||||
|
||||
if (!aggregateBundle || includedRows.length === 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
await this.submitAggregateBundle(
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
);
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
this.submissionsInProgress--;
|
||||
this.addTask(() => this.tryAggregating());
|
||||
|
||||
return submissionResult;
|
||||
if (bundleSubmitted) {
|
||||
this.addTask(() => this.tryAggregating());
|
||||
}
|
||||
}
|
||||
|
||||
async handleFailedRow(row: BundleRow, currentBlockNumber: BigNumber) {
|
||||
handleFailedRow(row: BundleRow, currentBlockNumber: BigNumber) {
|
||||
if (row.nextEligibilityDelay.lte(this.config.maxEligibilityDelay)) {
|
||||
await this.bundleTable.update({
|
||||
this.bundleTable.update({
|
||||
...row,
|
||||
eligibleAfter: currentBlockNumber.add(row.nextEligibilityDelay),
|
||||
nextEligibilityDelay: row.nextEligibilityDelay.mul(2),
|
||||
});
|
||||
} else {
|
||||
await this.bundleTable.remove(row);
|
||||
this.bundleTable.update({
|
||||
...row,
|
||||
status: "failed",
|
||||
});
|
||||
}
|
||||
|
||||
this.unconfirmedRowIds.delete(row.id);
|
||||
@@ -266,23 +328,10 @@ export default class BundleService {
|
||||
async submitAggregateBundle(
|
||||
aggregateBundle: Bundle,
|
||||
includedRows: BundleRow[],
|
||||
expectedFee: BigNumber,
|
||||
expectedMaxCost: BigNumber,
|
||||
) {
|
||||
const maxUnconfirmedActions = (
|
||||
this.config.maxUnconfirmedAggregations *
|
||||
this.config.maxAggregationSize
|
||||
);
|
||||
|
||||
const actionCount = countActions(aggregateBundle);
|
||||
|
||||
while (
|
||||
this.unconfirmedActionCount + actionCount > maxUnconfirmedActions
|
||||
) {
|
||||
// FIXME (merge-ok): Polling
|
||||
this.emit({ type: "waiting-unconfirmed-space" });
|
||||
await delay(1000);
|
||||
}
|
||||
|
||||
this.unconfirmedActionCount += actionCount;
|
||||
const releaseSemaphore = await this.submissionSemaphore.acquire();
|
||||
this.unconfirmedBundles.add(aggregateBundle);
|
||||
|
||||
for (const row of includedRows) {
|
||||
@@ -291,36 +340,53 @@ export default class BundleService {
|
||||
|
||||
this.addTask(async () => {
|
||||
try {
|
||||
const balanceBefore = await this.ethereumService.wallet.getBalance();
|
||||
|
||||
const receipt = await this.ethereumService.submitBundle(
|
||||
aggregateBundle,
|
||||
Infinity,
|
||||
300,
|
||||
);
|
||||
|
||||
const balanceAfter = await this.ethereumService.wallet.getBalance();
|
||||
|
||||
for (const row of includedRows) {
|
||||
this.confirmedBundles.set(row.hash, {
|
||||
bundle: row.bundle,
|
||||
this.bundleTable.update({
|
||||
...row,
|
||||
receipt,
|
||||
status: "confirmed",
|
||||
});
|
||||
}
|
||||
|
||||
const profit = balanceAfter.sub(balanceBefore);
|
||||
|
||||
/** What we paid to process the bundle */
|
||||
const cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
|
||||
|
||||
/** Fees collected from users */
|
||||
const actualFee = profit.add(cost);
|
||||
|
||||
this.emit({
|
||||
type: "submission-confirmed",
|
||||
data: {
|
||||
hash: receipt.transactionHash,
|
||||
bundleHashes: includedRows.map((row) => row.hash),
|
||||
blockNumber: receipt.blockNumber,
|
||||
profit: ethers.utils.formatEther(profit),
|
||||
cost: ethers.utils.formatEther(cost),
|
||||
expectedMaxCost: ethers.utils.formatEther(expectedMaxCost),
|
||||
actualFee: ethers.utils.formatEther(actualFee),
|
||||
expectedFee: ethers.utils.formatEther(expectedFee),
|
||||
},
|
||||
});
|
||||
|
||||
await this.bundleTable.remove(...includedRows);
|
||||
} finally {
|
||||
this.unconfirmedActionCount -= actionCount;
|
||||
this.unconfirmedBundles.delete(aggregateBundle);
|
||||
|
||||
for (const row of includedRows) {
|
||||
this.unconfirmedRowIds.delete(row.id);
|
||||
}
|
||||
|
||||
releaseSemaphore();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,38 +3,47 @@ import {
|
||||
Bundle,
|
||||
bundleFromDto,
|
||||
bundleToDto,
|
||||
Constraint,
|
||||
CreateTableMode,
|
||||
DataType,
|
||||
ethers,
|
||||
QueryClient,
|
||||
QueryTable,
|
||||
TableOptions,
|
||||
unsketchify,
|
||||
sqlite,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import assertExists from "../helpers/assertExists.ts";
|
||||
import ExplicitAny from "../helpers/ExplicitAny.ts";
|
||||
import { parseBundleDto } from "./parsers.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
import assert from "../helpers/assert.ts";
|
||||
|
||||
/**
|
||||
* Representation used when talking to the database. It's 'raw' in the sense
|
||||
* that it only uses primitive types, because the database cannot know about
|
||||
* custom classes like BigNumber.
|
||||
*
|
||||
* Note that this isn't as raw as it used to be - sqlite returns each row as an
|
||||
* array. This is still the raw representation of each field though.
|
||||
*/
|
||||
type RawRow = {
|
||||
id: number;
|
||||
status: string;
|
||||
hash: string;
|
||||
bundle: string;
|
||||
eligibleAfter: string;
|
||||
nextEligibilityDelay: string;
|
||||
submitError: string | null;
|
||||
receipt: string | null;
|
||||
};
|
||||
|
||||
const BundleStatuses = ["pending", "confirmed", "failed"] as const;
|
||||
type BundleStatus = typeof BundleStatuses[number];
|
||||
|
||||
type Row = {
|
||||
id: number;
|
||||
status: BundleStatus;
|
||||
hash: string;
|
||||
bundle: Bundle;
|
||||
eligibleAfter: BigNumber;
|
||||
nextEligibilityDelay: BigNumber;
|
||||
submitError?: string;
|
||||
receipt?: ethers.ContractReceipt;
|
||||
};
|
||||
|
||||
type InsertRow = Omit<Row, "id">;
|
||||
@@ -48,132 +57,233 @@ export function makeHash() {
|
||||
|
||||
export type BundleRow = Row;
|
||||
|
||||
const tableOptions: TableOptions = {
|
||||
id: { type: DataType.Serial, constraint: Constraint.PrimaryKey },
|
||||
hash: { type: DataType.VarChar },
|
||||
bundle: { type: DataType.VarChar },
|
||||
eligibleAfter: { type: DataType.VarChar },
|
||||
nextEligibilityDelay: { type: DataType.VarChar },
|
||||
};
|
||||
|
||||
function fromRawRow(rawRow: RawRow): Row {
|
||||
const parseResult = parseBundleDto(JSON.parse(rawRow.bundle));
|
||||
|
||||
if ("failures" in parseResult) {
|
||||
throw new Error(parseResult.failures.join("\n"));
|
||||
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
if (Array.isArray(rawRow)) {
|
||||
rawRow = {
|
||||
id: rawRow[0] as number,
|
||||
status: rawRow[1] as string,
|
||||
hash: rawRow[2] as string,
|
||||
bundle: rawRow[3] as string,
|
||||
eligibleAfter: rawRow[4] as string,
|
||||
nextEligibilityDelay: rawRow[5] as string,
|
||||
submitError: rawRow[6] as string | null,
|
||||
receipt: rawRow[7] as string | null,
|
||||
};
|
||||
}
|
||||
|
||||
const parseBundleResult = parseBundleDto(
|
||||
JSON.parse(rawRow.bundle),
|
||||
);
|
||||
|
||||
if ("failures" in parseBundleResult) {
|
||||
throw new Error(parseBundleResult.failures.join("\n"));
|
||||
}
|
||||
|
||||
const status = rawRow.status;
|
||||
if (!isValidStatus(status)) {
|
||||
throw new Error(`Not a valid bundle status: ${status}`);
|
||||
}
|
||||
|
||||
const rawReceipt = rawRow.receipt;
|
||||
|
||||
const receipt: ethers.ContractReceipt = rawReceipt
|
||||
? JSON.parse(rawReceipt)
|
||||
: nil;
|
||||
|
||||
return {
|
||||
...rawRow,
|
||||
bundle: bundleFromDto(parseResult.success),
|
||||
id: rawRow.id,
|
||||
status,
|
||||
hash: rawRow.hash,
|
||||
bundle: bundleFromDto(parseBundleResult.success),
|
||||
eligibleAfter: BigNumber.from(rawRow.eligibleAfter),
|
||||
nextEligibilityDelay: BigNumber.from(rawRow.nextEligibilityDelay),
|
||||
submitError: rawRow.submitError ?? nil,
|
||||
receipt,
|
||||
};
|
||||
}
|
||||
|
||||
function toInsertRawRow(row: InsertRow): InsertRawRow {
|
||||
return {
|
||||
...row,
|
||||
submitError: row.submitError ?? null,
|
||||
bundle: JSON.stringify(bundleToDto(row.bundle)),
|
||||
eligibleAfter: toUint256Hex(row.eligibleAfter),
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
};
|
||||
}
|
||||
|
||||
function toRawRow(row: Row): RawRow {
|
||||
return {
|
||||
...row,
|
||||
bundle: JSON.stringify(bundleToDto(row.bundle)),
|
||||
id: row.id,
|
||||
status: row.status,
|
||||
hash: row.hash,
|
||||
bundle: JSON.stringify(row.bundle),
|
||||
eligibleAfter: toUint256Hex(row.eligibleAfter),
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
submitError: row.submitError ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
};
|
||||
}
|
||||
|
||||
export default class BundleTable {
|
||||
queryTable: QueryTable<RawRow>;
|
||||
safeName: string;
|
||||
|
||||
private constructor(public queryClient: QueryClient, tableName: string) {
|
||||
this.queryTable = this.queryClient.table<RawRow>(tableName);
|
||||
this.safeName = unsketchify(this.queryTable.name);
|
||||
}
|
||||
|
||||
static async create(
|
||||
queryClient: QueryClient,
|
||||
tableName: string,
|
||||
): Promise<BundleTable> {
|
||||
const table = new BundleTable(queryClient, tableName);
|
||||
await table.queryTable.create(tableOptions, CreateTableMode.IfNotExists);
|
||||
|
||||
return table;
|
||||
}
|
||||
|
||||
static async createFresh(
|
||||
queryClient: QueryClient,
|
||||
tableName: string,
|
||||
constructor(
|
||||
public db: sqlite.DB,
|
||||
public onQuery = (_sql: string, _params?: sqlite.QueryParameterSet) => {},
|
||||
) {
|
||||
const table = new BundleTable(queryClient, tableName);
|
||||
await table.queryTable.drop(true);
|
||||
await table.queryTable.create(tableOptions, CreateTableMode.IfNotExists);
|
||||
|
||||
return table;
|
||||
this.dbQuery(`
|
||||
CREATE TABLE IF NOT EXISTS bundles (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
status TEXT NOT NULL,
|
||||
hash TEXT NOT NULL,
|
||||
bundle TEXT NOT NULL,
|
||||
eligibleAfter TEXT NOT NULL,
|
||||
nextEligibilityDelay TEXT NOT NULL,
|
||||
submitError TEXT,
|
||||
receipt TEXT
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
async add(...rows: InsertRow[]) {
|
||||
await this.queryTable.insert(...rows.map(toInsertRawRow));
|
||||
dbQuery(sql: string, params?: sqlite.QueryParameterSet) {
|
||||
this.onQuery(sql, params);
|
||||
return this.db.query(sql, params);
|
||||
}
|
||||
|
||||
async update(row: Row) {
|
||||
await this.queryTable.where({ id: row.id }).update(toRawRow(row));
|
||||
add(...rows: InsertRow[]) {
|
||||
for (const row of rows) {
|
||||
const rawRow = toInsertRawRow(row);
|
||||
|
||||
this.dbQuery(
|
||||
`
|
||||
INSERT INTO bundles (
|
||||
id,
|
||||
status,
|
||||
hash,
|
||||
bundle,
|
||||
eligibleAfter,
|
||||
nextEligibilityDelay,
|
||||
submitError,
|
||||
receipt
|
||||
) VALUES (
|
||||
:id,
|
||||
:status,
|
||||
:hash,
|
||||
:bundle,
|
||||
:eligibleAfter,
|
||||
:nextEligibilityDelay,
|
||||
:submitError,
|
||||
:receipt
|
||||
)
|
||||
`,
|
||||
{
|
||||
":status": rawRow.status,
|
||||
":hash": rawRow.hash,
|
||||
":bundle": rawRow.bundle,
|
||||
":eligibleAfter": rawRow.eligibleAfter,
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async remove(...rows: Row[]) {
|
||||
await Promise.all(rows.map((row) =>
|
||||
this.queryTable
|
||||
.where({ id: assertExists(row.id) })
|
||||
.delete()
|
||||
));
|
||||
}
|
||||
update(row: Row) {
|
||||
const rawRow = toRawRow(row);
|
||||
|
||||
async findEligible(blockNumber: BigNumber, limit: number) {
|
||||
const rows: RawRow[] = await this.queryClient.query(
|
||||
this.dbQuery(
|
||||
`
|
||||
SELECT * from ${this.safeName}
|
||||
UPDATE bundles
|
||||
SET
|
||||
status = :status,
|
||||
hash = :hash,
|
||||
bundle = :bundle,
|
||||
eligibleAfter = :eligibleAfter,
|
||||
nextEligibilityDelay = :nextEligibilityDelay,
|
||||
submitError = :submitError,
|
||||
receipt = :receipt
|
||||
WHERE
|
||||
"eligibleAfter" <= '${toUint256Hex(blockNumber)}'
|
||||
ORDER BY "id" ASC
|
||||
LIMIT ${limit}
|
||||
id = :id
|
||||
`,
|
||||
{
|
||||
":id": rawRow.id,
|
||||
":status": rawRow.status,
|
||||
":hash": rawRow.hash,
|
||||
":bundle": rawRow.bundle,
|
||||
":eligibleAfter": rawRow.eligibleAfter,
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
remove(...rows: Row[]) {
|
||||
for (const row of rows) {
|
||||
this.dbQuery(
|
||||
"DELETE FROM bundles WHERE id = :id",
|
||||
{ ":id": assertExists(row.id) },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
findEligible(blockNumber: BigNumber, limit: number): Row[] {
|
||||
const rows = this.dbQuery(
|
||||
`
|
||||
SELECT * from bundles
|
||||
WHERE
|
||||
eligibleAfter <= '${toUint256Hex(blockNumber)}' AND
|
||||
status = 'pending'
|
||||
ORDER BY id ASC
|
||||
LIMIT :limit
|
||||
`,
|
||||
{
|
||||
":limit": limit,
|
||||
},
|
||||
);
|
||||
|
||||
return rows.map(fromRawRow);
|
||||
}
|
||||
|
||||
async count(): Promise<bigint> {
|
||||
const result = await this.queryClient.query(
|
||||
`SELECT COUNT(*) FROM ${this.queryTable.name}`,
|
||||
findBundle(hash: string): Row | nil {
|
||||
const rows = this.dbQuery(
|
||||
"SELECT * from bundles WHERE hash = :hash",
|
||||
{ ":hash": hash },
|
||||
);
|
||||
return result[0].count as bigint;
|
||||
|
||||
return rows.map(fromRawRow)[0];
|
||||
}
|
||||
|
||||
async all(): Promise<Row[]> {
|
||||
const rawRows: RawRow[] = await this.queryClient.query(
|
||||
`SELECT * FROM ${this.queryTable.name}`,
|
||||
count(): number {
|
||||
const result = this.dbQuery("SELECT COUNT(*) FROM bundles")[0][0];
|
||||
assert(typeof result === "number");
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
all(): Row[] {
|
||||
const rawRows = this.dbQuery(
|
||||
"SELECT * FROM bundles",
|
||||
);
|
||||
|
||||
return rawRows.map(fromRawRow);
|
||||
}
|
||||
|
||||
async drop() {
|
||||
await this.queryTable.drop(true);
|
||||
drop() {
|
||||
this.dbQuery("DROP TABLE bundles");
|
||||
}
|
||||
|
||||
async clear() {
|
||||
return await this.queryClient.query(`
|
||||
DELETE from ${this.safeName}
|
||||
`);
|
||||
clear() {
|
||||
this.dbQuery("DELETE from bundles");
|
||||
}
|
||||
}
|
||||
|
||||
function toUint256Hex(n: BigNumber) {
|
||||
return `0x${n.toHexString().slice(2).padStart(64, "0")}`;
|
||||
}
|
||||
|
||||
function isValidStatus(status: unknown): status is BundleStatus {
|
||||
return typeof status === "string" &&
|
||||
BundleStatuses.includes(status as ExplicitAny);
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import AppEvent from "./AppEvent.ts";
|
||||
import toPublicKeyShort from "./helpers/toPublicKeyShort.ts";
|
||||
import AsyncReturnType from "../helpers/AsyncReturnType.ts";
|
||||
import ExplicitAny from "../helpers/ExplicitAny.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
|
||||
export type TxCheckResult = {
|
||||
failures: TransactionFailure[];
|
||||
@@ -42,10 +43,9 @@ type CallHelper<T> = {
|
||||
resultDecoder: (result: BytesLike) => T;
|
||||
};
|
||||
|
||||
type CallResult<T> = (
|
||||
type CallResult<T> =
|
||||
| { success: true; returnValue: T }
|
||||
| { success: false; returnValue: undefined }
|
||||
);
|
||||
| { success: false; returnValue: undefined };
|
||||
|
||||
type MapCallHelperReturns<T> = T extends CallHelper<unknown>[]
|
||||
? (T extends [CallHelper<infer First>, ...infer Rest]
|
||||
@@ -71,6 +71,8 @@ export default class EthereumService {
|
||||
constructor(
|
||||
public emit: (evt: AppEvent) => void,
|
||||
public wallet: Wallet,
|
||||
public provider: ethers.providers.Provider,
|
||||
public blsWalletWrapper: BlsWalletWrapper,
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
verificationGatewayAddress: string,
|
||||
utilitiesAddress: string,
|
||||
@@ -99,14 +101,49 @@ export default class EthereumService {
|
||||
utilitiesAddress: string,
|
||||
aggPrivateKey: string,
|
||||
): Promise<EthereumService> {
|
||||
const wallet = EthereumService.Wallet(aggPrivateKey);
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
provider.pollingInterval = env.RPC_POLLING_INTERVAL;
|
||||
const wallet = EthereumService.Wallet(provider, aggPrivateKey);
|
||||
|
||||
const blsWalletWrapper = await BlsWalletWrapper.connect(
|
||||
aggPrivateKey,
|
||||
verificationGatewayAddress,
|
||||
provider,
|
||||
);
|
||||
|
||||
const blsNonce = await blsWalletWrapper.Nonce();
|
||||
|
||||
if (blsNonce.eq(0)) {
|
||||
if (!env.AUTO_CREATE_INTERNAL_BLS_WALLET) {
|
||||
throw new Error([
|
||||
"Required internal bls wallet does not exist. Either enable",
|
||||
"AUTO_CREATE_INTERNAL_BLS_WALLET or run",
|
||||
"./programs/createInternalBlsWallet.ts",
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
await (await VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
wallet,
|
||||
).processBundle(blsWalletWrapper.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
}
|
||||
|
||||
const nextNonce = BigNumber.from(await wallet.getTransactionCount());
|
||||
const chainId = await wallet.getChainId();
|
||||
const blsWalletSigner = await initBlsWalletSigner({ chainId });
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey: aggPrivateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
|
||||
return new EthereumService(
|
||||
emit,
|
||||
wallet,
|
||||
provider,
|
||||
blsWalletWrapper,
|
||||
blsWalletSigner,
|
||||
verificationGatewayAddress,
|
||||
utilitiesAddress,
|
||||
@@ -116,16 +153,10 @@ export default class EthereumService {
|
||||
|
||||
async BlockNumber(): Promise<BigNumber> {
|
||||
return BigNumber.from(
|
||||
await this.wallet.provider.getBlockNumber(),
|
||||
await this.provider.getBlockNumber(),
|
||||
);
|
||||
}
|
||||
|
||||
async waitForNextBlock() {
|
||||
await new Promise((resolve) => {
|
||||
this.wallet.provider.once("block", resolve);
|
||||
});
|
||||
}
|
||||
|
||||
// TODO (merge-ok): Consider: We may want to fail operations
|
||||
// that are not at the next expected nonce, including all
|
||||
// current pending transactions for that wallet.
|
||||
@@ -211,10 +242,10 @@ export default class EthereumService {
|
||||
async callStaticSequenceWithMeasure<Measure, CallReturn>(
|
||||
measureCall: CallHelper<Measure>,
|
||||
calls: CallHelper<CallReturn>[],
|
||||
): (Promise<{
|
||||
): Promise<{
|
||||
measureResults: CallResult<Measure>[];
|
||||
callResults: CallResult<CallReturn>[];
|
||||
}>) {
|
||||
}> {
|
||||
const fullCalls: CallHelper<unknown>[] = [measureCall];
|
||||
|
||||
for (const call of calls) {
|
||||
@@ -261,7 +292,10 @@ export default class EthereumService {
|
||||
const processBundleArgs: Parameters<VerificationGateway["processBundle"]> =
|
||||
[
|
||||
bundle,
|
||||
{ nonce: this.NextNonce() },
|
||||
{
|
||||
nonce: this.NextNonce(),
|
||||
...await this.GasConfig(),
|
||||
},
|
||||
];
|
||||
|
||||
const attempt = async () => {
|
||||
@@ -331,8 +365,38 @@ export default class EthereumService {
|
||||
throw new Error("Expected return or throw from attempt loop");
|
||||
}
|
||||
|
||||
private static Wallet(privateKey: string) {
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
async GasConfig() {
|
||||
const block = await this.provider.getBlock("latest");
|
||||
const previousBaseFee = block.baseFeePerGas;
|
||||
assert(previousBaseFee !== null && previousBaseFee !== nil);
|
||||
|
||||
// Increase the basefee we're willing to pay to improve the chance of our
|
||||
// transaction getting included. As per EIP-1559, we only pay the actual
|
||||
// basefee anyway, *but* we also pass this fee onto users which don't have
|
||||
// this benefit (they'll pay regardless of where basefee lands).
|
||||
//
|
||||
// This means there's a tradeoff here - low values risk our transactions not
|
||||
// being included, high values pass on unnecessary fees to users.
|
||||
//
|
||||
const baseFeeIncrease = previousBaseFee.mul(
|
||||
env.PREVIOUS_BASE_FEE_PERCENT_INCREASE,
|
||||
).div(100);
|
||||
|
||||
return {
|
||||
maxFeePerGas: previousBaseFee
|
||||
.add(baseFeeIncrease)
|
||||
// Remember that basefee is burned, not provided to miners. Miners
|
||||
// *only* get the priority fee, so they have no reason to care about our
|
||||
// transaction if the priority fee is zero.
|
||||
.add(env.PRIORITY_FEE_PER_GAS),
|
||||
maxPriorityFeePerGas: env.PRIORITY_FEE_PER_GAS,
|
||||
};
|
||||
}
|
||||
|
||||
private static Wallet(
|
||||
provider: ethers.providers.Provider,
|
||||
privateKey: string,
|
||||
) {
|
||||
const wallet = new Wallet(privateKey, provider);
|
||||
|
||||
if (env.USE_TEST_NET) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Application, oakCors } from "../../deps.ts";
|
||||
import { Application, oakCors, sqlite } from "../../deps.ts";
|
||||
|
||||
import * as env from "../env.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
@@ -8,7 +8,6 @@ import AdminRouter from "./AdminRouter.ts";
|
||||
import AdminService from "./AdminService.ts";
|
||||
import errorHandler from "./errorHandler.ts";
|
||||
import notFoundHandler from "./notFoundHandler.ts";
|
||||
import createQueryClient from "./createQueryClient.ts";
|
||||
import Mutex from "../helpers/Mutex.ts";
|
||||
import Clock from "../helpers/Clock.ts";
|
||||
import getNetworkConfig from "../helpers/getNetworkConfig.ts";
|
||||
@@ -22,11 +21,18 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
|
||||
const clock = Clock.create();
|
||||
|
||||
const queryClient = createQueryClient(emit);
|
||||
const bundleTableMutex = new Mutex();
|
||||
const bundleTable = await BundleTable.create(
|
||||
queryClient,
|
||||
env.BUNDLE_TABLE_NAME,
|
||||
|
||||
const bundleTable = new BundleTable(
|
||||
new sqlite.DB(env.DB_PATH),
|
||||
(sql, params) => {
|
||||
if (env.LOG_QUERIES) {
|
||||
emit({
|
||||
type: "db-query",
|
||||
data: { sql, params },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const ethereumService = await EthereumService.create(
|
||||
@@ -39,12 +45,13 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
const aggregationStrategy = new AggregationStrategy(
|
||||
ethereumService.blsWalletSigner,
|
||||
ethereumService,
|
||||
AggregationStrategy.defaultConfig,
|
||||
emit,
|
||||
);
|
||||
|
||||
const bundleService = new BundleService(
|
||||
emit,
|
||||
clock,
|
||||
queryClient,
|
||||
bundleTableMutex,
|
||||
bundleTable,
|
||||
ethereumService.blsWalletSigner,
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { QueryClient } from "../../deps.ts";
|
||||
|
||||
import * as env from "../env.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
|
||||
export default function createQueryClient(
|
||||
emit: (evt: AppEvent) => void,
|
||||
/**
|
||||
* Sadly, there appears to be a singleton inside QueryClient, which forces us
|
||||
* to re-use it during testing.
|
||||
*/
|
||||
existingClient?: QueryClient,
|
||||
): QueryClient {
|
||||
const client = existingClient ?? new QueryClient({
|
||||
hostname: env.PG.HOST,
|
||||
port: env.PG.PORT,
|
||||
user: env.PG.USER,
|
||||
password: env.PG.PASSWORD,
|
||||
database: env.PG.DB_NAME,
|
||||
tls: {
|
||||
enforce: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (env.LOG_QUERIES) {
|
||||
const originalQuery = client.query.bind(client);
|
||||
|
||||
client.query = async (sql, params) => {
|
||||
emit({
|
||||
type: "db-query",
|
||||
data: { sql, params: params ?? [] },
|
||||
});
|
||||
|
||||
return await originalQuery(sql, params);
|
||||
};
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Bundle } from "../../../deps.ts";
|
||||
import plus from "./plus.ts";
|
||||
|
||||
export default function countActions(bundle: Bundle) {
|
||||
return bundle.operations.map((op) => op.actions.length).reduce(plus, 0);
|
||||
}
|
||||
3
aggregator/src/app/helpers/never.ts
Normal file
3
aggregator/src/app/helpers/never.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function never(value: never): never {
|
||||
throw new Error(`Unexpected value: ${value}`);
|
||||
}
|
||||
@@ -1,18 +1,17 @@
|
||||
import { QueryClient } from "../../deps.ts";
|
||||
import Mutex from "../helpers/Mutex.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
|
||||
export default async function runQueryGroup<T>(
|
||||
emit: (evt: AppEvent) => void,
|
||||
query: (sql: string) => void,
|
||||
mutex: Mutex,
|
||||
queryClient: QueryClient,
|
||||
body: () => Promise<T>,
|
||||
) {
|
||||
const lock = await mutex.Lock();
|
||||
let completed = false;
|
||||
|
||||
try {
|
||||
queryClient.query("BEGIN");
|
||||
query("BEGIN");
|
||||
const result = await body();
|
||||
completed = true;
|
||||
return result;
|
||||
@@ -25,6 +24,6 @@ export default async function runQueryGroup<T>(
|
||||
throw error;
|
||||
} finally {
|
||||
lock.release();
|
||||
await queryClient.query(completed ? "COMMIT" : "ROLLBACK");
|
||||
query(completed ? "COMMIT" : "ROLLBACK");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,49 @@
|
||||
import {
|
||||
optionalNumberEnv,
|
||||
requireBigNumberEnv,
|
||||
requireBoolEnv,
|
||||
requireEnv,
|
||||
requireIntEnv,
|
||||
requireNumberEnv,
|
||||
} from "./helpers/envTools.ts";
|
||||
import nil from "./helpers/nil.ts";
|
||||
|
||||
export const RPC_URL = requireEnv("RPC_URL");
|
||||
export const RPC_POLLING_INTERVAL = requireIntEnv("RPC_POLLING_INTERVAL");
|
||||
|
||||
export const ORIGIN = requireEnv("ORIGIN");
|
||||
export const PORT = requireIntEnv("PORT");
|
||||
|
||||
export const USE_TEST_NET = requireBoolEnv("USE_TEST_NET");
|
||||
|
||||
export const NETWORK_CONFIG_PATH = requireEnv("NETWORK_CONFIG_PATH");
|
||||
export const NETWORK_CONFIG_PATH = Deno.env.get("IS_DOCKER") === "true"
|
||||
? "/app/networkConfig.json"
|
||||
: requireEnv("NETWORK_CONFIG_PATH");
|
||||
|
||||
export const PRIVATE_KEY_AGG = requireEnv("PRIVATE_KEY_AGG");
|
||||
export const PRIVATE_KEY_ADMIN = requireEnv("PRIVATE_KEY_ADMIN");
|
||||
|
||||
export const PG = {
|
||||
HOST: requireEnv("PG_HOST"),
|
||||
PORT: requireEnv("PG_PORT"),
|
||||
USER: requireEnv("PG_USER"),
|
||||
PASSWORD: requireEnv("PG_PASSWORD"),
|
||||
DB_NAME: requireEnv("PG_DB_NAME"),
|
||||
};
|
||||
|
||||
export const BUNDLE_TABLE_NAME = requireEnv("BUNDLE_TABLE_NAME");
|
||||
export const DB_PATH = requireEnv("DB_PATH");
|
||||
|
||||
/**
|
||||
* Query limit used when processing potentially large numbers of bundles.
|
||||
* (Using batching if needed.)
|
||||
*/
|
||||
export const BUNDLE_QUERY_LIMIT = requireIntEnv("BUNDLE_QUERY_LIMIT");
|
||||
|
||||
/**
|
||||
* Maximum retry delay in blocks before a failed bundle is discarded.
|
||||
*/
|
||||
export const MAX_ELIGIBILITY_DELAY = requireIntEnv("MAX_ELIGIBILITY_DELAY");
|
||||
|
||||
export const MAX_AGGREGATION_SIZE = requireIntEnv("MAX_AGGREGATION_SIZE");
|
||||
/**
|
||||
* Approximate maximum gas of aggregate bundles.
|
||||
*
|
||||
* It's approximate because we use the sum of the marginal gas estimates and add
|
||||
* the bundle overhead, which is not exactly the same as the gas used when
|
||||
* putting the bundle together.
|
||||
*/
|
||||
export const MAX_GAS_PER_BUNDLE = requireIntEnv("MAX_GAS_PER_BUNDLE");
|
||||
|
||||
export const MAX_AGGREGATION_DELAY_MILLIS = requireIntEnv(
|
||||
"MAX_AGGREGATION_DELAY_MILLIS",
|
||||
@@ -48,10 +55,43 @@ export const MAX_UNCONFIRMED_AGGREGATIONS = requireIntEnv(
|
||||
|
||||
export const LOG_QUERIES = requireBoolEnv("LOG_QUERIES");
|
||||
|
||||
export const REQUIRE_FEES = requireBoolEnv("REQUIRE_FEES");
|
||||
|
||||
export const BREAKEVEN_OPERATION_COUNT = requireNumberEnv(
|
||||
"BREAKEVEN_OPERATION_COUNT",
|
||||
);
|
||||
|
||||
export const ALLOW_LOSSES = requireBoolEnv("ALLOW_LOSSES");
|
||||
|
||||
export const FEE_TYPE = requireEnv("FEE_TYPE");
|
||||
export const FEE_PER_GAS = requireBigNumberEnv("FEE_PER_GAS");
|
||||
export const FEE_PER_BYTE = requireBigNumberEnv("FEE_PER_BYTE");
|
||||
|
||||
if (!/^(ether|token:0x[0-9a-fA-F]*)$/.test(FEE_TYPE)) {
|
||||
throw new Error(`FEE_TYPE has invalid format: "${FEE_TYPE}"`);
|
||||
}
|
||||
|
||||
export const ETH_VALUE_IN_TOKENS = optionalNumberEnv("ETH_VALUE_IN_TOKENS");
|
||||
|
||||
if (FEE_TYPE.startsWith("token:") && ETH_VALUE_IN_TOKENS === nil) {
|
||||
throw new Error([
|
||||
"Missing ETH_VALUE_IN_TOKENS, which is required because FEE_TYPE is a",
|
||||
"token",
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
export const AUTO_CREATE_INTERNAL_BLS_WALLET = requireBoolEnv(
|
||||
"AUTO_CREATE_INTERNAL_BLS_WALLET",
|
||||
);
|
||||
|
||||
export const PRIORITY_FEE_PER_GAS = requireBigNumberEnv("PRIORITY_FEE_PER_GAS");
|
||||
|
||||
/**
|
||||
* Used to determine the expected basefee when submitting bundles. Note that
|
||||
* this gets passed onto users.
|
||||
*/
|
||||
export const PREVIOUS_BASE_FEE_PERCENT_INCREASE = requireNumberEnv(
|
||||
"PREVIOUS_BASE_FEE_PERCENT_INCREASE",
|
||||
);
|
||||
|
||||
export const BUNDLE_CHECKING_CONCURRENCY = requireIntEnv(
|
||||
"BUNDLE_CHECKING_CONCURRENCY",
|
||||
);
|
||||
|
||||
@@ -62,3 +62,19 @@ export function requireNumberEnv(envName: string): number {
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function optionalNumberEnv(envName: string): number | nil {
|
||||
const strValue = optionalEnv(envName);
|
||||
|
||||
if (strValue === nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const value = Number(strValue);
|
||||
|
||||
if (!Number.isFinite(value)) {
|
||||
throw new Error(`Failed to parse ${envName} as number: ${strValue}`);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import AggregationStrategy from "../src/app/AggregationStrategy.ts";
|
||||
import { BundleRow } from "../src/app/BundleTable.ts";
|
||||
import { assertEquals, BigNumber } from "./deps.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
import nil from "../src/helpers/nil.ts";
|
||||
import { assertEquals, BigNumber, ethers } from "./deps.ts";
|
||||
|
||||
import Fixture from "./helpers/Fixture.ts";
|
||||
|
||||
Fixture.test("zero fee estimate from default test config", async (fx) => {
|
||||
Fixture.test("nonzero fee estimate from default test config", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
@@ -23,11 +25,9 @@ Fixture.test("zero fee estimate from default test config", async (fx) => {
|
||||
|
||||
const feeEstimation = await fx.aggregationStrategy.estimateFee(bundle);
|
||||
|
||||
assertEquals(feeEstimation, {
|
||||
feeDetected: BigNumber.from(0),
|
||||
feeRequired: BigNumber.from(0),
|
||||
successes: [true],
|
||||
});
|
||||
assertEquals(feeEstimation.feeDetected, BigNumber.from(0));
|
||||
assert(feeEstimation.feeRequired.gt(0));
|
||||
assertEquals(feeEstimation.successes, [true]);
|
||||
});
|
||||
|
||||
Fixture.test("includes bundle in aggregation when estimated fee is provided", async (fx) => {
|
||||
@@ -37,12 +37,15 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
fx.blsWalletSigner,
|
||||
fx.ethereumService,
|
||||
{
|
||||
maxAggregationSize: 12,
|
||||
maxGasPerBundle: 1500000,
|
||||
fees: {
|
||||
type: `token:${fx.testErc20.address}`,
|
||||
perGas: BigNumber.from(1000000000),
|
||||
perByte: BigNumber.from(10000000000000),
|
||||
type: "token",
|
||||
address: fx.testErc20.address,
|
||||
allowLosses: true,
|
||||
breakevenOperationCount: 4.5,
|
||||
ethValueInTokens: 1300,
|
||||
},
|
||||
bundleCheckingConcurrency: 8,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -70,7 +73,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
|
||||
const feeEstimation = await aggregationStrategy.estimateFee(bundle);
|
||||
|
||||
const safetyDivisor = 100;
|
||||
const safetyDivisor = 5;
|
||||
const safetyPremium = feeEstimation.feeRequired.div(safetyDivisor);
|
||||
|
||||
// Due to small fluctuations is gas estimation, we add a little safety premium
|
||||
@@ -97,6 +100,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
|
||||
const bundleRow: BundleRow = {
|
||||
id: 0,
|
||||
status: "pending",
|
||||
hash: "0x0",
|
||||
bundle,
|
||||
eligibleAfter: BigNumber.from(0),
|
||||
@@ -105,9 +109,69 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
|
||||
const aggregationResult = await aggregationStrategy.run([bundleRow]);
|
||||
|
||||
assertEquals(aggregationResult, {
|
||||
aggregateBundle: bundle,
|
||||
includedRows: [bundleRow],
|
||||
failedRows: [],
|
||||
});
|
||||
assertEquals(aggregationResult.aggregateBundle, bundle);
|
||||
assertEquals(aggregationResult.includedRows, [bundleRow]);
|
||||
assertEquals(aggregationResult.failedRows, []);
|
||||
});
|
||||
|
||||
Fixture.test("includes submitError on failed row when bundle callStaticSequence fails", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const aggregationStrategy = new AggregationStrategy(
|
||||
fx.blsWalletSigner,
|
||||
fx.ethereumService,
|
||||
{
|
||||
maxGasPerBundle: 1500000,
|
||||
fees: {
|
||||
type: "token",
|
||||
address: fx.testErc20.address,
|
||||
allowLosses: true,
|
||||
breakevenOperationCount: 4.5,
|
||||
ethValueInTokens: 1300,
|
||||
},
|
||||
bundleCheckingConcurrency: 8,
|
||||
},
|
||||
);
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"transferFrom",
|
||||
[
|
||||
"0x0000000000000000000000000000000000000000",
|
||||
wallet.address,
|
||||
ethers.BigNumber.from(
|
||||
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||
),
|
||||
],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const bundleRow: BundleRow = {
|
||||
id: 0,
|
||||
status: "pending",
|
||||
hash: "0x0",
|
||||
bundle,
|
||||
eligibleAfter: BigNumber.from(0),
|
||||
nextEligibilityDelay: BigNumber.from(1),
|
||||
};
|
||||
|
||||
const aggregationResult = await aggregationStrategy.run([bundleRow]);
|
||||
|
||||
const expectedFailedRow = {
|
||||
...bundleRow,
|
||||
submitError: "ERC20: insufficient allowance",
|
||||
};
|
||||
|
||||
assertEquals(aggregationResult.aggregateBundle, nil);
|
||||
assertEquals(aggregationResult.includedRows, []);
|
||||
assertEquals(aggregationResult.failedRows, [expectedFailedRow]);
|
||||
});
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { assertEquals, assertBundleSucceeds, Operation } from "./deps.ts";
|
||||
import { assertBundleSucceeds, assertEquals, Operation } from "./deps.ts";
|
||||
|
||||
import Fixture from "./helpers/Fixture.ts";
|
||||
|
||||
Fixture.test("adds valid bundle", async (fx) => {
|
||||
const bundleService = await fx.createBundleService();
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const tx = wallet.sign({
|
||||
@@ -20,15 +20,15 @@ Fixture.test("adds valid bundle", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
const bundleService = await fx.createBundleService();
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet, otherWallet] = await fx.setupWallets(2);
|
||||
|
||||
const operation: Operation = {
|
||||
@@ -53,7 +53,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
// sig test)
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -62,11 +62,11 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
const bundleService = await fx.createBundleService();
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const tx = wallet.sign({
|
||||
@@ -83,7 +83,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -92,13 +92,13 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["duplicate-nonce"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test(
|
||||
"rejects bundle with invalid signature and nonce from the past",
|
||||
async (fx) => {
|
||||
const bundleService = await fx.createBundleService();
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet, otherWallet] = await fx.setupWallets(2);
|
||||
|
||||
const operation: Operation = {
|
||||
@@ -125,7 +125,7 @@ Fixture.test(
|
||||
// https://github.com/thehubbleproject/hubble-bls/pull/20
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -138,12 +138,12 @@ Fixture.test(
|
||||
);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
},
|
||||
);
|
||||
|
||||
Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
const bundleService = await fx.createBundleService();
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const tx = wallet.sign({
|
||||
@@ -160,11 +160,11 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
});
|
||||
|
||||
// TODO (merge-ok): Add a mechanism for limiting the number of stored
|
||||
@@ -172,7 +172,7 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
// Fixture.test(
|
||||
// "when future txs reach maxFutureTxs, the oldest ones are dropped",
|
||||
// async (fx) => {
|
||||
// const bundleService = await fx.createBundleService({
|
||||
// const bundleService = fx.createBundleService({
|
||||
// ...BundleService.defaultConfig,
|
||||
// maxFutureTxs: 3,
|
||||
// });
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import Range from "../src/helpers/Range.ts";
|
||||
import {
|
||||
assertEquals,
|
||||
assertBundleSucceeds,
|
||||
assertEquals,
|
||||
BigNumber,
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
@@ -14,23 +13,21 @@ import Fixture, {
|
||||
|
||||
const oneToken = ethers.utils.parseUnits("1.0", 18);
|
||||
|
||||
async function createBundleService(
|
||||
function createBundleService(
|
||||
fx: Fixture,
|
||||
feesOverride?: Partial<typeof aggregationStrategyDefaultTestConfig["fees"]>,
|
||||
feesOverride?: typeof aggregationStrategyDefaultTestConfig["fees"],
|
||||
) {
|
||||
return await fx.createBundleService(
|
||||
{
|
||||
...bundleServiceDefaultTestConfig,
|
||||
maxAggregationSize: 24,
|
||||
},
|
||||
return fx.createBundleService(
|
||||
bundleServiceDefaultTestConfig,
|
||||
{
|
||||
...aggregationStrategyDefaultTestConfig,
|
||||
maxAggregationSize: 24,
|
||||
fees: {
|
||||
type: `token:${fx.testErc20.address}`,
|
||||
perGas: BigNumber.from(10_000_000_000),
|
||||
perByte: BigNumber.from(100_000_000_000_000),
|
||||
...feesOverride,
|
||||
maxGasPerBundle: 3000000,
|
||||
fees: feesOverride ?? {
|
||||
type: "token",
|
||||
address: fx.testErc20.address,
|
||||
allowLosses: true,
|
||||
breakevenOperationCount: 4.5,
|
||||
ethValueInTokens: 1300,
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -67,7 +64,7 @@ function approveAndSendTokensToOrigin(
|
||||
}
|
||||
|
||||
Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
|
||||
const bundleService = await createBundleService(fx);
|
||||
const bundleService = createBundleService(fx);
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
@@ -91,7 +88,7 @@ Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
BigNumber.from(1000),
|
||||
);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
@@ -101,11 +98,11 @@ Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
BigNumber.from(1000),
|
||||
);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
});
|
||||
|
||||
Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
const bundleService = await createBundleService(fx);
|
||||
const bundleService = createBundleService(fx);
|
||||
|
||||
const [wallet] = await fx.setupWallets(1, {
|
||||
tokenBalance: oneToken,
|
||||
@@ -115,21 +112,28 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
approveAndSendTokensToOrigin(fx, await wallet.Nonce(), oneToken),
|
||||
);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
const bundleResponse = await bundleService.add(bundle);
|
||||
assertBundleSucceeds(bundleResponse);
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
oneToken,
|
||||
);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
await bundleService.waitForConfirmations();
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
if ("failures" in bundleResponse) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
const bundleRow = await bundleService.bundleTable.findBundle(
|
||||
bundleResponse.hash,
|
||||
);
|
||||
|
||||
assertEquals(bundleRow?.status, "confirmed");
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
BigNumber.from(0),
|
||||
@@ -137,22 +141,47 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
});
|
||||
|
||||
Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
const bundleService = await createBundleService(fx, {
|
||||
const es = fx.ethereumService;
|
||||
|
||||
const bundleService = createBundleService(fx, {
|
||||
type: "ether",
|
||||
perByte: BigNumber.from(1),
|
||||
perGas: BigNumber.from(1),
|
||||
allowLosses: true,
|
||||
breakevenOperationCount: 4.5,
|
||||
});
|
||||
|
||||
const fee = BigNumber.from(2_000_000); // wei
|
||||
|
||||
const [wallet] = await fx.setupWallets(1, { tokenBalance: 0 });
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
await (await fx.adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: fee,
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const es = fx.ethereumService;
|
||||
const estimation = await bundleService.aggregationStrategy.estimateFee(
|
||||
wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
assertEquals(estimation.successes, [true]);
|
||||
|
||||
const fee = estimation.feeRequired
|
||||
.add(estimation.feeRequired.div(5)); // +20% safety margin
|
||||
|
||||
await (await fx.adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: fee
|
||||
.sub(1), // Already sent 1 wei before
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
@@ -167,21 +196,28 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
const bundleResponse = await bundleService.add(bundle);
|
||||
assertBundleSucceeds(bundleResponse);
|
||||
|
||||
assertEquals(
|
||||
await fx.adminWallet.provider.getBalance(wallet.address),
|
||||
fee,
|
||||
);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
await bundleService.waitForConfirmations();
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
if ("failures" in bundleResponse) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
const bundleRow = await bundleService.bundleTable.findBundle(
|
||||
bundleResponse.hash,
|
||||
);
|
||||
|
||||
assertEquals(bundleRow?.status, "confirmed");
|
||||
assertEquals(
|
||||
await fx.adminWallet.provider.getBalance(wallet.address),
|
||||
BigNumber.from(0),
|
||||
@@ -189,18 +225,24 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
});
|
||||
|
||||
Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) => {
|
||||
const bundleService = await createBundleService(fx);
|
||||
const breakevenOperationCount = 4.5;
|
||||
|
||||
const [wallet1, wallet2] = await fx.setupWallets(2, {
|
||||
const bundleService = createBundleService(fx, {
|
||||
type: "token",
|
||||
address: fx.testErc20.address,
|
||||
allowLosses: true,
|
||||
breakevenOperationCount,
|
||||
ethValueInTokens: 1,
|
||||
});
|
||||
|
||||
const wallets = await fx.setupWallets(10, {
|
||||
tokenBalance: oneToken.mul(10),
|
||||
});
|
||||
|
||||
const nonce1 = await wallet1.Nonce();
|
||||
const nonce2 = await wallet2.Nonce();
|
||||
const nonce = await wallets[0].Nonce();
|
||||
|
||||
async function addBundle(
|
||||
wallet: BlsWalletWrapper,
|
||||
nonce: BigNumber,
|
||||
fee: BigNumber,
|
||||
) {
|
||||
const bundle = wallet.sign(
|
||||
@@ -210,152 +252,49 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
}
|
||||
|
||||
// 6 good bundles from wallet 1 (each pays one token)
|
||||
await addBundle(wallet1, nonce1.add(0), oneToken);
|
||||
await addBundle(wallet1, nonce1.add(1), oneToken);
|
||||
await addBundle(wallet1, nonce1.add(2), oneToken);
|
||||
await addBundle(wallet1, nonce1.add(3), oneToken);
|
||||
await addBundle(wallet1, nonce1.add(4), oneToken);
|
||||
await addBundle(wallet1, nonce1.add(5), oneToken);
|
||||
// For the purposes of this test, we don't want the bundleService prematurely
|
||||
// running a submission on fewer bundles than we're trying to process
|
||||
bundleService.config.breakevenOperationCount = Infinity;
|
||||
|
||||
// 6 good bundles
|
||||
await addBundle(wallets[0], oneToken);
|
||||
await addBundle(wallets[1], oneToken);
|
||||
await addBundle(wallets[2], oneToken);
|
||||
await addBundle(wallets[3], oneToken);
|
||||
await addBundle(wallets[4], oneToken);
|
||||
await addBundle(wallets[5], oneToken);
|
||||
|
||||
// 7th bundle should fail because 1 wei is an insufficient fee
|
||||
await addBundle(wallet1, nonce1.add(6), BigNumber.from(1));
|
||||
await addBundle(wallets[6], BigNumber.from(1));
|
||||
|
||||
// 3 more good bundles. These are from a different wallet so that the nonces
|
||||
// can be correct independent of the success/failure of bundle #7 above.
|
||||
await addBundle(wallet2, nonce2.add(0), oneToken);
|
||||
await addBundle(wallet2, nonce2.add(1), oneToken);
|
||||
await addBundle(wallet2, nonce2.add(2), oneToken);
|
||||
// 3 more good bundles
|
||||
await addBundle(wallets[7], oneToken);
|
||||
await addBundle(wallets[8], oneToken);
|
||||
await addBundle(wallets[9], oneToken);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 10n);
|
||||
// Restore this value now that all the bundles are added together
|
||||
bundleService.config.breakevenOperationCount = breakevenOperationCount;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 10);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
await bundleService.waitForConfirmations();
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
const remainingBundles = fx.allBundles(bundleService);
|
||||
const remainingPendingBundles = remainingBundles
|
||||
.filter((bundle) => bundle.status === "pending");
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet1.address),
|
||||
oneToken.mul(4), // 6 tokens spent from wallet 1
|
||||
);
|
||||
assertEquals(remainingBundles.length, 10);
|
||||
assertEquals(remainingPendingBundles.length, 1);
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet2.address),
|
||||
oneToken.mul(7), // 3 tokens spent from wallet 2
|
||||
);
|
||||
});
|
||||
|
||||
Fixture.test("submits 9/10 bundles when 7th has insufficient gas-based fee", async (fx) => {
|
||||
const bundleService = await createBundleService(fx, {
|
||||
// This test is targeting the logic which needs to run when the
|
||||
// calldata-based gas shortcut doesn't work. We just set the per byte fee to
|
||||
// zero to make that clear.
|
||||
perByte: BigNumber.from(0),
|
||||
});
|
||||
|
||||
const baseFee = BigNumber.from(1_000_000).mul(1e9); // Note 1
|
||||
const fee = BigNumber.from(1_950_000).mul(1e9);
|
||||
|
||||
const [wallet1, wallet2] = await fx.setupWallets(2, {
|
||||
tokenBalance: fee.mul(10),
|
||||
});
|
||||
|
||||
const nonce1 = await wallet1.Nonce();
|
||||
const nonce2 = await wallet2.Nonce();
|
||||
|
||||
async function addBundle(
|
||||
wallet: BlsWalletWrapper,
|
||||
nonce: BigNumber,
|
||||
fee: BigNumber,
|
||||
) {
|
||||
const bundle = wallet.sign(
|
||||
approveAndSendTokensToOrigin(fx, nonce, fee),
|
||||
);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
}
|
||||
|
||||
// 6 good bundles from wallet 1 (each pays one token)
|
||||
await addBundle(wallet1, nonce1.add(0), fee.add(baseFee)); // Note 1
|
||||
await addBundle(wallet1, nonce1.add(1), fee);
|
||||
await addBundle(wallet1, nonce1.add(2), fee);
|
||||
await addBundle(wallet1, nonce1.add(3), fee);
|
||||
await addBundle(wallet1, nonce1.add(4), fee);
|
||||
await addBundle(wallet1, nonce1.add(5), fee);
|
||||
|
||||
// Note 1: The first bundle has a base fee added because there's an overhead
|
||||
// of doing a bundle. This is a bit unrealistic but it makes the test less
|
||||
// brittle.
|
||||
|
||||
// 7th bundle should fail because 1 wei is an insufficient fee
|
||||
await addBundle(wallet1, nonce1.add(6), BigNumber.from(1));
|
||||
|
||||
// 3 more good bundles. These are from a different wallet so that the nonces
|
||||
// can be correct independent of the success/failure of bundle #7 above.
|
||||
await addBundle(wallet2, nonce2.add(0), fee);
|
||||
await addBundle(wallet2, nonce2.add(1), fee);
|
||||
await addBundle(wallet2, nonce2.add(2), fee);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 10n);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
await bundleService.waitForConfirmations();
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet1.address),
|
||||
fee.mul(4).sub(baseFee), // 6 fees spent from wallet 1
|
||||
);
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet2.address),
|
||||
fee.mul(7), // 3 fees spent from wallet 2
|
||||
);
|
||||
});
|
||||
|
||||
Fixture.test("submits 1/3 bundles when bundle#3 fails the shortcut fee test but bundle#2 also fails the full fee test", async (fx) => {
|
||||
const bundleService = await createBundleService(fx, {
|
||||
perGas: BigNumber.from(100_000_000_000),
|
||||
});
|
||||
|
||||
const [wallet] = await fx.setupWallets(2, {
|
||||
tokenBalance: oneToken.mul(10),
|
||||
});
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundleFees = [
|
||||
// Passes
|
||||
BigNumber.from(140_000_000).mul(1e9),
|
||||
|
||||
// Passes shortcut test but fails full test
|
||||
BigNumber.from(80_000_000).mul(1e9),
|
||||
|
||||
// Fails shortcut test
|
||||
BigNumber.from(1),
|
||||
];
|
||||
|
||||
for (const i of Range(bundleFees.length)) {
|
||||
const bundle = wallet.sign(
|
||||
approveAndSendTokensToOrigin(fx, nonce.add(i), bundleFees[i]),
|
||||
);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
}
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 3n);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
await bundleService.waitForConfirmations();
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 2n);
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
oneToken.mul(10).sub(bundleFees[0]),
|
||||
);
|
||||
await Promise.all(wallets.map((wallet, i) =>
|
||||
(async () => {
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
// Every wallet should have successfully spent one token, except the 7th
|
||||
i === 6 ? oneToken.mul(10) : oneToken.mul(9),
|
||||
);
|
||||
})()
|
||||
));
|
||||
});
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import { assertEquals, assertBundleSucceeds, BigNumber } from "./deps.ts";
|
||||
import Fixture, {
|
||||
aggregationStrategyDefaultTestConfig,
|
||||
bundleServiceDefaultTestConfig,
|
||||
} from "./helpers/Fixture.ts";
|
||||
import { assertBundleSucceeds, assertEquals, BigNumber } from "./deps.ts";
|
||||
import Fixture, { bundleServiceDefaultTestConfig } from "./helpers/Fixture.ts";
|
||||
import Range from "../src/helpers/Range.ts";
|
||||
import { AggregationStrategyConfig } from "../src/app/AggregationStrategy.ts";
|
||||
import nil from "../src/helpers/nil.ts";
|
||||
|
||||
const bundleServiceConfig = {
|
||||
...bundleServiceDefaultTestConfig,
|
||||
maxAggregationSize: 5,
|
||||
maxAggregationDelayMillis: 5000,
|
||||
};
|
||||
|
||||
const aggregationStrategyConfig = {
|
||||
...aggregationStrategyDefaultTestConfig,
|
||||
maxAggregationSize: 5,
|
||||
const aggregationStrategyConfig: AggregationStrategyConfig = {
|
||||
maxGasPerBundle: 900000,
|
||||
fees: nil,
|
||||
bundleCheckingConcurrency: 8,
|
||||
};
|
||||
|
||||
Fixture.test("submits a single action in a timed submission", async (fx) => {
|
||||
const bundleService = await fx.createBundleService(
|
||||
const bundleService = fx.createBundleService(
|
||||
bundleServiceConfig,
|
||||
aggregationStrategyConfig,
|
||||
);
|
||||
@@ -38,13 +37,14 @@ Fixture.test("submits a single action in a timed submission", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
const bundleResponse = await bundleService.add(bundle);
|
||||
assertBundleSucceeds(bundleResponse);
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
BigNumber.from(1000),
|
||||
);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
@@ -54,28 +54,40 @@ Fixture.test("submits a single action in a timed submission", async (fx) => {
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
BigNumber.from(1001),
|
||||
);
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
if ("failures" in bundleResponse) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
const bundleRow = await bundleService.bundleTable.findBundle(
|
||||
bundleResponse.hash,
|
||||
);
|
||||
assertEquals(bundleRow?.status, "confirmed");
|
||||
|
||||
const bundleReceipt = bundleService.receiptFromBundle(bundleRow!);
|
||||
assertEquals(bundleReceipt?.bundleHash, bundleResponse.hash);
|
||||
});
|
||||
|
||||
Fixture.test("submits a full submission without delay", async (fx) => {
|
||||
const bundleService = await fx.createBundleService(
|
||||
const bundleService = fx.createBundleService(
|
||||
bundleServiceConfig,
|
||||
aggregationStrategyConfig,
|
||||
);
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const walletNonce = await wallet.Nonce();
|
||||
const wallets = await fx.setupWallets(5);
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = Range(5).map((i) =>
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -92,35 +104,33 @@ Fixture.test("submits a full submission without delay", async (fx) => {
|
||||
// Check mints have occurred, ensuring a submission has occurred even though
|
||||
// the clock has not advanced
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
await fx.testErc20.balanceOf(firstWallet.address),
|
||||
BigNumber.from(1005), // 1000 (initial) + 5 * 1 (mint txs)
|
||||
);
|
||||
});
|
||||
|
||||
Fixture.test(
|
||||
[
|
||||
"submits submission from over-full bundle table without delay and submits",
|
||||
"leftover bundles after delay",
|
||||
].join(" "),
|
||||
"submits multiple aggregations when provided with too many user bundles",
|
||||
async (fx) => {
|
||||
const bundleService = await fx.createBundleService(
|
||||
const bundleService = fx.createBundleService(
|
||||
bundleServiceConfig,
|
||||
aggregationStrategyConfig,
|
||||
);
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const walletNonce = await wallet.Nonce();
|
||||
const wallets = await fx.setupWallets(7);
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = Range(7).map((i) =>
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
@@ -128,7 +138,7 @@ Fixture.test(
|
||||
);
|
||||
|
||||
// Prevent submission from triggering on max aggregation size.
|
||||
bundleService.config.maxAggregationSize = Infinity;
|
||||
bundleService.config.breakevenOperationCount = Infinity;
|
||||
|
||||
for (const b of bundles) {
|
||||
assertBundleSucceeds(await bundleService.add(b));
|
||||
@@ -137,36 +147,34 @@ Fixture.test(
|
||||
// Restore max aggregation size for testing. (This way we hit the edge case
|
||||
// that the aggregator has access to more actions than it can fit into a
|
||||
// single submission, which happens but is race-dependent.)
|
||||
bundleService.config.maxAggregationSize = 5;
|
||||
bundleService.config.breakevenOperationCount = 4.5;
|
||||
|
||||
await bundleService.submissionTimer.trigger();
|
||||
await bundleService.waitForConfirmations();
|
||||
|
||||
// Check mints have occurred, ensuring a submission has occurred even though the
|
||||
// clock has not advanced
|
||||
if ((fx.allBundles(bundleService)).length > 0) {
|
||||
await bundleService.submissionTimer.trigger();
|
||||
await bundleService.waitForConfirmations();
|
||||
}
|
||||
|
||||
// Check mints have occurred
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
BigNumber.from(1005), // 1000 (initial) + 5 * 1 (mint txs)
|
||||
);
|
||||
|
||||
// Leftover txs
|
||||
const remainingBundles = await fx.allBundles(bundleService);
|
||||
assertEquals(remainingBundles.length, 2);
|
||||
|
||||
await bundleService.submissionTimer.trigger();
|
||||
await bundleService.waitForConfirmations();
|
||||
|
||||
assertEquals(
|
||||
await fx.testErc20.balanceOf(wallet.address),
|
||||
await fx.testErc20.balanceOf(firstWallet.address),
|
||||
BigNumber.from(1007), // 1000 (initial) + 7 * 1 (mint txs)
|
||||
);
|
||||
|
||||
const confirmationEvents = fx.appEvents.filter((ev) =>
|
||||
ev.type === "submission-confirmed"
|
||||
);
|
||||
|
||||
assertEquals(confirmationEvents.length, 2);
|
||||
},
|
||||
);
|
||||
|
||||
Fixture.test(
|
||||
"submits 3 bundles in reverse (incorrect) nonce order",
|
||||
async (fx) => {
|
||||
const bundleService = await fx.createBundleService(
|
||||
const bundleService = fx.createBundleService(
|
||||
bundleServiceConfig,
|
||||
aggregationStrategyConfig,
|
||||
);
|
||||
@@ -205,8 +213,11 @@ Fixture.test(
|
||||
);
|
||||
assertEquals(await wallet.Nonce(), BigNumber.from(2));
|
||||
// 2 mints should be left as both failed submission pre-check
|
||||
let remainingBundles = await fx.allBundles(bundleService);
|
||||
assertEquals(remainingBundles.length, 2);
|
||||
let remainingBundles = fx.allBundles(bundleService);
|
||||
let remainingPendingBundles = remainingBundles.filter((bundle) =>
|
||||
bundle.status === "pending"
|
||||
);
|
||||
assertEquals(remainingPendingBundles.length, 2);
|
||||
|
||||
// Re-run submissions
|
||||
await bundleService.submissionTimer.trigger();
|
||||
@@ -220,8 +231,11 @@ Fixture.test(
|
||||
);
|
||||
assertEquals(await wallet.Nonce(), BigNumber.from(3));
|
||||
// 1 mints (nonce 3) should be left as it failed submission pre-check
|
||||
remainingBundles = await fx.allBundles(bundleService);
|
||||
assertEquals(remainingBundles.length, 1);
|
||||
remainingBundles = fx.allBundles(bundleService);
|
||||
remainingPendingBundles = remainingBundles.filter((bundle) =>
|
||||
bundle.status === "pending"
|
||||
);
|
||||
assertEquals(remainingPendingBundles.length, 1);
|
||||
|
||||
// Simulate 1 block being mined
|
||||
await fx.mine(1);
|
||||
@@ -237,13 +251,16 @@ Fixture.test(
|
||||
BigNumber.from(1003), // 1000 (initial) + 3 * 1 (mint txs)
|
||||
);
|
||||
assertEquals(await wallet.Nonce(), BigNumber.from(4));
|
||||
remainingBundles = await fx.allBundles(bundleService);
|
||||
assertEquals(remainingBundles.length, 0);
|
||||
remainingBundles = fx.allBundles(bundleService);
|
||||
remainingPendingBundles = remainingBundles.filter((bundle) =>
|
||||
bundle.status === "pending"
|
||||
);
|
||||
assertEquals(remainingPendingBundles.length, 0);
|
||||
},
|
||||
);
|
||||
|
||||
Fixture.test("retains failing bundle when its eligibility delay is smaller than MAX_ELIGIBILITY_DELAY", async (fx) => {
|
||||
const bundleService = await fx.createBundleService(
|
||||
const bundleService = fx.createBundleService(
|
||||
{
|
||||
...bundleServiceConfig,
|
||||
maxEligibilityDelay: 300,
|
||||
@@ -274,16 +291,16 @@ Fixture.test("retains failing bundle when its eligibility delay is smaller than
|
||||
await bundleService.runPendingTasks();
|
||||
assertBundleSucceeds(res);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
});
|
||||
|
||||
Fixture.test("removes failing bundle when its eligibility delay is larger than MAX_ELIGIBILITY_DELAY", async (fx) => {
|
||||
const bundleService = await fx.createBundleService(
|
||||
Fixture.test("updates status of failing bundle when its eligibility delay is larger than MAX_ELIGIBILITY_DELAY", async (fx) => {
|
||||
const bundleService = fx.createBundleService(
|
||||
{
|
||||
...bundleServiceConfig,
|
||||
maxEligibilityDelay: 300,
|
||||
@@ -314,7 +331,7 @@ Fixture.test("removes failing bundle when its eligibility delay is larger than M
|
||||
await bundleService.runPendingTasks();
|
||||
assertBundleSucceeds(res);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
const [bundleRow] = await bundleService.bundleTable.all();
|
||||
|
||||
@@ -326,5 +343,11 @@ Fixture.test("removes failing bundle when its eligibility delay is larger than M
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0n);
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
|
||||
if ("failures" in res) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
const failedBundleRow = await bundleService.bundleTable.findBundle(res.hash);
|
||||
assertEquals(failedBundleRow?.status, "failed");
|
||||
});
|
||||
|
||||
@@ -1,38 +1,13 @@
|
||||
import { assertEquals, BigNumber } from "./deps.ts";
|
||||
import { assertEquals, BigNumber, sqlite } from "./deps.ts";
|
||||
|
||||
import BundleTable, { BundleRow } from "../src/app/BundleTable.ts";
|
||||
import createQueryClient from "../src/app/createQueryClient.ts";
|
||||
|
||||
let counter = 0;
|
||||
|
||||
function test(name: string, fn: (bundleTable: BundleTable) => Promise<void>) {
|
||||
Deno.test({
|
||||
name,
|
||||
sanitizeResources: false,
|
||||
fn: async () => {
|
||||
const tableName = `bundles_test_${counter++}_${Date.now()}`;
|
||||
|
||||
const queryClient = createQueryClient(() => {});
|
||||
const table = await BundleTable.create(queryClient, tableName);
|
||||
|
||||
try {
|
||||
await fn(table);
|
||||
} finally {
|
||||
try {
|
||||
await table.drop();
|
||||
await queryClient.disconnect();
|
||||
} catch (error) {
|
||||
console.error("cleanup error:", error);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
import nil from "../src/helpers/nil.ts";
|
||||
|
||||
const sampleRows: BundleRow[] = [
|
||||
{
|
||||
id: 0,
|
||||
id: 1,
|
||||
hash: "0x0",
|
||||
status: "pending",
|
||||
bundle: {
|
||||
senderPublicKeys: [["0x01", "0x02", "0x03", "0x04"]],
|
||||
operations: [
|
||||
@@ -51,21 +26,24 @@ const sampleRows: BundleRow[] = [
|
||||
},
|
||||
eligibleAfter: BigNumber.from(0),
|
||||
nextEligibilityDelay: BigNumber.from(1),
|
||||
submitError: nil,
|
||||
receipt: nil,
|
||||
},
|
||||
];
|
||||
|
||||
test("Starts with zero transactions", async (table) => {
|
||||
assertEquals(await table.count(), 0n);
|
||||
Deno.test("Starts with zero transactions", () => {
|
||||
const table = new BundleTable(new sqlite.DB());
|
||||
assertEquals(table.count(), 0);
|
||||
});
|
||||
|
||||
test("Has one transaction after adding transaction", async (table) => {
|
||||
await table.add(sampleRows[0]);
|
||||
|
||||
assertEquals(await table.count(), 1n);
|
||||
Deno.test("Has one transaction after adding transaction", () => {
|
||||
const table = new BundleTable(new sqlite.DB());
|
||||
table.add(sampleRows[0]);
|
||||
assertEquals(table.count(), 1);
|
||||
});
|
||||
|
||||
test("Can retrieve transaction", async (table) => {
|
||||
await table.add(sampleRows[0]);
|
||||
|
||||
assertEquals(await table.all(), [{ ...sampleRows[0] }]);
|
||||
Deno.test("Can retrieve transaction", () => {
|
||||
const table = new BundleTable(new sqlite.DB());
|
||||
table.add(sampleRows[0]);
|
||||
assertEquals(table.all(), [{ ...sampleRows[0] }]);
|
||||
});
|
||||
|
||||
@@ -15,7 +15,7 @@ export function assertEquals<L, R extends L>(left: L, right: R) {
|
||||
|
||||
export function assertBundleSucceeds(res: AddBundleResponse) {
|
||||
if ("failures" in res) {
|
||||
throw new AssertionError("expected bundle to succeed");
|
||||
throw new AssertionError(`expected bundle to succeed. failures: ${JSON.stringify(res.failures)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,11 @@ import {
|
||||
MockERC20,
|
||||
MockERC20__factory,
|
||||
NetworkConfig,
|
||||
QueryClient,
|
||||
sqlite,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import testRng from "./testRng.ts";
|
||||
import EthereumService from "../../src/app/EthereumService.ts";
|
||||
import createQueryClient from "../../src/app/createQueryClient.ts";
|
||||
import Range from "../../src/helpers/Range.ts";
|
||||
import Mutex from "../../src/helpers/Mutex.ts";
|
||||
import TestClock from "./TestClock.ts";
|
||||
@@ -23,31 +22,31 @@ import nil, { isNotNil } from "../../src/helpers/nil.ts";
|
||||
import getNetworkConfig from "../../src/helpers/getNetworkConfig.ts";
|
||||
import BundleService from "../../src/app/BundleService.ts";
|
||||
import BundleTable, { BundleRow } from "../../src/app/BundleTable.ts";
|
||||
import AggregationStrategy from "../../src/app/AggregationStrategy.ts";
|
||||
import AggregationStrategy, {
|
||||
AggregationStrategyConfig,
|
||||
} from "../../src/app/AggregationStrategy.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type ExplicitAny = any;
|
||||
|
||||
let existingClient: QueryClient | nil = nil;
|
||||
|
||||
export const bundleServiceDefaultTestConfig:
|
||||
typeof BundleService.defaultConfig = {
|
||||
bundleQueryLimit: 100,
|
||||
maxAggregationSize: 12,
|
||||
breakevenOperationCount: 4.5,
|
||||
maxAggregationDelayMillis: 5000,
|
||||
maxUnconfirmedAggregations: 3,
|
||||
maxEligibilityDelay: 300,
|
||||
};
|
||||
|
||||
export const aggregationStrategyDefaultTestConfig:
|
||||
typeof AggregationStrategy.defaultConfig = {
|
||||
maxAggregationSize: 12,
|
||||
fees: {
|
||||
type: "ether",
|
||||
perGas: BigNumber.from(0),
|
||||
perByte: BigNumber.from(0),
|
||||
},
|
||||
};
|
||||
export const aggregationStrategyDefaultTestConfig: AggregationStrategyConfig = {
|
||||
maxGasPerBundle: 1500000,
|
||||
fees: {
|
||||
type: "ether",
|
||||
allowLosses: true,
|
||||
breakevenOperationCount: 4.5,
|
||||
},
|
||||
bundleCheckingConcurrency: 8,
|
||||
};
|
||||
|
||||
export default class Fixture {
|
||||
static test(
|
||||
@@ -147,33 +146,36 @@ export default class Fixture {
|
||||
return this.rng.seed("blsPrivateKey", ...extraSeeds).address();
|
||||
}
|
||||
|
||||
async createBundleService(
|
||||
createBundleService(
|
||||
config = bundleServiceDefaultTestConfig,
|
||||
aggregationStrategyConfig = aggregationStrategyDefaultTestConfig,
|
||||
) {
|
||||
const suffix = this.rng.seed("table-name-suffix").address().slice(2, 12);
|
||||
existingClient = createQueryClient(this.emit, existingClient);
|
||||
const queryClient = existingClient;
|
||||
|
||||
const tablesMutex = new Mutex();
|
||||
|
||||
const tableName = `bundles_test_${suffix}`;
|
||||
const table = await BundleTable.createFresh(queryClient, tableName);
|
||||
const table = new BundleTable(
|
||||
new sqlite.DB(),
|
||||
(sql, params) => {
|
||||
if (env.LOG_QUERIES) {
|
||||
this.emit({
|
||||
type: "db-query",
|
||||
data: { sql, params },
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const aggregationStrategy = (
|
||||
const aggregationStrategy =
|
||||
aggregationStrategyConfig === aggregationStrategyDefaultTestConfig
|
||||
? this.aggregationStrategy
|
||||
: new AggregationStrategy(
|
||||
this.blsWalletSigner,
|
||||
this.ethereumService,
|
||||
aggregationStrategyConfig,
|
||||
)
|
||||
);
|
||||
);
|
||||
|
||||
const bundleService = new BundleService(
|
||||
this.emit,
|
||||
this.clock,
|
||||
queryClient,
|
||||
tablesMutex,
|
||||
table,
|
||||
this.blsWalletSigner,
|
||||
@@ -191,16 +193,18 @@ export default class Fixture {
|
||||
}
|
||||
|
||||
async mine(numBlocks: number): Promise<void> {
|
||||
const provider = this.ethereumService.wallet
|
||||
.provider as ethers.providers.JsonRpcProvider;
|
||||
for (let i = 0; i < numBlocks; i++) {
|
||||
await provider.send("evm_mine", []);
|
||||
// Sending 0 eth instead of using evm_mine since geth doesn't support it.
|
||||
await (await this.adminWallet.sendTransaction({
|
||||
to: this.adminWallet.address,
|
||||
value: 0,
|
||||
})).wait();
|
||||
}
|
||||
}
|
||||
|
||||
allBundles(
|
||||
bundleService: BundleService,
|
||||
): Promise<BundleRow[]> {
|
||||
): BundleRow[] {
|
||||
return bundleService.bundleTable.all();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
ETHERSCAN_API_KEY=
|
||||
|
||||
ROPSTEN_URL=fill_me_in
|
||||
RINKEBY_URL=fill_me_in
|
||||
ARBITRUM_TESTNET_URL=https://rinkeby.arbitrum.io/rpc
|
||||
ARBITRUM_TESTNET_URL=TODO_REMOVE
|
||||
ARBITRUM_GOERLI_URL=https://goerli-rollup.arbitrum.io/rpc
|
||||
ARBITRUM_URL=https://arb1.arbitrum.io/rpc
|
||||
OPTIMISM_LOCAL_URL=http://localhost:8545
|
||||
OPTIMISM_TESETNET_URL=https://kovan.optimism.io
|
||||
OPTIMISM_URL=https://mainnet.optimism.io
|
||||
OPTIMISM_GOERLI_URL=https://goerli.optimism.io
|
||||
|
||||
# Only used for deploying the deployer contract at the same address on each evm network
|
||||
DEPLOYER_MNEMONIC="sock poet alone around radar forum quiz session observe rebel another choice"
|
||||
@@ -18,8 +18,6 @@ DEPLOYER_CONTRACT_ADDRESS=0x036d996D6855B83cd80142f2933d8C2617dA5617
|
||||
MAIN_MNEMONIC="test test test test test test test test test test test junk"
|
||||
|
||||
PRIVATE_KEY_AGG=0000000000000000000000000000000000000000000000000000000000000a99
|
||||
PRIVATE_KEY_AGG_OKOV=0000000000000000000000000000000000000000000000000000000000000001
|
||||
PRIVATE_KEY_AGG_RINKARBY=0000000000000000000000000000000000000000000000000000000000000001
|
||||
PRIVATE_KEY_AGG_ARB1=0000000000000000000000000000000000000000000000000000000000000001
|
||||
PRIVATE_KEY_002=0000000000000000000000000000000000000000000000000000000000000002
|
||||
PRIVATE_KEY_003=0000000000000000000000000000000000000000000000000000000000000003
|
||||
|
||||
@@ -37,5 +37,16 @@ module.exports = {
|
||||
],
|
||||
// TODO (merge-ok) Remove and fix lint error
|
||||
"node/no-unpublished-import": ["warn"],
|
||||
// https://github.com/typescript-eslint/typescript-eslint/blob/main/docs/linting/TROUBLESHOOTING.md#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
"no-undef": "off",
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
// chai expect statements
|
||||
files: ["*.test.ts"],
|
||||
rules: {
|
||||
"no-unused-expressions": "off",
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
5
contracts/.gitignore
vendored
5
contracts/.gitignore
vendored
@@ -4,7 +4,7 @@
|
||||
node_modules
|
||||
coverage
|
||||
coverage.json
|
||||
/typechain
|
||||
/typechain-types
|
||||
networks/local.json
|
||||
|
||||
#Hardhat files
|
||||
@@ -17,3 +17,6 @@ cache-ovm
|
||||
|
||||
#editor files
|
||||
.vscode
|
||||
|
||||
#yarn
|
||||
yarn-error.log
|
||||
|
||||
@@ -25,4 +25,4 @@ will be made between that one BLS transaction and 31 normal token transfers.
|
||||
| Commit | Tx Type | Number Txs | L1 Calldata Units Used | L1 Transaction Units | L2 Computation Units | L2 Storage Units | L1 Calldata Cost | L2 Tx Cost | L2 Storage Cost | L2 Computation Cost | Total Cost (ETH) | Tx Hash |
|
||||
| ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |---------------------| ----------- |------------------|
|
||||
| 116c920b2469d279773c2546b0f00575828c11c2 | BLS | 31 | 23388 | 1 | 312116 | 1 | 0.0015691 | 0.0001342 | 0.0000292 | 0.0001821 | 0.0019145 | 0xae4c5f62536743630eab5056671296e130bcd9d64650013a86c268fd59c6bc81 |
|
||||
| 116c920b2469d279773c2546b0f00575828c11c2 | Normal | 31 | 60388 | 31 | 25730 | 0 | 0.0040514 | 0.0041596 | 0.0000000 | 0.0000150 | 0.0082260 | 0x78cfceea76233ed83a49d67919ec4e6ce30d71a15cbcb64821514a1eabed257c |
|
||||
| 116c920b2469d279773c2546b0f00575828c11c2 | Normal | 31 | 60388 | 31 | 25730 | 0 | 0.0040514 | 0.0041596 | 0.0000000 | 0.0000150 | 0.0082260 | 0x78cfceea76233ed83a49d67919ec4e6ce30d71a15cbcb64821514a1eabed257c |
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
# BLS Contract Wallet
|
||||
|
||||
Lower-cost layer 2 transactions via a smart contract wallet.
|
||||
|
||||
**Note:** _These contracts are in the process of being formally audited and are currently NOT recommended for production use._
|
||||
|
||||
## Background
|
||||
|
||||
Smart contract wallets give users additional safety mechanisms independent of any wallet UI they may use, but are expensive to deploy (and use on) on Ethereum's layer 1.
|
||||
|
||||
Layer 2 solutions like Optimism and Arbitrum greatly lower this cost-barrier, and allow more users to benefit from smart contract wallets. This is primarily due to these being general purpose computation solutions.
|
||||
@@ -9,9 +13,11 @@ Layer 2 solutions like Optimism and Arbitrum greatly lower this cost-barrier, an
|
||||
DApps bridged to layer 2 will be more usable than those only on layer 1 thanks to faster transactions at lower-cost, but there are further gas savings to be had by dapps and users.
|
||||
|
||||
## Savings
|
||||
|
||||
Parameters of external layer 2 transactions are stored on layer 1 when "co-ordinators" record state changes. Reducing the size/number of parameters sent to layer 2 calls greatly reduces the layer 1 cost co-ordinators would need to recoup from users.
|
||||
|
||||
So as well as the benefits of smart contract wallets and layer 2 usage, gas savings from reduced call data is achieved in 3 ways:
|
||||
|
||||
1. single aggregated signature (BLS)
|
||||
2. de-duplicate of parameters across aggregated txs
|
||||
3. compressed parameters
|
||||
@@ -19,38 +25,50 @@ So as well as the benefits of smart contract wallets and layer 2 usage, gas savi
|
||||
Note: each of these savings is proportional to the number of transactions submitted in a batch. So when using all three methods, additional savings are roughly O(3n).
|
||||
|
||||
# Usage
|
||||
|
||||
1. Create bls keypair from signer/wallet
|
||||
2. Sign creation message and either: send it via an agreggator, or directly pass in a call to the Verification Gateway contract
|
||||
- receive contract wallet address
|
||||
|
||||
- receive contract wallet address
|
||||
|
||||
3. Create contract wallet with existing ECDSA keypair
|
||||
|
||||
## See it in action
|
||||
|
||||
See `extension`
|
||||
|
||||
# Components
|
||||
|
||||
## Layer 2 contract: Verification Gateway
|
||||
|
||||
Creates contract wallets deterministically (create2) with the hash of respective bls public keys. It verifies a set of actions (`Operation`) that have been signed with a known bls keypair, then calls the corresponding wallet passing parameters for it to action. Generally this will be an aggregated signature for many different wallets' Operations (`Bundle`).
|
||||
|
||||
## Layer 2 contract: BLS Wallet
|
||||
|
||||
A smart contract wallet for users to interact with layer 2 dapps. Created via the aforementioned verification gateway.
|
||||
Wallets use the proxy upgrade method, and can call upon their proxy admin to change their implementation. Wallets can also choose to set a different contract as their trusted verification gateway.
|
||||
|
||||
### Upgradability
|
||||
|
||||
The verification gateway (VG1) is the `owner` of a single proxy admin (PA1), and is responsible for all VG1 wallets. A wallet can call `walletAdminCall` on VG1 to then call `upgrade` to change it's implementation.
|
||||
If in the future a new verification gateway is created (say VG2/PA2), a wallet can choose to set it's trusted gateway to this instead. That means VG1 will no longer be permitted to make arbitrary calls to the wallet, only VG2. Note: PA1 will remain as the proxy admin of the wallet. The wallet can change this to PA2 via an admin call on VG1 to `changeProxyAdmin`.
|
||||
|
||||
## Client tool: BLS Wallet/Signer
|
||||
|
||||
Wallets (eg Metamask, Argent, ...) to implement BLS keypair generation and signing.
|
||||
|
||||
## Relayer node: Aggregators
|
||||
|
||||
Network to take bls-signed messages, aggregate signatures, then action them via a call to the Verification Gateway.
|
||||
|
||||
## Layer 2 node: Coordinators
|
||||
|
||||
Network that takes layer 2 transactions and creates blocks. general purpose computation solutions (Optimism, Arbitrum, zkSync)
|
||||
|
||||
## Message format
|
||||
|
||||
For a smart contract wallet to perform an action, the signed message must contain:
|
||||
|
||||
- the hash of the bls public key that signed the message (the full public key is mapped in the Verification Gateway)
|
||||
- nonce of the smart contract wallet
|
||||
- address of the smart contract for the wallet to call
|
||||
@@ -59,23 +77,26 @@ For a smart contract wallet to perform an action, the signed message must contai
|
||||
- amount to transfer
|
||||
|
||||
## Layer 2 contract: Further optimisations
|
||||
|
||||
While the Verification Gateway requires only one aggregated signature (rather than each signature of a set of messages and data), the other optimisations can be gained incrementally via preceding smart contracts.
|
||||
|
||||
| Compressed data | No duplicates | Aggregated signature | Contract to call |
|
||||
|----|----|----|----|
|
||||
| ✓ | ✓ | ✓ | Decompressor |
|
||||
| | ✓ | ✓ | Expander |
|
||||
| | | ✓ | Verification Gateway |
|
||||
| Compressed data | No duplicates | Aggregated signature | Contract to call |
|
||||
| --------------- | ------------- | -------------------- | -------------------- |
|
||||
| ✓ | ✓ | ✓ | Decompressor |
|
||||
| | ✓ | ✓ | Expander |
|
||||
| | | ✓ | Verification Gateway |
|
||||
|
||||
# Diagrams
|
||||
|
||||
## Optimistic Rollups
|
||||
|
||||
"Currently every tx on OR puts an ECDSA signature on chain." - BWH
|
||||
|
||||
Simplification of Optimism's L2 solution:
|
||||

|
||||
|
||||
## Transactions via BLS signature aggregator
|
||||
|
||||
"We want to replace this with a BLS signature." - BWH
|
||||
|
||||
Proposed solution to make use of [BLS](https://github.com/thehubbleproject/hubble-contracts/blob/master/contracts/libs/BLS.sol) lib:
|
||||
@@ -91,15 +112,26 @@ Proposed solution to make use of [BLS](https://github.com/thehubbleproject/hubbl
|
||||
For each network, the deployer contract can be deployed with the following script (only needed once)
|
||||
`DEPLOY_DEPLOYER=true yarn hardhat run scripts/deploy-deployer.ts --network <network-name>`
|
||||
|
||||
## Integration tests
|
||||
|
||||
To run integration tests:
|
||||
|
||||
1. cd into `./contracts` and run `yarn start-hardhat`
|
||||
2. cd into `./aggregator` and run `./programs/aggregator.ts`
|
||||
3. from `./contracts`, run `yarn test-integration`.
|
||||
|
||||
## Optimism's L2 (paused)
|
||||
|
||||
- clone https://github.com/ethereum-optimism/optimism
|
||||
- follow instructions (using latest version of docker)
|
||||
- in `opt/`, run script - `docker-compose up`
|
||||
- L1 - http://localhost:9545 (chainId 31337)
|
||||
- L2 - http://localhost:8545 (chainId 420)
|
||||
- L1 - http://localhost:9545 (chainId 31337)
|
||||
- L2 - http://localhost:8545 (chainId 420)
|
||||
|
||||
## Deploy scripts
|
||||
Specify network - `yarn hardhat run scripts/<#_script.ts> --network arbitrum-testnet`
|
||||
|
||||
Specify network - `yarn hardhat run scripts/<#_script.ts> --network arbitrum-goerli`
|
||||
|
||||
# License
|
||||
|
||||
MIT
|
||||
|
||||
1
contracts/clients/.gitignore
vendored
1
contracts/clients/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
/node_modules
|
||||
/dist
|
||||
yarn-error.log
|
||||
/.nyc_output
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
*
|
||||
!/dist/src/**/*
|
||||
!/dist/typechain/**/*
|
||||
!/dist/typechain-types/**/*
|
||||
!/src/**/*
|
||||
!/package.json
|
||||
!/README.md
|
||||
|
||||
5
contracts/clients/.nycrc
Normal file
5
contracts/clients/.nycrc
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "@istanbuljs/nyc-config-typescript",
|
||||
"all": true,
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://www.npmjs.com/package/bls-wallet-clients)
|
||||
|
||||
*Client libraries for interacting with BLS Wallet components*
|
||||
_Client libraries for interacting with BLS Wallet components_
|
||||
|
||||
## Network Config
|
||||
|
||||
@@ -22,21 +22,54 @@ const netCfg: NetworkConfig = await getConfig(
|
||||
|
||||
Exposes typed functions for interacting with the Aggregator's HTTP API.
|
||||
|
||||
### Add a bundle to an aggregator
|
||||
|
||||
```ts
|
||||
import { Aggregator } from 'bls-wallet-clients';
|
||||
import { Aggregator } from "bls-wallet-clients";
|
||||
|
||||
const aggregator = new Aggregator('https://rinkarby.blswallet.org');
|
||||
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
|
||||
const resp = await aggregator.add(bundle); // See BlsWalletWrapper section below
|
||||
// Aggregator did not accept bundle
|
||||
if ("failures" in resp) {
|
||||
throw new Error(resp.failures.join(", "));
|
||||
}
|
||||
```
|
||||
|
||||
await aggregator.add(...);
|
||||
### Get the bundle receipt that contains the transaction hash you can lookup on a block explorer
|
||||
|
||||
You will have to poll for the bundle receipt once you have added a bundle to an aggregator. The transaction hash is located on the bundle receipt. The property you need is `bundleReceipt.transactionHash`. This represents the transaction hash for the bundle submitted to the Verification Gatewaty, and can be used in a block explorer.
|
||||
|
||||
Note this transaction is reprentative of the entire bundle submitted by the aggregator, and does not represent individual operations. To retrieve information about individual operations, use the get `getOperationResults` helper method which is explained under the [VerificationGateway](#verificationgateway) section below.
|
||||
|
||||
```ts
|
||||
import { Aggregator } from "bls-wallet-clients";
|
||||
|
||||
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
|
||||
const resp = await aggregator.add(bundle); // See BlsWalletWrapper section below
|
||||
// Aggregator did not accept bundle
|
||||
if ("failures" in resp) {
|
||||
throw new Error(resp.failures.join(", "));
|
||||
}
|
||||
|
||||
let receipt;
|
||||
while (!receipt) {
|
||||
receipt = await aggregator.lookupReceipt(resp.hash);
|
||||
// There was an issue submitting the bundle on chain
|
||||
if (receipt && "submitError" in receipt) {
|
||||
throw new Error(receipt.submitError);
|
||||
}
|
||||
// Some function which waits i.e. setTimeout
|
||||
await sleep(5000);
|
||||
}
|
||||
```
|
||||
|
||||
## BlsWalletWrapper
|
||||
|
||||
Wraps a BLS wallet, storing the private key and providing `.sign(...)` to
|
||||
produce a `Bundle`, that can be used with `aggregator.add(...)`.
|
||||
produce a `Bundle`, that can be used with `aggregator.add(...)`. Make sure the bls wallet you're trying to use has enough ETH to send transactions. You can either fund a wallet before it's created, or after the wallet is lazily created from its first transaction (bundle).
|
||||
|
||||
```ts
|
||||
import { BlsWalletWrapper } from 'bls-wallet-clients';
|
||||
import { BlsWalletWrapper } from "bls-wallet-clients";
|
||||
|
||||
const wallet = await BlsWalletWrapper.connect(
|
||||
privateKey,
|
||||
@@ -50,10 +83,10 @@ const bundle = wallet.sign({
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: someToken.address, // An ethers.Contract
|
||||
encodedFunction: someToken.interface.encodeFunctionData(
|
||||
"transfer",
|
||||
["0x...some address...", ethers.BigNumber.from(1).pow(18)],
|
||||
),
|
||||
encodedFunction: someToken.interface.encodeFunctionData("transfer", [
|
||||
"0x...some address...",
|
||||
ethers.BigNumber.from(1).pow(18),
|
||||
]),
|
||||
},
|
||||
// Additional actions can go here. When using multiple actions, they'll
|
||||
// either all succeed or all fail.
|
||||
@@ -63,6 +96,183 @@ const bundle = wallet.sign({
|
||||
await aggregator.add(bundle);
|
||||
```
|
||||
|
||||
### Sending a regular ETH transaction
|
||||
|
||||
```ts
|
||||
// Follow the same steps as the first BlsWalletWrapper example, but construct the bundle actions like so:
|
||||
const amountToTransfer = ethers.utils.parseUnits("1");
|
||||
const reciever = "0x1234...";
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: amountToTransfer, // amount of ETH you want to transfer
|
||||
contractAddress: reciever, // receiver address. Can be a contract address or an EOA
|
||||
encodedFunction: "0x", // leave this as "0x" when just sending ETH
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Constructing actions to be agnostic to both ETH transfers and contract interactions
|
||||
|
||||
```ts
|
||||
// Follow the same steps as the first BlsWalletWrapper example, but construct the bundle actions like so:
|
||||
const transactions = [
|
||||
{
|
||||
value: ethers.utils.parseUnits("1"), // amount of ETH you want to transfer
|
||||
to: "0x1234...", // to address. Can be a contract address or an EOA
|
||||
},
|
||||
];
|
||||
|
||||
const actions: ActionData[] = transactions.map((tx) => ({
|
||||
ethValue: tx.value ?? "0",
|
||||
contractAddress: tx.to,
|
||||
encodedFunction: tx.data ?? "0x", // in this example, there is no data property on the tx object, so "0x" will be used
|
||||
}));
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce,
|
||||
actions,
|
||||
});
|
||||
```
|
||||
|
||||
## Estimating and paying fees
|
||||
|
||||
User bundles must pay fees to compensate the aggregator. Fees can be paid by adding an additional action to the users bundle that pays tx.origin. For more info on how fees work, see [aggregator fees](../../aggregator/README.md#fees).
|
||||
|
||||
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero wei, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
|
||||
|
||||
### Paying aggregator fees with native currency (ETH)
|
||||
|
||||
```ts
|
||||
import { BlsWalletWrapper, Aggregator } from "bls-wallet-clients";
|
||||
|
||||
const wallet = await BlsWalletWrapper.connect(
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
provider,
|
||||
);
|
||||
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
|
||||
|
||||
// Create a fee estimate bundle
|
||||
const estimateFeeBundle = wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
{
|
||||
ethValue: 1,
|
||||
// Provide 1 wei with this action so that the fee transfer to
|
||||
// tx.origin can be included in the gas estimate.
|
||||
contractAddress: aggregatorUtilitiesContract.address,
|
||||
encodedFunction:
|
||||
aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const feeEstimate = await aggregator.estimateFee(estimateFeeBundle);
|
||||
|
||||
// Add a safety premium to the fee to account for fluctuations in gas estimation
|
||||
const safetyDivisor = 5;
|
||||
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
|
||||
const safetyPremium = feeRequired.div(safetyDivisor);
|
||||
const safeFee = feeRequired.add(safetyPremium);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
{
|
||||
ethValue: safeFee, // fee amount
|
||||
contractAddress: aggregatorUtilitiesContract.address,
|
||||
encodedFunction:
|
||||
aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Paying aggregator fees with custom currency (ERC20)
|
||||
|
||||
The aggregator must be set up to accept ERC20 tokens in order for this to work.
|
||||
|
||||
```ts
|
||||
import { BlsWalletWrapper, Aggregator } from "bls-wallet-clients";
|
||||
|
||||
const wallet = await BlsWalletWrapper.connect(
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
provider,
|
||||
);
|
||||
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
|
||||
|
||||
// Create a fee estimate bundle
|
||||
const estimateFeeBundle = wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: tokenContract.address,
|
||||
encodedFunction: tokenContract.interface.encodeFunctionData("approve", [
|
||||
aggregatorUtilitiesContract.address,
|
||||
1,
|
||||
]),
|
||||
},
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: aggregatorUtilitiesContract.address,
|
||||
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
"sendTokenToTxOrigin",
|
||||
[tokenContract.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const feeEstimate = await aggregator.estimateFee(estimateFeeBundle);
|
||||
|
||||
// Add a safety premium to the fee to account for fluctuations in gas estimation
|
||||
const safetyDivisor = 5;
|
||||
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
|
||||
const safetyPremium = feeRequired.div(safetyDivisor);
|
||||
const safeFee = feeRequired.add(safetyPremium);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
|
||||
// Note the additional approve action when transfering ERC20 tokens
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: tokenContract.address,
|
||||
encodedFunction: tokenContract.interface.encodeFunctionData("approve", [
|
||||
aggregatorUtilitiesContract.address,
|
||||
safeFee, // fee amount
|
||||
]),
|
||||
},
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: aggregatorUtilitiesContract.address,
|
||||
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
"sendTokenToTxOrigin",
|
||||
[
|
||||
tokenContract.address,
|
||||
safeFee, // fee amount
|
||||
],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## VerificationGateway
|
||||
|
||||
Exposes `VerificationGateway` and `VerificationGateway__factory` generated by
|
||||
@@ -70,7 +280,7 @@ Exposes `VerificationGateway` and `VerificationGateway__factory` generated by
|
||||
interactions with the `VerificationGateway`.
|
||||
|
||||
```ts
|
||||
import { VerificationGateway__factory } from 'bls-wallet-clients';
|
||||
import { VerificationGateway__factory } from "bls-wallet-clients";
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
@@ -80,6 +290,31 @@ const verificationGateway = VerificationGateway__factory.connect(
|
||||
await verificationGateway.processBundle(bundle);
|
||||
```
|
||||
|
||||
You can get the results of the operations in a bundle using `getOperationResults`.
|
||||
|
||||
```ts
|
||||
import { getOperationResults, decodeError } from 'bls-wallet-clients';
|
||||
|
||||
...
|
||||
|
||||
const txn = await verificationGateway.processBundle(bundle);
|
||||
const txnReceipt = txn.wait();
|
||||
const opResults = getOperationResults(txnReceipt);
|
||||
|
||||
// Includes data from WalletOperationProcessed event,
|
||||
// as well as parsed errors with action index
|
||||
const { error } = opResults[0];
|
||||
console.log(error?.actionIndex); // ex. 0 (as BigNumber)
|
||||
console.log(error?.message); // ex. "some require failure message"
|
||||
|
||||
// If you want more granular ability to decode an error message
|
||||
// you can use the decodeError function.
|
||||
const errorData = '0x5c66760100000000.............000000000000';
|
||||
const opResultError = decodeError(errorData);
|
||||
console.log(opResultError.actionIndex); // ex. 0 (as BigNumber)
|
||||
console.log(opResultError.message); // ex. "ERC20: insufficient allowance"
|
||||
```
|
||||
|
||||
## Signer
|
||||
|
||||
Utilities for signing, aggregating and verifying transaction bundles using the
|
||||
@@ -96,11 +331,17 @@ import ethers from "ethers";
|
||||
import { initBlsWalletSigner } from "bls-wallet-clients";
|
||||
|
||||
(async () => {
|
||||
const signer = await initBlsWalletSigner({ chainId: 10 });
|
||||
|
||||
const privateKey = "0x...256 bits of private hex data here";
|
||||
const verificationGatewayAddress = "0x123...456";
|
||||
|
||||
const signer = await initBlsWalletSigner({
|
||||
chainId: 10,
|
||||
privateKey,
|
||||
verificationGatewayAddress
|
||||
});
|
||||
|
||||
const someToken = new ethers.Contract(
|
||||
...
|
||||
// See https://docs.ethers.io/v5/getting-started/
|
||||
);
|
||||
|
||||
@@ -112,12 +353,11 @@ import { initBlsWalletSigner } from "bls-wallet-clients";
|
||||
|
||||
// If you don't want to call a function and just send `ethValue` above,
|
||||
// use '0x' to signify an empty byte array here
|
||||
encodedFunction: someToken.interface.encodeFunctionData(
|
||||
"transfer",
|
||||
["0x...some address...", ethers.BigNumber.from(10).pow(18)],
|
||||
),
|
||||
encodedFunction: someToken.interface.encodeFunctionData("transfer", [
|
||||
"0x...some address...",
|
||||
ethers.BigNumber.from(10).pow(18),
|
||||
]),
|
||||
},
|
||||
privateKey,
|
||||
);
|
||||
|
||||
// Send bundle to an aggregator or use it with VerificationGateway directly.
|
||||
@@ -152,3 +392,8 @@ yarn link
|
||||
cd other/project/dir
|
||||
yarn "link bls-wallet-clients"
|
||||
```
|
||||
|
||||
## Troubleshooting tips
|
||||
|
||||
- Make sure your bls-wallet-clients package is up-to-date and check out our [releases page](https://github.com/web3well/bls-wallet/releases) for info on breaking changes.
|
||||
- Check network values such as the verification gateway address or the aggregator url are up-to-date. The most up-to-date values are located in the relevant [network config](./../contracts/networks) file. If you're deploying to a custom network, you'll have to check these against your own records as these won't be in the network directory.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bls-wallet-clients",
|
||||
"version": "0.6.0",
|
||||
"version": "0.8.2-1452ef5",
|
||||
"description": "Client libraries for interacting with BLS Wallet components",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
@@ -13,24 +13,29 @@
|
||||
"yarn": ">=1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && mkdir dist && cp -rH typechain dist/typechain && find ./dist/typechain -type f \\! -name '*.d.ts' -name '*.ts' -delete && tsc",
|
||||
"build": "rm -rf dist && mkdir dist && cp -rH typechain-types dist/typechain-types && find ./dist/typechain-types -type f \\! -name '*.d.ts' -name '*.ts' -delete && tsc",
|
||||
"watch": "tsc -w",
|
||||
"pretest": "yarn build",
|
||||
"test": "mocha dist/**/*.test.js",
|
||||
"test": "nyc --reporter=text --reporter=html mocha --require ts-node/register --require source-map-support/register --require ./test/init.ts --recursive **/*.test.ts",
|
||||
"premerge": "yarn test",
|
||||
"publish-experimental": "node scripts/showVersion.js >.version && npm version $(node scripts/showBaseVersion.js)-$(git rev-parse HEAD | head -c7) --allow-same-version && npm publish --tag experimental && npm version $(cat .version) && rm .version",
|
||||
"publish-experimental-dry-run": "node scripts/showVersion.js >.version && npm version $(node scripts/showBaseVersion.js)-$(git rev-parse HEAD | head -c7) --allow-same-version && npm publish --tag experimental --dry-run && npm version $(cat .version) && rm .version"
|
||||
},
|
||||
"dependencies": {
|
||||
"@thehubbleproject/bls": "^0.5.1",
|
||||
"ethers": "5.5.4"
|
||||
"ethers": "^5.7.2",
|
||||
"node-fetch": "2.6.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"chai": "^4.3.6",
|
||||
"mocha": "^9.2.2",
|
||||
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
||||
"@types/chai": "^4.3.4",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^10.0.1",
|
||||
"chai": "^4.3.7",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"mocha": "^10.2.0",
|
||||
"nyc": "^15.1.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^4.6.2"
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import nodeFetch from "node-fetch";
|
||||
import { ContractReceipt } from "ethers";
|
||||
import { Bundle, bundleToDto } from "./signer";
|
||||
|
||||
// TODO: Rename to BundleFailure?
|
||||
@@ -33,15 +35,35 @@ export type EstimateFeeResponse = {
|
||||
successes: boolean[];
|
||||
};
|
||||
|
||||
export type BundleReceipt = {
|
||||
transactionIndex: string;
|
||||
blockHash: string;
|
||||
blockNumber: string;
|
||||
export type BundleReceiptError = {
|
||||
submitError: string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* The BLS Wallet specific values in a {@link BundleReceipt}.
|
||||
*/
|
||||
export type BlsBundleReceipt = {
|
||||
bundleHash: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* The bundle receipt returned from a BLS Wallet Aggregator instance. It is a combination of an ethers {@link ContractReceipt} and a {@link BlsBundleReceipt} type.
|
||||
*/
|
||||
export type BundleReceipt = ContractReceipt & BlsBundleReceipt;
|
||||
|
||||
/**
|
||||
* Client used to interact with a BLS Wallet Aggregator instance
|
||||
*/
|
||||
export default class Aggregator {
|
||||
// Fetch implementation to use
|
||||
private readonly fetchImpl;
|
||||
origin: string;
|
||||
|
||||
/**
|
||||
* Constructs an Aggregator object
|
||||
*
|
||||
* @param url URL of the aggregator instance
|
||||
*/
|
||||
constructor(url: string) {
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
@@ -50,8 +72,16 @@ export default class Aggregator {
|
||||
}
|
||||
|
||||
this.origin = new URL(url).origin;
|
||||
// Prefer runtime's imeplmentation of fetch over node-fetch
|
||||
this.fetchImpl = "fetch" in globalThis ? fetch.bind(globalThis) : nodeFetch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a bundle to the aggregator
|
||||
*
|
||||
* @param bundle Bundle to send
|
||||
* @returns The hash of the bundle or an array of failures if the aggregator did not accept the bundle
|
||||
*/
|
||||
async add(
|
||||
bundle: Bundle,
|
||||
): Promise<{ hash: string } | { failures: TransactionFailure[] }> {
|
||||
@@ -68,24 +98,36 @@ export default class Aggregator {
|
||||
return json;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the fee required for a bundle by the aggreagtor to submit it.
|
||||
*
|
||||
* @param bundle Bundle to estimates the fee for
|
||||
* @returns Estimate of the fee needed to submit the bundle
|
||||
*/
|
||||
async estimateFee(bundle: Bundle): Promise<EstimateFeeResponse> {
|
||||
const result = await this.jsonPost("/estimateFee", bundleToDto(bundle));
|
||||
|
||||
return result as EstimateFeeResponse;
|
||||
}
|
||||
|
||||
async lookupReceipt(hash: string): Promise<BundleReceipt | undefined> {
|
||||
const response = await fetch(`${this.origin}/bundleReceipt/${hash}`);
|
||||
|
||||
if (response.status === 404) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
/**
|
||||
* Looks for a transaction receipt for a Bundle sent to the aggregator.
|
||||
* This will return undefined if the bundle has not yet been submitted by the aggregator.
|
||||
*
|
||||
* @param hash Hash of the bundle to find a transaction receipt for.
|
||||
* @returns The bundle receipt, a submission error if the aggregator was unable to submit the bundle on chain, or undefined if the receipt was not found.
|
||||
*/
|
||||
async lookupReceipt(
|
||||
hash: string,
|
||||
): Promise<BundleReceipt | BundleReceiptError | undefined> {
|
||||
return this.jsonGet<BundleReceipt | BundleReceiptError>(
|
||||
`${this.origin}/bundleReceipt/${hash}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Note: This should be private instead of exposed. Leaving as is for compatibility.
|
||||
async jsonPost(path: string, body: unknown): Promise<unknown> {
|
||||
const resp = await fetch(`${this.origin}${path}`, {
|
||||
const resp = await this.fetchImpl(`${this.origin}${path}`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
headers: {
|
||||
@@ -105,4 +147,20 @@ export default class Aggregator {
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
private async jsonGet<T>(path: string): Promise<T | undefined> {
|
||||
const resp = await this.fetchImpl(path);
|
||||
const respText = await resp.text();
|
||||
if (!respText) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const json = JSON.parse(respText);
|
||||
const isValidNonEmptyJson = json && Object.keys(json).length;
|
||||
if (isValidNonEmptyJson) {
|
||||
return json as T;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
453
contracts/clients/src/BlsProvider.ts
Normal file
453
contracts/clients/src/BlsProvider.ts
Normal file
@@ -0,0 +1,453 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { Deferrable } from "ethers/lib/utils";
|
||||
|
||||
import { ActionData, Bundle, PublicKey } from "./signer/types";
|
||||
import Aggregator, { BundleReceipt } from "./Aggregator";
|
||||
import BlsSigner, {
|
||||
TransactionBatchResponse,
|
||||
// Used for sendTransactionBatch TSdoc comment
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
TransactionBatch,
|
||||
UncheckedBlsSigner,
|
||||
_constructorGuard,
|
||||
} from "./BlsSigner";
|
||||
import poll from "./helpers/poll";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import {
|
||||
AggregatorUtilities__factory,
|
||||
BLSWallet__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "../typechain-types";
|
||||
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
|
||||
|
||||
/** Public key linked to actions parsed from a bundle */
|
||||
export type PublicKeyLinkedToActions = {
|
||||
publicKey: PublicKey;
|
||||
actions: Array<ActionData>;
|
||||
};
|
||||
|
||||
export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
readonly aggregator: Aggregator;
|
||||
readonly verificationGatewayAddress: string;
|
||||
readonly aggregatorUtilitiesAddress: string;
|
||||
|
||||
/**
|
||||
* @param aggregatorUrl The url for an aggregator instance
|
||||
* @param verificationGatewayAddress Verification gateway contract address
|
||||
* @param aggregatorUtilitiesAddress Aggregator utilities contract address
|
||||
* @param url Rpc url
|
||||
* @param network The network the provider should connect to
|
||||
*/
|
||||
constructor(
|
||||
aggregatorUrl: string,
|
||||
verificationGatewayAddress: string,
|
||||
aggregatorUtilitiesAddress: string,
|
||||
url?: string,
|
||||
network?: ethers.providers.Networkish,
|
||||
) {
|
||||
super(url, network);
|
||||
this.aggregator = new Aggregator(aggregatorUrl);
|
||||
this.verificationGatewayAddress = verificationGatewayAddress;
|
||||
this.aggregatorUtilitiesAddress = aggregatorUtilitiesAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transaction Transaction request object
|
||||
* @returns An estimate of the amount of gas that would be required to submit the transaction to the network
|
||||
*/
|
||||
override async estimateGas(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<BigNumber> {
|
||||
const resolvedTransaction = await ethers.utils.resolveProperties(
|
||||
transaction,
|
||||
);
|
||||
|
||||
if (!resolvedTransaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
if (!resolvedTransaction.from) {
|
||||
throw new TypeError("Transaction.from should be defined");
|
||||
}
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: resolvedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: resolvedTransaction.to.toString(),
|
||||
encodedFunction: resolvedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const nonce = await this.getTransactionCount(
|
||||
resolvedTransaction.from.toString(),
|
||||
);
|
||||
|
||||
const actionWithFeePaymentAction =
|
||||
this._addFeePaymentActionForFeeEstimation([action]);
|
||||
|
||||
// TODO: (merge-ok) bls-wallet #560 Estimate fee without requiring a signed bundle
|
||||
// There is no way to estimate the cost of a bundle without signing a bundle. The
|
||||
// alternative would be to use a signer instance in this method which is undesirable,
|
||||
// as this would result in tight coupling between a provider and a signer.
|
||||
const throwawayPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
const throwawayBlsWalletWrapper = await BlsWalletWrapper.connect(
|
||||
throwawayPrivateKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(
|
||||
throwawayBlsWalletWrapper.sign({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
}),
|
||||
);
|
||||
|
||||
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
|
||||
return addSafetyPremiumToFee(feeRequired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends transaction to be executed. Adds the signed bundle to the aggregator
|
||||
*
|
||||
* @param signedTransaction A signed bundle
|
||||
* @returns A transaction response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
override async sendTransaction(
|
||||
signedTransaction: string | Promise<string>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
const resolvedTransaction = await signedTransaction;
|
||||
const bundle: Bundle = JSON.parse(resolvedTransaction);
|
||||
|
||||
if (bundle.operations.length > 1) {
|
||||
throw new Error(
|
||||
"Can only operate on single operations. Call provider.sendTransactionBatch instead",
|
||||
);
|
||||
}
|
||||
|
||||
const result = await this.aggregator.add(bundle);
|
||||
|
||||
if ("failures" in result) {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
const actionData: ActionData = {
|
||||
ethValue: bundle.operations[0].actions[0].ethValue,
|
||||
contractAddress: bundle.operations[0].actions[0].contractAddress,
|
||||
encodedFunction: bundle.operations[0].actions[0].encodedFunction,
|
||||
};
|
||||
|
||||
return await this._constructTransactionResponse(
|
||||
actionData,
|
||||
bundle.senderPublicKeys[0],
|
||||
result.hash,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param signedTransactionBatch A signed {@link TransactionBatch}
|
||||
* @returns A transaction batch response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
async sendTransactionBatch(
|
||||
signedTransactionBatch: string,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
const bundle: Bundle = JSON.parse(signedTransactionBatch);
|
||||
|
||||
const result = await this.aggregator.add(bundle);
|
||||
|
||||
if ("failures" in result) {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
|
||||
bundle.senderPublicKeys.map((publicKey, i) => {
|
||||
const operation = bundle.operations[i];
|
||||
const actions = operation.actions;
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
actions,
|
||||
};
|
||||
});
|
||||
|
||||
return await this._constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions,
|
||||
result.hash,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param privateKey Private key for the account the signer represents
|
||||
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
|
||||
* @returns A new BlsSigner instance
|
||||
*/
|
||||
override getSigner(
|
||||
privateKey: string,
|
||||
addressOrIndex?: string | number,
|
||||
): BlsSigner {
|
||||
return new BlsSigner(_constructorGuard, this, privateKey, addressOrIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param privateKey Private key for the account the signer represents
|
||||
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
|
||||
* @returns A new UncheckedBlsSigner instance
|
||||
*/
|
||||
override getUncheckedSigner(
|
||||
privateKey: string,
|
||||
addressOrIndex?: string,
|
||||
): UncheckedBlsSigner {
|
||||
return this.getSigner(privateKey, addressOrIndex).connectUnchecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transaction receipt associated with the transaction (bundle) hash
|
||||
*
|
||||
* @remarks The transaction hash argument corresponds to a bundle hash and cannot be used on a block explorer.
|
||||
* Instead, the transaction hash returned in the transaction receipt from this method can be used in a block explorer.
|
||||
*
|
||||
* @param transactionHash The transaction hash returned from the BlsProvider and BlsSigner sendTransaction methods. This is technically a bundle hash
|
||||
* @returns The transaction receipt that corressponds to the transaction hash (bundle hash)
|
||||
*/
|
||||
override async getTransactionReceipt(
|
||||
transactionHash: string | Promise<string>,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
const resolvedTransactionHash = await transactionHash;
|
||||
return this._getTransactionReceipt(resolvedTransactionHash, 1, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transaction receipt associated with the transaction (bundle) hash
|
||||
*
|
||||
* @remarks The transaction hash argument cannot be used on a block explorer. It instead corresponds to a bundle hash.
|
||||
* The transaction hash returned in the transaction receipt from this method can be used in a block explorer.
|
||||
*
|
||||
* @param transactionHash The transaction hash returned from sending a transaction. This is technically a bundle hash
|
||||
* @param confirmations (Not used) the number of confirmations to wait for before returning the transaction receipt
|
||||
* @param retries The number of retries to poll the receipt for
|
||||
* @returns
|
||||
*/
|
||||
override async waitForTransaction(
|
||||
transactionHash: string,
|
||||
confirmations?: number,
|
||||
retries?: number,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
return this._getTransactionReceipt(
|
||||
transactionHash,
|
||||
confirmations ?? 1,
|
||||
retries ?? 20,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address The address that the method gets the transaction count from
|
||||
* @param blockTag The specific block tag to get the transaction count from
|
||||
* @returns The number of transactions an account has sent
|
||||
*/
|
||||
override async getTransactionCount(
|
||||
address: string | Promise<string>,
|
||||
blockTag?:
|
||||
| ethers.providers.BlockTag
|
||||
| Promise<ethers.providers.BlockTag>
|
||||
| undefined,
|
||||
): Promise<number> {
|
||||
const walletContract = BLSWallet__factory.connect(await address, this);
|
||||
|
||||
const code = await walletContract.provider.getCode(address, blockTag);
|
||||
|
||||
if (code === "0x") {
|
||||
// The wallet doesn't exist yet. Wallets are lazily created, so the nonce
|
||||
// is effectively zero, since that will be accepted as valid for a first
|
||||
// operation that also creates the wallet.
|
||||
return 0;
|
||||
}
|
||||
|
||||
return Number(await walletContract.nonce());
|
||||
}
|
||||
|
||||
async _getTransactionReceipt(
|
||||
transactionHash: string,
|
||||
confirmations: number,
|
||||
retries: number,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
const getBundleReceipt = async () =>
|
||||
await this.aggregator.lookupReceipt(transactionHash);
|
||||
const bundleExists = (result: BundleReceipt) => !result;
|
||||
|
||||
const bundleReceipt = await poll(
|
||||
getBundleReceipt,
|
||||
bundleExists,
|
||||
retries,
|
||||
2000,
|
||||
);
|
||||
|
||||
if (!bundleReceipt) {
|
||||
throw new Error(
|
||||
`Could not find bundle receipt for transaction hash: ${transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
to: bundleReceipt.to,
|
||||
from: bundleReceipt.from,
|
||||
contractAddress: bundleReceipt.contractAddress,
|
||||
transactionIndex: bundleReceipt.transactionIndex,
|
||||
root: bundleReceipt.root,
|
||||
gasUsed: bundleReceipt.gasUsed,
|
||||
logsBloom: bundleReceipt.logsBloom,
|
||||
blockHash: bundleReceipt.blockHash,
|
||||
transactionHash: bundleReceipt.transactionHash,
|
||||
logs: bundleReceipt.logs,
|
||||
blockNumber: bundleReceipt.blockNumber,
|
||||
confirmations: bundleReceipt.confirmations ?? confirmations,
|
||||
cumulativeGasUsed: bundleReceipt.effectiveGasPrice,
|
||||
effectiveGasPrice: bundleReceipt.effectiveGasPrice,
|
||||
byzantium: bundleReceipt.byzantium,
|
||||
type: bundleReceipt.type,
|
||||
status: bundleReceipt.status,
|
||||
};
|
||||
}
|
||||
|
||||
_addFeePaymentActionForFeeEstimation(
|
||||
actions: Array<ActionData>,
|
||||
): Array<ActionData> {
|
||||
const aggregatorUtilitiesContract = AggregatorUtilities__factory.connect(
|
||||
this.aggregatorUtilitiesAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
return [
|
||||
...actions,
|
||||
{
|
||||
// Provide 1 wei with this action so that the fee transfer to
|
||||
// tx.origin can be included in the gas estimate.
|
||||
ethValue: 1,
|
||||
contractAddress: this.aggregatorUtilitiesAddress,
|
||||
encodedFunction:
|
||||
aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
_addFeePaymentActionWithSafeFee(
|
||||
actions: Array<ActionData>,
|
||||
fee: BigNumber,
|
||||
): Array<ActionData> {
|
||||
const aggregatorUtilitiesContract = AggregatorUtilities__factory.connect(
|
||||
this.aggregatorUtilitiesAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
return [
|
||||
...actions,
|
||||
{
|
||||
ethValue: fee,
|
||||
contractAddress: this.aggregatorUtilitiesAddress,
|
||||
encodedFunction:
|
||||
aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async _constructTransactionResponse(
|
||||
action: ActionData,
|
||||
publicKey: PublicKey,
|
||||
hash: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
const chainId = await this.send("eth_chainId", []);
|
||||
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
publicKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
const from = await BlsWalletWrapper.AddressFromPublicKey(
|
||||
publicKey,
|
||||
verificationGateway,
|
||||
);
|
||||
|
||||
return {
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId: parseInt(chainId, 16),
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async _constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions: Array<PublicKeyLinkedToActions>,
|
||||
hash: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
const chainId = await this.send("eth_chainId", []);
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
const transactions: Array<ethers.providers.TransactionResponse> = [];
|
||||
|
||||
for (const publicKeyLinkedToActions of publicKeysLinkedToActions) {
|
||||
const from = await BlsWalletWrapper.AddressFromPublicKey(
|
||||
publicKeyLinkedToActions.publicKey,
|
||||
verificationGateway,
|
||||
);
|
||||
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
publicKeyLinkedToActions.publicKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
for (const action of publicKeyLinkedToActions.actions) {
|
||||
if (action.contractAddress === this.aggregatorUtilitiesAddress) {
|
||||
break;
|
||||
}
|
||||
|
||||
transactions.push({
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce!.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId: parseInt(chainId, 16),
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transactions,
|
||||
awaitBatchReceipt: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
539
contracts/clients/src/BlsSigner.ts
Normal file
539
contracts/clients/src/BlsSigner.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { ethers, BigNumber, Signer, Bytes, BigNumberish } from "ethers";
|
||||
import {
|
||||
AccessListish,
|
||||
Deferrable,
|
||||
hexlify,
|
||||
isBytes,
|
||||
RLP,
|
||||
} from "ethers/lib/utils";
|
||||
|
||||
import BlsProvider, { PublicKeyLinkedToActions } from "./BlsProvider";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
|
||||
import { ActionData, bundleToDto } from "./signer";
|
||||
|
||||
export const _constructorGuard = {};
|
||||
|
||||
/**
|
||||
* Based on draft wallet_batchTransactions rpc proposal https://hackmd.io/HFHohGDbRSGgUFI2rk22bA?view
|
||||
*
|
||||
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Transaction gas limit
|
||||
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Miner tip aka priority fee
|
||||
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) The maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
|
||||
* @property nonce - Integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
|
||||
* @property chainId - Chain ID that this transaction is valid on
|
||||
* @property accessList - (THIS PROPERTY IS NOT USED BY BLS WALLET) EIP-2930 access list
|
||||
*/
|
||||
export type BatchOptions = {
|
||||
gas?: BigNumberish;
|
||||
maxPriorityFeePerGas: BigNumberish;
|
||||
maxFeePerGas: BigNumberish;
|
||||
nonce: BigNumberish;
|
||||
chainId: number;
|
||||
accessList?: AccessListish;
|
||||
};
|
||||
|
||||
/**
|
||||
* @property transactions - An array of Ethers transaction objects
|
||||
* @property batchOptions - Optional batch options taken into account by smart contract wallets. See {@link BatchOptions}
|
||||
*/
|
||||
export type TransactionBatch = {
|
||||
transactions: Array<ethers.providers.TransactionRequest>;
|
||||
batchOptions?: BatchOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* @property transactions - An array of Ethers transaction response objects
|
||||
* @property awaitBatchReceipt - A function that returns a promise that resolves to a transaction receipt
|
||||
*/
|
||||
export interface TransactionBatchResponse {
|
||||
transactions: Array<ethers.providers.TransactionResponse>;
|
||||
awaitBatchReceipt: (
|
||||
confirmations?: number,
|
||||
) => Promise<ethers.providers.TransactionReceipt>;
|
||||
}
|
||||
|
||||
export default class BlsSigner extends Signer {
|
||||
override readonly provider: BlsProvider;
|
||||
readonly verificationGatewayAddress!: string;
|
||||
readonly aggregatorUtilitiesAddress!: string;
|
||||
wallet!: BlsWalletWrapper;
|
||||
_index: number;
|
||||
_address: string;
|
||||
|
||||
readonly initPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* @param constructorGuard Prevents BlsSigner constructor being called directly
|
||||
* @param provider BlsProvider accociated with this signer
|
||||
* @param privateKey Private key for the account this signer represents
|
||||
* @param addressOrIndex (Not used) Address or index of this account, managed by the connected Ethereum node
|
||||
*/
|
||||
constructor(
|
||||
constructorGuard: Record<string, unknown>,
|
||||
provider: BlsProvider,
|
||||
privateKey: string | Promise<string>,
|
||||
readonly addressOrIndex?: string | number,
|
||||
) {
|
||||
super();
|
||||
this.provider = provider;
|
||||
this.verificationGatewayAddress = this.provider.verificationGatewayAddress;
|
||||
this.aggregatorUtilitiesAddress = this.provider.aggregatorUtilitiesAddress;
|
||||
this.initPromise = this.initializeWallet(privateKey);
|
||||
|
||||
if (constructorGuard !== _constructorGuard) {
|
||||
throw new Error(
|
||||
"do not call the BlsSigner constructor directly; use provider.getSigner",
|
||||
);
|
||||
}
|
||||
|
||||
if (addressOrIndex === null || addressOrIndex === undefined) {
|
||||
addressOrIndex = 0;
|
||||
}
|
||||
|
||||
if (typeof addressOrIndex === "string") {
|
||||
this._address = this.provider.formatter.address(addressOrIndex);
|
||||
this._index = null as any;
|
||||
} else if (typeof addressOrIndex === "number") {
|
||||
this._address = null as any;
|
||||
this._index = addressOrIndex;
|
||||
} else {
|
||||
throw new Error(`
|
||||
invalid address or index. addressOrIndex: ${addressOrIndex}`);
|
||||
}
|
||||
}
|
||||
|
||||
/** Instantiates a BLS Wallet and then connects the signer to it */
|
||||
private async initializeWallet(privateKey: string | Promise<string>) {
|
||||
const resolvedPrivateKey = await privateKey;
|
||||
this.wallet = await BlsWalletWrapper.connect(
|
||||
resolvedPrivateKey,
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends transactions to be executed. Converts the TransactionRequest
|
||||
* to a bundle and adds it to the aggregator
|
||||
*
|
||||
* @remarks The transaction hash returned in the transaction response does
|
||||
* NOT correspond to a transaction hash that can be viewed on a block
|
||||
* explorer. It instead represents the bundle hash, which can be used to
|
||||
* get a transaction receipt that has a hash that can be used on a block explorer
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns A transaction response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
override async sendTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
await this.initPromise;
|
||||
|
||||
if (!transaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: validatedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: validatedTransaction.to!.toString(),
|
||||
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
[action],
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
const result = await this.provider.aggregator.add(bundle);
|
||||
|
||||
if ("failures" in result) {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
return await this.provider._constructTransactionResponse(
|
||||
action,
|
||||
bundle.senderPublicKeys[0],
|
||||
result.hash,
|
||||
nonce,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transactionBatch A transaction batch object
|
||||
* @returns A transaction batch response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
async sendTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
await this.initPromise;
|
||||
|
||||
const validatedTransactionBatch = await this._validateTransactionBatch(
|
||||
transactionBatch,
|
||||
);
|
||||
|
||||
let nonce: BigNumber;
|
||||
if (transactionBatch.batchOptions) {
|
||||
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
const actions: Array<ActionData> = transactionBatch.transactions.map(
|
||||
(transaction) => {
|
||||
return {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to!.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
BigNumber.from(feeEstimate.feeRequired),
|
||||
);
|
||||
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
actions,
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
const result = await this.provider.aggregator.add(bundle);
|
||||
|
||||
if ("failures" in result) {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
|
||||
bundle.senderPublicKeys.map((publicKey, i) => {
|
||||
const operation = bundle.operations[i];
|
||||
const actions = operation.actions;
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
actions,
|
||||
};
|
||||
});
|
||||
|
||||
return await this.provider._constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions,
|
||||
result.hash,
|
||||
nonce,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The address associated with the BlsSigner
|
||||
*/
|
||||
async getAddress(): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (this._address) {
|
||||
return this._address;
|
||||
}
|
||||
|
||||
this._address = this.wallet.address;
|
||||
return this._address;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method passes calls through to the underlying node and allows users to unlock EOA accounts through this provider.
|
||||
* The personal namespace is used to manage keys for ECDSA signing. BLS keys are not supported natively by execution clients.
|
||||
*/
|
||||
async unlock(password: string): Promise<boolean> {
|
||||
const provider = this.provider;
|
||||
|
||||
const address = await this.getAddress();
|
||||
|
||||
return provider.send("personal_unlockAccount", [
|
||||
address.toLowerCase(),
|
||||
password,
|
||||
null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @remarks Signs a transaction that can be executed by the BlsProvider
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns A signed bundle as a string
|
||||
*/
|
||||
override async signTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<string> {
|
||||
await this.initPromise;
|
||||
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: validatedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: validatedTransaction.to!.toString(),
|
||||
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
|
||||
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
|
||||
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
[action],
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
|
||||
return JSON.stringify(bundleToDto(bundle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a transaction batch that can be executed by the BlsProvider
|
||||
*
|
||||
* @param transactionBatch A transaction batch object
|
||||
* @returns A signed bundle containing all transactions from the transaction batch as a string
|
||||
*/
|
||||
async signTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<string> {
|
||||
await this.initPromise;
|
||||
|
||||
const validatedTransactionBatch = await this._validateTransactionBatch(
|
||||
transactionBatch,
|
||||
);
|
||||
|
||||
let nonce: BigNumber;
|
||||
if (transactionBatch.batchOptions) {
|
||||
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
const actions: Array<ActionData> = transactionBatch.transactions.map(
|
||||
(transaction) => {
|
||||
return {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to!.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
BigNumber.from(feeEstimate.feeRequired),
|
||||
);
|
||||
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
actions,
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
|
||||
return JSON.stringify(bundleToDto(bundle));
|
||||
}
|
||||
|
||||
/** Signs a message */
|
||||
// TODO: bls-wallet #201 Come back to this once we support EIP-1271
|
||||
override async signMessage(message: Bytes | string): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (isBytes(message)) {
|
||||
message = hexlify(message);
|
||||
}
|
||||
|
||||
const signedMessage = this.wallet.signMessage(message);
|
||||
return RLP.encode(signedMessage);
|
||||
}
|
||||
|
||||
override connect(provider: ethers.providers.Provider): BlsSigner {
|
||||
throw new Error("cannot alter JSON-RPC Signer connection");
|
||||
}
|
||||
|
||||
async _signTypedData(
|
||||
domain: any,
|
||||
types: Record<string, Array<any>>,
|
||||
value: Record<string, any>,
|
||||
): Promise<string> {
|
||||
throw new Error("_signTypedData() is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A new Signer object which does not perform additional checks when sending a transaction
|
||||
*/
|
||||
connectUnchecked(): BlsSigner {
|
||||
return new UncheckedBlsSigner(
|
||||
_constructorGuard,
|
||||
this.provider,
|
||||
this.wallet?.blsWalletSigner.privateKey ??
|
||||
(async (): Promise<string> => {
|
||||
await this.initPromise;
|
||||
return this.wallet.blsWalletSigner.privateKey;
|
||||
})(),
|
||||
this._address || this._index,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transaction Transaction request object
|
||||
* @returns Transaction hash for the transaction, corresponds to a bundle hash
|
||||
*/
|
||||
async sendUncheckedTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<string> {
|
||||
const transactionResponse = await this.sendTransaction(transaction);
|
||||
return transactionResponse.hash;
|
||||
}
|
||||
|
||||
async _legacySignMessage(message: Bytes | string): Promise<string> {
|
||||
throw new Error("_legacySignMessage() is not implemented");
|
||||
}
|
||||
|
||||
async _validateTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionRequest> {
|
||||
const resolvedTransaction = await ethers.utils.resolveProperties(
|
||||
transaction,
|
||||
);
|
||||
|
||||
if (!resolvedTransaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
if (!resolvedTransaction.from) {
|
||||
resolvedTransaction.from = await this.getAddress();
|
||||
}
|
||||
|
||||
return resolvedTransaction;
|
||||
}
|
||||
|
||||
async _validateTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<TransactionBatch> {
|
||||
const signerAddress = await this.getAddress();
|
||||
|
||||
const validatedTransactions: Array<ethers.providers.TransactionRequest> =
|
||||
transactionBatch.transactions.map((transaction, i) => {
|
||||
if (!transaction.to) {
|
||||
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
|
||||
}
|
||||
|
||||
if (!transaction.from) {
|
||||
transaction.from = signerAddress;
|
||||
}
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
};
|
||||
});
|
||||
|
||||
const validatedBatchOptions = transactionBatch.batchOptions
|
||||
? await this._validateBatchOptions(transactionBatch.batchOptions)
|
||||
: transactionBatch.batchOptions;
|
||||
|
||||
return {
|
||||
transactions: validatedTransactions,
|
||||
batchOptions: validatedBatchOptions,
|
||||
};
|
||||
}
|
||||
|
||||
async _validateBatchOptions(
|
||||
batchOptions: BatchOptions,
|
||||
): Promise<BatchOptions> {
|
||||
const expectedChainId = await this.getChainId();
|
||||
|
||||
if (batchOptions.chainId !== expectedChainId) {
|
||||
throw new Error(
|
||||
`Supplied chain ID ${batchOptions.chainId} does not match the expected chain ID ${expectedChainId}`,
|
||||
);
|
||||
}
|
||||
|
||||
batchOptions.nonce = BigNumber.from(batchOptions.nonce);
|
||||
return batchOptions;
|
||||
}
|
||||
}
|
||||
|
||||
export class UncheckedBlsSigner extends BlsSigner {
|
||||
/**
|
||||
* As with other transaction methods, the transaction hash returned represents the bundle hash, NOT a transaction hash you can use on a block explorer
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns The transaction response object with only the transaction hash property populated with a valid value
|
||||
*/
|
||||
override async sendTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
await this.initPromise;
|
||||
|
||||
const transactionResponse = await super.sendTransaction(transaction);
|
||||
return {
|
||||
hash: transactionResponse.hash,
|
||||
nonce: NaN,
|
||||
gasLimit: BigNumber.from(0),
|
||||
gasPrice: BigNumber.from(0),
|
||||
data: "",
|
||||
value: BigNumber.from(0),
|
||||
chainId: 0,
|
||||
confirmations: 0,
|
||||
from: "",
|
||||
wait: (confirmations?: number) => {
|
||||
return this.provider.waitForTransaction(
|
||||
transactionResponse.hash,
|
||||
confirmations,
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
74
contracts/clients/src/BlsWalletContracts.ts
Normal file
74
contracts/clients/src/BlsWalletContracts.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { providers } from "ethers";
|
||||
import {
|
||||
BNPairingPrecompileCostEstimator,
|
||||
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
|
||||
Create2Deployer,
|
||||
Create2Deployer__factory as Create2DeployerFactory,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
BLSOpen,
|
||||
BLSOpen__factory as BLSOpenFactory,
|
||||
BLSExpander,
|
||||
BLSExpander__factory as BLSExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
MockERC20,
|
||||
MockERC20__factory as MockERC20Factory,
|
||||
} from "../typechain-types";
|
||||
import { NetworkConfig } from "./NetworkConfig";
|
||||
|
||||
/**
|
||||
* BLS Wallet Contracts
|
||||
*/
|
||||
export type BlsWalletContracts = Readonly<{
|
||||
create2Deployer: Create2Deployer;
|
||||
precompileCostEstimator: BNPairingPrecompileCostEstimator;
|
||||
verificationGateway: VerificationGateway;
|
||||
blsLibrary: BLSOpen;
|
||||
blsExpander: BLSExpander;
|
||||
aggregatorUtilities: AggregatorUtilities;
|
||||
testToken: MockERC20;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Connects to all deployed BLS Wallet contracts using a Network Config
|
||||
*
|
||||
* @param provider ether.js provider
|
||||
* @param networkConfig NetworkConfig containing contract dpeloyment information
|
||||
* @returns BLS Wallet contracts connected to provider
|
||||
*/
|
||||
export const connectToContracts = async (
|
||||
provider: providers.Provider,
|
||||
{ addresses }: NetworkConfig,
|
||||
): Promise<BlsWalletContracts> => {
|
||||
const [
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
] = await Promise.all([
|
||||
Create2DeployerFactory.connect(addresses.create2Deployer, provider),
|
||||
BNPairingPrecompileCostEstimatorFactory.connect(
|
||||
addresses.create2Deployer,
|
||||
provider,
|
||||
),
|
||||
VerificationGatewayFactory.connect(addresses.verificationGateway, provider),
|
||||
BLSOpenFactory.connect(addresses.blsLibrary, provider),
|
||||
BLSExpanderFactory.connect(addresses.blsExpander, provider),
|
||||
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
|
||||
MockERC20Factory.connect(addresses.testToken, provider),
|
||||
]);
|
||||
|
||||
return {
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
};
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { solidityKeccak256 } from "ethers/lib/utils";
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { keccak256, solidityKeccak256, solidityPack } from "ethers/lib/utils";
|
||||
import {
|
||||
BlsWalletSigner,
|
||||
initBlsWalletSigner,
|
||||
@@ -12,100 +13,162 @@ import {
|
||||
|
||||
import {
|
||||
BLSWallet,
|
||||
// eslint-disable-next-line camelcase
|
||||
BLSWallet__factory,
|
||||
// eslint-disable-next-line camelcase
|
||||
TransparentUpgradeableProxy__factory,
|
||||
// eslint-disable-next-line camelcase
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
} from "../typechain";
|
||||
} from "../typechain-types";
|
||||
|
||||
import getRandomBlsPrivateKey from "./signer/getRandomBlsPrivateKey";
|
||||
|
||||
type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
|
||||
|
||||
/**
|
||||
* Class representing a BLS Wallet
|
||||
*/
|
||||
export default class BlsWalletWrapper {
|
||||
public address: string;
|
||||
private constructor(
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
public privateKey: string,
|
||||
public address: string,
|
||||
public walletContract: BLSWallet,
|
||||
) {}
|
||||
) {
|
||||
this.address = walletContract.address;
|
||||
}
|
||||
|
||||
/** Get the wallet contract address for the given key, if it exists. */
|
||||
static async BLSWallet(
|
||||
privateKey: string,
|
||||
verificationGateway: VerificationGateway,
|
||||
): Promise<BLSWallet> {
|
||||
const contractAddress = await BlsWalletWrapper.Address(
|
||||
privateKey,
|
||||
verificationGateway.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
return BLSWallet__factory.connect(
|
||||
contractAddress,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the address for this wallet.
|
||||
*
|
||||
* This could be:
|
||||
* - The address the wallet is registered to on the VerificationGateway.
|
||||
* - The expected address if it has not already be created/registered.
|
||||
* - The original wallet address before it was recovered to another key pair.
|
||||
*
|
||||
* Throws an exception if wallet was recovered to a different private key.
|
||||
*
|
||||
* @param privateKey private key associated with the wallet
|
||||
* @param verificationGatewayAddress address of the VerficationGateway contract
|
||||
* @param signerOrProvider ethers.js Signer or Provider
|
||||
* @returns The wallet's address
|
||||
*/
|
||||
static async Address(
|
||||
privateKey: string,
|
||||
verificationGatewayAddress: string,
|
||||
signerOrProvider: SignerOrProvider,
|
||||
/**
|
||||
* Internal value associated with the bls-wallet-signer library that can be
|
||||
* provided as an optimization, otherwise it will be created
|
||||
* automatically.
|
||||
*/
|
||||
blsWalletSigner?: BlsWalletSigner,
|
||||
): Promise<string> {
|
||||
blsWalletSigner ??= await this.#BlsWalletSigner(signerOrProvider);
|
||||
const blsWalletSigner = await this.#BlsWalletSigner(
|
||||
signerOrProvider,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
);
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
const pubKeyHash = blsWalletSigner.getPublicKeyHash();
|
||||
|
||||
const [proxyAdminAddress, blsWalletLogicAddress] = await Promise.all([
|
||||
verificationGateway.walletProxyAdmin(),
|
||||
verificationGateway.blsWalletLogic(),
|
||||
]);
|
||||
|
||||
const initFunctionParams =
|
||||
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
|
||||
verificationGatewayAddress,
|
||||
]);
|
||||
|
||||
return ethers.utils.getCreate2Address(
|
||||
verificationGatewayAddress,
|
||||
blsWalletSigner.getPublicKeyHash(privateKey),
|
||||
ethers.utils.solidityKeccak256(
|
||||
["bytes", "bytes"],
|
||||
[
|
||||
TransparentUpgradeableProxy__factory.bytecode,
|
||||
ethers.utils.defaultAbiCoder.encode(
|
||||
["address", "address", "bytes"],
|
||||
[blsWalletLogicAddress, proxyAdminAddress, initFunctionParams],
|
||||
),
|
||||
],
|
||||
),
|
||||
const existingAddress = await verificationGateway.walletFromHash(
|
||||
pubKeyHash,
|
||||
);
|
||||
const hasExistingAddress = !BigNumber.from(existingAddress).isZero();
|
||||
if (hasExistingAddress) {
|
||||
return existingAddress;
|
||||
}
|
||||
|
||||
const expectedAddress = await this.ExpectedAddress(
|
||||
verificationGateway,
|
||||
pubKeyHash,
|
||||
);
|
||||
this.validateWalletNotRecovered(
|
||||
blsWalletSigner,
|
||||
verificationGateway,
|
||||
expectedAddress,
|
||||
);
|
||||
|
||||
return expectedAddress;
|
||||
}
|
||||
|
||||
/** Get the wallet contract address for the given public key */
|
||||
static async AddressFromPublicKey(
|
||||
publicKey: PublicKey,
|
||||
verificationGateway: VerificationGateway,
|
||||
): Promise<string> {
|
||||
const pubKeyHash = keccak256(solidityPack(["uint256[4]"], [publicKey]));
|
||||
|
||||
const existingAddress = await verificationGateway.walletFromHash(
|
||||
pubKeyHash,
|
||||
);
|
||||
if (!BigNumber.from(existingAddress).isZero()) {
|
||||
return existingAddress;
|
||||
}
|
||||
|
||||
return this.ExpectedAddress(verificationGateway, pubKeyHash);
|
||||
}
|
||||
|
||||
static async getRandomBlsPrivateKey(): Promise<string> {
|
||||
return getRandomBlsPrivateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Instantiate a `BLSWallet` associated with the provided key if the
|
||||
* Instantiate a `BLSWallet` associated with the provided private key.
|
||||
* associated wallet contract already exists.
|
||||
*
|
||||
* Throws an exception if wallet was recovered to a different private key.
|
||||
*
|
||||
* @param privateKey private key associated with the wallet
|
||||
* @param verificationGatewayAddress address of the VerficationGateway contract
|
||||
* @param provider ethers.js Provider
|
||||
* @returns a BLS Wallet
|
||||
*/
|
||||
static async connect(
|
||||
privateKey: string,
|
||||
verificationGatewayAddress: string,
|
||||
provider: ethers.providers.Provider,
|
||||
): Promise<BlsWalletWrapper> {
|
||||
const network = await provider.getNetwork();
|
||||
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId: network.chainId,
|
||||
});
|
||||
|
||||
const contractAddress = await BlsWalletWrapper.Address(
|
||||
privateKey,
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
provider,
|
||||
);
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId: (await verificationGateway.provider.getNetwork()).chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
|
||||
const walletContract = BLSWallet__factory.connect(
|
||||
contractAddress,
|
||||
provider,
|
||||
const blsWalletWrapper = new BlsWalletWrapper(
|
||||
blsWalletSigner,
|
||||
await BlsWalletWrapper.BLSWallet(privateKey, verificationGateway),
|
||||
);
|
||||
|
||||
return new BlsWalletWrapper(
|
||||
blsWalletSigner,
|
||||
privateKey,
|
||||
contractAddress,
|
||||
walletContract,
|
||||
return blsWalletWrapper;
|
||||
}
|
||||
|
||||
async syncWallet(verificationGateway: VerificationGateway) {
|
||||
this.address = await BlsWalletWrapper.Address(
|
||||
this.blsWalletSigner.privateKey,
|
||||
verificationGateway.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
this.walletContract = BLSWallet__factory.connect(
|
||||
this.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +177,9 @@ export default class BlsWalletWrapper {
|
||||
* block.
|
||||
*/
|
||||
async Nonce(): Promise<BigNumber> {
|
||||
const code = await this.walletContract.provider.getCode(this.address);
|
||||
const code = await this.walletContract.provider.getCode(
|
||||
this.walletContract.address,
|
||||
);
|
||||
|
||||
if (code === "0x") {
|
||||
// The wallet doesn't exist yet. Wallets are lazily created, so the nonce
|
||||
@@ -164,12 +229,12 @@ export default class BlsWalletWrapper {
|
||||
|
||||
/** Sign an operation, producing a `Bundle` object suitable for use with an aggregator. */
|
||||
sign(operation: Operation): Bundle {
|
||||
return this.blsWalletSigner.sign(operation, this.privateKey);
|
||||
return this.blsWalletSigner.sign(operation, this.walletContract.address);
|
||||
}
|
||||
|
||||
/** Sign a message */
|
||||
signMessage(message: string): Signature {
|
||||
return this.blsWalletSigner.signMessage(message, this.privateKey);
|
||||
return this.blsWalletSigner.signMessage(message);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +243,7 @@ export default class BlsWalletWrapper {
|
||||
* @returns Wallet's BLS public key.
|
||||
*/
|
||||
PublicKey(): PublicKey {
|
||||
return this.blsWalletSigner.getPublicKey(this.privateKey);
|
||||
return this.blsWalletSigner.getPublicKey();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -187,17 +252,164 @@ export default class BlsWalletWrapper {
|
||||
* @returns Wallet's BLS public key as a string.
|
||||
*/
|
||||
PublicKeyStr(): string {
|
||||
return this.blsWalletSigner.getPublicKeyStr(this.privateKey);
|
||||
return this.blsWalletSigner.getPublicKeyStr();
|
||||
}
|
||||
|
||||
async getSetRecoveryHashBundle(
|
||||
salt: string,
|
||||
recoverWalletAddress: string,
|
||||
): Promise<Bundle> {
|
||||
const saltHash = ethers.utils.formatBytes32String(salt);
|
||||
const walletHash = this.blsWalletSigner.getPublicKeyHash();
|
||||
const recoveryHash = ethers.utils.solidityKeccak256(
|
||||
["address", "bytes32", "bytes32"],
|
||||
[recoverWalletAddress, walletHash, saltHash],
|
||||
);
|
||||
|
||||
return this.sign({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: this.walletContract.address,
|
||||
encodedFunction: this.walletContract.interface.encodeFunctionData(
|
||||
"setRecoveryHash",
|
||||
[recoveryHash],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
async getRecoverWalletBundle(
|
||||
recoveryAddress: string,
|
||||
newPrivateKey: string,
|
||||
recoverySalt: string,
|
||||
verificationGateway: VerificationGateway,
|
||||
): Promise<Bundle> {
|
||||
const updatedWallet = await BlsWalletWrapper.connect(
|
||||
newPrivateKey,
|
||||
verificationGateway.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
const addressMessage = solidityPack(["address"], [recoveryAddress]);
|
||||
const addressSignature = updatedWallet.signMessage(addressMessage);
|
||||
|
||||
const recoveryWalletHash = await verificationGateway.hashFromWallet(
|
||||
recoveryAddress,
|
||||
);
|
||||
const saltHash = ethers.utils.formatBytes32String(recoverySalt);
|
||||
|
||||
return this.sign({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: verificationGateway.address,
|
||||
encodedFunction: verificationGateway.interface.encodeFunctionData(
|
||||
"recoverWallet",
|
||||
[
|
||||
addressSignature,
|
||||
recoveryWalletHash,
|
||||
saltHash,
|
||||
updatedWallet.PublicKey(),
|
||||
],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
static async #BlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
verificationGatewayAddress: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
return await initBlsWalletSigner({ chainId });
|
||||
return await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the BlsWalletSigner instance to a new private key and chainId
|
||||
*
|
||||
* @returns The updated BlsWalletSigner object
|
||||
*/
|
||||
async setBlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
const newBlsWalletSigner = await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress: this.walletContract.address,
|
||||
});
|
||||
|
||||
this.blsWalletSigner = newBlsWalletSigner;
|
||||
return newBlsWalletSigner;
|
||||
}
|
||||
|
||||
// Calculates the expected address the wallet will be created at
|
||||
private static async ExpectedAddress(
|
||||
verificationGateway: VerificationGateway,
|
||||
pubKeyHash: string,
|
||||
): Promise<string> {
|
||||
const [proxyAdminAddress, blsWalletLogicAddress] = await Promise.all([
|
||||
verificationGateway.walletProxyAdmin(),
|
||||
verificationGateway.blsWalletLogic(),
|
||||
]);
|
||||
|
||||
const initFunctionParams =
|
||||
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
|
||||
verificationGateway.address,
|
||||
]);
|
||||
|
||||
return ethers.utils.getCreate2Address(
|
||||
verificationGateway.address,
|
||||
pubKeyHash,
|
||||
ethers.utils.solidityKeccak256(
|
||||
["bytes", "bytes"],
|
||||
[
|
||||
TransparentUpgradeableProxy__factory.bytecode,
|
||||
ethers.utils.defaultAbiCoder.encode(
|
||||
["address", "address", "bytes"],
|
||||
[blsWalletLogicAddress, proxyAdminAddress, initFunctionParams],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private static async validateWalletNotRecovered(
|
||||
blsWalletSigner: BlsWalletSigner,
|
||||
verificationGateway: VerificationGateway,
|
||||
walletAddress: string,
|
||||
): Promise<void> {
|
||||
const pubKeyHash = blsWalletSigner.getPublicKeyHash();
|
||||
const existingPubKeyHash = await verificationGateway.hashFromWallet(
|
||||
walletAddress,
|
||||
);
|
||||
|
||||
const walletIsAlreadyRegistered =
|
||||
!BigNumber.from(existingPubKeyHash).isZero();
|
||||
const pubKeyHashesDoNotMatch = pubKeyHash !== existingPubKeyHash;
|
||||
|
||||
if (walletIsAlreadyRegistered && pubKeyHashesDoNotMatch) {
|
||||
throw new Error(
|
||||
`wallet at ${walletAddress} has been recovered from public key hash ${pubKeyHash} to ${existingPubKeyHash}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
contracts/clients/src/MultiNetworkConfig.ts
Normal file
61
contracts/clients/src/MultiNetworkConfig.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NetworkConfig, validateConfig } from "./NetworkConfig";
|
||||
|
||||
/**
|
||||
* Config representing the deployed state of bls-wallet contracts
|
||||
* across multiple networks.
|
||||
*/
|
||||
export type MultiNetworkConfig = {
|
||||
[networkKey: string]: NetworkConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unvalidated MultiNetworkConfig
|
||||
*/
|
||||
export type UnvalidatedMultiNetworkConfig = Record<
|
||||
string,
|
||||
Record<string, Record<string, unknown>>
|
||||
>;
|
||||
|
||||
type ReadFileFunc = (filePath: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Validates and returns a multi-network config.
|
||||
*
|
||||
* @param cfg The config object to validate.
|
||||
*/
|
||||
export function validateMultiConfig(
|
||||
cfg: MultiNetworkConfig,
|
||||
): MultiNetworkConfig {
|
||||
const isEmpty = !Object.keys(cfg).length;
|
||||
if (isEmpty) {
|
||||
throw new Error("config is empty");
|
||||
}
|
||||
|
||||
const multiConfig: MultiNetworkConfig = {};
|
||||
for (const [networkKey, networkConfig] of Object.entries(cfg)) {
|
||||
try {
|
||||
multiConfig[networkKey] = validateConfig(networkConfig);
|
||||
} catch (err) {
|
||||
const castErr = err as Error;
|
||||
const newErr = new Error(`${networkKey}: ${castErr.message}`);
|
||||
newErr.stack = castErr.stack;
|
||||
throw newErr;
|
||||
}
|
||||
}
|
||||
return multiConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves, validates, and returns a multi-network config.
|
||||
*
|
||||
* @param networkConfigPath Path to config JSON file.
|
||||
* @param readFileFunc Callback to retrieve the config. This could be via fetch, fs.readFile, etc.
|
||||
*/
|
||||
export async function getMultiConfig(
|
||||
configPath: string,
|
||||
readFileFunc: ReadFileFunc,
|
||||
): Promise<NetworkConfig> {
|
||||
const cfg = JSON.parse(await readFileFunc(configPath));
|
||||
validateMultiConfig(cfg);
|
||||
return cfg;
|
||||
}
|
||||
@@ -43,13 +43,14 @@ export type NetworkConfig = {
|
||||
};
|
||||
|
||||
type ReadFileFunc = (filePath: string) => Promise<string>;
|
||||
type UnvalidatedConfig = Record<string, Record<string, unknown>>;
|
||||
|
||||
/**
|
||||
* Validates and returns a network config.
|
||||
*
|
||||
* @param cfg The config object to validate.
|
||||
*/
|
||||
export function validateConfig(cfg: any): NetworkConfig {
|
||||
export function validateConfig(cfg: UnvalidatedConfig): NetworkConfig {
|
||||
return {
|
||||
parameters: assertUnknownRecord(cfg.parameters),
|
||||
addresses: {
|
||||
@@ -75,6 +76,7 @@ export function validateConfig(cfg: any): NetworkConfig {
|
||||
|
||||
/**
|
||||
* Retrieves, validates, and returns a network config.
|
||||
* @deprecated Use getMultiConfig instead.
|
||||
*
|
||||
* @param networkConfigPath Path to config JSON file.
|
||||
* @param readFileFunc Callback to retrieve the config. This could be via fetch, fs.readFile, etc.
|
||||
|
||||
182
contracts/clients/src/OperationResults.ts
Normal file
182
contracts/clients/src/OperationResults.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { BigNumber, ContractReceipt, utils } from "ethers";
|
||||
import assert from "./helpers/assert";
|
||||
import { ActionData } from "./signer";
|
||||
|
||||
export const errorSelectors = {
|
||||
Error: calculateAndCheckSelector("Error(string)", "0x08c379a0"),
|
||||
|
||||
Panic: calculateAndCheckSelector("Panic(uint256)", "0x4e487b71"),
|
||||
|
||||
ActionError: calculateAndCheckSelector(
|
||||
"ActionError(uint256,bytes)",
|
||||
"0x5c667601",
|
||||
),
|
||||
};
|
||||
|
||||
const actionErrorId = utils
|
||||
.keccak256(new TextEncoder().encode("ActionError(uint256,bytes)"))
|
||||
.slice(0, 10);
|
||||
|
||||
assert(actionErrorId === "0x5c667601");
|
||||
|
||||
export type OperationResultError = {
|
||||
actionIndex?: BigNumber;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type OperationResult = {
|
||||
walletAddress: string;
|
||||
nonce: BigNumber;
|
||||
actions: ActionData[];
|
||||
success: boolean;
|
||||
results: string[];
|
||||
error?: OperationResultError;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if a operation result error string is valid and returns
|
||||
* the decoded error.
|
||||
*
|
||||
* @param errorData An error string returned by an operation result.
|
||||
*/
|
||||
export const decodeError = (errorData: string): OperationResultError => {
|
||||
if (!errorData.startsWith(errorSelectors.ActionError)) {
|
||||
throw new Error(
|
||||
[
|
||||
`errorResult does not begin with ActionError selector`,
|
||||
`(${errorSelectors.ActionError}): ${errorData}`,
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
// remove methodId (4bytes after 0x)
|
||||
const actionErrorArgBytes = `0x${errorData.slice(10)}`;
|
||||
|
||||
let actionIndex: BigNumber | undefined;
|
||||
let message: string;
|
||||
|
||||
try {
|
||||
const [actionIndexDecoded, actionErrorData] = utils.defaultAbiCoder.decode(
|
||||
["uint256", "bytes"],
|
||||
actionErrorArgBytes,
|
||||
) as [BigNumber, string];
|
||||
|
||||
actionIndex = actionIndexDecoded;
|
||||
|
||||
const actionErrorDataBody = `0x${actionErrorData.slice(10)}`;
|
||||
|
||||
if (actionErrorData.startsWith(errorSelectors.Error)) {
|
||||
[message] = utils.defaultAbiCoder.decode(["string"], actionErrorDataBody);
|
||||
} else if (actionErrorData.startsWith(errorSelectors.Panic)) {
|
||||
const [panicCode] = utils.defaultAbiCoder.decode(
|
||||
["uint256"],
|
||||
actionErrorDataBody,
|
||||
) as [BigNumber];
|
||||
|
||||
message = [
|
||||
`Panic: ${panicCode.toHexString()}`,
|
||||
"(See Panic(uint256) in the solidity docs:",
|
||||
"https://docs.soliditylang.org/_/downloads/en/latest/pdf/)",
|
||||
].join(" ");
|
||||
} else {
|
||||
message = `Unexpected action error data: ${actionErrorData}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message = `Unexpected error data: ${errorData}`;
|
||||
}
|
||||
|
||||
return {
|
||||
actionIndex,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
const getError = (
|
||||
success: boolean,
|
||||
results: string[],
|
||||
): OperationResultError | undefined => {
|
||||
if (success) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Single event "WalletOperationProcessed(address indexed wallet, uint256 nonce, bool success, bytes[] results)"
|
||||
// Get the first (only) result from "results" argument.
|
||||
const [errorData] = results;
|
||||
|
||||
return decodeError(errorData);
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the results of operations (and actions) run through VerificationGateway.processBundle.
|
||||
* Decodes unsuccessful operations into an error message and the index of the action that failed.
|
||||
*
|
||||
* @param transactionReceipt Transaction receipt from a VerificationGateway.processBundle transaction
|
||||
* @returns An array of decoded operation results
|
||||
*/
|
||||
export const getOperationResults = (
|
||||
txnReceipt: ContractReceipt,
|
||||
): OperationResult[] => {
|
||||
const walletOpProcessedEvents = txnReceipt.events?.filter(
|
||||
(e) => e.event === "WalletOperationProcessed",
|
||||
);
|
||||
if (!walletOpProcessedEvents?.length) {
|
||||
throw new Error(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
return walletOpProcessedEvents.reduce<OperationResult[]>(
|
||||
(opResults, { args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
const { wallet, nonce, actions: rawActions, success, results } = args;
|
||||
|
||||
const actions = rawActions.map(
|
||||
({
|
||||
ethValue,
|
||||
contractAddress,
|
||||
encodedFunction,
|
||||
}: {
|
||||
ethValue: BigNumber;
|
||||
contractAddress: string;
|
||||
encodedFunction: string;
|
||||
}) => ({
|
||||
ethValue,
|
||||
contractAddress,
|
||||
encodedFunction,
|
||||
}),
|
||||
);
|
||||
const error = getError(success, results);
|
||||
|
||||
return [
|
||||
...opResults,
|
||||
{
|
||||
walletAddress: wallet,
|
||||
nonce,
|
||||
actions,
|
||||
success,
|
||||
results,
|
||||
error,
|
||||
},
|
||||
];
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
function calculateSelector(signature: string) {
|
||||
return utils.keccak256(new TextEncoder().encode(signature)).slice(0, 10);
|
||||
}
|
||||
|
||||
function calculateAndCheckSelector(signature: string, expected: string) {
|
||||
const selector = calculateSelector(signature);
|
||||
|
||||
assert(
|
||||
selector === expected,
|
||||
`Selector for ${signature} was not ${expected}`,
|
||||
);
|
||||
|
||||
return selector;
|
||||
}
|
||||
18
contracts/clients/src/helpers/addSafetyDivisorToFee.ts
Normal file
18
contracts/clients/src/helpers/addSafetyDivisorToFee.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { BigNumber } from "ethers";
|
||||
|
||||
/**
|
||||
* Used to add a small safety premium to estimated fees. This protects
|
||||
* against small fluctuations is gas estimation, and thus increases
|
||||
* the chance that bundles get accepted during aggregation.
|
||||
*
|
||||
* @param feeEstimate fee required for bundle
|
||||
* @param safetyDivisor optional safety divisor. Default is 5
|
||||
* @returns fee estimate with added safety premium
|
||||
*/
|
||||
export default function addSafetyPremiumToFee(
|
||||
feeEstimate: BigNumber,
|
||||
safetyDivisor: number = 5,
|
||||
): BigNumber {
|
||||
const safetyPremium = feeEstimate.div(safetyDivisor);
|
||||
return feeEstimate.add(safetyPremium);
|
||||
}
|
||||
22
contracts/clients/src/helpers/poll.ts
Normal file
22
contracts/clients/src/helpers/poll.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export default async function poll(
|
||||
fn: Function,
|
||||
fnCondition: Function,
|
||||
retries: number,
|
||||
milliseconds: number,
|
||||
) {
|
||||
let retryCount = 0;
|
||||
let result = await fn();
|
||||
|
||||
while (fnCondition(result) && retryCount <= retries) {
|
||||
await wait(milliseconds);
|
||||
result = await fn();
|
||||
retryCount++;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function wait(milliseconds = 1000) {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(resolve, milliseconds);
|
||||
});
|
||||
}
|
||||
@@ -1,32 +1,68 @@
|
||||
import Aggregator from "./Aggregator";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import BlsProvider from "./BlsProvider";
|
||||
import BlsSigner from "./BlsSigner";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { VerificationGateway__factory } from "../typechain/factories/VerificationGateway__factory";
|
||||
import type { VerificationGateway } from "../typechain/VerificationGateway";
|
||||
import { VerificationGateway__factory } from "../typechain-types/factories/contracts/VerificationGateway__factory";
|
||||
import type { VerificationGateway } from "../typechain-types/contracts/VerificationGateway";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { AggregatorUtilities__factory } from "../typechain/factories/AggregatorUtilities__factory";
|
||||
import type { AggregatorUtilities } from "../typechain/AggregatorUtilities";
|
||||
import { AggregatorUtilities__factory } from "../typechain-types/factories/contracts/AggregatorUtilities__factory";
|
||||
import type { AggregatorUtilities } from "../typechain-types/contracts/AggregatorUtilities";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { ERC20__factory } from "../typechain/factories/ERC20__factory";
|
||||
import type { ERC20 } from "../typechain/ERC20";
|
||||
import { ERC20__factory } from "../typechain-types/factories/@openzeppelin/contracts/token/ERC20/ERC20__factory";
|
||||
import type { ERC20 } from "../typechain-types/@openzeppelin/contracts/token/ERC20/ERC20";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { MockERC20__factory } from "../typechain/factories/MockERC20__factory";
|
||||
import type { MockERC20 } from "../typechain/MockERC20";
|
||||
import { MockERC20__factory } from "../typechain-types/factories/contracts/mock/MockERC20__factory";
|
||||
import type { MockERC20 } from "../typechain-types/contracts/mock/MockERC20";
|
||||
|
||||
import { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
import {
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
} from "./MultiNetworkConfig";
|
||||
|
||||
import {
|
||||
OperationResult,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
OperationResultError,
|
||||
} from "./OperationResults";
|
||||
import { BlsWalletContracts, connectToContracts } from "./BlsWalletContracts";
|
||||
|
||||
export * from "./signer";
|
||||
|
||||
const Experimental_ = {
|
||||
BlsProvider,
|
||||
BlsSigner,
|
||||
};
|
||||
|
||||
/**
|
||||
* The Experimental namespace exposes APIs that are unstable.
|
||||
* Unstable in the sense that the APIs will be less functional, less well-tested, and/or are expected to change.
|
||||
*/
|
||||
namespace Experimental {
|
||||
export const BlsProvider = Experimental_.BlsProvider;
|
||||
export const BlsSigner = Experimental_.BlsSigner;
|
||||
}
|
||||
|
||||
export {
|
||||
Aggregator,
|
||||
BlsWalletWrapper,
|
||||
NetworkConfig,
|
||||
getConfig,
|
||||
validateConfig,
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
OperationResult,
|
||||
OperationResultError,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
// eslint-disable-next-line camelcase
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway,
|
||||
@@ -39,4 +75,7 @@ export {
|
||||
// eslint-disable-next-line camelcase
|
||||
MockERC20__factory,
|
||||
MockERC20,
|
||||
BlsWalletContracts,
|
||||
connectToContracts,
|
||||
Experimental,
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
import { arrayify, keccak256 } from "ethers/lib/utils";
|
||||
|
||||
export default arrayify(keccak256("0xfeedbee5"));
|
||||
@@ -1,8 +1,8 @@
|
||||
import { keccak256, solidityPack } from "ethers/lib/utils";
|
||||
import { Operation } from "./types";
|
||||
|
||||
export default (chainId: number) =>
|
||||
(operation: Operation): string => {
|
||||
export default () =>
|
||||
(operation: Operation, walletAddress: string): string => {
|
||||
let encodedActionData = "0x";
|
||||
|
||||
for (const action of operation.actions) {
|
||||
@@ -18,7 +18,7 @@ export default (chainId: number) =>
|
||||
}
|
||||
|
||||
return solidityPack(
|
||||
["uint256", "uint256", "bytes32"],
|
||||
[chainId, operation.nonce, keccak256(encodedActionData)],
|
||||
["address", "uint256", "bytes32"],
|
||||
[walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
);
|
||||
};
|
||||
|
||||
15
contracts/clients/src/signer/getDomain.ts
Normal file
15
contracts/clients/src/signer/getDomain.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { arrayify, solidityPack } from "ethers/lib/utils";
|
||||
import { utils } from "ethers";
|
||||
|
||||
export default (
|
||||
chainId: number,
|
||||
verificationGatewayAddress: string,
|
||||
type: string,
|
||||
): Uint8Array => {
|
||||
const encoded = solidityPack(
|
||||
["uint256", "address", "string"],
|
||||
[chainId, verificationGatewayAddress, type],
|
||||
);
|
||||
|
||||
return arrayify(utils.keccak256(encoded));
|
||||
};
|
||||
@@ -1,8 +1,12 @@
|
||||
import { signer } from "@thehubbleproject/bls";
|
||||
import { PublicKey } from "./types";
|
||||
|
||||
export default (signerFactory: signer.BlsSignerFactory, domain: Uint8Array) =>
|
||||
(privateKey: string): PublicKey => {
|
||||
export default (
|
||||
signerFactory: signer.BlsSignerFactory,
|
||||
domain: Uint8Array,
|
||||
privateKey: string,
|
||||
) =>
|
||||
(): PublicKey => {
|
||||
const signer = signerFactory.getSigner(domain, privateKey);
|
||||
return signer.pubkey;
|
||||
};
|
||||
|
||||
@@ -3,11 +3,15 @@ import { signer } from "@thehubbleproject/bls";
|
||||
|
||||
import getPublicKey from "./getPublicKey";
|
||||
|
||||
export default (signerFactory: signer.BlsSignerFactory, domain: Uint8Array) =>
|
||||
(privateKey: string): string => {
|
||||
const publicKey = getPublicKey(signerFactory, domain)(privateKey);
|
||||
export default (
|
||||
signerFactory: signer.BlsSignerFactory,
|
||||
domain: Uint8Array,
|
||||
privateKey: string,
|
||||
) =>
|
||||
(): string => {
|
||||
const publicKey = getPublicKey(signerFactory, domain, privateKey);
|
||||
return solidityKeccak256(
|
||||
["uint256", "uint256", "uint256", "uint256"],
|
||||
publicKey,
|
||||
publicKey(),
|
||||
);
|
||||
};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user