mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-11 14:57:56 -05:00
Compare commits
1298 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7671d78a1b | ||
|
|
9476d34132 | ||
|
|
f8dba7891b | ||
|
|
405e23a7a0 | ||
|
|
48637336b3 | ||
|
|
d0be22aa49 | ||
|
|
5d98448c66 | ||
|
|
c72b0ea971 | ||
|
|
3251deca54 | ||
|
|
906538a295 | ||
|
|
ada85008ad | ||
|
|
d75868c182 | ||
|
|
abde5526c2 | ||
|
|
823ec47c42 | ||
|
|
2317b38e28 | ||
|
|
f31a35bd0e | ||
|
|
9ccd2545e8 | ||
|
|
25ec77c45c | ||
|
|
8bbb6d3a74 | ||
|
|
797ec3528f | ||
|
|
4af97846bb | ||
|
|
9a9b0284b3 | ||
|
|
60effa15a1 | ||
|
|
e4c7dfb01c | ||
|
|
50b957ad4f | ||
|
|
54227a57b0 | ||
|
|
f9ce7be5b5 | ||
|
|
2a20bfeb8d | ||
|
|
275d593b5c | ||
|
|
00d948376a | ||
|
|
0e51ecb5fe | ||
|
|
576e778855 | ||
|
|
4d170f73dd | ||
|
|
cd010324a5 | ||
|
|
29542e4c98 | ||
|
|
1ff60d5dd1 | ||
|
|
ebf415c573 | ||
|
|
b296a01a80 | ||
|
|
0e7e42154b | ||
|
|
0940333e3c | ||
|
|
831632ce8a | ||
|
|
534c5aa6ae | ||
|
|
1701a1b7bd | ||
|
|
dae68465ba | ||
|
|
f3c0f57b0b | ||
|
|
24b011af9b | ||
|
|
4fd11c0356 | ||
|
|
a58dcdaee7 | ||
|
|
162072155d | ||
|
|
e675062a53 | ||
|
|
95a107f6b8 | ||
|
|
5bd09ec4e5 | ||
|
|
b995c6d6a0 | ||
|
|
7c3aee1f70 | ||
|
|
a6f844b822 | ||
|
|
d7f1917ed7 | ||
|
|
8edaab3a71 | ||
|
|
c5d83f4650 | ||
|
|
ddb8f72864 | ||
|
|
32c9c6c7ed | ||
|
|
48e472389d | ||
|
|
72c7b7c3c5 | ||
|
|
1f52f159e4 | ||
|
|
9c5c3ba950 | ||
|
|
d7369af0cf | ||
|
|
fc5a668ed6 | ||
|
|
40a3335a38 | ||
|
|
9b2ab4bb48 | ||
|
|
a609cb1f8e | ||
|
|
553d9ba81d | ||
|
|
ddbe420f53 | ||
|
|
fbab084b52 | ||
|
|
fff8dd397f | ||
|
|
926637130a | ||
|
|
0047f14a0f | ||
|
|
0d52ddb20f | ||
|
|
ffd7037573 | ||
|
|
daa51bce52 | ||
|
|
69fa24daff | ||
|
|
01a908e26f | ||
|
|
8c91d2faa3 | ||
|
|
b2ab3b766d | ||
|
|
c2afe790cf | ||
|
|
c736179e75 | ||
|
|
dc488ecc00 | ||
|
|
3032cf0917 | ||
|
|
e29c18c63f | ||
|
|
e42ccf2f1f | ||
|
|
ef8f2c7a13 | ||
|
|
0da9350fd6 | ||
|
|
5cacd017d1 | ||
|
|
02c8565e98 | ||
|
|
52d49e16d8 | ||
|
|
b84aebfaa9 | ||
|
|
bdca7833d3 | ||
|
|
56fa5b6e82 | ||
|
|
2cf961ff13 | ||
|
|
9e8269b841 | ||
|
|
1139b4e4f1 | ||
|
|
f5026347e1 | ||
|
|
638e1a1fb2 | ||
|
|
fb5537a118 | ||
|
|
4eeb9afb25 | ||
|
|
1e86c15892 | ||
|
|
f8a8c490e8 | ||
|
|
28f3983fb2 | ||
|
|
b653eaf5ab | ||
|
|
84a89cb5b6 | ||
|
|
9922c3a79c | ||
|
|
f4fdf7148d | ||
|
|
51f4a9588a | ||
|
|
0b1f037ba7 | ||
|
|
000126d226 | ||
|
|
d401e745cf | ||
|
|
21e0532fc0 | ||
|
|
6d1f96ab2e | ||
|
|
008dcf0b09 | ||
|
|
1eed20fa6e | ||
|
|
126d82266b | ||
|
|
ff1f253012 | ||
|
|
c34db600cd | ||
|
|
a37ae25990 | ||
|
|
b48dc20db4 | ||
|
|
4d20166d6f | ||
|
|
7a63b3aa4d | ||
|
|
bc0d57007f | ||
|
|
28226f71e5 | ||
|
|
5ad1314efc | ||
|
|
10c7d54d12 | ||
|
|
0cdc6430e4 | ||
|
|
e1906cdbb7 | ||
|
|
a3b4877c11 | ||
|
|
b7ac4fd77c | ||
|
|
c88b05d0ec | ||
|
|
f6ab5d93ed | ||
|
|
c07dc63896 | ||
|
|
70ca00f089 | ||
|
|
6156b86b22 | ||
|
|
639f6133bf | ||
|
|
1ec0330adb | ||
|
|
cb5932776c | ||
|
|
115907c74f | ||
|
|
5529e078d9 | ||
|
|
4fd593ac1d | ||
|
|
ff27bd0469 | ||
|
|
cac4669cb9 | ||
|
|
5fe9170c3e | ||
|
|
ad9350eb68 | ||
|
|
513df2229e | ||
|
|
f08fa1e9ff | ||
|
|
aa8cd1d681 | ||
|
|
e6326835bb | ||
|
|
cb216fa7d7 | ||
|
|
7978ed0690 | ||
|
|
d86cf09716 | ||
|
|
dc6ebc24d6 | ||
|
|
7b07df3aba | ||
|
|
2aa7e352f5 | ||
|
|
de12b3c62f | ||
|
|
e1248c6b63 | ||
|
|
b988fbc92c | ||
|
|
96bfb32e5b | ||
|
|
7435d9976e | ||
|
|
2dae355817 | ||
|
|
e734209df0 | ||
|
|
4c9c0ed898 | ||
|
|
fcbfd7cc62 | ||
|
|
13f34dd02d | ||
|
|
b565c33193 | ||
|
|
c1404502f3 | ||
|
|
74f9d9020c | ||
|
|
d2c6cff629 | ||
|
|
25469d50e4 | ||
|
|
548301d32d | ||
|
|
4da348d9e2 | ||
|
|
51d7681626 | ||
|
|
7c3fbd4d40 | ||
|
|
ac7cd956a8 | ||
|
|
5423f65503 | ||
|
|
fdf80fc5fb | ||
|
|
3c0f36f444 | ||
|
|
32c6b13e7d | ||
|
|
e3bbd393d8 | ||
|
|
b0ba263eb4 | ||
|
|
b04281cdac | ||
|
|
e196a101ff | ||
|
|
995ec24d1f | ||
|
|
96c61e9932 | ||
|
|
8b76734316 | ||
|
|
519e6f88c4 | ||
|
|
7f803bd10a | ||
|
|
1e71ef3c78 | ||
|
|
708a6d0c2d | ||
|
|
42ea205f79 | ||
|
|
52974e01f6 | ||
|
|
13837e6729 | ||
|
|
7a911a3634 | ||
|
|
4ac580d1df | ||
|
|
30b16185fb | ||
|
|
a9e511c3f0 | ||
|
|
b203072196 | ||
|
|
ee17357e03 | ||
|
|
f6770cf2d4 | ||
|
|
cd8065be16 | ||
|
|
94d6bc8280 | ||
|
|
24a871814e | ||
|
|
3567167ccb | ||
|
|
b4e6be0f98 | ||
|
|
e86db6f72e | ||
|
|
3a8446fda1 | ||
|
|
c1c518e922 | ||
|
|
bcc16373f9 | ||
|
|
0c65c78d7e | ||
|
|
1468c91dc0 | ||
|
|
5ce0943f00 | ||
|
|
8ccad86fdf | ||
|
|
fbb0fe3a1a | ||
|
|
bc0c272d65 | ||
|
|
3a0a1e5f41 | ||
|
|
6de210b81b | ||
|
|
7239d51a57 | ||
|
|
5ce1723ed5 | ||
|
|
547bb7f34d | ||
|
|
059b1d4b1a | ||
|
|
a01d7426ea | ||
|
|
80d551d14e | ||
|
|
e2d8399dbf | ||
|
|
7522fb655f | ||
|
|
fa489ab75f | ||
|
|
41ac18fb86 | ||
|
|
c390ac0321 | ||
|
|
2adb7ace4c | ||
|
|
f90e79d306 | ||
|
|
f3b5552fb2 | ||
|
|
4130335f5a | ||
|
|
43cf586131 | ||
|
|
f4c2e21c9b | ||
|
|
f8223693fa | ||
|
|
e35b160e87 | ||
|
|
cd570155da | ||
|
|
7a9f26d218 | ||
|
|
13779ffe3b | ||
|
|
699f84cede | ||
|
|
e858c3c826 | ||
|
|
4052997b15 | ||
|
|
2f8df2fe3a | ||
|
|
1452ef5f4a | ||
|
|
c963d622a1 | ||
|
|
ca307ef52c | ||
|
|
597920944a | ||
|
|
aa21c603fe | ||
|
|
c260e60443 | ||
|
|
480ee1d3d0 | ||
|
|
25c73d6697 | ||
|
|
af44452199 | ||
|
|
c9c03a699d | ||
|
|
cbbf32225a | ||
|
|
5dfa46f126 | ||
|
|
f5f6f139a3 | ||
|
|
46987d1002 | ||
|
|
334816adda | ||
|
|
d3d4541b55 | ||
|
|
0927798ea0 | ||
|
|
d00f59173f | ||
|
|
7fc77523cc | ||
|
|
0aa0f4efa2 | ||
|
|
8db993a76f | ||
|
|
a1892cb9a1 | ||
|
|
f2c428871f | ||
|
|
d621f982d6 | ||
|
|
1052cc6c28 | ||
|
|
af420d9a7b | ||
|
|
fde1c61e93 | ||
|
|
8bc62681f6 | ||
|
|
bac557a113 | ||
|
|
db968c34fd | ||
|
|
f535662407 | ||
|
|
e5a426a991 | ||
|
|
d5422da19d | ||
|
|
12286389b1 | ||
|
|
6968940f0c | ||
|
|
ea12f8fc42 | ||
|
|
1464476cec | ||
|
|
306b90e83d | ||
|
|
d7348e581d | ||
|
|
5b529facd1 | ||
|
|
4d7a83705b | ||
|
|
8e9cd18934 | ||
|
|
87eb27053a | ||
|
|
dd1a19b8d8 | ||
|
|
a9bc94c641 | ||
|
|
e416c13635 | ||
|
|
2b57673de4 | ||
|
|
edae8c17f0 | ||
|
|
e1e654eac5 | ||
|
|
9202cee106 | ||
|
|
c72a843c85 | ||
|
|
15f28dfe76 | ||
|
|
0e8588e819 | ||
|
|
080568cb93 | ||
|
|
6b4f6ccd96 | ||
|
|
1d887e696e | ||
|
|
51e6666776 | ||
|
|
74d31d561f | ||
|
|
a666a2b9c9 | ||
|
|
d2da8d8e7e | ||
|
|
1018131c27 | ||
|
|
e1a306f9f2 | ||
|
|
847bbddd25 | ||
|
|
3bf2d78215 | ||
|
|
76ea1cabcd | ||
|
|
f4e1c9b250 | ||
|
|
5d34594bfd | ||
|
|
b6e62ed303 | ||
|
|
771335002b | ||
|
|
c9d88ce73b | ||
|
|
39e346f057 | ||
|
|
5769d08a0e | ||
|
|
99ed78bf49 | ||
|
|
ef97000344 | ||
|
|
55ad789c3a | ||
|
|
2a59616b2f | ||
|
|
54f3fbcf41 | ||
|
|
a4aaa42964 | ||
|
|
07da3694f2 | ||
|
|
a968e86a20 | ||
|
|
c5e3461d60 | ||
|
|
81c3d1c965 | ||
|
|
30ddc6f8ab | ||
|
|
0ca6f4f8fa | ||
|
|
365084ed86 | ||
|
|
2daff8b542 | ||
|
|
027f899fe3 | ||
|
|
9f76ceb9f8 | ||
|
|
a348962e42 | ||
|
|
3894fe68a2 | ||
|
|
8972ccbc5c | ||
|
|
4b42d2d960 | ||
|
|
16a043279b | ||
|
|
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 | ||
|
|
59473044a0 | ||
|
|
6af93bde8c | ||
|
|
a800935f51 | ||
|
|
a54b677720 | ||
|
|
2d6ee184a6 | ||
|
|
0b2cca4816 | ||
|
|
e4cde3496d | ||
|
|
29d8884754 | ||
|
|
28ebbe62f4 | ||
|
|
d7f21c96a5 | ||
|
|
5934cf19bd | ||
|
|
7c242ad985 | ||
|
|
b4b757ce8b | ||
|
|
470ac78044 | ||
|
|
cf16d8f833 | ||
|
|
1e666325ba | ||
|
|
78646e8d11 | ||
|
|
016c1e59d8 | ||
|
|
fe9eacd061 | ||
|
|
d39c16bfdb | ||
|
|
4f9ddac4b8 | ||
|
|
fee9d200c0 | ||
|
|
e214c6ef35 | ||
|
|
6dbee9fad2 | ||
|
|
2fb5cefa5b | ||
|
|
a92a39481b | ||
|
|
b6fc2164a3 | ||
|
|
772e44dd60 | ||
|
|
009e3762f7 | ||
|
|
ae7886aba3 | ||
|
|
3b79d553b9 | ||
|
|
f48979b4da | ||
|
|
b49964287a | ||
|
|
5e29d4e4e7 | ||
|
|
e5e8fa187a | ||
|
|
92bc376ba6 | ||
|
|
13ff3155df | ||
|
|
91b88cd603 | ||
|
|
264491a5b0 | ||
|
|
8b5bfcafba | ||
|
|
850bde5f46 | ||
|
|
c95de82ee3 | ||
|
|
5f5f039a1c | ||
|
|
d740fdc594 | ||
|
|
b37555fcb4 | ||
|
|
4d6b836961 | ||
|
|
9e386dd784 | ||
|
|
86fc7c5895 | ||
|
|
0afefe9c1e | ||
|
|
71a3520a0b | ||
|
|
b97c8b5e6d | ||
|
|
73967cef4e | ||
|
|
3506fb6117 | ||
|
|
5918b12a75 | ||
|
|
fef73de225 | ||
|
|
321a2556b1 | ||
|
|
03d61443e8 | ||
|
|
77d9ff8823 | ||
|
|
f0cfe0d941 | ||
|
|
711f898d96 | ||
|
|
7def3c9b2e | ||
|
|
139d8e04e4 | ||
|
|
59388de662 | ||
|
|
1928d59c58 | ||
|
|
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 | ||
|
|
17ccc5b00c | ||
|
|
ce7f958fb7 | ||
|
|
4dae24f4ec | ||
|
|
471be6f0e0 | ||
|
|
0ef2799980 | ||
|
|
f31ee249b8 | ||
|
|
935bdf16c3 | ||
|
|
94c9060d57 | ||
|
|
e671e73e0f | ||
|
|
140f5e7094 | ||
|
|
1b5e2eaec4 | ||
|
|
3982007d76 | ||
|
|
b39ed6653f | ||
|
|
cbdb771f33 | ||
|
|
dd944ccf54 | ||
|
|
e58903eb9b | ||
|
|
b538fa70ec | ||
|
|
fdd1c020c6 | ||
|
|
7165a4c2aa | ||
|
|
0cbe1ceaff | ||
|
|
7c8b8383e4 | ||
|
|
d2b18f3f50 | ||
|
|
fafc15c897 | ||
|
|
b52c8e6e90 | ||
|
|
c8e7c7370a | ||
|
|
53c8e62cf6 | ||
|
|
436094914f | ||
|
|
745aed060f | ||
|
|
df53392ce7 | ||
|
|
7d82903f69 | ||
|
|
5e8827b595 | ||
|
|
d2c1e6af1e | ||
|
|
21dd23953d | ||
|
|
a6e9a46822 | ||
|
|
34b52e7fb9 | ||
|
|
9cb51a0237 | ||
|
|
365157bed8 | ||
|
|
0458aee26c | ||
|
|
2a758abe46 | ||
|
|
0b7580a442 | ||
|
|
d25f144247 | ||
|
|
f54c1f6bda | ||
|
|
0c6396d36d | ||
|
|
7dbc0ba2c3 | ||
|
|
50448d753a | ||
|
|
074d58b8a8 | ||
|
|
114a5fdf6c | ||
|
|
6508139ce1 | ||
|
|
02d64c113f | ||
|
|
ddf743f48d | ||
|
|
6510604943 | ||
|
|
f4823e6e18 | ||
|
|
af44e9da34 | ||
|
|
40b207d278 | ||
|
|
796eb8327b | ||
|
|
7806342ddf | ||
|
|
43c2c9c33b | ||
|
|
151aba1516 | ||
|
|
e2ab533e9c | ||
|
|
f5d7b6060d | ||
|
|
769679c800 | ||
|
|
a68faa69c6 | ||
|
|
09f684172b | ||
|
|
2cc3720e4e | ||
|
|
994cf3596b | ||
|
|
ca32615f25 | ||
|
|
2d93ba0fc8 | ||
|
|
43d39efbe7 | ||
|
|
c8ed6765db | ||
|
|
91d04a691e | ||
|
|
5f905c333f | ||
|
|
f9945e7e67 | ||
|
|
4c2fe175c1 | ||
|
|
59e00ccfdd | ||
|
|
c38bf579b3 | ||
|
|
7f5fc5826b | ||
|
|
24912a885b | ||
|
|
b50546c3b6 | ||
|
|
c0145fc8a6 | ||
|
|
311c28ad8d | ||
|
|
a7ef6a1856 | ||
|
|
5a899337ee | ||
|
|
46f9d2d081 | ||
|
|
57cf1e5bd4 | ||
|
|
0d46468cda | ||
|
|
382e0e3c38 | ||
|
|
123e467f3e | ||
|
|
77f8e4cd0a | ||
|
|
bea30d9171 | ||
|
|
d34c7a5c76 | ||
|
|
dc0c11f666 | ||
|
|
b945a3b332 | ||
|
|
6c88bc4c17 | ||
|
|
3dcae29081 | ||
|
|
45ae166d1e | ||
|
|
6abef927d6 | ||
|
|
5c7031e856 | ||
|
|
4dfb806011 | ||
|
|
4cc167a4f5 | ||
|
|
8e622e417d | ||
|
|
1e4b67fd3d | ||
|
|
40b8479b88 | ||
|
|
c8ebbc32a6 | ||
|
|
909e327c23 | ||
|
|
5c109a2bf1 | ||
|
|
839878d939 | ||
|
|
cfb2ca722f | ||
|
|
3c12de650b | ||
|
|
0a688929f9 | ||
|
|
e8ce975266 | ||
|
|
1a528f7d7f | ||
|
|
2a132ea0d1 | ||
|
|
9776346118 | ||
|
|
5746457528 | ||
|
|
45def92203 | ||
|
|
3b8726379d | ||
|
|
b65ac362fe | ||
|
|
36c90691a2 | ||
|
|
b97d900847 | ||
|
|
3863d79698 | ||
|
|
8f06ad67fa | ||
|
|
ae357101bc | ||
|
|
cca9755eec | ||
|
|
1db72196b3 | ||
|
|
98aeed62ba | ||
|
|
320f65be95 | ||
|
|
7dbf86badc | ||
|
|
d12bca78e2 | ||
|
|
cbf4effa61 | ||
|
|
220397c68e | ||
|
|
69928caabf | ||
|
|
82cc9c747d | ||
|
|
f2bad77629 | ||
|
|
e3359c23cd | ||
|
|
3564039d06 | ||
|
|
95a99d1bbe | ||
|
|
3e6edaa2a6 | ||
|
|
222f2f2f78 | ||
|
|
bbc38a485e | ||
|
|
e2b09fa0f3 | ||
|
|
e44dcd8184 | ||
|
|
cd845bc82b | ||
|
|
533e4fa506 | ||
|
|
c1990801f5 | ||
|
|
f4d397ffcd | ||
|
|
e1ffb4702c | ||
|
|
d08551463f | ||
|
|
3b7cccb424 | ||
|
|
59a3bef630 | ||
|
|
2126104e32 | ||
|
|
518e79e4f5 | ||
|
|
e89dfafddb | ||
|
|
b7adb9e8ed | ||
|
|
0eeb639030 | ||
|
|
a8534c537c | ||
|
|
961bb03e6b | ||
|
|
bde49c64f0 | ||
|
|
7085165e22 | ||
|
|
3f9b87ee8d | ||
|
|
a7ed295fd3 | ||
|
|
50dbd9fcbf | ||
|
|
10af16d09d | ||
|
|
0ead5e5bbf | ||
|
|
66b3c881da | ||
|
|
3dc82bef1a | ||
|
|
5f7be704f5 | ||
|
|
52e60f08dd | ||
|
|
87d3cf252f | ||
|
|
d29222d068 | ||
|
|
e3195ecc9d | ||
|
|
00a8c86eb1 | ||
|
|
61e2fa9a09 | ||
|
|
49ba499612 | ||
|
|
1eb3951c8d | ||
|
|
144ac1d98a | ||
|
|
d3ec9f4c42 | ||
|
|
23d1706536 | ||
|
|
36c5187065 | ||
|
|
7f18c86631 | ||
|
|
f5b5a7143e | ||
|
|
347b121be7 | ||
|
|
5878d7cb9c | ||
|
|
e69643564c | ||
|
|
c4d467ccc6 | ||
|
|
88fa3d497d | ||
|
|
2a65b5d4d6 | ||
|
|
8c9c0ef660 | ||
|
|
46dc12b6b0 | ||
|
|
ee88268cd9 | ||
|
|
7fcf83c61e | ||
|
|
761369f7d2 | ||
|
|
0d31a34ee0 | ||
|
|
8402c55e5f | ||
|
|
f30451f3ee | ||
|
|
a75c7aeb9f | ||
|
|
fb1b359041 | ||
|
|
c7852137df | ||
|
|
386d0e24ab | ||
|
|
73ba8e74a0 | ||
|
|
9e9d8448ff | ||
|
|
85ff3a9de1 | ||
|
|
c17805d0a4 | ||
|
|
b2a7ffc007 | ||
|
|
a7a502d355 | ||
|
|
ea10069c83 | ||
|
|
44442b7125 | ||
|
|
b389a9ceb9 | ||
|
|
2c78bc0698 | ||
|
|
d4d4095c4c | ||
|
|
978eba89bb | ||
|
|
2a05ddea72 | ||
|
|
67560a5fc1 | ||
|
|
2aa96ecc4e | ||
|
|
a3b485ce2e | ||
|
|
786f325bfd | ||
|
|
cf2cea4427 | ||
|
|
274b586783 | ||
|
|
ee9afc553e | ||
|
|
a21f27261e | ||
|
|
43b7f4933d | ||
|
|
40a092d378 | ||
|
|
921e647050 | ||
|
|
c001486538 | ||
|
|
2d4edc1a26 | ||
|
|
1d458dede0 | ||
|
|
e22e530888 | ||
|
|
f58e62dfe5 | ||
|
|
7192febed7 | ||
|
|
14bf6f0761 | ||
|
|
f6926cae05 | ||
|
|
d5d11beba1 | ||
|
|
37c7c8112e | ||
|
|
5c6d04f43a | ||
|
|
c460dfa2c1 | ||
|
|
4830bcb97b | ||
|
|
a419775438 | ||
|
|
19329f250f | ||
|
|
9c5ce61909 | ||
|
|
41ebbb4c50 | ||
|
|
9d720c8807 | ||
|
|
c9ee85d5ff | ||
|
|
217f8677bc | ||
|
|
4bb058116f | ||
|
|
2563e2c903 | ||
|
|
1955b336ba | ||
|
|
9da8edfd1f | ||
|
|
15949e34e0 | ||
|
|
bb3f98e1b3 | ||
|
|
1b1b8a9aed | ||
|
|
93fbd4c40d | ||
|
|
0b03646a50 | ||
|
|
1bbca930eb | ||
|
|
b991dbf662 | ||
|
|
21934bc264 | ||
|
|
5c120ccf47 | ||
|
|
20bed17d1c | ||
|
|
116c920b24 | ||
|
|
5badee400a | ||
|
|
6c4e37e615 | ||
|
|
86f87dc682 | ||
|
|
c7031aa270 | ||
|
|
46f13a2b47 | ||
|
|
74514e6539 | ||
|
|
8a775c7ef0 | ||
|
|
841585934f | ||
|
|
9f79514e55 | ||
|
|
a03ea2b702 | ||
|
|
f98a9fb419 | ||
|
|
b5d0a87e8e | ||
|
|
3d8b1d1f6e | ||
|
|
da1be463c2 | ||
|
|
8623551a9d | ||
|
|
71883003e8 | ||
|
|
896008c776 | ||
|
|
624ba669ed | ||
|
|
2482db602f | ||
|
|
97cc585cda | ||
|
|
d8116f101f | ||
|
|
5b2f3299f4 | ||
|
|
7e423bcb49 | ||
|
|
1db480088d | ||
|
|
82c70fb71c | ||
|
|
25cb605df5 | ||
|
|
3fe198c1fc | ||
|
|
79f114aaa6 | ||
|
|
b40948b923 | ||
|
|
12f7f475dd | ||
|
|
31e4d4e4ce | ||
|
|
e891f8594b | ||
|
|
4bac66f155 | ||
|
|
eb08298f4b | ||
|
|
6539d2261a | ||
|
|
1602478ff9 | ||
|
|
b2dc66bfae | ||
|
|
1956d4bf9d | ||
|
|
0dc1295842 | ||
|
|
1c0f78b5f8 | ||
|
|
74b81f714c | ||
|
|
32e04d1e58 | ||
|
|
ecc5925d13 | ||
|
|
e9c51b93fc | ||
|
|
3560273724 | ||
|
|
059c640ee2 | ||
|
|
e0e82404d1 | ||
|
|
972503c9d7 | ||
|
|
090aec6e59 | ||
|
|
4718606c60 | ||
|
|
5d76af45f6 | ||
|
|
8416b95e32 | ||
|
|
f3191d0898 | ||
|
|
121faeb382 | ||
|
|
4f4c58a9c8 | ||
|
|
8b8a6fecb1 | ||
|
|
91017ba01e | ||
|
|
a212f4c879 | ||
|
|
0ad717e3fe | ||
|
|
1fd6cf51f7 | ||
|
|
fc8e3e215c | ||
|
|
b4b0845d9e | ||
|
|
10bff07882 | ||
|
|
80fa214f88 | ||
|
|
3bbcdb29e7 | ||
|
|
fdcc2031ed | ||
|
|
5fddf6c675 | ||
|
|
ca72e1ff2a | ||
|
|
fc6c1d4857 | ||
|
|
c29d9aabd7 | ||
|
|
36be23700f | ||
|
|
491bd0df81 | ||
|
|
582293a0fb | ||
|
|
a0257e03c6 | ||
|
|
789490e94b | ||
|
|
7fd03bbfc7 | ||
|
|
b5c1a0ee6b | ||
|
|
ae787081dc | ||
|
|
08da19d41a | ||
|
|
49d65eb940 | ||
|
|
9763c2a02f | ||
|
|
2356f6d7ba | ||
|
|
7f16cb6d9d | ||
|
|
904a6103fa | ||
|
|
1154c0c38f | ||
|
|
a61bffb083 | ||
|
|
820d7fba44 | ||
|
|
5060496582 | ||
|
|
b29e9f3010 | ||
|
|
81f5af33ef | ||
|
|
33709591e3 | ||
|
|
07a607c452 | ||
|
|
0a5b08301d | ||
|
|
920e06f09a | ||
|
|
39121de450 | ||
|
|
c1ad41cadb | ||
|
|
93e702ea59 | ||
|
|
c3eb07c933 | ||
|
|
ce33deb854 | ||
|
|
e0b468a1f5 | ||
|
|
c87581bede | ||
|
|
d1e5037e58 | ||
|
|
ed3dad1858 | ||
|
|
a9775fcb5c | ||
|
|
1b5af23b3f | ||
|
|
eeb828da1f | ||
|
|
65c95b12aa | ||
|
|
b0be99a523 | ||
|
|
24ba7578b3 | ||
|
|
ac4f9d7f7c | ||
|
|
9b410e97dc | ||
|
|
78603e7cf3 | ||
|
|
2eaf374a10 | ||
|
|
5cac3e4935 | ||
|
|
2a29cd1f02 | ||
|
|
85d690a85f | ||
|
|
e28ae5300f | ||
|
|
a353386751 | ||
|
|
046bb15ead | ||
|
|
34c6d3949d | ||
|
|
1b19f61a77 | ||
|
|
ffcfb2c59c | ||
|
|
8ea1e9916c | ||
|
|
ead6462561 | ||
|
|
97e52b6a15 | ||
|
|
f9a26bf814 | ||
|
|
f30cd8529b | ||
|
|
c2f258fc52 | ||
|
|
6fd93d2482 | ||
|
|
ea725cf3bd | ||
|
|
18955f5664 | ||
|
|
948fe8a447 | ||
|
|
d6658805ce | ||
|
|
c00a2ee515 | ||
|
|
6de0c84c91 | ||
|
|
13465a85cd | ||
|
|
a6249b6eec | ||
|
|
cb15a127cf | ||
|
|
96e068c9a3 | ||
|
|
be9ba22059 | ||
|
|
ab6a0f758b | ||
|
|
beff971a54 | ||
|
|
5cf6e43789 | ||
|
|
cbfb70ba67 | ||
|
|
c4cc4261c4 | ||
|
|
e366d0af4c | ||
|
|
b88473060e | ||
|
|
c38c6f51c5 | ||
|
|
999de14ddf | ||
|
|
9ae1619ccd | ||
|
|
084e622dae | ||
|
|
491fdb1e2a | ||
|
|
015e993781 | ||
|
|
5eb68b0f2d | ||
|
|
b9c41a2fdc | ||
|
|
64eef97b56 | ||
|
|
039b6492b5 | ||
|
|
72c7b360ef | ||
|
|
7ff4e2181e | ||
|
|
7b24e56d39 | ||
|
|
1cc6dd0438 | ||
|
|
098515260f | ||
|
|
8e9104a429 | ||
|
|
2d3c9111d0 | ||
|
|
1a37314016 | ||
|
|
78bd156c80 | ||
|
|
d958820497 | ||
|
|
07147f6698 | ||
|
|
d980f4b98b | ||
|
|
72013c1624 | ||
|
|
c5659b1025 | ||
|
|
3f45825dc8 | ||
|
|
82be7da1a9 | ||
|
|
d44b17422a | ||
|
|
952d209334 | ||
|
|
e758539a2a | ||
|
|
6ef32a1cad | ||
|
|
1b141a3a31 | ||
|
|
eec5b441ae |
47
.github/actions/build-upload-extension/action.yml
vendored
Normal file
47
.github/actions/build-upload-extension/action.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: Build & Upload Extension
|
||||
description: Builds & uploads extension for a broswer to a Github release
|
||||
|
||||
inputs:
|
||||
node-version:
|
||||
description: 'NodeJS version to use for setup & build'
|
||||
required: true
|
||||
browser:
|
||||
description: 'Which browser to build the extension for'
|
||||
required: true
|
||||
file-name:
|
||||
description: 'The name of the browser asset to upload'
|
||||
required: true
|
||||
tag-name:
|
||||
description: 'Tag name of the release. Commonly github.ref in an on.release workflow'
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ inputs.node-version }}
|
||||
cache: yarn
|
||||
cache-dependency-path: extension/yarn.lock
|
||||
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: |
|
||||
envsubst < config.release.json > config.json
|
||||
yarn install --frozen-lockfile
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: |
|
||||
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: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
tag: ${{ inputs.tag-name }}
|
||||
# Note: This path is from repo root
|
||||
# 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 &
|
||||
13
.github/actions/local-contract-deploy-hardhat/action.yml
vendored
Normal file
13
.github/actions/local-contract-deploy-hardhat/action.yml
vendored
Normal file
@@ -0,0 +1,13 @@
|
||||
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 run scripts/deploy_all.ts --network gethDev
|
||||
24
.github/actions/setup-contracts-clients/action.yml
vendored
Normal file
24
.github/actions/setup-contracts-clients/action.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Setup Contracts & Clients
|
||||
description: Sets up contracts & clients
|
||||
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
cache: yarn
|
||||
cache-dependency-path: |
|
||||
contracts/yarn.lock
|
||||
contracts/clients/yarn.lock
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: |
|
||||
cp .env.example .env
|
||||
yarn install --frozen-lockfile
|
||||
yarn build
|
||||
|
||||
- working-directory: ./contracts/clients
|
||||
shell: bash
|
||||
run: yarn install --frozen-lockfile
|
||||
23
.github/labeler.yml
vendored
23
.github/labeler.yml
vendored
@@ -1,10 +1,25 @@
|
||||
aggregator:
|
||||
- aggregator/*
|
||||
- aggregator/**/*
|
||||
aggregator-proxy:
|
||||
- aggregator-proxy/*
|
||||
- aggregator-proxy/**/*
|
||||
automation:
|
||||
- .github/*
|
||||
- .github/**/*
|
||||
extension:
|
||||
- extension/*
|
||||
- extension/**/*
|
||||
contracts:
|
||||
- contracts/**/*
|
||||
- contracts/*
|
||||
# Don't label client only changes.
|
||||
- any: ['contracts/**/*', '!contracts/clients/**/*']
|
||||
clients:
|
||||
- clients/**/*
|
||||
signer:
|
||||
- signer/**/*
|
||||
- 'contracts/clients/*'
|
||||
- 'contracts/clients/**/*'
|
||||
documentation:
|
||||
- 'docs/*'
|
||||
- 'docs/**/*'
|
||||
- '*.md'
|
||||
- '**/*.md'
|
||||
- '**/**/*.md'
|
||||
|
||||
20
.github/release-drafter.yml
vendored
Normal file
20
.github/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
categories:
|
||||
- title: 'aggregator'
|
||||
label: 'aggregator'
|
||||
- title: 'aggregator-proxy'
|
||||
label: 'aggregator-proxy'
|
||||
- title: 'contracts'
|
||||
label: 'contracts'
|
||||
- title: 'clients'
|
||||
label: 'clients'
|
||||
- title: 'docs'
|
||||
label: 'documentation'
|
||||
- title: 'extension'
|
||||
label: 'extension'
|
||||
version-resolver:
|
||||
default: minor
|
||||
prerelease: true
|
||||
template: |
|
||||
## What’s Changed
|
||||
|
||||
$CHANGES
|
||||
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
|
||||
32
.github/workflows/aggregator-proxy.yml
vendored
Normal file
32
.github/workflows/aggregator-proxy.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: aggregator-proxy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator-proxy/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'aggregator-proxy/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./aggregator-proxy
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
cache: yarn
|
||||
cache-dependency-path: aggregator-proxy/yarn.lock
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn build
|
||||
80
.github/workflows/aggregator.yml
vendored
Normal file
80
.github/workflows/aggregator.yml
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
name: aggregator
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
# Check for breaking changes from contracts
|
||||
- 'contracts/**'
|
||||
- '.github/workflows/aggregator.yml'
|
||||
pull_request:
|
||||
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:
|
||||
working-directory: ./aggregator
|
||||
|
||||
env:
|
||||
DENO_VERSION: 1.x
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: ${{ env.DENO_VERSION }}
|
||||
- run: deno lint .
|
||||
|
||||
todos-fixmes:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: ${{ env.DENO_VERSION }}
|
||||
- run: ./programs/lintTodos.ts
|
||||
|
||||
typescript:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: ${{ env.DENO_VERSION }}
|
||||
- run: ./programs/checkTs.ts
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: ${{ env.DENO_VERSION }}
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
|
||||
# Setup node & contracts
|
||||
- working-directory: ./contracts
|
||||
run: yarn start &
|
||||
- working-directory: ./contracts
|
||||
run: ./scripts/wait-for-rpc.sh
|
||||
- working-directory: ./contracts
|
||||
run: ./scripts/wait-for-contract-deploy.sh
|
||||
|
||||
- run: cp .env.local.example .env
|
||||
- run: deno test --allow-net --allow-env --allow-read
|
||||
28
.github/workflows/clients.yml
vendored
Normal file
28
.github/workflows/clients.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: clients
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'contracts/clients/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'contracts/clients/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./contracts/clients
|
||||
|
||||
env:
|
||||
DENO_VERSION: 1.x
|
||||
|
||||
jobs:
|
||||
|
||||
test-unit:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
- run: yarn test
|
||||
45
.github/workflows/contracts.yml
vendored
Normal file
45
.github/workflows/contracts.yml
vendored
Normal file
@@ -0,0 +1,45 @@
|
||||
name: contracts
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'contracts/**'
|
||||
- '!contracts/clients/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'contracts/**'
|
||||
- '!contracts/clients/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./contracts
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
- run: yarn lint
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- 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
|
||||
52
.github/workflows/extension-release.yml
vendored
Normal file
52
.github/workflows/extension-release.yml
vendored
Normal file
@@ -0,0 +1,52 @@
|
||||
name: extension-release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./extension
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
chrome:
|
||||
runs-on: ubuntu-latest
|
||||
environment: extension-release
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/build-upload-extension
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
browser: chrome
|
||||
file-name: chrome.zip
|
||||
tag-name: ${{ github.ref }}
|
||||
|
||||
firefox:
|
||||
runs-on: ubuntu-latest
|
||||
environment: extension-release
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/build-upload-extension
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
browser: firefox
|
||||
file-name: firefox.xpi
|
||||
tag-name: ${{ github.ref }}
|
||||
|
||||
opera:
|
||||
runs-on: ubuntu-latest
|
||||
environment: extension-release
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/build-upload-extension
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
browser: opera
|
||||
file-name: opera.crx
|
||||
tag-name: ${{ github.ref }}
|
||||
47
.github/workflows/extension.yml
vendored
Normal file
47
.github/workflows/extension.yml
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
name: extension
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'extension/**'
|
||||
pull_request:
|
||||
paths:
|
||||
- 'extension/**'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./extension
|
||||
|
||||
env:
|
||||
NODEJS_VERSION: 16.x
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
cache: yarn
|
||||
cache-dependency-path: extension/yarn.lock
|
||||
- run: yarn install --frozen-lockfile
|
||||
- run: yarn lint
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
cache: yarn
|
||||
cache-dependency-path: extension/yarn.lock
|
||||
- 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
|
||||
2
.github/workflows/labeler.yml
vendored
2
.github/workflows/labeler.yml
vendored
@@ -6,6 +6,6 @@ jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@main
|
||||
- uses: actions/labeler@v4
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
24
.github/workflows/release-drafter.yml
vendored
Normal file
24
.github/workflows/release-drafter.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
with:
|
||||
config-name: release-drafter.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
.data
|
||||
.DS_Store
|
||||
.idea
|
||||
61
CONTRIBUTING.md
Normal file
61
CONTRIBUTING.md
Normal file
@@ -0,0 +1,61 @@
|
||||
# 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.
|
||||
|
||||
## Getting started
|
||||
|
||||
To get an overview of the project, see [System Overview](docs/system_overview.md)
|
||||
|
||||
To setup the repo for local use, see [Local Development](docs/local_development.md)
|
||||
|
||||
## Issues
|
||||
|
||||
### Create a new issue
|
||||
|
||||
First search for an [existing issue](https://github.com/web3well/bls-wallet/issues). If you find one, add any new insight, helpful context, or some reactions. Otherwise, you can [open a new issue](https://github.com/web3well/bls-wallet/issues/new). Be sure to label it with anything relevant.
|
||||
|
||||
### Solve an issue
|
||||
|
||||
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
|
||||
|
||||
1. [Fork the repo](https://github.com/web3well/bls-wallet/fork)
|
||||
2. Checkout a new branch
|
||||
3. Make your changes
|
||||
|
||||
### Quality Checks
|
||||
|
||||
- You should add new/update test cases for new features or bug fixes to ensure that your changes work properly and will not be broken by other future changes.
|
||||
- Type checking and code linting should all pass.
|
||||
- For ambiguous Typescript typing, prefer `unknown` over `any`.
|
||||
|
||||
## Commit your update
|
||||
|
||||
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
|
||||
|
||||
A list or paragraph of more specific details
|
||||
```
|
||||
|
||||
## Pull Request
|
||||
|
||||
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 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 gratitude and revel in your masterful coding and/or documentational skills.
|
||||
|
||||
### Thanks
|
||||
|
||||
To [github/docs CONTRIBUTING.md](https://github.com/github/docs/blob/main/CONTRIBUTING.md) for being a great contribution template.
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2021 BLS Wallet
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
181
README.md
181
README.md
@@ -1,161 +1,58 @@
|
||||
# bls-wallet
|
||||

|
||||
|
||||
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?
|
||||
|
||||
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](./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
|
||||
|
||||
See each component's directory `README` for more details.
|
||||
[contracts](./contracts/)
|
||||
|
||||
### Aggregator
|
||||
Solidity smart contracts for wallets, BLS signature verification, and deployment/testing tools.
|
||||
|
||||
Service which aggregates BLS wallet transactions.
|
||||
[aggregator](./aggregator/)
|
||||
|
||||
### Clients
|
||||
Service which accepts BLS signed transactions and bundles them into one for submission.
|
||||
|
||||
TS/JS Client libraries for web apps and services.
|
||||
[aggregator-proxy](./aggregator-proxy/)
|
||||
|
||||
### Contracts
|
||||
npm package for proxying to another aggregator instance.
|
||||
|
||||
`bls-wallet` Solidity contracts.
|
||||
[bls-wallet-clients](./contracts/clients/)
|
||||
|
||||
### Extension
|
||||
npm package which provides easy to use constructs to interact with the contracts and aggregator.
|
||||
|
||||
Quill browser extension used to manage BLS Wallets and sign transactions.
|
||||
[extension](./extension/)
|
||||
|
||||
### Signer
|
||||
Prototype browser extension used to manage BLS Wallets and sign transactions.
|
||||
|
||||
TS/JS BLS Signing lib.
|
||||
## Contract Deployments
|
||||
|
||||
## Dependencies
|
||||
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)
|
||||
|
||||
### Required
|
||||
- [Arbitrum Goerli](./contracts/networks/arbitrum-goerli.json)
|
||||
- [Optimism Goerli](./contracts/networks/optimism-goerli.json)
|
||||
|
||||
- [NodeJS](https://nodejs.org)
|
||||
- [Yarn](https://yarnpkg.com/getting-started/install) (`npm install -g yarn`)
|
||||
- [Deno](https://deno.land/#installation)
|
||||
## Ways to Contribute
|
||||
|
||||
### Optional (Recomended)
|
||||
- [Work on an open issue](https://github.com/web3well/bls-wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
- [Use BLS Wallet](./docs/use_bls_wallet_clients.md) in your project and [share it with us](https://github.com/web3well/bls-wallet/discussions)
|
||||
- [Report a bug or request a feature](https://github.com/web3well/bls-wallet/issues/new)
|
||||
- [Ask a question or answer an existing one](https://github.com/web3well/bls-wallet/discussions)
|
||||
- [Try or add to our documentation](https://github.com/web3well/bls-wallet/tree/main/docs)
|
||||
|
||||
- [nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
|
||||
- [docker-compose](https://docs.docker.com/compose/install/)
|
||||
- [MetaMask](https://metamask.io/)
|
||||
|
||||
## Setup
|
||||
|
||||
Run the repo setup script
|
||||
```sh
|
||||
./setup.ts
|
||||
```
|
||||
|
||||
Then choose to target either a local Hardhat node or the Arbitrum Testnet.
|
||||
|
||||
### Local
|
||||
|
||||
Start a local Hardhat node for RPC use.
|
||||
```sh
|
||||
cd ./contracts
|
||||
yarn hardhat node
|
||||
```
|
||||
|
||||
You can use any two of the private keys displayed (PK0 & PK1) to update these values in `./aggregator/.env`.
|
||||
```
|
||||
...
|
||||
PRIVATE_KEY_AGG=PK0
|
||||
PRIVATE_KEY_ADMIN=PK1
|
||||
...
|
||||
```
|
||||
|
||||
Set this value in `./contracts/.env` (This mnemonic is special to hardhat and has funds).
|
||||
```
|
||||
...
|
||||
DEPLOYER_MNEMONIC="test test test test test test test test test test test junk"
|
||||
...
|
||||
```
|
||||
|
||||
Deploy the PrecompileCostEstimator contract.
|
||||
```sh
|
||||
yarn hardhat run scripts/0_deploy_precompile_cost_estimator.ts --network gethDev
|
||||
```
|
||||
Copy the address that is output.
|
||||
|
||||
Update `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s `COST_ESTIMATOR_ADDRESS` to the value of that address;
|
||||
```solidity
|
||||
...
|
||||
address private constant COST_ESTIMATOR_ADDRESS = 0x57047C275bbCb44D85DFA50AD562bA968EEba95A;
|
||||
...
|
||||
```
|
||||
|
||||
Deploy all remaining `bls-wallet` contracts.
|
||||
```sh
|
||||
yarn hardhat run scripts/deploy_all.ts --network gethDev
|
||||
```
|
||||
|
||||
### Arbitrum Testnet (Rinkeby Arbitrum Testnet)
|
||||
|
||||
You will need two ETH addresses with Rinkeby ETH and their private keys (PK0 & PK1) for running the aggregator. It is NOT recommended that you use any primary wallets with ETH Mainnet assets.
|
||||
|
||||
You can get Rinkeby ETH at https://app.mycrypto.com/faucet, and transfer it into the Arbitrum testnet via https://bridge.arbitrum.io/. Make sure when doing so that your network is set to Rinkeby in MetaMask.
|
||||
|
||||
Update these values in `./aggregator/.env`.
|
||||
```
|
||||
RPC_URL=https://rinkeby.arbitrum.io/rpc
|
||||
...
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/rinkarby.json
|
||||
PRIVATE_KEY_AGG=PK0
|
||||
PRIVATE_KEY_ADMIN=PK1
|
||||
...
|
||||
```
|
||||
|
||||
And then update this value in `./extension/.env`.
|
||||
```
|
||||
...
|
||||
|
||||
CHAIN_RPC_URL=https://rinkeby.arbitrum.io/rpc
|
||||
...
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```sh
|
||||
docker-compose up -d postgres # Or see local postgres instructions in ./aggregator/README.md#PostgreSQL
|
||||
cd ./aggregator
|
||||
./programs/aggregator.ts
|
||||
```
|
||||
|
||||
In a seperate terminal/shell instance
|
||||
```sh
|
||||
cd ./extension
|
||||
yarn run dev:chrome # or dev:firefox, dev:opera
|
||||
```
|
||||
|
||||
### Chrome
|
||||
|
||||
1. Go to Chrome's [extension page](chrome://extensions).
|
||||
2. Enable `Developer mode`.
|
||||
3. Either click `Load unpacked extension...` and select `./extension/extension/chrome` or drag that folder into the page.
|
||||
|
||||
### Firefox
|
||||
|
||||
1. Go to Firefox's [debugging page](about:debugging#/runtime/this-firefox).
|
||||
2. Click `Load Temporary Add-on...`.
|
||||
3. Select `./extension/extension/firefox/manifest.json`.
|
||||
|
||||
## Testing/using updates to ./clients
|
||||
|
||||
For `extension`:
|
||||
```sh
|
||||
cd ./contracts/clients
|
||||
yarn build
|
||||
yarn link
|
||||
cd ../extension
|
||||
yarn link bls-wallet-clients
|
||||
```
|
||||
|
||||
For aggregator, you will need to push up a release canidate (-rc) version to 'bls-wallet-clients' on npm and update the version in `./aggregtor/src/deps.ts` until a local linking solution for deno is found. See https://github.com/alephjs/esm.sh/discussions/216 for details.
|
||||
|
||||
`./aggregtor/deps.ts`
|
||||
```typescript
|
||||
...
|
||||
} from "https://cdn.skypack.dev/bls-wallet-clients@x.y.z-rc.w?dts";
|
||||
...
|
||||
```
|
||||
See our [contribution instructions & guidelines](./CONTRIBUTING.md) for more details.
|
||||
|
||||
2
aggregator-proxy/.gitignore
vendored
Normal file
2
aggregator-proxy/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
5
aggregator-proxy/.npmignore
Normal file
5
aggregator-proxy/.npmignore
Normal file
@@ -0,0 +1,5 @@
|
||||
*
|
||||
!/dist/src/**/*
|
||||
!/src/**/*
|
||||
!/package.json
|
||||
!/README.md
|
||||
3
aggregator-proxy/.vscode/settings.json
vendored
Normal file
3
aggregator-proxy/.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"editor.rulers": [80]
|
||||
}
|
||||
51
aggregator-proxy/README.md
Normal file
51
aggregator-proxy/README.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Aggregator Proxy
|
||||
|
||||
[](https://www.npmjs.com/package/bls-wallet-aggregator-proxy)
|
||||
|
||||
This package makes it easy to provide an aggregator by proxying another. The primary use-case is to expose a free aggregator based on one that requires payment by augmenting the bundles with transactions that pay `tx.origin`.
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
npm install bls-wallet-aggregator-proxy
|
||||
yarn install bls-wallet-aggregator-proxy
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
import {
|
||||
runAggregatorProxy,
|
||||
|
||||
// AggregatorProxyCallback,
|
||||
// ^ Alternatively, for manual control, import AggregatorProxyCallback to
|
||||
// just generate the req,res callback for use with http.createServer
|
||||
} from "bls-wallet-aggregator-proxy";
|
||||
|
||||
runAggregatorProxy(
|
||||
"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",
|
||||
() => {
|
||||
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
|
||||
14
aggregator-proxy/manualTests/echoServer.ts
Normal file
14
aggregator-proxy/manualTests/echoServer.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { runAggregatorProxy } from "../src";
|
||||
|
||||
runAggregatorProxy(
|
||||
'https://arbitrum-goerli.blswallet.org',
|
||||
async b => {
|
||||
console.log('proxying bundle', JSON.stringify(b, null, 2));
|
||||
return b;
|
||||
},
|
||||
8080,
|
||||
'0.0.0.0',
|
||||
() => {
|
||||
console.log('Proxying aggregator on port 8080');
|
||||
},
|
||||
);
|
||||
33
aggregator-proxy/package.json
Normal file
33
aggregator-proxy/package.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "bls-wallet-aggregator-proxy",
|
||||
"version": "0.1.1",
|
||||
"main": "dist/src/index.js",
|
||||
"repository": "https://github.com/web3well/bls-wallet/aggregator-proxy",
|
||||
"author": "Andrew Morris",
|
||||
"license": "MIT",
|
||||
"private": false,
|
||||
"engines": {
|
||||
"node": ">=16.0.0",
|
||||
"yarn": ">=1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && tsc"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/cors": "^3.3.0",
|
||||
"@koa/router": "^10.1.1",
|
||||
"@types/koa": "^2.13.4",
|
||||
"@types/koa-bodyparser": "^4.3.7",
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"bls-wallet-clients": "0.9.0-405e23a",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
"koa": "^2.13.4",
|
||||
"koa-bodyparser": "^4.3.0",
|
||||
"node-fetch": "2",
|
||||
"typescript": "^4.6.4"
|
||||
}
|
||||
}
|
||||
73
aggregator-proxy/src/AggregatorProxyCallback.ts
Normal file
73
aggregator-proxy/src/AggregatorProxyCallback.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import fetch from 'node-fetch';
|
||||
import Koa from 'koa';
|
||||
import cors from '@koa/cors';
|
||||
import Router from '@koa/router';
|
||||
import bodyParser from 'koa-bodyparser';
|
||||
import { Bundle, bundleFromDto, Aggregator } from 'bls-wallet-clients';
|
||||
import reporter from 'io-ts-reporters';
|
||||
|
||||
import BundleDto from './BundleDto';
|
||||
|
||||
(globalThis as any).fetch ??= fetch;
|
||||
|
||||
export default function AggregatorProxyCallback(
|
||||
upstreamAggregatorUrl: string,
|
||||
bundleTransformer: (clientBundle: Bundle) => Bundle | Promise<Bundle>,
|
||||
) {
|
||||
const app = new Koa();
|
||||
app.use(cors());
|
||||
const upstreamAggregator = new Aggregator(upstreamAggregatorUrl);
|
||||
|
||||
const router = new Router();
|
||||
|
||||
router.post('/bundle', bodyParser(), async (ctx) => {
|
||||
const decodeResult = BundleDto.decode(ctx.request.body);
|
||||
|
||||
if ('left' in decodeResult) {
|
||||
ctx.status = 400;
|
||||
ctx.body = reporter.report(decodeResult);
|
||||
return;
|
||||
}
|
||||
|
||||
const clientBundle = bundleFromDto(decodeResult.right);
|
||||
const transformedBundle = await bundleTransformer(clientBundle);
|
||||
|
||||
const addResult = await upstreamAggregator.add(transformedBundle);
|
||||
|
||||
ctx.status = 200;
|
||||
ctx.body = addResult;
|
||||
});
|
||||
|
||||
router.post('/estimateFee', bodyParser(), async (ctx) => {
|
||||
const decodeResult = BundleDto.decode(ctx.request.body);
|
||||
|
||||
if ('left' in decodeResult) {
|
||||
ctx.status = 400;
|
||||
ctx.body = reporter.report(decodeResult);
|
||||
return;
|
||||
}
|
||||
|
||||
const clientBundle = bundleFromDto(decodeResult.right);
|
||||
const transformedBundle = await bundleTransformer(clientBundle);
|
||||
|
||||
const estimateFeeResult = await upstreamAggregator.estimateFee(transformedBundle);
|
||||
|
||||
ctx.status = 200;
|
||||
ctx.body = estimateFeeResult;
|
||||
});
|
||||
|
||||
router.get('/bundleReceipt/:hash', bodyParser(), async (ctx) => {
|
||||
const lookupResult = await upstreamAggregator.lookupReceipt(ctx.params.hash);
|
||||
|
||||
if (lookupResult === undefined) {
|
||||
ctx.status = 404;
|
||||
} else {
|
||||
ctx.status = 200;
|
||||
ctx.body = lookupResult;
|
||||
}
|
||||
});
|
||||
|
||||
app.use(router.routes());
|
||||
|
||||
return app.callback();
|
||||
}
|
||||
21
aggregator-proxy/src/BundleDto.ts
Normal file
21
aggregator-proxy/src/BundleDto.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import * as io from 'io-ts';
|
||||
|
||||
import { BundleDto } from 'bls-wallet-clients';
|
||||
|
||||
const BundleDto = io.type({
|
||||
signature: io.tuple([io.string, io.string]),
|
||||
senderPublicKeys: io.array(
|
||||
io.tuple([io.string, io.string, io.string, io.string])
|
||||
),
|
||||
operations: io.array(io.type({
|
||||
nonce: io.string,
|
||||
gas: io.string,
|
||||
actions: io.array(io.type({
|
||||
ethValue: io.string,
|
||||
contractAddress: io.string,
|
||||
encodedFunction: io.string,
|
||||
})),
|
||||
})),
|
||||
});
|
||||
|
||||
export default BundleDto;
|
||||
2
aggregator-proxy/src/index.ts
Normal file
2
aggregator-proxy/src/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default as AggregatorProxyCallback } from './AggregatorProxyCallback';
|
||||
export { default as runAggregatorProxy } from './runAggregatorProxy';
|
||||
20
aggregator-proxy/src/runAggregatorProxy.ts
Normal file
20
aggregator-proxy/src/runAggregatorProxy.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import http from 'http';
|
||||
|
||||
import { Bundle } from 'bls-wallet-clients';
|
||||
import AggregatorProxyCallback from './AggregatorProxyCallback';
|
||||
|
||||
export default function runAggregatorProxy(
|
||||
upstreamAggregatorUrl: string,
|
||||
bundleTransformer: (clientBundle: Bundle) => Bundle | Promise<Bundle>,
|
||||
port?: number,
|
||||
hostname?: string,
|
||||
listeningListener?: () => void,
|
||||
) {
|
||||
const server = http.createServer(
|
||||
AggregatorProxyCallback(upstreamAggregatorUrl, bundleTransformer),
|
||||
);
|
||||
|
||||
server.listen(port, hostname, listeningListener);
|
||||
|
||||
return server;
|
||||
}
|
||||
101
aggregator-proxy/tsconfig.json
Normal file
101
aggregator-proxy/tsconfig.json
Normal file
@@ -0,0 +1,101 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig.json to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Enable incremental compilation */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
|
||||
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
|
||||
/* Modules */
|
||||
"module": "commonjs", /* Specify what module code is generated. */
|
||||
// "rootDir": "./", /* Specify the root folder within your source files. */
|
||||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like `./node_modules/@types`. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files */
|
||||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
|
||||
|
||||
/* Emit */
|
||||
"declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
"declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
"sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account `null` and `undefined`. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for `bind`, `call`, and `apply` methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when `this` is given the type `any`. */
|
||||
// "useUnknownInCatchVariables": true, /* Type catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when a local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true /* Skip type checking all .d.ts files. */
|
||||
}
|
||||
}
|
||||
1502
aggregator-proxy/yarn.lock
Normal file
1502
aggregator-proxy/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,28 +1,46 @@
|
||||
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=0x0000000000000000000000000000000000000000000000000000000000000a99
|
||||
PRIVATE_KEY_ADMIN=
|
||||
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_FUTURE_BUNDLES=1000
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
IS_OPTIMISM=false
|
||||
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS=0x420000000000000000000000000000000000000F
|
||||
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE=2
|
||||
|
||||
42
aggregator/.env.local.example
Normal file
42
aggregator/.env.local.example
Normal file
@@ -0,0 +1,42 @@
|
||||
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
|
||||
|
||||
IS_OPTIMISM=false
|
||||
39
aggregator/.env.test
Normal file
39
aggregator/.env.test
Normal file
@@ -0,0 +1,39 @@
|
||||
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
|
||||
|
||||
IS_OPTIMISM=false
|
||||
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.14.1
|
||||
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,9 +6,66 @@ 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).
|
||||
Install [Deno](deno.land)
|
||||
|
||||
### Configuration
|
||||
|
||||
@@ -23,74 +80,44 @@ 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) |
|
||||
| IS_OPTIMISM | false | Optimism's strategy for charging for L1 fees requires special logic in the aggregator. In addition to gasEstimate * gasPrice, we need to replicate Optimism's calculation and pass it on to the user |
|
||||
| OPTIMISM_GAS_PRICE_ORACLE_ADDRESS | 0x420000000000000000000000000000000000000F | Address for the Optimism gas price oracle contract. Required when IS_OPTIMISM is true |
|
||||
| OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE | 2 | Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for the optimism-specific calculation. This gets passed on to users. Required when IS_OPTIMISM is true |
|
||||
|
||||
## Running
|
||||
|
||||
@@ -102,6 +129,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 +151,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 +253,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
|
||||
@@ -143,6 +263,44 @@ are actually imported whenever you run something). There's also a bunch of other
|
||||
checking going on. As the name suggests, it's a good idea to make sure this
|
||||
script completes successfully before merging into main.
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
#### TS "Duplicate identifier" error
|
||||
|
||||
If you see TypeScript errors like below when attempting to run a script/command
|
||||
from Deno such as `./programs/aggregator.ts`:
|
||||
|
||||
```sh
|
||||
TS2300 [ERROR]: Duplicate identifier 'TypedArray'.
|
||||
type TypedArray =
|
||||
~~~~~~~~~~
|
||||
at https://cdn.esm.sh/v59/node.ns.d.ts:508:10
|
||||
|
||||
'TypedArray' was also declared here.
|
||||
type TypedArray =
|
||||
~~~~~~~~~~
|
||||
at https://cdn.esm.sh/v62/node.ns.d.ts:508:10
|
||||
```
|
||||
|
||||
You need to reload modules (`-r`):
|
||||
|
||||
```sh
|
||||
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?
|
||||
- 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
|
||||
@@ -159,10 +317,9 @@ script completes successfully before merging into main.
|
||||
- **`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
|
||||
@@ -177,16 +334,9 @@ script completes successfully before merging into main.
|
||||
## 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
|
||||
{
|
||||
@@ -200,19 +350,9 @@ script completes successfully before merging into main.
|
||||
|
||||
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 {
|
||||
@@ -235,7 +375,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
|
||||
@@ -243,5 +383,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.
|
||||
|
||||
@@ -1,185 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event",
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address",
|
||||
},
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "sender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
];
|
||||
@@ -1,311 +0,0 @@
|
||||
export default [
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "name",
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "symbol",
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "supply",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "constructor",
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "Approval",
|
||||
"type": "event",
|
||||
},
|
||||
{
|
||||
"anonymous": false,
|
||||
"inputs": [
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "from",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": true,
|
||||
"internalType": "address",
|
||||
"name": "to",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"indexed": false,
|
||||
"internalType": "uint256",
|
||||
"name": "value",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "Transfer",
|
||||
"type": "event",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "owner",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
],
|
||||
"name": "allowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "approve",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "account",
|
||||
"type": "address",
|
||||
},
|
||||
],
|
||||
"name": "balanceOf",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "decimals",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint8",
|
||||
"name": "",
|
||||
"type": "uint8",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "subtractedValue",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "decreaseAllowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "spender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "addedValue",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "increaseAllowance",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "_to",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "_amount",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "mint",
|
||||
"outputs": [],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "name",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "symbol",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "string",
|
||||
"name": "",
|
||||
"type": "string",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [],
|
||||
"name": "totalSupply",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"stateMutability": "view",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "transfer",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
{
|
||||
"inputs": [
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "sender",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "address",
|
||||
"name": "recipient",
|
||||
"type": "address",
|
||||
},
|
||||
{
|
||||
"internalType": "uint256",
|
||||
"name": "amount",
|
||||
"type": "uint256",
|
||||
},
|
||||
],
|
||||
"name": "transferFrom",
|
||||
"outputs": [
|
||||
{
|
||||
"internalType": "bool",
|
||||
"name": "",
|
||||
"type": "bool",
|
||||
},
|
||||
],
|
||||
"stateMutability": "nonpayable",
|
||||
"type": "function",
|
||||
},
|
||||
];
|
||||
@@ -2,6 +2,7 @@ export { delay } from "https://deno.land/std@0.103.0/async/delay.ts";
|
||||
export { parse as parseArgs } from "https://deno.land/std@0.103.0/flags/mod.ts";
|
||||
export { exists } from "https://deno.land/std@0.103.0/fs/mod.ts";
|
||||
export { dirname } from "https://deno.land/std@0.103.0/path/mod.ts";
|
||||
export { oakCors } from "https://deno.land/x/cors@v1.2.0/mod.ts";
|
||||
|
||||
import { config as dotEnvConfig } from "https://deno.land/x/dotenv@v2.0.0/mod.ts";
|
||||
export { dotEnvConfig };
|
||||
@@ -26,47 +27,57 @@ export {
|
||||
Contract,
|
||||
ethers,
|
||||
Wallet,
|
||||
} from "https://esm.sh/ethers@5.5.1";
|
||||
} from "https://esm.sh/ethers@5.7.2";
|
||||
|
||||
import { ethers } from "https://esm.sh/ethers@5.5.1";
|
||||
import { ethers } from "https://esm.sh/ethers@5.7.2";
|
||||
export type {
|
||||
BaseContract,
|
||||
BigNumberish,
|
||||
BytesLike,
|
||||
} 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 {
|
||||
ActionData,
|
||||
AggregatorUtilities,
|
||||
BlsWalletSigner,
|
||||
Bundle,
|
||||
BundleDto,
|
||||
ERC20,
|
||||
MockERC20,
|
||||
NetworkConfig,
|
||||
Operation,
|
||||
OperationResultError,
|
||||
PublicKey,
|
||||
Signature,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.5.0-d6d7dd4";
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
AggregatorUtilitiesFactory,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletWrapper,
|
||||
BundleCompressor,
|
||||
ContractsConnector,
|
||||
decodeError,
|
||||
Erc20Compressor,
|
||||
ERC20Factory,
|
||||
FallbackCompressor,
|
||||
getConfig,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.5.0-d6d7dd4";
|
||||
MockERC20Factory,
|
||||
VerificationGatewayFactory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
|
||||
// Workaround for esbuild's export-star bug
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.5.0-d6d7dd4";
|
||||
const {
|
||||
bundleFromDto,
|
||||
bundleToDto,
|
||||
initBlsWalletSigner,
|
||||
} = blsWalletClients;
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
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 * 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";
|
||||
|
||||
export type { TableOptions } from "https://deno.land/x/postquery@v0.1.1/mod.ts";
|
||||
export { pick } from "npm:@s-libs/micro-dash@15.2.0";
|
||||
|
||||
@@ -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";
|
||||
@@ -9,12 +9,12 @@ const client = new AggregatorClient(env.ORIGIN);
|
||||
const fx = await Fixture.create(import.meta.url);
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.contract.address,
|
||||
encodedFunction: fx.testErc20.contract.interface.encodeFunctionData(
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 20],
|
||||
),
|
||||
@@ -23,7 +23,7 @@ const bundle = wallet.sign({
|
||||
|
||||
console.log("sending", bundle);
|
||||
|
||||
const failures = await client.add(bundle);
|
||||
if (failures.length) {
|
||||
throw new Error(failures.join(", "));
|
||||
const res = await client.add(bundle);
|
||||
if ("failures" in res) {
|
||||
throw new Error(res.failures.join(", "));
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
@@ -17,6 +17,7 @@ const bundle: Bundle = {
|
||||
senderPublicKeys: [[dummyHex(32), dummyHex(32), dummyHex(32), dummyHex(32)]],
|
||||
operations: [{
|
||||
nonce: BigNumber.from(0),
|
||||
gas: BigNumber.from(0),
|
||||
actions: [{
|
||||
ethValue: BigNumber.from(0),
|
||||
contractAddress: dummyHex(20),
|
||||
@@ -27,7 +28,7 @@ const bundle: Bundle = {
|
||||
|
||||
console.log("sending", bundle);
|
||||
|
||||
const failures = await client.add(bundle);
|
||||
if (failures.length) {
|
||||
throw new Error(failures.join(", "));
|
||||
const res = await client.add(bundle);
|
||||
if ("failures" in res) {
|
||||
throw new Error(res.failures.join(", "));
|
||||
}
|
||||
|
||||
17
aggregator/manualTests/callOptimismGasPriceOracle.ts
Executable file
17
aggregator/manualTests/callOptimismGasPriceOracle.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import * as env from "../src/env.ts";
|
||||
import { ethers } from "../deps.ts";
|
||||
import OptimismGasPriceOracle from "../src/app/OptimismGasPriceOracle.ts";
|
||||
|
||||
const oracle = new OptimismGasPriceOracle(
|
||||
new ethers.providers.JsonRpcProvider(env.RPC_URL),
|
||||
);
|
||||
|
||||
const { l1BaseFee, overhead, scalar, decimals } = await oracle.getAllParams();
|
||||
|
||||
console.log({
|
||||
l1BaseFee: `${(l1BaseFee.toNumber() / 1e9).toFixed(3)} gwei`,
|
||||
overhead: `${overhead.toNumber()} L1 gas`,
|
||||
scalar: scalar.toNumber() / (10 ** decimals.toNumber()),
|
||||
});
|
||||
@@ -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 = await wallet.signWithGasEstimate({
|
||||
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";
|
||||
|
||||
|
||||
17
aggregator/manualTests/getOptimismL1Fee.ts
Executable file
17
aggregator/manualTests/getOptimismL1Fee.ts
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import getOptimismL1Fee from "../src/helpers/getOptimismL1Fee.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const txHash = Deno.args[0];
|
||||
|
||||
if (!txHash.startsWith("0x")) {
|
||||
throw new Error("First arg should be tx hash");
|
||||
}
|
||||
|
||||
const l1Fee = await getOptimismL1Fee(provider, txHash);
|
||||
|
||||
console.log(`${ethers.utils.formatEther(l1Fee)} ETH`);
|
||||
15
aggregator/manualTests/getRawTransaction.ts
Executable file
15
aggregator/manualTests/getRawTransaction.ts
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import getRawTransaction from "../src/helpers/getRawTransaction.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const txHash = Deno.args[0];
|
||||
|
||||
if (!txHash.startsWith("0x")) {
|
||||
throw new Error("First arg should be tx hash");
|
||||
}
|
||||
|
||||
console.log(await getRawTransaction(provider, txHash));
|
||||
@@ -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;
|
||||
}
|
||||
10
aggregator/manualTests/helpers/receiptOf.ts
Normal file
10
aggregator/manualTests/helpers/receiptOf.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ethers } from "../../deps.ts";
|
||||
|
||||
export default async function receiptOf(
|
||||
responsePromise: Promise<ethers.providers.TransactionResponse>,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
const response = await responsePromise;
|
||||
const receipt = await response.wait();
|
||||
|
||||
return receipt;
|
||||
}
|
||||
@@ -1,43 +1,114 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
|
||||
|
||||
import { AggregatorClient, delay, ethers } from "../deps.ts";
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20Factory,
|
||||
} 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 MockErc20 from "../test/helpers/MockErc20.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();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = new MockErc20(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.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 bundle = wallet.sign({
|
||||
const mintAction: ActionData = {
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.address,
|
||||
encodedFunction: testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
};
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
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 = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.contract.address,
|
||||
encodedFunction: testErc20.contract.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
}],
|
||||
actions: [mintAction, feeAction],
|
||||
});
|
||||
|
||||
console.log("Sending mint bundle to aggregator");
|
||||
|
||||
const failures = await client.add(bundle);
|
||||
assert(failures.length === 0, failures.map((f) => f.description).join(", "));
|
||||
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");
|
||||
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,11 +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 } from "../deps.ts";
|
||||
import { delay, ethers, MockERC20Factory } from "../deps.ts";
|
||||
|
||||
import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
import MockErc20 from "../test/helpers/MockErc20.ts";
|
||||
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
|
||||
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
|
||||
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
@@ -15,20 +14,19 @@ const ethereumService = await EthereumService.create(
|
||||
(evt) => {
|
||||
console.log(evt);
|
||||
},
|
||||
addresses.verificationGateway,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
const testErc20 = new MockErc20(addresses.testToken, provider);
|
||||
const [wallet] = await TestBlsWallets(provider, 1);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
const startBalance = await testErc20.balanceOf(wallet.address);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.contract.address,
|
||||
encodedFunction: testErc20.contract.interface.encodeFunctionData(
|
||||
contractAddress: testErc20.address,
|
||||
encodedFunction: testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 20],
|
||||
),
|
||||
@@ -47,7 +45,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(),
|
||||
|
||||
144
aggregator/manualTests/mintNViaAggregator.ts
Executable file
144
aggregator/manualTests/mintNViaAggregator.ts
Executable file
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
Bundle,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20Factory,
|
||||
} 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 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.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, "(1 wei to make estimateFee work)");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
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", i, "(based on estimateFee)");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
value: fee.sub(balance),
|
||||
})).wait();
|
||||
}
|
||||
|
||||
const feeAction: ActionData = {
|
||||
ethValue: fee,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
bundles.push(
|
||||
await wallet.signWithGasEstimate({
|
||||
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);
|
||||
}
|
||||
34
aggregator/manualTests/registerWallet.ts
Executable file
34
aggregator/manualTests/registerWallet.ts
Executable file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ContractsConnector, ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import receiptOf from "./helpers/receiptOf.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const connector = await ContractsConnector.create(adminWallet);
|
||||
|
||||
const addressRegistry = await connector.AddressRegistry();
|
||||
const blsPublicKeyRegistry = await connector.BLSPublicKeyRegistry();
|
||||
|
||||
await receiptOf(
|
||||
addressRegistry.register("0xCB1ca1e8DF1055636d7D07c3099c9de3c65CAAB4"),
|
||||
);
|
||||
|
||||
await receiptOf(
|
||||
blsPublicKeyRegistry.register(
|
||||
// You can get this in Quill by running this in the console of the wallet
|
||||
// page (the page you get by clicking on the extension icon)
|
||||
// JSON.stringify(debug.wallets[0].blsWalletSigner.getPublicKey())
|
||||
|
||||
[
|
||||
"0x0ad7e63a4bbfdad440beda1fe7fdfb77a59f2a6d991700c6cf4c3654a52389a9",
|
||||
"0x0adaa93bdfda0f6b259a80c1af7ccf3451c35c1e175483927a8052bdbf59f801",
|
||||
"0x1f56aa1bb1419c741f0a474e51f33da0ffc81ea870e2e2c440db72539a9efb9e",
|
||||
"0x2f1f7e5d586d6ca5de3c8c198c3be3b998a2b6df7ee8a367a1e58f8b36fd524d",
|
||||
],
|
||||
),
|
||||
);
|
||||
25
aggregator/manualTests/send1Wei.ts
Executable file
25
aggregator/manualTests/send1Wei.ts
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import { ethers } from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
|
||||
const adminWallet = AdminWallet(provider);
|
||||
const to = ethers.constants.AddressZero;
|
||||
|
||||
console.log("sending 1 wei");
|
||||
console.log(`${adminWallet.address} -> ${to}`);
|
||||
|
||||
const txn = await adminWallet.sendTransaction({
|
||||
value: "0x01",
|
||||
to,
|
||||
});
|
||||
|
||||
console.log(`txn hash ${txn.hash}`);
|
||||
console.log("waiting ...");
|
||||
|
||||
await txn.wait();
|
||||
|
||||
console.log("done");
|
||||
@@ -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,
|
||||
@@ -6,13 +6,14 @@ import {
|
||||
BlsWalletWrapper,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
|
||||
import * as env from "../test/env.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import MockErc20 from "../test/helpers/MockErc20.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;
|
||||
|
||||
@@ -35,21 +43,20 @@ const { addresses } = await getNetworkConfig();
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const testErc20 = new MockErc20(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
|
||||
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...");
|
||||
|
||||
for (const wallet of sendWallets) {
|
||||
const testErc20 = new MockErc20(
|
||||
const testErc20 = MockERC20Factory.connect(
|
||||
addresses.testToken,
|
||||
adminWallet,
|
||||
);
|
||||
@@ -83,7 +90,7 @@ let txsAdded = 0;
|
||||
let txsCompleted = 0;
|
||||
let sendWalletIndex = 0;
|
||||
|
||||
pollingLoop(() => {
|
||||
pollingLoop(async () => {
|
||||
// Send transactions
|
||||
|
||||
const lead = txsSent - txsCompleted;
|
||||
@@ -95,20 +102,20 @@ pollingLoop(() => {
|
||||
const nonce = nextNonceMap.get(sendWallet)!;
|
||||
nextNonceMap.set(sendWallet, nonce.add(1));
|
||||
|
||||
const bundle = sendWallet.sign({
|
||||
const bundle = await sendWallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
contractAddress: testErc20.contract.address,
|
||||
encodedFunction: testErc20.contract.interface.encodeFunctionData(
|
||||
"trasnfer",
|
||||
contractAddress: testErc20.address,
|
||||
encodedFunction: testErc20.interface.encodeFunctionData(
|
||||
"transfer",
|
||||
[recvWallet.address, 1],
|
||||
),
|
||||
}],
|
||||
});
|
||||
|
||||
client.add(bundle).then((failures) => {
|
||||
if (failures.length > 0) {
|
||||
const res = client.add(bundle).then((failures) => {
|
||||
if ("failures" in res) {
|
||||
console.log({ failures });
|
||||
}
|
||||
|
||||
|
||||
56
aggregator/manualTests/zeroAddressError.ts
Normal file
56
aggregator/manualTests/zeroAddressError.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient, ethers, MockERC20Factory } 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);
|
||||
// },
|
||||
// env.PRIVATE_KEY_AGG,
|
||||
// );
|
||||
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
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,98 +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";
|
||||
|
||||
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`;
|
||||
|
||||
const commitShort = (await shell.Line("git", "rev-parse", "HEAD")).slice(0, 7);
|
||||
await ensureFreshBuildDir();
|
||||
await copyTypescriptFiles();
|
||||
await buildDockerImage();
|
||||
await tarballTypescriptFiles();
|
||||
|
||||
const isDirty = (await shell.Lines("git", "status", "--porcelain")).length > 0;
|
||||
|
||||
const envHashShort = (await shell.Line("shasum", "-a", "256", dotEnvPath))
|
||||
.slice(0, 7);
|
||||
|
||||
const buildName = [
|
||||
"git",
|
||||
commitShort,
|
||||
...(isDirty ? ["dirty"] : []),
|
||||
"env",
|
||||
envName,
|
||||
envHashShort,
|
||||
].join("-");
|
||||
|
||||
try {
|
||||
await Deno.remove(buildDir, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.name === "NotFound") {
|
||||
// We don't care that remove failed due to NotFound (why do we need to catch
|
||||
// an exception to handle this normal use case? 🤔)
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
if (args.push) {
|
||||
await pushDockerImage();
|
||||
}
|
||||
|
||||
await Deno.mkdir(buildDir);
|
||||
|
||||
await Deno.copyFile(dotEnvPath, `${buildDir}/.env`);
|
||||
|
||||
for (const f of await allFiles()) {
|
||||
if (!f.endsWith(".ts")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log("Processing", f);
|
||||
await Deno.mkdir(dirname(`${buildDir}/ts/${f}`), { recursive: true });
|
||||
await Deno.copyFile(f, `${buildDir}/ts/${f}`);
|
||||
}
|
||||
|
||||
const sudoDockerArg = args["sudo-docker"] === true ? ["sudo"] : [];
|
||||
|
||||
await shell.run(
|
||||
...sudoDockerArg,
|
||||
"docker",
|
||||
"build",
|
||||
repoDir,
|
||||
"-t",
|
||||
`aggregator:${buildName}`,
|
||||
);
|
||||
|
||||
const dockerImageName = `aggregator-${buildName}-docker-image`;
|
||||
|
||||
await shell.run(
|
||||
...sudoDockerArg,
|
||||
"docker",
|
||||
"save",
|
||||
"--output",
|
||||
`${repoDir}/build/${dockerImageName}.tar`,
|
||||
`aggregator:${buildName}`,
|
||||
);
|
||||
|
||||
if (sudoDockerArg.length > 0) {
|
||||
// chown to the current user
|
||||
const username = await shell.Line("whoami");
|
||||
|
||||
await shell.run(
|
||||
"sudo",
|
||||
"chown",
|
||||
username,
|
||||
`${repoDir}/build/${dockerImageName}.tar`,
|
||||
);
|
||||
}
|
||||
|
||||
await shell.run(
|
||||
"gzip",
|
||||
`${repoDir}/build/${dockerImageName}.tar`,
|
||||
);
|
||||
|
||||
console.log("Aggregator build complete");
|
||||
console.log("\nAggregator build complete");
|
||||
|
||||
async function allFiles() {
|
||||
return [
|
||||
@@ -105,3 +49,131 @@ async function allFiles() {
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
async function Tag() {
|
||||
const commitShort = (await shell.Line("git", "rev-parse", "HEAD")).slice(
|
||||
0,
|
||||
7,
|
||||
);
|
||||
|
||||
const isDirty =
|
||||
(await shell.Lines("git", "status", "--porcelain")).length > 0;
|
||||
|
||||
return [
|
||||
"git",
|
||||
commitShort,
|
||||
...(isDirty ? ["dirty"] : []),
|
||||
].join("-");
|
||||
}
|
||||
|
||||
async function ensureFreshBuildDir() {
|
||||
try {
|
||||
await Deno.remove(buildDir, { recursive: true });
|
||||
} catch (error) {
|
||||
if (error.name === "NotFound") {
|
||||
// We don't care that remove failed due to NotFound (why do we need to
|
||||
// catch an exception to handle this normal use case? 🤔)
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
await Deno.mkdir(buildDir);
|
||||
}
|
||||
|
||||
async function copyTypescriptFiles() {
|
||||
for (const f of await allFiles()) {
|
||||
if (!f.endsWith(".ts")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log("Processing", f);
|
||||
await Deno.mkdir(dirname(`${buildDir}/ts/${f}`), { recursive: true });
|
||||
await Deno.copyFile(f, `${buildDir}/ts/${f}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function tarballTypescriptFiles() {
|
||||
// TypeScript insists on looking inside files that aren't explicitly imported.
|
||||
// Therefore, after the build, we convert these files into a tarball so that
|
||||
// we don't interfere with the main project.
|
||||
|
||||
const currDir = Deno.cwd();
|
||||
Deno.chdir(buildDir);
|
||||
await shell.run("tar", "-czf", "ts.tar.gz", "ts");
|
||||
await shell.run("rm", "-rf", "ts");
|
||||
Deno.chdir(currDir);
|
||||
}
|
||||
|
||||
async function buildDockerImage() {
|
||||
const tag = await Tag();
|
||||
const imageName = args.imageName ?? "aggregator";
|
||||
const imageNameAndTag = `${imageName}:${tag}`;
|
||||
|
||||
const sudoDockerArg = args.sudoDocker ? ["sudo"] : [];
|
||||
|
||||
await shell.run(
|
||||
...sudoDockerArg,
|
||||
"docker",
|
||||
"build",
|
||||
repoDir,
|
||||
"-t",
|
||||
imageNameAndTag,
|
||||
);
|
||||
|
||||
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",
|
||||
tarFilePath,
|
||||
imageNameAndTag,
|
||||
);
|
||||
|
||||
if (sudoDockerArg.length > 0) {
|
||||
// chown to the current user
|
||||
const username = await shell.Line("whoami");
|
||||
|
||||
await shell.run(
|
||||
"sudo",
|
||||
"chown",
|
||||
username,
|
||||
tarFilePath,
|
||||
);
|
||||
}
|
||||
|
||||
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`);
|
||||
}
|
||||
}
|
||||
|
||||
5
aggregator/programs/checkTs.ts
Executable file
5
aggregator/programs/checkTs.ts
Executable file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env
|
||||
|
||||
import { checkTs } from "./helpers/typescript.ts";
|
||||
|
||||
await checkTs();
|
||||
@@ -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
|
||||
|
||||
|
||||
43
aggregator/programs/createInternalBlsWallet.ts
Executable file
43
aggregator/programs/createInternalBlsWallet.ts
Executable file
@@ -0,0 +1,43 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
|
||||
|
||||
import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
VerificationGatewayFactory,
|
||||
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 = VerificationGatewayFactory.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(
|
||||
await internalBlsWallet.signWithGasEstimate({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}),
|
||||
)).wait();
|
||||
|
||||
console.log("Created successfully");
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --unstable --allow-read
|
||||
|
||||
// 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}`);
|
||||
}
|
||||
13
aggregator/programs/helpers/git.ts
Normal file
13
aggregator/programs/helpers/git.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import * as shell from "./shell.ts";
|
||||
|
||||
export async function allFiles() {
|
||||
return [
|
||||
...await shell.Lines("git", "ls-files"),
|
||||
...await shell.Lines(
|
||||
"git",
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
),
|
||||
];
|
||||
}
|
||||
22
aggregator/programs/helpers/lint.ts
Normal file
22
aggregator/programs/helpers/lint.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import * as shell from "./shell.ts";
|
||||
import { allFiles } from "./git.ts";
|
||||
|
||||
// TODO (merge-ok) Consider turning this into a standard eslint rule
|
||||
export async function lintTodosFixmes(): Promise<void> { // merge-ok
|
||||
const searchArgs = [
|
||||
"egrep",
|
||||
"--color",
|
||||
"-ni",
|
||||
"todo|fixme", // merge-ok
|
||||
...(await allFiles()),
|
||||
];
|
||||
|
||||
const matches = await shell.Lines(...searchArgs);
|
||||
|
||||
const notOkMatches = matches.filter((m) => !m.includes("merge-ok"));
|
||||
|
||||
if (notOkMatches.length > 0) {
|
||||
console.error(notOkMatches.join("\n"));
|
||||
throw new Error(`${notOkMatches.length} todos/fixmes found`); // merge-ok
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
export async function run(...cmd: string[]): Promise<void> {
|
||||
// https://github.com/web3well/bls-wallet/issues/595
|
||||
// deno-lint-ignore no-deprecated-deno-api
|
||||
const process = Deno.run({ cmd, stdout: "inherit", stderr: "inherit" });
|
||||
|
||||
const unloadListener = () => {
|
||||
@@ -20,6 +22,8 @@ export async function run(...cmd: string[]): Promise<void> {
|
||||
}
|
||||
|
||||
export async function String(...cmd: string[]): Promise<string> {
|
||||
// https://github.com/web3well/bls-wallet/issues/595
|
||||
// deno-lint-ignore no-deprecated-deno-api
|
||||
const process = Deno.run({ cmd, stdout: "piped" });
|
||||
|
||||
if (process.stdout === null) {
|
||||
|
||||
25
aggregator/programs/helpers/typescript.ts
Normal file
25
aggregator/programs/helpers/typescript.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { allFiles } from "./git.ts";
|
||||
import * as shell from "./shell.ts";
|
||||
import nil from "../../src/helpers/nil.ts";
|
||||
import repoDir from "../../src/helpers/repoDir.ts";
|
||||
|
||||
export async function checkTs(): Promise<void> {
|
||||
let testFilePath: string | nil = nil;
|
||||
|
||||
try {
|
||||
const tsFiles = (await allFiles()).filter((f) => f.endsWith(".ts"));
|
||||
|
||||
testFilePath = await Deno.makeTempFile({ suffix: ".ts" });
|
||||
|
||||
await Deno.writeTextFile(
|
||||
testFilePath,
|
||||
tsFiles.map((f) => `import "${repoDir}/${f}";`).join("\n"),
|
||||
);
|
||||
|
||||
await shell.run("deno", "check", testFilePath);
|
||||
} finally {
|
||||
if (testFilePath !== nil) {
|
||||
await Deno.remove(testFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
aggregator/programs/lintTodos.ts
Executable file
7
aggregator/programs/lintTodos.ts
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-env
|
||||
|
||||
// TODO (merge-ok) Consider turning this into a standard eslint rule
|
||||
|
||||
import { lintTodosFixmes } from "./helpers/lint.ts"; // merge-ok
|
||||
|
||||
await lintTodosFixmes(); // merge-ok
|
||||
@@ -1,8 +1,9 @@
|
||||
#!/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 { lintTodosFixmes } from "./helpers/lint.ts"; // merge-ok
|
||||
import { checkTs } from "./helpers/typescript.ts";
|
||||
import * as shell from "./helpers/shell.ts";
|
||||
import repoDir from "../src/helpers/repoDir.ts";
|
||||
import nil from "../src/helpers/nil.ts";
|
||||
import { envName } from "../src/helpers/dotEnvPath.ts";
|
||||
|
||||
Deno.chdir(repoDir);
|
||||
@@ -27,44 +28,8 @@ function Checks(): Check[] {
|
||||
["lint", async () => {
|
||||
await shell.run("deno", "lint", ".");
|
||||
}],
|
||||
["todos and fixmes", async () => { // merge-ok
|
||||
const searchArgs = [
|
||||
"egrep",
|
||||
"--color",
|
||||
"-ni",
|
||||
"todo|fixme", // merge-ok
|
||||
...(await allFiles()),
|
||||
];
|
||||
|
||||
const matches = await shell.Lines(...searchArgs);
|
||||
|
||||
const notOkMatches = matches.filter((m) => !m.includes("merge-ok"));
|
||||
|
||||
if (notOkMatches.length > 0) {
|
||||
console.error(notOkMatches.join("\n"));
|
||||
throw new Error(`${notOkMatches.length} todos/fixmes found`); // merge-ok
|
||||
}
|
||||
}],
|
||||
["typescript", async () => {
|
||||
let testFilePath: string | nil = nil;
|
||||
|
||||
try {
|
||||
const tsFiles = (await allFiles()).filter((f) => f.endsWith(".ts"));
|
||||
|
||||
testFilePath = await Deno.makeTempFile({ suffix: ".ts" });
|
||||
|
||||
await Deno.writeTextFile(
|
||||
testFilePath,
|
||||
tsFiles.map((f) => `import "${repoDir}/${f}";`).join("\n"),
|
||||
);
|
||||
|
||||
await shell.run("deno", "cache", "--unstable", testFilePath);
|
||||
} finally {
|
||||
if (testFilePath !== nil) {
|
||||
await Deno.remove(testFilePath);
|
||||
}
|
||||
}
|
||||
}],
|
||||
["todos and fixmes", lintTodosFixmes], // merge-ok
|
||||
["typescript", checkTs],
|
||||
["test", async () => {
|
||||
await shell.run(
|
||||
"deno",
|
||||
@@ -77,7 +42,6 @@ function Checks(): Check[] {
|
||||
"--allow-net",
|
||||
"--allow-env",
|
||||
"--allow-read",
|
||||
"--unstable",
|
||||
"--",
|
||||
"--env",
|
||||
envName,
|
||||
@@ -85,15 +49,3 @@ function Checks(): Check[] {
|
||||
}],
|
||||
];
|
||||
}
|
||||
|
||||
async function allFiles() {
|
||||
return [
|
||||
...await shell.Lines("git", "ls-files"),
|
||||
...await shell.Lines(
|
||||
"git",
|
||||
"ls-files",
|
||||
"--others",
|
||||
"--exclude-standard",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --unstable --allow-read
|
||||
#!/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();
|
||||
}
|
||||
}
|
||||
|
||||
729
aggregator/src/app/AggregationStrategy.ts
Normal file
729
aggregator/src/app/AggregationStrategy.ts
Normal file
@@ -0,0 +1,729 @@
|
||||
import {
|
||||
BigNumber,
|
||||
BlsWalletSigner,
|
||||
Bundle,
|
||||
decodeError,
|
||||
ERC20,
|
||||
ERC20Factory,
|
||||
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 * as env from "../env.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
import { BundleRow } from "./BundleTable.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;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: BigNumber;
|
||||
expectedMaxCost: BigNumber;
|
||||
failedRows: BundleRow[];
|
||||
};
|
||||
|
||||
export type AggregationStrategyConfig =
|
||||
typeof AggregationStrategy["defaultConfig"];
|
||||
|
||||
export default class AggregationStrategy {
|
||||
static defaultConfig = {
|
||||
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,
|
||||
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, bundleOverheadLen } = await this
|
||||
.measureBundleOverhead();
|
||||
|
||||
let aggregateBundle = this.blsWalletSigner.aggregate([]);
|
||||
let aggregateGas = bundleOverheadGas;
|
||||
const includedRows: BundleRow[] = [];
|
||||
const failedRows: BundleRow[] = [];
|
||||
let expectedFee = BigNumber.from(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;
|
||||
|
||||
if (newIncludedRows.length === 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (includedRows.length === 0) {
|
||||
return {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
};
|
||||
}
|
||||
|
||||
const aggregateBundleCheck = await this.#checkBundle(
|
||||
aggregateBundle,
|
||||
BigNumber.from(0),
|
||||
);
|
||||
|
||||
let result: AggregationStrategyResult = {
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost: aggregateBundleCheck.expectedMaxCost,
|
||||
failedRows,
|
||||
};
|
||||
|
||||
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,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
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,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
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 bundleEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
const newAggregateGas = aggregateGas.add(bundleEstimate);
|
||||
|
||||
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.
|
||||
this.emit({
|
||||
type: "aggregate-bundle-exceeds-max-gas",
|
||||
data: {
|
||||
hash: row.hash,
|
||||
gasEstimate: bundleEstimate.toNumber(),
|
||||
aggregateGasEstimate: newAggregateGas.toNumber(),
|
||||
maxGasPerBundle: this.config.maxGasPerBundle,
|
||||
},
|
||||
});
|
||||
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();
|
||||
|
||||
const { measureResults, callResults: processBundleResults } = await es
|
||||
.callStaticSequenceWithMeasure(
|
||||
feeToken
|
||||
? es.Call(feeToken, "balanceOf", [es.wallet.address])
|
||||
: es.Call(es.aggregatorUtilities, "ethBalanceOf", [
|
||||
es.wallet.address,
|
||||
]),
|
||||
await Promise.all(bundles.map(async (bundle) =>
|
||||
es.Call(
|
||||
es.bundleCompressor.blsExpanderDelegator,
|
||||
"run",
|
||||
[await es.bundleCompressor.compress(bundle)],
|
||||
)
|
||||
)),
|
||||
);
|
||||
|
||||
return Range(bundles.length).map((i) => {
|
||||
const [before, after] = [measureResults[i], measureResults[i + 1]];
|
||||
assert(before.success);
|
||||
assert(after.success);
|
||||
|
||||
const bundleResult = processBundleResults[i];
|
||||
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 [operationStatuses, results] = bundleResult.returnValue;
|
||||
|
||||
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 {
|
||||
if (this.config.fees?.type !== "token") {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return ERC20Factory.connect(
|
||||
this.config.fees.address,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
}
|
||||
|
||||
async #measureFeeInfo(bundle: Bundle, bundleOverheadGas?: BigNumber) {
|
||||
if (this.config.fees === nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
bundleOverheadGas ??=
|
||||
(await this.measureBundleOverhead()).bundleOverheadGas;
|
||||
|
||||
const gasEstimate = await this.ethereumService
|
||||
.estimateEffectiveCompressedGas(
|
||||
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;
|
||||
gasEstimate: BigNumber;
|
||||
bundleOverheadGas?: BigNumber;
|
||||
expectedFee: BigNumber;
|
||||
requiredFee: BigNumber;
|
||||
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: false,
|
||||
gasEstimate: feeInfo.gasEstimate,
|
||||
bundleOverheadGas: feeInfo.bundleOverheadGas,
|
||||
expectedFee: fee,
|
||||
requiredFee: feeInfo.requiredFee,
|
||||
expectedMaxCost: feeInfo.expectedMaxCost,
|
||||
errorReason: {
|
||||
message: [
|
||||
"Insufficient fee",
|
||||
`(provided: ${ethers.utils.formatEther(fee)},`,
|
||||
`required: ${ethers.utils.formatEther(feeInfo.requiredFee)})`,
|
||||
].join(" "),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const gasEstimate = feeInfo?.gasEstimate ??
|
||||
await this.ethereumService.estimateEffectiveCompressedGas(bundle);
|
||||
|
||||
return {
|
||||
success,
|
||||
gasEstimate,
|
||||
expectedFee: fee,
|
||||
requiredFee: feeInfo?.requiredFee ?? BigNumber.from(0),
|
||||
expectedMaxCost: feeInfo?.expectedMaxCost ?? BigNumber.from(0),
|
||||
errorReason,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async measureBundleOverhead() {
|
||||
// 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.
|
||||
|
||||
const es = this.ethereumService;
|
||||
const wallet = es.blsWalletWrapper;
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
// 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));
|
||||
|
||||
const bundle1 = wallet.sign({ nonce, gas: 1_000_000, actions: [] });
|
||||
|
||||
const bundle2 = wallet.sign({
|
||||
nonce: nonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [],
|
||||
});
|
||||
|
||||
const [oneOpGasEstimate, twoOpGasEstimate] = await Promise.all([
|
||||
es.estimateEffectiveCompressedGas(bundle1),
|
||||
es.estimateEffectiveCompressedGas(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
const opMarginalGasEstimate = twoOpGasEstimate.sub(oneOpGasEstimate);
|
||||
|
||||
const bundleOverheadGas = oneOpGasEstimate.sub(opMarginalGasEstimate);
|
||||
|
||||
const [compressedBundle1, compressedBundle12] = await Promise.all([
|
||||
es.bundleCompressor.compress(bundle1),
|
||||
es.bundleCompressor.compress(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
const [oneOpLen, twoOpLen] = await Promise.all([
|
||||
es.wallet.signTransaction({
|
||||
to: es.expanderEntryPoint.address,
|
||||
data: compressedBundle1,
|
||||
}).then((tx) => ethers.utils.hexDataLength(tx)),
|
||||
es.wallet.signTransaction({
|
||||
to: es.expanderEntryPoint.address,
|
||||
data: compressedBundle12,
|
||||
}).then((tx) => ethers.utils.hexDataLength(tx)),
|
||||
]);
|
||||
|
||||
const opMarginalLen = twoOpLen - oneOpLen;
|
||||
const bundleOverheadLen = oneOpLen - opMarginalLen;
|
||||
|
||||
return {
|
||||
bundleOverheadGas,
|
||||
bundleOverheadLen,
|
||||
};
|
||||
}
|
||||
|
||||
async #TokenDecimals(): Promise<number> {
|
||||
if (this.#tokenDecimals === nil) {
|
||||
const token = this.#FeeToken();
|
||||
assert(token !== nil);
|
||||
this.#tokenDecimals = await token.decimals();
|
||||
}
|
||||
|
||||
return this.#tokenDecimals;
|
||||
}
|
||||
}
|
||||
54
aggregator/src/app/AggregationStrategyRouter.ts
Normal file
54
aggregator/src/app/AggregationStrategyRouter.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Router } from "../../deps.ts";
|
||||
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,
|
||||
) {
|
||||
const router = new Router({ prefix: "/" });
|
||||
|
||||
router.post(
|
||||
"estimateFee",
|
||||
BundleHandler(async (ctx, bundle) => {
|
||||
let result: AsyncReturnType<AggregationStrategy["estimateFee"]>;
|
||||
|
||||
try {
|
||||
result = await aggregationStrategy.estimateFee(bundle);
|
||||
} catch (error) {
|
||||
if (error instanceof ClientReportableError) {
|
||||
ctx.response.status = 500;
|
||||
ctx.response.body = error.message;
|
||||
return;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
|
||||
ctx.response.body = {
|
||||
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,
|
||||
};
|
||||
}),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,14 +1,57 @@
|
||||
import { HTTPMethods } from "../../deps.ts";
|
||||
|
||||
type RowId = number | undefined;
|
||||
|
||||
type AppEvent = (
|
||||
type AppEvent =
|
||||
| { type: "starting" }
|
||||
| { 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;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: string;
|
||||
expectedMaxCost: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "failed-row";
|
||||
data: {
|
||||
publicKeyShorts: string[];
|
||||
submitError?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "aggregate-bundle-unprofitable";
|
||||
data: {
|
||||
reason?: string;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "aggregate-bundle-exceeds-max-gas";
|
||||
data: {
|
||||
hash: string;
|
||||
gasEstimate: number;
|
||||
aggregateGasEstimate: number;
|
||||
maxGasPerBundle: number;
|
||||
};
|
||||
}
|
||||
| { type: "unprofitable-despite-breakeven-operations" }
|
||||
| {
|
||||
type: "submission-attempt";
|
||||
data: { publicKeyShorts: string[]; attemptNumber: number };
|
||||
data: {
|
||||
publicKeyShorts: string[];
|
||||
attemptNumber: number;
|
||||
txLen: number;
|
||||
compressedTxLen: number;
|
||||
};
|
||||
}
|
||||
| {
|
||||
type: "submission-attempt-failed";
|
||||
@@ -18,15 +61,25 @@ type AppEvent = (
|
||||
error: Error;
|
||||
};
|
||||
}
|
||||
| { type: "submission-sent"; data: { rowIds: RowId[] } }
|
||||
| { type: "submission-sent"; data: { hash: string } }
|
||||
| {
|
||||
type: "submission-confirmed";
|
||||
data: { rowIds: RowId[]; blockNumber: number };
|
||||
data: {
|
||||
hash: string;
|
||||
bundleHashes: string[];
|
||||
blockNumber: number;
|
||||
profit: string;
|
||||
cost: string;
|
||||
expectedMaxCost: string;
|
||||
actualFee: string;
|
||||
expectedFee: string;
|
||||
};
|
||||
}
|
||||
| { type: "warning"; data: string }
|
||||
| {
|
||||
type: "bundle-added";
|
||||
data: {
|
||||
hash: string;
|
||||
publicKeyShorts: string[];
|
||||
};
|
||||
}
|
||||
@@ -49,7 +102,6 @@ type AppEvent = (
|
||||
status: number;
|
||||
duration: number;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export default AppEvent;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Router } from "../../deps.ts";
|
||||
import failRequest from "./helpers/failRequest.ts";
|
||||
import BundleHandler from "./helpers/BundleHandler.ts";
|
||||
|
||||
import nil from "../helpers/nil.ts";
|
||||
import BundleService from "./BundleService.ts";
|
||||
|
||||
export default function BundleRouter(bundleService: BundleService) {
|
||||
@@ -10,15 +10,48 @@ export default function BundleRouter(bundleService: BundleService) {
|
||||
router.post(
|
||||
"bundle",
|
||||
BundleHandler(async (ctx, bun) => {
|
||||
const failures = await bundleService.add(bun);
|
||||
const result = await bundleService.add(bun);
|
||||
|
||||
if (failures.length > 0) {
|
||||
return failRequest(ctx, failures);
|
||||
if ("failures" in result) {
|
||||
return failRequest(ctx, result.failures);
|
||||
}
|
||||
|
||||
ctx.response.body = { failures: [] };
|
||||
ctx.response.body = result;
|
||||
}),
|
||||
);
|
||||
|
||||
router.get(
|
||||
"bundleReceipt/:hash",
|
||||
(ctx) => {
|
||||
const bundleRow = bundleService.lookupBundle(ctx.params.hash!);
|
||||
|
||||
if (bundleRow?.receipt === nil) {
|
||||
ctx.response.status = 404;
|
||||
|
||||
ctx.response.body = {
|
||||
submitError: bundleRow?.submitError,
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = bundleService.receiptFromBundle(bundleRow);
|
||||
},
|
||||
);
|
||||
|
||||
router.get(
|
||||
"aggregateBundle/:subBundleHash",
|
||||
(ctx) => {
|
||||
const bundleRows = bundleService.lookupAggregateBundle(ctx.params.subBundleHash!);
|
||||
|
||||
if (bundleRows === nil || !bundleRows?.length) {
|
||||
ctx.response.status = 404;
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = bundleRows;
|
||||
},
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import {
|
||||
BigNumber,
|
||||
BlsWalletSigner,
|
||||
BlsWalletWrapper,
|
||||
Bundle,
|
||||
delay,
|
||||
QueryClient,
|
||||
ethers,
|
||||
Semaphore,
|
||||
VerificationGatewayFactory,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import { IClock } from "../helpers/Clock.ts";
|
||||
import Mutex from "../helpers/Mutex.ts";
|
||||
import toShortPublicKey from "./helpers/toPublicKeyShort.ts";
|
||||
|
||||
import TransactionFailure from "./TransactionFailure.ts";
|
||||
import SubmissionTimer from "./SubmissionTimer.ts";
|
||||
@@ -15,20 +20,31 @@ import runQueryGroup from "./runQueryGroup.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import BundleTable, { BundleRow } from "./BundleTable.ts";
|
||||
import toShortPublicKey from "./helpers/toPublicKeyShort.ts";
|
||||
import plus from "./helpers/plus.ts";
|
||||
import AggregationStrategy from "./AggregationStrategy.ts";
|
||||
import nil from "../helpers/nil.ts";
|
||||
import getOptimismL1Fee from "../helpers/getOptimismL1Fee.ts";
|
||||
import ExplicitAny from "../helpers/ExplicitAny.ts";
|
||||
|
||||
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,
|
||||
isOptimism: env.IS_OPTIMISM,
|
||||
};
|
||||
|
||||
unconfirmedBundles = new Set<Bundle>();
|
||||
unconfirmedActionCount = 0;
|
||||
unconfirmedRowIds = new Set<number>();
|
||||
|
||||
submissionSemaphore: Semaphore;
|
||||
submissionTimer: SubmissionTimer;
|
||||
submissionsInProgress = 0;
|
||||
|
||||
@@ -39,36 +55,41 @@ 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,
|
||||
public ethereumService: EthereumService,
|
||||
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;
|
||||
}
|
||||
|
||||
async runPendingTasks() {
|
||||
while (this.pendingTaskPromises.size > 0) {
|
||||
await Promise.all(Array.from(this.pendingTaskPromises));
|
||||
}
|
||||
}
|
||||
|
||||
addTask(task: () => Promise<unknown>) {
|
||||
if (this.stopping) {
|
||||
return;
|
||||
@@ -86,19 +107,19 @@ export default class BundleService {
|
||||
return;
|
||||
}
|
||||
|
||||
const eligibleBundleRows = await this.bundleTable.findEligible(
|
||||
const eligibleRows = this.bundleTable.findEligible(
|
||||
await this.ethereumService.BlockNumber(),
|
||||
this.config.bundleQueryLimit,
|
||||
);
|
||||
|
||||
const actionCount = eligibleBundleRows
|
||||
.filter((r) => !this.unconfirmedRowIds.has(r.id!))
|
||||
.map((r) => countActions(r.bundle))
|
||||
const opCount = eligibleRows
|
||||
.filter((r) => !this.unconfirmedRowIds.has(r.id))
|
||||
.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();
|
||||
@@ -108,42 +129,61 @@ 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,
|
||||
);
|
||||
}
|
||||
|
||||
async add(bundle: Bundle): Promise<TransactionFailure[]> {
|
||||
async add(
|
||||
bundle: Bundle,
|
||||
): Promise<AddBundleResponse> {
|
||||
if (bundle.operations.length !== bundle.senderPublicKeys.length) {
|
||||
return [
|
||||
{
|
||||
type: "invalid-format",
|
||||
description:
|
||||
"number of operations does not match number of public keys",
|
||||
},
|
||||
];
|
||||
return {
|
||||
failures: [
|
||||
{
|
||||
type: "invalid-format",
|
||||
description:
|
||||
"number of operations does not match number of public keys",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
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) {
|
||||
const signedCorrectly = this.blsWalletSigner.verify(
|
||||
bundle,
|
||||
walletAddresses,
|
||||
);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: "invalid signature",
|
||||
description:
|
||||
`invalid bundle signature for signature ${bundle.signature}`,
|
||||
});
|
||||
}
|
||||
|
||||
failures.push(...await this.ethereumService.checkNonces(bundle));
|
||||
|
||||
if (failures.length > 0) {
|
||||
return failures;
|
||||
return { failures };
|
||||
}
|
||||
|
||||
return await this.runQueryGroup(async () => {
|
||||
await this.bundleTable.add({
|
||||
const hash = await this.hashBundle(bundle);
|
||||
|
||||
this.bundleTable.add({
|
||||
status: "pending",
|
||||
hash,
|
||||
bundle,
|
||||
eligibleAfter: await this.ethereumService.BlockNumber(),
|
||||
nextEligibilityDelay: BigNumber.from(1),
|
||||
@@ -152,124 +192,271 @@ export default class BundleService {
|
||||
this.emit({
|
||||
type: "bundle-added",
|
||||
data: {
|
||||
hash,
|
||||
publicKeyShorts: bundle.senderPublicKeys.map(toShortPublicKey),
|
||||
},
|
||||
});
|
||||
|
||||
this.addTask(() => this.tryAggregating());
|
||||
|
||||
return [];
|
||||
return { hash };
|
||||
});
|
||||
}
|
||||
|
||||
lookupBundle(hash: string) {
|
||||
return this.bundleTable.findBundle(hash);
|
||||
}
|
||||
|
||||
lookupAggregateBundle(subBundleHash: string) {
|
||||
const subBundle = this.bundleTable.findBundle(subBundleHash);
|
||||
return this.bundleTable.findAggregateBundle(subBundle?.aggregateHash!);
|
||||
}
|
||||
|
||||
receiptFromBundle(bundle: BundleRow) {
|
||||
if (!bundle.receipt) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const { receipt, hash, aggregateHash } = bundle;
|
||||
|
||||
return {
|
||||
bundleHash: hash,
|
||||
aggregateBundleHash: aggregateHash,
|
||||
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 hashBundle(bundle: Bundle): Promise<string> {
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const verifyMethodName = "verify";
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === verifyMethodName,
|
||||
)?.inputs[0];
|
||||
|
||||
const validatedBundle = {
|
||||
...bundle,
|
||||
operations: operationsWithZeroGas,
|
||||
};
|
||||
|
||||
const encodedBundleWithZeroSignature = ethers.utils.defaultAbiCoder.encode(
|
||||
[bundleType as ExplicitAny],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
const chainId = (await this.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
return ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
}
|
||||
|
||||
async runSubmission() {
|
||||
this.submissionsInProgress++;
|
||||
|
||||
const submissionResult = await this.runQueryGroup(async () => {
|
||||
const bundleSubmitted = await this.runQueryGroup(async () => {
|
||||
const currentBlockNumber = await this.ethereumService.BlockNumber();
|
||||
|
||||
const eligibleBundleRows = await this.bundleTable.findEligible(
|
||||
let eligibleRows = this.bundleTable.findEligible(
|
||||
currentBlockNumber,
|
||||
this.config.bundleQueryLimit,
|
||||
);
|
||||
|
||||
let aggregateBundle: Bundle | null = null;
|
||||
const includedRows: BundleRow[] = [];
|
||||
// TODO (merge-ok): Count gas instead, have idea
|
||||
// or way to query max gas per txn (submission).
|
||||
let actionCount = 0;
|
||||
// Exclude rows that are already pending.
|
||||
eligibleRows = eligibleRows.filter(
|
||||
(row) => !this.unconfirmedRowIds.has(row.id),
|
||||
);
|
||||
|
||||
for (const row of eligibleBundleRows) {
|
||||
if (this.unconfirmedRowIds.has(row.id!)) {
|
||||
continue;
|
||||
this.emit({
|
||||
type: "running-strategy",
|
||||
data: {
|
||||
eligibleRows: eligibleRows.length,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
bundleOverheadCost,
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
failedRows,
|
||||
} = await this
|
||||
.aggregationStrategy.run(eligibleRows);
|
||||
|
||||
this.emit({
|
||||
type: "completed-strategy",
|
||||
data: {
|
||||
includedRows: includedRows.length,
|
||||
bundleOverheadCost: ethers.utils.formatEther(bundleOverheadCost),
|
||||
bundleOverheadLen,
|
||||
expectedFee: ethers.utils.formatEther(expectedFee),
|
||||
expectedMaxCost: ethers.utils.formatEther(expectedMaxCost),
|
||||
},
|
||||
});
|
||||
|
||||
if (aggregateBundle) {
|
||||
const aggregateBundleHash = await this.hashBundle(aggregateBundle);
|
||||
for (const row of includedRows) {
|
||||
row.aggregateHash = aggregateBundleHash;
|
||||
}
|
||||
}
|
||||
|
||||
const rowActionCount = countActions(row.bundle);
|
||||
for (const failedRow of failedRows) {
|
||||
this.emit({
|
||||
type: "failed-row",
|
||||
data: {
|
||||
publicKeyShorts: failedRow.bundle.senderPublicKeys.map(
|
||||
toShortPublicKey,
|
||||
),
|
||||
submitError: failedRow.submitError,
|
||||
},
|
||||
});
|
||||
|
||||
if (actionCount + rowActionCount > this.config.maxAggregationSize) {
|
||||
break;
|
||||
}
|
||||
|
||||
const candidateBundle = this.blsWalletSigner.aggregate([
|
||||
...(aggregateBundle ? [aggregateBundle] : []),
|
||||
row.bundle,
|
||||
]);
|
||||
|
||||
if (await this.ethereumService.checkBundle(candidateBundle)) {
|
||||
aggregateBundle = candidateBundle;
|
||||
includedRows.push(row);
|
||||
actionCount += rowActionCount;
|
||||
} else {
|
||||
await this.handleFailedRow(row, currentBlockNumber);
|
||||
}
|
||||
this.handleFailedRow(failedRow, currentBlockNumber);
|
||||
}
|
||||
|
||||
if (!aggregateBundle || includedRows.length === 0) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const maxUnconfirmedActions = (
|
||||
this.config.maxUnconfirmedAggregations *
|
||||
this.config.maxAggregationSize
|
||||
await this.submitAggregateBundle(
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
);
|
||||
|
||||
while (
|
||||
this.unconfirmedActionCount + actionCount > maxUnconfirmedActions
|
||||
) {
|
||||
// FIXME (merge-ok): Polling
|
||||
this.emit({ type: "waiting-unconfirmed-space" });
|
||||
await delay(1000);
|
||||
}
|
||||
|
||||
this.unconfirmedActionCount += actionCount;
|
||||
this.unconfirmedBundles.add(aggregateBundle);
|
||||
|
||||
for (const row of includedRows) {
|
||||
this.unconfirmedRowIds.add(row.id!);
|
||||
}
|
||||
|
||||
// TODO (merge-ok): Use a task
|
||||
(async () => {
|
||||
try {
|
||||
const recpt = await this.ethereumService.submitBundle(
|
||||
aggregateBundle,
|
||||
Infinity,
|
||||
300,
|
||||
);
|
||||
|
||||
this.emit({
|
||||
type: "submission-confirmed",
|
||||
data: {
|
||||
rowIds: includedRows.map((row) => row.id),
|
||||
blockNumber: recpt.blockNumber,
|
||||
},
|
||||
});
|
||||
|
||||
await this.bundleTable.remove(...includedRows);
|
||||
} finally {
|
||||
this.unconfirmedActionCount -= actionCount;
|
||||
this.unconfirmedBundles.delete(aggregateBundle);
|
||||
|
||||
for (const row of includedRows) {
|
||||
this.unconfirmedRowIds.delete(row.id!);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return true;
|
||||
});
|
||||
|
||||
this.submissionsInProgress--;
|
||||
this.addTask(() => this.tryAggregating());
|
||||
|
||||
return submissionResult;
|
||||
if (bundleSubmitted) {
|
||||
this.addTask(() => this.tryAggregating());
|
||||
}
|
||||
}
|
||||
|
||||
async handleFailedRow(row: BundleRow, currentBlockNumber: BigNumber) {
|
||||
await this.bundleTable.update({
|
||||
...row,
|
||||
eligibleAfter: currentBlockNumber.add(row.nextEligibilityDelay),
|
||||
nextEligibilityDelay: row.nextEligibilityDelay.mul(2),
|
||||
handleFailedRow(row: BundleRow, currentBlockNumber: BigNumber) {
|
||||
if (row.nextEligibilityDelay.lte(this.config.maxEligibilityDelay)) {
|
||||
this.bundleTable.update({
|
||||
...row,
|
||||
eligibleAfter: currentBlockNumber.add(row.nextEligibilityDelay),
|
||||
nextEligibilityDelay: row.nextEligibilityDelay.mul(2),
|
||||
});
|
||||
} else {
|
||||
this.bundleTable.update({
|
||||
...row,
|
||||
status: "failed",
|
||||
});
|
||||
}
|
||||
|
||||
this.unconfirmedRowIds.delete(row.id);
|
||||
}
|
||||
|
||||
async submitAggregateBundle(
|
||||
aggregateBundle: Bundle,
|
||||
includedRows: BundleRow[],
|
||||
expectedFee: BigNumber,
|
||||
expectedMaxCost: BigNumber,
|
||||
) {
|
||||
const releaseSemaphore = await this.submissionSemaphore.acquire();
|
||||
this.unconfirmedBundles.add(aggregateBundle);
|
||||
|
||||
for (const row of includedRows) {
|
||||
this.unconfirmedRowIds.add(row.id);
|
||||
}
|
||||
|
||||
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.bundleTable.update({
|
||||
...row,
|
||||
receipt,
|
||||
status: "confirmed",
|
||||
});
|
||||
}
|
||||
|
||||
const profit = balanceAfter.sub(balanceBefore);
|
||||
|
||||
/** What we paid to process the bundle */
|
||||
let cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
|
||||
|
||||
if (this.config.isOptimism) {
|
||||
cost = cost.add(
|
||||
await getOptimismL1Fee(
|
||||
this.ethereumService.provider,
|
||||
receipt.transactionHash,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** 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),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.unconfirmedBundles.delete(aggregateBundle);
|
||||
|
||||
for (const row of includedRows) {
|
||||
this.unconfirmedRowIds.delete(row.id);
|
||||
}
|
||||
|
||||
releaseSemaphore();
|
||||
}
|
||||
});
|
||||
this.unconfirmedRowIds.delete(row.id!);
|
||||
}
|
||||
|
||||
async waitForConfirmations() {
|
||||
@@ -289,11 +476,3 @@ export default class BundleService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function countActions(bundle: Bundle) {
|
||||
return bundle.operations.map((op) => op.actions.length).reduce(plus, 0);
|
||||
}
|
||||
|
||||
function plus(a: number, b: number) {
|
||||
return a + b;
|
||||
}
|
||||
|
||||
@@ -3,172 +3,308 @@ import {
|
||||
Bundle,
|
||||
bundleFromDto,
|
||||
bundleToDto,
|
||||
Constraint,
|
||||
CreateTableMode,
|
||||
DataType,
|
||||
QueryClient,
|
||||
QueryTable,
|
||||
TableOptions,
|
||||
unsketchify,
|
||||
ethers,
|
||||
sqlite,
|
||||
} from "../../deps.ts";
|
||||
import assert from "../helpers/assert.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;
|
||||
id: number;
|
||||
status: string;
|
||||
hash: string;
|
||||
bundle: string;
|
||||
eligibleAfter: string;
|
||||
nextEligibilityDelay: string;
|
||||
submitError: string | null;
|
||||
receipt: string | null;
|
||||
aggregateHash: string | null;
|
||||
};
|
||||
|
||||
const BundleStatuses = ["pending", "confirmed", "failed"] as const;
|
||||
type BundleStatus = typeof BundleStatuses[number];
|
||||
|
||||
type Row = {
|
||||
id?: number;
|
||||
id: number;
|
||||
status: BundleStatus;
|
||||
hash: string;
|
||||
bundle: Bundle;
|
||||
eligibleAfter: BigNumber;
|
||||
nextEligibilityDelay: BigNumber;
|
||||
submitError?: string;
|
||||
receipt?: ethers.ContractReceipt;
|
||||
aggregateHash?: string;
|
||||
};
|
||||
|
||||
type InsertRow = Omit<Row, "id">;
|
||||
type InsertRawRow = Omit<RawRow, "id">;
|
||||
|
||||
export type BundleRow = Row;
|
||||
|
||||
const tableOptions: TableOptions = {
|
||||
id: { type: DataType.Serial, constraint: Constraint.PrimaryKey },
|
||||
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,
|
||||
aggregateHash: rawRow[8] 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 {
|
||||
id: rawRow.id,
|
||||
bundle: bundleFromDto(parseResult.success),
|
||||
status,
|
||||
hash: rawRow.hash,
|
||||
bundle: bundleFromDto(parseBundleResult.success),
|
||||
eligibleAfter: BigNumber.from(rawRow.eligibleAfter),
|
||||
nextEligibilityDelay: BigNumber.from(rawRow.nextEligibilityDelay),
|
||||
submitError: rawRow.submitError ?? nil,
|
||||
receipt,
|
||||
aggregateHash: rawRow.aggregateHash ?? nil,
|
||||
};
|
||||
}
|
||||
|
||||
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),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
};
|
||||
}
|
||||
|
||||
function toRawRow(row: Row): RawRow {
|
||||
const rawRow: RawRow = {
|
||||
bundle: JSON.stringify(bundleToDto(row.bundle)),
|
||||
return {
|
||||
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),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
};
|
||||
|
||||
if ("id" in row) {
|
||||
rawRow.id = row.id;
|
||||
}
|
||||
|
||||
return rawRow;
|
||||
}
|
||||
|
||||
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);
|
||||
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,
|
||||
aggregateHash TEXT
|
||||
)
|
||||
`);
|
||||
}
|
||||
|
||||
return table;
|
||||
dbQuery(sql: string, params?: sqlite.QueryParameterSet) {
|
||||
this.onQuery(sql, params);
|
||||
return this.db.query(sql, params);
|
||||
}
|
||||
|
||||
async add(...rows: Row[]) {
|
||||
await this.queryTable.insert(...rows.map(toRawRow));
|
||||
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,
|
||||
aggregateHash
|
||||
) VALUES (
|
||||
:id,
|
||||
:status,
|
||||
:hash,
|
||||
:bundle,
|
||||
:eligibleAfter,
|
||||
:nextEligibilityDelay,
|
||||
:submitError,
|
||||
:receipt,
|
||||
:aggregateHash
|
||||
)
|
||||
`,
|
||||
{
|
||||
":status": rawRow.status,
|
||||
":hash": rawRow.hash,
|
||||
":bundle": rawRow.bundle,
|
||||
":eligibleAfter": rawRow.eligibleAfter,
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async addWithNewId(...rows: Row[]) {
|
||||
const rowsWithoutIds = rows.map((row) => {
|
||||
const withoutId = { ...row };
|
||||
delete withoutId.id;
|
||||
return withoutId;
|
||||
});
|
||||
update(row: Row) {
|
||||
const rawRow = toRawRow(row);
|
||||
|
||||
return await this.add(...rowsWithoutIds);
|
||||
}
|
||||
|
||||
async update(row: Row) {
|
||||
assert(row.id !== undefined);
|
||||
await this.queryTable.where({ id: row.id }).update(toRawRow(row));
|
||||
}
|
||||
|
||||
async remove(...rows: Row[]) {
|
||||
await Promise.all(rows.map((row) =>
|
||||
this.queryTable
|
||||
.where({ id: assertExists(row.id) })
|
||||
.delete()
|
||||
));
|
||||
}
|
||||
|
||||
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,
|
||||
aggregateHash = :aggregateHash
|
||||
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,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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}`,
|
||||
findAggregateBundle(aggregateHash: string): Row[] | nil {
|
||||
const rows = this.dbQuery(
|
||||
`
|
||||
SELECT * from bundles
|
||||
WHERE
|
||||
aggregateHash = :aggregateHash AND
|
||||
status = 'confirmed'
|
||||
ORDER BY id ASC
|
||||
`,
|
||||
{ ":aggregateHash": aggregateHash },
|
||||
);
|
||||
|
||||
return rows.map(fromRawRow);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
import {
|
||||
AggregatorUtilities,
|
||||
BaseContract,
|
||||
BigNumber,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletSigner,
|
||||
BlsWalletWrapper,
|
||||
Bundle,
|
||||
BundleCompressor,
|
||||
BytesLike,
|
||||
ContractsConnector,
|
||||
delay,
|
||||
Erc20Compressor,
|
||||
ethers,
|
||||
FallbackCompressor,
|
||||
initBlsWalletSigner,
|
||||
VerificationGateway,
|
||||
// deno-lint-ignore camelcase
|
||||
VerificationGateway__factory,
|
||||
Wallet,
|
||||
} from "../../deps.ts";
|
||||
|
||||
@@ -17,6 +23,11 @@ import TransactionFailure from "./TransactionFailure.ts";
|
||||
import assert from "../helpers/assert.ts";
|
||||
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";
|
||||
import hexToUint8Array from "../helpers/hexToUint8Array.ts";
|
||||
import OptimismGasPriceOracle from "./OptimismGasPriceOracle.ts";
|
||||
|
||||
export type TxCheckResult = {
|
||||
failures: TransactionFailure[];
|
||||
@@ -28,58 +39,156 @@ export type CreateWalletResult = {
|
||||
failures: TransactionFailure[];
|
||||
};
|
||||
|
||||
export default class EthereumService {
|
||||
verificationGateway: VerificationGateway;
|
||||
type Call = Parameters<
|
||||
AggregatorUtilities["callStatic"]["performSequence"]
|
||||
>[0][number];
|
||||
|
||||
type CallHelper<T> = {
|
||||
value: Call;
|
||||
resultDecoder: (result: BytesLike) => T;
|
||||
};
|
||||
|
||||
type CallResult<T> =
|
||||
| { success: true; returnValue: T }
|
||||
| { success: false; returnValue: undefined };
|
||||
|
||||
type MapCallHelperReturns<T> = T extends CallHelper<unknown>[]
|
||||
? (T extends [CallHelper<infer First>, ...infer Rest]
|
||||
? [CallResult<First>, ...MapCallHelperReturns<Rest>]
|
||||
: [])
|
||||
: never;
|
||||
|
||||
// It appears that ethers' callStatic with unwrap single element arrays.
|
||||
// However, when passing the bytes into decodeFunctionResult, this unwrap
|
||||
// doesn't happen. The result is always an array, so we can fix this
|
||||
// inconsistency by wrapping the type in an array if it's not already an array.
|
||||
type EnforceArray<T> = T extends unknown[] ? T : [T];
|
||||
|
||||
type DecodeReturnType<
|
||||
Contract extends BaseContract,
|
||||
Method extends keyof Contract["callStatic"],
|
||||
> = EnforceArray<AsyncReturnType<Contract["callStatic"][Method]>>;
|
||||
|
||||
type ExpanderEntryPoint = AsyncReturnType<
|
||||
ContractsConnector["ExpanderEntryPoint"]
|
||||
>;
|
||||
|
||||
export default class EthereumService {
|
||||
constructor(
|
||||
public emit: (evt: AppEvent) => void,
|
||||
public wallet: Wallet,
|
||||
public provider: ethers.providers.Provider,
|
||||
public chainId: number,
|
||||
public blsWalletWrapper: BlsWalletWrapper,
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
verificationGatewayAddress: string,
|
||||
public nextNonce: number,
|
||||
) {
|
||||
this.verificationGateway = VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
this.wallet,
|
||||
);
|
||||
}
|
||||
public verificationGateway: VerificationGateway,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
public expanderEntryPoint: ExpanderEntryPoint,
|
||||
public bundleCompressor: BundleCompressor,
|
||||
public nextNonce: BigNumber,
|
||||
) {}
|
||||
|
||||
NextNonce() {
|
||||
const result = this.nextNonce++;
|
||||
const result = this.nextNonce;
|
||||
this.nextNonce = this.nextNonce.add(1);
|
||||
return result;
|
||||
}
|
||||
|
||||
static async create(
|
||||
emit: (evt: AppEvent) => void,
|
||||
verificationGatewayAddress: string,
|
||||
aggPrivateKey: string,
|
||||
): Promise<EthereumService> {
|
||||
const wallet = EthereumService.Wallet(aggPrivateKey);
|
||||
const nextNonce = (await wallet.getTransactionCount());
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
provider.pollingInterval = env.RPC_POLLING_INTERVAL;
|
||||
const wallet = EthereumService.Wallet(provider, aggPrivateKey);
|
||||
|
||||
const contractsConnector = await ContractsConnector.create(wallet);
|
||||
|
||||
const [
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
blsExpanderDelegator,
|
||||
erc20Expander,
|
||||
blsRegistration,
|
||||
fallbackExpander,
|
||||
expanderEntryPoint,
|
||||
] = await Promise.all([
|
||||
contractsConnector.VerificationGateway(),
|
||||
contractsConnector.AggregatorUtilities(),
|
||||
contractsConnector.BLSExpanderDelegator(),
|
||||
contractsConnector.ERC20Expander(),
|
||||
contractsConnector.BLSRegistration(),
|
||||
contractsConnector.FallbackExpander(),
|
||||
contractsConnector.ExpanderEntryPoint(),
|
||||
]);
|
||||
|
||||
const blsWalletWrapper = await BlsWalletWrapper.connect(
|
||||
aggPrivateKey,
|
||||
verificationGateway.address,
|
||||
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.processBundle(
|
||||
await blsWalletWrapper.signWithGasEstimate({
|
||||
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: verificationGateway.address,
|
||||
});
|
||||
|
||||
const bundleCompressor = new BundleCompressor(blsExpanderDelegator);
|
||||
|
||||
const [erc20Compressor, blsRegistrationCompressor, fallbackCompressor] =
|
||||
await Promise.all([
|
||||
Erc20Compressor.wrap(erc20Expander),
|
||||
BlsRegistrationCompressor.wrap(blsRegistration),
|
||||
FallbackCompressor.wrap(fallbackExpander),
|
||||
]);
|
||||
|
||||
await bundleCompressor.addCompressor(erc20Compressor);
|
||||
await bundleCompressor.addCompressor(blsRegistrationCompressor);
|
||||
await bundleCompressor.addCompressor(fallbackCompressor);
|
||||
|
||||
return new EthereumService(
|
||||
emit,
|
||||
wallet,
|
||||
provider,
|
||||
chainId,
|
||||
blsWalletWrapper,
|
||||
blsWalletSigner,
|
||||
verificationGatewayAddress,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
expanderEntryPoint,
|
||||
bundleCompressor,
|
||||
nextNonce,
|
||||
);
|
||||
}
|
||||
|
||||
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.
|
||||
@@ -115,6 +224,85 @@ export default class EthereumService {
|
||||
return failures;
|
||||
}
|
||||
|
||||
Call<
|
||||
Contract extends BaseContract,
|
||||
Method extends keyof Contract["callStatic"],
|
||||
>(
|
||||
contract: Contract,
|
||||
method: Method,
|
||||
args: Parameters<Contract["callStatic"][Method]>,
|
||||
): CallHelper<DecodeReturnType<Contract, Method>> {
|
||||
return {
|
||||
value: {
|
||||
contractAddress: contract.address,
|
||||
encodedFunction: contract.interface.encodeFunctionData(
|
||||
method as ExplicitAny,
|
||||
args,
|
||||
),
|
||||
},
|
||||
resultDecoder: (data) =>
|
||||
contract.interface.decodeFunctionResult(
|
||||
method as ExplicitAny,
|
||||
data,
|
||||
) as DecodeReturnType<Contract, Method>,
|
||||
};
|
||||
}
|
||||
|
||||
async callStaticSequence<Calls extends CallHelper<unknown>[]>(
|
||||
...calls: Calls
|
||||
): Promise<MapCallHelperReturns<Calls>> {
|
||||
const rawResults = await this.aggregatorUtilities.callStatic
|
||||
.performSequence(
|
||||
calls.map((c) => c.value),
|
||||
);
|
||||
|
||||
const results: CallResult<unknown>[] = rawResults.map(
|
||||
([success, result], i) => {
|
||||
if (!success) {
|
||||
return { success };
|
||||
}
|
||||
|
||||
return {
|
||||
success,
|
||||
returnValue: calls[i].resultDecoder(result),
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
return results as MapCallHelperReturns<Calls>;
|
||||
}
|
||||
|
||||
async callStaticSequenceWithMeasure<Measure, CallReturn>(
|
||||
measureCall: CallHelper<Measure>,
|
||||
calls: CallHelper<CallReturn>[],
|
||||
): Promise<{
|
||||
measureResults: CallResult<Measure>[];
|
||||
callResults: CallResult<CallReturn>[];
|
||||
}> {
|
||||
const fullCalls: CallHelper<unknown>[] = [measureCall];
|
||||
|
||||
for (const call of calls) {
|
||||
fullCalls.push(call, measureCall);
|
||||
}
|
||||
|
||||
const fullResults: CallResult<unknown>[] = await this.callStaticSequence(
|
||||
...fullCalls,
|
||||
);
|
||||
|
||||
const measureResults: CallResult<unknown>[] = fullResults.filter(
|
||||
(_r, i) => i % 2 === 0,
|
||||
);
|
||||
|
||||
const callResults = fullResults.filter(
|
||||
(_r, i) => i % 2 === 1,
|
||||
);
|
||||
|
||||
return {
|
||||
measureResults: measureResults as CallResult<Measure>[],
|
||||
callResults: callResults as CallResult<CallReturn>[],
|
||||
};
|
||||
}
|
||||
|
||||
async checkBundle(bundle: Bundle) {
|
||||
try {
|
||||
const { successes } = await this.verificationGateway.callStatic
|
||||
@@ -134,19 +322,33 @@ export default class EthereumService {
|
||||
assert(bundle.operations.length > 0, "Cannot process empty bundle");
|
||||
assert(maxAttempts > 0, "Must have at least one attempt");
|
||||
|
||||
const processBundleArgs: Parameters<VerificationGateway["processBundle"]> =
|
||||
[
|
||||
bundle,
|
||||
{ nonce: this.NextNonce() },
|
||||
];
|
||||
const compressedBundle = await this.bundleCompressor.compress(bundle);
|
||||
|
||||
const [rawTx, rawCompressedTx] = await Promise.all([
|
||||
this.verificationGateway.populateTransaction.processBundle(bundle).then(
|
||||
(tx) => this.wallet.signTransaction(tx),
|
||||
),
|
||||
this.wallet.signTransaction({
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
}),
|
||||
]);
|
||||
|
||||
const txLen = ethers.utils.hexDataLength(rawTx);
|
||||
const compressedTxLen = ethers.utils.hexDataLength(rawCompressedTx);
|
||||
|
||||
const txRequest: ethers.providers.TransactionRequest = {
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
nonce: this.NextNonce(),
|
||||
...await this.GasConfig(),
|
||||
};
|
||||
|
||||
const attempt = async () => {
|
||||
let txResponse: ethers.providers.TransactionResponse;
|
||||
let response: ethers.providers.TransactionResponse;
|
||||
|
||||
try {
|
||||
txResponse = await this.verificationGateway.processBundle(
|
||||
...processBundleArgs,
|
||||
);
|
||||
response = await this.wallet.sendTransaction(txRequest);
|
||||
} catch (error) {
|
||||
if (/\binvalid transaction nonce\b/.test(error.message)) {
|
||||
// This can occur when the nonce is in the future, which can
|
||||
@@ -162,7 +364,10 @@ export default class EthereumService {
|
||||
}
|
||||
|
||||
try {
|
||||
return { type: "receipt" as const, value: await txResponse.wait() };
|
||||
return {
|
||||
type: "complete" as const,
|
||||
value: await response.wait(),
|
||||
};
|
||||
} catch (error) {
|
||||
return { type: "waitError" as const, value: error };
|
||||
}
|
||||
@@ -173,12 +378,12 @@ export default class EthereumService {
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
this.emit({
|
||||
type: "submission-attempt",
|
||||
data: { attemptNumber: i + 1, publicKeyShorts },
|
||||
data: { attemptNumber: i + 1, publicKeyShorts, txLen, compressedTxLen },
|
||||
});
|
||||
|
||||
const attemptResult = await attempt();
|
||||
|
||||
if (attemptResult.type === "receipt") {
|
||||
if (attemptResult.type === "complete") {
|
||||
return attemptResult.value;
|
||||
}
|
||||
|
||||
@@ -207,8 +412,134 @@ 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);
|
||||
/**
|
||||
* Estimates the amount of effective gas needed to process the bundle using
|
||||
* compression.
|
||||
*
|
||||
* Here 'effective' gas means the number you need to multiply by gasPrice in
|
||||
* order to get the right fee. There are a few cases here:
|
||||
*
|
||||
* 1. L1 chains (used in testing, eg gethDev)
|
||||
* - Effective gas is equal to regular gas
|
||||
* 2. Arbitrum
|
||||
* - The Arbitrum node already responds with effective gas when calling
|
||||
* estimateGas
|
||||
* 3. Optimism
|
||||
* - We estimate Optimism's calculation for the amount of L1 gas it will
|
||||
* charge for, and then convert that into an equivalend amount of L2 gas.
|
||||
*/
|
||||
async estimateEffectiveCompressedGas(bundle: Bundle): Promise<BigNumber> {
|
||||
const compressedBundle = await this.bundleCompressor.compress(bundle);
|
||||
|
||||
let gasEstimate = await this.wallet.estimateGas({
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
});
|
||||
|
||||
if (env.IS_OPTIMISM) {
|
||||
const extraGasEstimate = await this.estimateOptimismL2GasNeededForL1Gas(
|
||||
compressedBundle,
|
||||
gasEstimate,
|
||||
);
|
||||
|
||||
gasEstimate = gasEstimate.add(extraGasEstimate);
|
||||
}
|
||||
|
||||
return gasEstimate;
|
||||
}
|
||||
|
||||
async GasConfig(block?: ethers.providers.Block) {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the L1 gas that Optimism will charge us for and expresses it as
|
||||
* an amount of equivalent L2 gas.
|
||||
*
|
||||
* This is very similar to what Arbitrum does, but in Arbitrum it's built-in,
|
||||
* and you actually sign for that additional L2 gas. On Optimism, you only
|
||||
* sign for the actual L2 gas, and optimism just adds the L1 fee.
|
||||
*
|
||||
* For our purposes, this works as a way to normalize the behavior between
|
||||
* the different chains.
|
||||
*/
|
||||
async estimateOptimismL2GasNeededForL1Gas(
|
||||
compressedBundle: string,
|
||||
gasLimit: BigNumber,
|
||||
): Promise<BigNumber> {
|
||||
const block = await this.provider.getBlock("latest");
|
||||
const gasConfig = await this.GasConfig(block);
|
||||
|
||||
const txBytes = await this.wallet.signTransaction({
|
||||
type: 2,
|
||||
chainId: this.chainId,
|
||||
nonce: this.nextNonce,
|
||||
to: this.expanderEntryPoint.address,
|
||||
data: compressedBundle,
|
||||
...gasConfig,
|
||||
gasLimit,
|
||||
});
|
||||
|
||||
let l1Gas = 0;
|
||||
|
||||
for (const byte of hexToUint8Array(txBytes)) {
|
||||
if (byte === 0) {
|
||||
l1Gas += 4;
|
||||
} else {
|
||||
l1Gas += 16;
|
||||
}
|
||||
}
|
||||
|
||||
const gasOracle = new OptimismGasPriceOracle(this.provider);
|
||||
|
||||
const { l1BaseFee, overhead, scalar, decimals } = await gasOracle
|
||||
.getAllParams();
|
||||
|
||||
const scalarNum = scalar.toNumber() / (10 ** decimals.toNumber());
|
||||
|
||||
l1Gas += overhead.toNumber();
|
||||
|
||||
assert(block.baseFeePerGas !== null && block.baseFeePerGas !== nil);
|
||||
assert(env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil);
|
||||
|
||||
const adjustedL1BaseFee = l1BaseFee.toNumber() * scalarNum *
|
||||
(1 + env.OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE / 100);
|
||||
|
||||
const feeRatio = adjustedL1BaseFee / block.baseFeePerGas.toNumber();
|
||||
|
||||
return BigNumber.from(
|
||||
Math.ceil(feeRatio * l1Gas),
|
||||
);
|
||||
}
|
||||
|
||||
private static Wallet(
|
||||
provider: ethers.providers.Provider,
|
||||
privateKey: string,
|
||||
) {
|
||||
const wallet = new Wallet(privateKey, provider);
|
||||
|
||||
if (env.USE_TEST_NET) {
|
||||
|
||||
16
aggregator/src/app/HealthRouter.ts
Normal file
16
aggregator/src/app/HealthRouter.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { Router } from "../../deps.ts";
|
||||
import HealthService from "./HealthService.ts";
|
||||
|
||||
export default function HealthRouter(healthService: HealthService) {
|
||||
const router = new Router({ prefix: "/" });
|
||||
|
||||
router.get(
|
||||
"health",
|
||||
async (ctx) => {
|
||||
const healthResults = await healthService.getHealth();
|
||||
console.log(`Status: ${healthResults.status}\n`);
|
||||
ctx.response.status = healthResults.status == 'healthy' ? 200 : 503;
|
||||
ctx.response.body = { status: healthResults.status };
|
||||
});
|
||||
return router;
|
||||
}
|
||||
11
aggregator/src/app/HealthService.ts
Normal file
11
aggregator/src/app/HealthService.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export type ResourceHealth = 'healthy' | 'unhealthy';
|
||||
|
||||
type HealthCheckResult = {
|
||||
status: ResourceHealth,
|
||||
};
|
||||
|
||||
export default class HealthService {
|
||||
getHealth(): Promise<HealthCheckResult> {
|
||||
return Promise.resolve({ status: 'healthy' });
|
||||
}
|
||||
}
|
||||
52
aggregator/src/app/OptimismGasPriceOracle.ts
Normal file
52
aggregator/src/app/OptimismGasPriceOracle.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { BigNumber, ethers } from "../../deps.ts";
|
||||
import assert from "../helpers/assert.ts";
|
||||
import { OPTIMISM_GAS_PRICE_ORACLE_ADDRESS } from "../env.ts";
|
||||
|
||||
export default class OptimismGasPriceOracle {
|
||||
constructor(
|
||||
public provider: ethers.providers.Provider,
|
||||
) {}
|
||||
|
||||
private async callFn(method: string, blockTag?: ethers.providers.BlockTag) {
|
||||
const outputBytes = await this.provider.call({
|
||||
to: OPTIMISM_GAS_PRICE_ORACLE_ADDRESS,
|
||||
data: ethers.utils.id(method),
|
||||
}, blockTag);
|
||||
|
||||
const result = ethers.utils.defaultAbiCoder.decode(
|
||||
["uint256"],
|
||||
outputBytes,
|
||||
)[0];
|
||||
|
||||
assert(result instanceof BigNumber);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
async l1BaseFee(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("l1BaseFee()", blockTag);
|
||||
}
|
||||
|
||||
async overhead(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("overhead()", blockTag);
|
||||
}
|
||||
|
||||
async scalar(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("scalar()", blockTag);
|
||||
}
|
||||
|
||||
async decimals(blockTag?: ethers.providers.BlockTag) {
|
||||
return await this.callFn("decimals()", blockTag);
|
||||
}
|
||||
|
||||
async getAllParams(blockTag?: ethers.providers.BlockTag) {
|
||||
const [l1BaseFee, overhead, scalar, decimals] = await Promise.all([
|
||||
this.l1BaseFee(blockTag),
|
||||
this.overhead(blockTag),
|
||||
this.scalar(blockTag),
|
||||
this.decimals(blockTag),
|
||||
]);
|
||||
|
||||
return { l1BaseFee, overhead, scalar, decimals };
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ type TransactionFailure = (
|
||||
| { type: "invalid-format"; description: string }
|
||||
| { type: "invalid-signature"; description: string }
|
||||
| { type: "duplicate-nonce"; description: string }
|
||||
| { type: "insufficient-reward"; description: string }
|
||||
| { type: "insufficient-fee"; description: string }
|
||||
| { type: "unpredictable-gas-limit"; description: string }
|
||||
| { type: "invalid-creation"; description: string }
|
||||
);
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Router } from "../../deps.ts";
|
||||
import failRequest from "./helpers/failRequest.ts";
|
||||
import BundleHandler from "./helpers/BundleHandler.ts";
|
||||
|
||||
import WalletService from "./WalletService.ts";
|
||||
|
||||
export default function WalletRouter(walletService: WalletService) {
|
||||
const router = new Router({ prefix: "/" });
|
||||
|
||||
router.post(
|
||||
"wallet",
|
||||
BundleHandler(async (ctx, bundle) => {
|
||||
const { wallet, failures } = await walletService.createWallet(bundle);
|
||||
if (failures.length) {
|
||||
failRequest(ctx, failures);
|
||||
return;
|
||||
}
|
||||
|
||||
ctx.response.body = wallet;
|
||||
}),
|
||||
);
|
||||
|
||||
return router;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { BlsWalletWrapper, Bundle } from "../../deps.ts";
|
||||
import TransactionFailure from "./TransactionFailure.ts";
|
||||
|
||||
type CreateWalletResult = {
|
||||
wallet: BlsWalletWrapper;
|
||||
failures: TransactionFailure[];
|
||||
};
|
||||
|
||||
export default class WalletService {
|
||||
createWallet(_bun: Bundle): Promise<CreateWalletResult> {
|
||||
throw new Error("WalletService: createWallet not implemented");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Application } from "../../deps.ts";
|
||||
import { Application, oakCors, sqlite } from "../../deps.ts";
|
||||
|
||||
import * as env from "../env.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
@@ -8,43 +8,54 @@ 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";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import WalletRouter from "./WalletRouter.ts";
|
||||
import WalletService from "./WalletService.ts";
|
||||
import BundleTable from "./BundleTable.ts";
|
||||
import AggregationStrategy from "./AggregationStrategy.ts";
|
||||
import AggregationStrategyRouter from "./AggregationStrategyRouter.ts";
|
||||
import HealthService from "./HealthService.ts";
|
||||
import HealthRouter from "./HealthRouter.ts";
|
||||
|
||||
export default async function app(emit: (evt: AppEvent) => void) {
|
||||
const { addresses } = await getNetworkConfig();
|
||||
emit({ type: "starting" });
|
||||
|
||||
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(
|
||||
emit,
|
||||
addresses.verificationGateway,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
const walletService = new WalletService();
|
||||
const aggregationStrategy = new AggregationStrategy(
|
||||
ethereumService.blsWalletSigner,
|
||||
ethereumService,
|
||||
AggregationStrategy.defaultConfig,
|
||||
emit,
|
||||
);
|
||||
|
||||
const bundleService = new BundleService(
|
||||
emit,
|
||||
clock,
|
||||
queryClient,
|
||||
bundleTableMutex,
|
||||
bundleTable,
|
||||
ethereumService.blsWalletSigner,
|
||||
ethereumService,
|
||||
aggregationStrategy,
|
||||
);
|
||||
|
||||
const adminService = new AdminService(
|
||||
@@ -52,13 +63,17 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
bundleTable,
|
||||
);
|
||||
|
||||
const healthService = new HealthService();
|
||||
|
||||
const routers = [
|
||||
BundleRouter(bundleService),
|
||||
WalletRouter(walletService),
|
||||
AdminRouter(adminService),
|
||||
AggregationStrategyRouter(aggregationStrategy),
|
||||
HealthRouter(healthService),
|
||||
];
|
||||
|
||||
const app = new Application();
|
||||
app.use(oakCors()); // Enables CORS for all routes
|
||||
|
||||
app.use(async (ctx, next) => {
|
||||
const startTime = Date.now();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
10
aggregator/src/app/helpers/ClientReportableError.ts
Normal file
10
aggregator/src/app/helpers/ClientReportableError.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* For errors that are ok to send down to the client.
|
||||
* (Since blindly sending error information down to the client would be a
|
||||
* security issue.)
|
||||
*/
|
||||
export default class ClientReportableError extends Error {
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
5
aggregator/src/app/helpers/bigSum.ts
Normal file
5
aggregator/src/app/helpers/bigSum.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { BigNumber } from "../../../deps.ts";
|
||||
|
||||
export default function bigSum(values: BigNumber[]) {
|
||||
return values.reduce((a, b) => a.add(b), BigNumber.from(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}`);
|
||||
}
|
||||
3
aggregator/src/app/helpers/plus.ts
Normal file
3
aggregator/src/app/helpers/plus.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export default function plus(a: number, b: number) {
|
||||
return a + b;
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
import { BundleDto } from "../../deps.ts";
|
||||
|
||||
type ParseResult<T> = (
|
||||
type ParseResult<T> =
|
||||
| { success: T }
|
||||
| { failures: string[] }
|
||||
);
|
||||
| { failures: string[] };
|
||||
|
||||
type Parser<T> = (value: unknown) => ParseResult<T>;
|
||||
|
||||
@@ -96,14 +95,12 @@ export function parseArray<T>(
|
||||
};
|
||||
}
|
||||
|
||||
type DataTuple<ParserTuple> = (
|
||||
ParserTuple extends Parser<unknown>[] ? (
|
||||
type DataTuple<ParserTuple> = ParserTuple extends Parser<unknown>[] ? (
|
||||
ParserTuple extends [Parser<infer T>, ...infer Tail]
|
||||
? [T, ...DataTuple<Tail>]
|
||||
: []
|
||||
)
|
||||
: never
|
||||
);
|
||||
: never;
|
||||
|
||||
export function parseTuple<ParserTuple extends Parser<unknown>[]>(
|
||||
...parserTuple: ParserTuple
|
||||
@@ -188,6 +185,7 @@ const parseActionDataDto: Parser<ActionDataDto> = parseObject({
|
||||
|
||||
const parseOperationDto: Parser<OperationDto> = parseObject({
|
||||
nonce: parseHex(),
|
||||
gas: parseHex(),
|
||||
actions: parseArray(parseActionDataDto),
|
||||
});
|
||||
|
||||
|
||||
@@ -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,38 +1,51 @@
|
||||
import assert from "./helpers/assert.ts";
|
||||
import {
|
||||
optionalEnv,
|
||||
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");
|
||||
export const MAX_FUTURE_BUNDLES = requireIntEnv("MAX_FUTURE_BUNDLES");
|
||||
|
||||
export const MAX_AGGREGATION_SIZE = requireIntEnv("MAX_AGGREGATION_SIZE");
|
||||
/**
|
||||
* Maximum retry delay in blocks before a failed bundle is discarded.
|
||||
*/
|
||||
export const MAX_ELIGIBILITY_DELAY = requireIntEnv("MAX_ELIGIBILITY_DELAY");
|
||||
|
||||
/**
|
||||
* 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",
|
||||
@@ -43,3 +56,79 @@ 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");
|
||||
|
||||
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",
|
||||
);
|
||||
|
||||
/**
|
||||
* Optimism's strategy for charging for L1 fees requires special logic in the
|
||||
* aggregator. In addition to gasEstimate * gasPrice, we need to replicate
|
||||
* Optimism's calculation and pass it on to the user.
|
||||
*/
|
||||
export const IS_OPTIMISM = requireBoolEnv("IS_OPTIMISM");
|
||||
|
||||
/**
|
||||
* Address for the Optimism gas price oracle contract. Required when
|
||||
* IS_OPTIMISM is true.
|
||||
*/
|
||||
export const OPTIMISM_GAS_PRICE_ORACLE_ADDRESS = optionalEnv(
|
||||
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS",
|
||||
);
|
||||
|
||||
/**
|
||||
* Similar to PREVIOUS_BASE_FEE_PERCENT_INCREASE, but for the L1 basefee for
|
||||
* the optimism-specific calculation. This gets passed on to users.
|
||||
* Required when IS_OPTIMISM is true.
|
||||
*/
|
||||
export const OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE = optionalNumberEnv(
|
||||
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE",
|
||||
);
|
||||
|
||||
if (IS_OPTIMISM) {
|
||||
assert(
|
||||
OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE !== nil,
|
||||
"OPTIMISM_L1_BASE_FEE_PERCENT_INCREASE is required when IS_OPTIMISM is true",
|
||||
);
|
||||
assert(
|
||||
OPTIMISM_GAS_PRICE_ORACLE_ADDRESS !== nil,
|
||||
"OPTIMISM_GAS_PRICE_ORACLE_ADDRESS is required when IS_OPTIMISM is true",
|
||||
);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user