mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-09 15:48:11 -05:00
Compare commits
308 Commits
private-ke
...
main
| 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 | ||
|
|
1468c91dc0 | ||
|
|
5ce0943f00 | ||
|
|
8ccad86fdf | ||
|
|
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 | ||
|
|
c260e60443 | ||
|
|
480ee1d3d0 | ||
|
|
25c73d6697 | ||
|
|
1052cc6c28 | ||
|
|
af420d9a7b | ||
|
|
fde1c61e93 | ||
|
|
8bc62681f6 | ||
|
|
bac557a113 | ||
|
|
1464476cec | ||
|
|
306b90e83d | ||
|
|
d7348e581d | ||
|
|
5b529facd1 | ||
|
|
4d7a83705b | ||
|
|
8e9cd18934 | ||
|
|
87eb27053a | ||
|
|
dd1a19b8d8 | ||
|
|
a9bc94c641 | ||
|
|
e416c13635 | ||
|
|
2b57673de4 | ||
|
|
edae8c17f0 | ||
|
|
e1e654eac5 | ||
|
|
9202cee106 | ||
|
|
1d887e696e | ||
|
|
51e6666776 | ||
|
|
74d31d561f | ||
|
|
a666a2b9c9 | ||
|
|
d2da8d8e7e | ||
|
|
1018131c27 | ||
|
|
e1a306f9f2 | ||
|
|
847bbddd25 | ||
|
|
a4aaa42964 | ||
|
|
07da3694f2 | ||
|
|
a968e86a20 | ||
|
|
c5e3461d60 | ||
|
|
81c3d1c965 | ||
|
|
30ddc6f8ab | ||
|
|
0ca6f4f8fa | ||
|
|
365084ed86 | ||
|
|
2daff8b542 | ||
|
|
027f899fe3 | ||
|
|
9f76ceb9f8 | ||
|
|
a348962e42 | ||
|
|
3894fe68a2 | ||
|
|
8972ccbc5c | ||
|
|
4b42d2d960 | ||
|
|
16a043279b | ||
|
|
59473044a0 | ||
|
|
a800935f51 | ||
|
|
a54b677720 | ||
|
|
2d6ee184a6 | ||
|
|
0b2cca4816 | ||
|
|
e4cde3496d | ||
|
|
29d8884754 | ||
|
|
28ebbe62f4 | ||
|
|
b4b757ce8b | ||
|
|
470ac78044 | ||
|
|
cf16d8f833 | ||
|
|
1e666325ba | ||
|
|
78646e8d11 | ||
|
|
ae7886aba3 | ||
|
|
86fc7c5895 | ||
|
|
0afefe9c1e | ||
|
|
71a3520a0b | ||
|
|
b97c8b5e6d | ||
|
|
73967cef4e | ||
|
|
3506fb6117 | ||
|
|
5918b12a75 | ||
|
|
fef73de225 | ||
|
|
321a2556b1 | ||
|
|
03d61443e8 | ||
|
|
77d9ff8823 | ||
|
|
f0cfe0d941 | ||
|
|
711f898d96 | ||
|
|
7def3c9b2e | ||
|
|
139d8e04e4 | ||
|
|
59388de662 | ||
|
|
1928d59c58 |
@@ -8,10 +8,6 @@ runs:
|
||||
shell: bash
|
||||
run: yarn hardhat node &
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat fundDeployer --network gethDev
|
||||
|
||||
- working-directory: ./contracts
|
||||
shell: bash
|
||||
run: yarn hardhat run scripts/deploy_all.ts --network gethDev
|
||||
|
||||
2
.github/workflows/aggregator.yml
vendored
2
.github/workflows/aggregator.yml
vendored
@@ -73,6 +73,8 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -11,6 +11,7 @@ You can watch a full end-to-end demo of the project [here](https://www.youtube.c
|
||||
- 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
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"bls-wallet-clients": "0.8.2-1452ef5",
|
||||
"bls-wallet-clients": "0.9.0-405e23a",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -9,6 +9,7 @@ const BundleDto = io.type({
|
||||
),
|
||||
operations: io.array(io.type({
|
||||
nonce: io.string,
|
||||
gas: io.string,
|
||||
actions: io.array(io.type({
|
||||
ethValue: io.string,
|
||||
contractAddress: io.string,
|
||||
|
||||
@@ -569,16 +569,6 @@
|
||||
version "5.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
|
||||
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
|
||||
dependencies:
|
||||
"@ethersproject/address" "^5.7.0"
|
||||
"@ethersproject/bignumber" "^5.7.0"
|
||||
"@ethersproject/bytes" "^5.7.0"
|
||||
"@ethersproject/constants" "^5.7.0"
|
||||
"@ethersproject/keccak256" "^5.7.0"
|
||||
"@ethersproject/logger" "^5.7.0"
|
||||
"@ethersproject/properties" "^5.7.0"
|
||||
"@ethersproject/rlp" "^5.7.0"
|
||||
"@ethersproject/signing-key" "^5.7.0"
|
||||
|
||||
"@ethersproject/units@5.6.0":
|
||||
version "5.6.0"
|
||||
@@ -887,10 +877,10 @@ bech32@1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bls-wallet-clients@0.8.2-1452ef5:
|
||||
version "0.8.2-1452ef5"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1452ef5.tgz#d76e938ca45ec5da44c8e59699d1bd5f6c69dcd2"
|
||||
integrity sha512-bg7WLr9NRbvDzj+zgkLNfaPzr1m0m13Cc8RJoZ2s6s+ic7WxSiwxTkZGc2SChFgmG8ZGi1O9DnR6//lrTsMVUA==
|
||||
bls-wallet-clients@0.9.0-405e23a:
|
||||
version "0.9.0-405e23a"
|
||||
resolved "https://registry.npmjs.org/bls-wallet-clients/-/bls-wallet-clients-0.9.0-405e23a.tgz#b66121f9ec0cb4e821965606ada203e6601b773d"
|
||||
integrity sha512-cMm6pq35VU30veCAHt6ArSavlqzXu+olQg+dzUH28fvqSeQsfWz2qiuBekGxSWOCfn8gX1j/8jHEhrGxXS509Q==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "^5.7.2"
|
||||
|
||||
@@ -40,3 +40,7 @@ PRIORITY_FEE_PER_GAS=0
|
||||
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
|
||||
|
||||
@@ -38,3 +38,5 @@ PRIORITY_FEE_PER_GAS=500000000
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
|
||||
IS_OPTIMISM=false
|
||||
|
||||
@@ -35,3 +35,5 @@ PRIORITY_FEE_PER_GAS=500000000
|
||||
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
|
||||
|
||||
BUNDLE_CHECKING_CONCURRENCY=8
|
||||
|
||||
IS_OPTIMISM=false
|
||||
|
||||
@@ -89,32 +89,35 @@ commands.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
| 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) |
|
||||
| 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
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export const keccak256 = ethers.utils.keccak256;
|
||||
export const shuffled: <T>(array: T[]) => T[] = ethers.utils.shuffled;
|
||||
|
||||
export type {
|
||||
ActionData,
|
||||
AggregatorUtilities,
|
||||
BlsWalletSigner,
|
||||
Bundle,
|
||||
@@ -53,23 +54,30 @@ export type {
|
||||
PublicKey,
|
||||
Signature,
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletWrapper,
|
||||
BundleCompressor,
|
||||
ContractsConnector,
|
||||
decodeError,
|
||||
ERC20__factory,
|
||||
Erc20Compressor,
|
||||
ERC20Factory,
|
||||
FallbackCompressor,
|
||||
getConfig,
|
||||
MockERC20__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.2-1452ef5";
|
||||
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.8.2-1452ef5";
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.9.0-405e23a";
|
||||
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
|
||||
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
|
||||
|
||||
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 { pick } from "npm:@s-libs/micro-dash@15.2.0";
|
||||
|
||||
@@ -9,7 +9,7 @@ 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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
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()),
|
||||
});
|
||||
@@ -19,7 +19,7 @@ await (await adminWallet.sendTransaction({
|
||||
value: 1,
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 1,
|
||||
|
||||
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));
|
||||
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,13 +1,13 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
@@ -22,7 +22,7 @@ const walletIndex = Number(walletIndexStr);
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const wallet = await TestBlsWallet(provider, walletIndex);
|
||||
@@ -48,21 +48,23 @@ const mintAction: ActionData = {
|
||||
),
|
||||
};
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
const feeEstimation = await client.estimateFee(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
console.log({ feeEstimation });
|
||||
|
||||
@@ -91,7 +93,7 @@ const feeAction: ActionData = {
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [mintAction, feeAction],
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { delay, ethers, MockERC20__factory } from "../deps.ts";
|
||||
import { delay, ethers, MockERC20Factory } from "../deps.ts";
|
||||
|
||||
import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -14,16 +14,14 @@ const ethereumService = await EthereumService.create(
|
||||
(evt) => {
|
||||
console.log(evt);
|
||||
},
|
||||
addresses.verificationGateway,
|
||||
addresses.utilities,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const 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,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
|
||||
import {
|
||||
ActionData,
|
||||
AggregatorClient,
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilitiesFactory,
|
||||
BigNumber,
|
||||
Bundle,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
import AdminWallet from "../src/chain/AdminWallet.ts";
|
||||
import assert from "../src/helpers/assert.ts";
|
||||
@@ -29,10 +29,10 @@ if (!Number.isFinite(walletN)) {
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
const sendEthToTxOrigin = AggregatorUtilities__factory
|
||||
const sendEthToTxOrigin = AggregatorUtilitiesFactory
|
||||
.createInterface()
|
||||
.encodeFunctionData("sendEthToTxOrigin");
|
||||
|
||||
@@ -60,24 +60,26 @@ const bundles: Bundle[] = [];
|
||||
for (const [i, wallet] of wallets.entries()) {
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
console.log("Funding wallet", i);
|
||||
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(wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}));
|
||||
const feeEstimation = await client.estimateFee(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
mintAction,
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: addresses.utilities,
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
assert(feeEstimation.feeType === "ether");
|
||||
|
||||
@@ -90,7 +92,7 @@ for (const [i, wallet] of wallets.entries()) {
|
||||
|
||||
// Ensure wallet can pay the fee
|
||||
if (balance.lt(fee)) {
|
||||
console.log("Funding wallet");
|
||||
console.log("Funding wallet", i, "(based on estimateFee)");
|
||||
|
||||
await (await adminWallet.sendTransaction({
|
||||
to: wallet.address,
|
||||
@@ -104,10 +106,12 @@ for (const [i, wallet] of wallets.entries()) {
|
||||
encodedFunction: sendEthToTxOrigin,
|
||||
};
|
||||
|
||||
bundles.push(wallet.sign({
|
||||
nonce,
|
||||
actions: [mintAction, feeAction],
|
||||
}));
|
||||
bundles.push(
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [mintAction, feeAction],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Sending mint bundles to aggregator");
|
||||
|
||||
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",
|
||||
],
|
||||
),
|
||||
);
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
BlsWalletWrapper,
|
||||
delay,
|
||||
ethers,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
} from "../deps.ts";
|
||||
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -43,7 +43,7 @@ const { addresses } = await getNetworkConfig();
|
||||
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
const adminWallet = AdminWallet(provider);
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
|
||||
const client = new AggregatorClient(env.ORIGIN);
|
||||
|
||||
@@ -56,7 +56,7 @@ const [recvWallet, ...sendWallets] = await Promise.all(
|
||||
log("Checking/minting test tokens...");
|
||||
|
||||
for (const wallet of sendWallets) {
|
||||
const testErc20 = MockERC20__factory.connect(
|
||||
const testErc20 = MockERC20Factory.connect(
|
||||
addresses.testToken,
|
||||
adminWallet,
|
||||
);
|
||||
@@ -90,7 +90,7 @@ let txsAdded = 0;
|
||||
let txsCompleted = 0;
|
||||
let sendWalletIndex = 0;
|
||||
|
||||
pollingLoop(() => {
|
||||
pollingLoop(async () => {
|
||||
// Send transactions
|
||||
|
||||
const lead = txsSent - txsCompleted;
|
||||
@@ -102,7 +102,7 @@ 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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
|
||||
|
||||
import { AggregatorClient, ethers, MockERC20__factory } from "../deps.ts";
|
||||
import { AggregatorClient, ethers, MockERC20Factory } from "../deps.ts";
|
||||
|
||||
// import EthereumService from "../src/app/EthereumService.ts";
|
||||
import * as env from "../test/env.ts";
|
||||
@@ -15,15 +15,13 @@ const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
|
||||
// (evt) => {
|
||||
// console.log(evt);
|
||||
// },
|
||||
// addresses.verificationGateway,
|
||||
// addresses.utilities,
|
||||
// env.PRIVATE_KEY_AGG,
|
||||
// );
|
||||
|
||||
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
|
||||
const testErc20 = MockERC20Factory.connect(addresses.testToken, provider);
|
||||
const wallet = await TestBlsWallet(provider);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [{
|
||||
ethValue: 0,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
VerificationGateway__factory,
|
||||
VerificationGatewayFactory,
|
||||
Wallet,
|
||||
} from "../deps.ts";
|
||||
import * as env from "../src/env.ts";
|
||||
@@ -14,7 +14,7 @@ const wallet = new Wallet(env.PRIVATE_KEY_AGG, provider);
|
||||
|
||||
const { addresses } = await getNetworkConfig();
|
||||
|
||||
const vg = VerificationGateway__factory.connect(
|
||||
const vg = VerificationGatewayFactory.connect(
|
||||
addresses.verificationGateway,
|
||||
wallet,
|
||||
);
|
||||
@@ -32,10 +32,12 @@ const nonce = await internalBlsWallet.Nonce();
|
||||
if (!nonce.eq(0)) {
|
||||
console.log("Already exists with nonce", nonce.toNumber());
|
||||
} else {
|
||||
await (await vg.processBundle(internalBlsWallet.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
await (await vg.processBundle(
|
||||
await internalBlsWallet.signWithGasEstimate({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}),
|
||||
)).wait();
|
||||
|
||||
console.log("Created successfully");
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Bundle,
|
||||
decodeError,
|
||||
ERC20,
|
||||
ERC20__factory,
|
||||
ERC20Factory,
|
||||
ethers,
|
||||
OperationResultError,
|
||||
Semaphore,
|
||||
@@ -70,6 +70,7 @@ export type AggregationStrategyResult = {
|
||||
aggregateBundle: Bundle | nil;
|
||||
includedRows: BundleRow[];
|
||||
bundleOverheadCost: BigNumber;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: BigNumber;
|
||||
expectedMaxCost: BigNumber;
|
||||
failedRows: BundleRow[];
|
||||
@@ -105,7 +106,8 @@ export default class AggregationStrategy {
|
||||
async run(eligibleRows: BundleRow[]): Promise<AggregationStrategyResult> {
|
||||
eligibleRows = await this.#filterRows(eligibleRows);
|
||||
|
||||
const bundleOverheadGas = await this.measureBundleOverheadGas();
|
||||
const { bundleOverheadGas, bundleOverheadLen } = await this
|
||||
.measureBundleOverhead();
|
||||
|
||||
let aggregateBundle = this.blsWalletSigner.aggregate([]);
|
||||
let aggregateGas = bundleOverheadGas;
|
||||
@@ -147,6 +149,7 @@ export default class AggregationStrategy {
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -164,6 +167,7 @@ export default class AggregationStrategy {
|
||||
bundleOverheadCost: bundleOverheadGas.mul(
|
||||
(await this.ethereumService.GasConfig()).maxFeePerGas,
|
||||
),
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost: aggregateBundleCheck.expectedMaxCost,
|
||||
failedRows,
|
||||
@@ -217,6 +221,7 @@ export default class AggregationStrategy {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -256,6 +261,7 @@ export default class AggregationStrategy {
|
||||
aggregateBundle: nil,
|
||||
includedRows: [],
|
||||
bundleOverheadCost: result.bundleOverheadCost,
|
||||
bundleOverheadLen: result.bundleOverheadLen,
|
||||
expectedFee: BigNumber.from(0),
|
||||
expectedMaxCost: BigNumber.from(0),
|
||||
failedRows,
|
||||
@@ -410,13 +416,21 @@ export default class AggregationStrategy {
|
||||
continue;
|
||||
}
|
||||
|
||||
const newAggregateGas = (aggregateGas
|
||||
.add(gasEstimate)
|
||||
.sub(bundleOverheadGas));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -451,14 +465,16 @@ export default class AggregationStrategy {
|
||||
.callStaticSequenceWithMeasure(
|
||||
feeToken
|
||||
? es.Call(feeToken, "balanceOf", [es.wallet.address])
|
||||
: es.Call(es.utilities, "ethBalanceOf", [es.wallet.address]),
|
||||
bundles.map((bundle) =>
|
||||
: es.Call(es.aggregatorUtilities, "ethBalanceOf", [
|
||||
es.wallet.address,
|
||||
]),
|
||||
await Promise.all(bundles.map(async (bundle) =>
|
||||
es.Call(
|
||||
es.verificationGateway,
|
||||
"processBundle",
|
||||
[bundle],
|
||||
es.bundleCompressor.blsExpanderDelegator,
|
||||
"run",
|
||||
[await es.bundleCompressor.compress(bundle)],
|
||||
)
|
||||
),
|
||||
)),
|
||||
);
|
||||
|
||||
return Range(bundles.length).map((i) => {
|
||||
@@ -514,7 +530,7 @@ export default class AggregationStrategy {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return ERC20__factory.connect(
|
||||
return ERC20Factory.connect(
|
||||
this.config.fees.address,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
@@ -525,10 +541,13 @@ export default class AggregationStrategy {
|
||||
return nil;
|
||||
}
|
||||
|
||||
bundleOverheadGas ??= await this.measureBundleOverheadGas();
|
||||
bundleOverheadGas ??=
|
||||
(await this.measureBundleOverhead()).bundleOverheadGas;
|
||||
|
||||
const gasEstimate = await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
const gasEstimate = await this.ethereumService
|
||||
.estimateEffectiveCompressedGas(
|
||||
bundle,
|
||||
);
|
||||
|
||||
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
|
||||
|
||||
@@ -612,13 +631,18 @@ export default class AggregationStrategy {
|
||||
expectedFee: fee,
|
||||
requiredFee: feeInfo.requiredFee,
|
||||
expectedMaxCost: feeInfo.expectedMaxCost,
|
||||
errorReason: { message: "Insufficient fee" },
|
||||
errorReason: {
|
||||
message: [
|
||||
"Insufficient fee",
|
||||
`(provided: ${ethers.utils.formatEther(fee)},`,
|
||||
`required: ${ethers.utils.formatEther(feeInfo.requiredFee)})`,
|
||||
].join(" "),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const gasEstimate = feeInfo?.gasEstimate ??
|
||||
await this.ethereumService.verificationGateway
|
||||
.estimateGas.processBundle(bundle);
|
||||
await this.ethereumService.estimateEffectiveCompressedGas(bundle);
|
||||
|
||||
return {
|
||||
success,
|
||||
@@ -631,7 +655,7 @@ export default class AggregationStrategy {
|
||||
});
|
||||
}
|
||||
|
||||
async measureBundleOverheadGas() {
|
||||
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
|
||||
@@ -647,19 +671,50 @@ export default class AggregationStrategy {
|
||||
// wallet creation would be included in the bundle overhead.
|
||||
assert(nonce.gt(0));
|
||||
|
||||
const bundle1 = wallet.sign({ nonce, actions: [] });
|
||||
const bundle2 = wallet.sign({ nonce: nonce.add(1), actions: [] });
|
||||
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.verificationGateway.estimateGas.processBundle(bundle1),
|
||||
es.verificationGateway.estimateGas.processBundle(
|
||||
es.estimateEffectiveCompressedGas(bundle1),
|
||||
es.estimateEffectiveCompressedGas(
|
||||
this.blsWalletSigner.aggregate([bundle1, bundle2]),
|
||||
),
|
||||
]);
|
||||
|
||||
const opMarginalGasEstimate = twoOpGasEstimate.sub(oneOpGasEstimate);
|
||||
|
||||
return oneOpGasEstimate.sub(opMarginalGasEstimate);
|
||||
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> {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HTTPMethods } from "../../deps.ts";
|
||||
|
||||
type AppEvent =
|
||||
| { type: "starting" }
|
||||
| { type: "listening"; data: { port: number } }
|
||||
| { type: "db-query"; data: { sql: string; params: unknown } }
|
||||
| { type: "waiting-unconfirmed-space" }
|
||||
@@ -15,6 +16,7 @@ type AppEvent =
|
||||
data: {
|
||||
includedRows: number;
|
||||
bundleOverheadCost: string;
|
||||
bundleOverheadLen: number;
|
||||
expectedFee: string;
|
||||
expectedMaxCost: string;
|
||||
};
|
||||
@@ -32,10 +34,24 @@ type AppEvent =
|
||||
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";
|
||||
|
||||
@@ -2,7 +2,6 @@ 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) {
|
||||
@@ -40,5 +39,19 @@ export default function BundleRouter(bundleService: BundleService) {
|
||||
},
|
||||
);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
delay,
|
||||
ethers,
|
||||
Semaphore,
|
||||
VerificationGatewayFactory,
|
||||
} from "../../deps.ts";
|
||||
|
||||
import { IClock } from "../helpers/Clock.ts";
|
||||
@@ -18,10 +19,12 @@ import * as env from "../env.ts";
|
||||
import runQueryGroup from "./runQueryGroup.ts";
|
||||
import EthereumService from "./EthereumService.ts";
|
||||
import AppEvent from "./AppEvent.ts";
|
||||
import BundleTable, { BundleRow, makeHash } from "./BundleTable.ts";
|
||||
import BundleTable, { BundleRow } from "./BundleTable.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[];
|
||||
@@ -34,6 +37,7 @@ export default class BundleService {
|
||||
maxAggregationDelayMillis: env.MAX_AGGREGATION_DELAY_MILLIS,
|
||||
maxUnconfirmedAggregations: env.MAX_UNCONFIRMED_AGGREGATIONS,
|
||||
maxEligibilityDelay: env.MAX_ELIGIBILITY_DELAY,
|
||||
isOptimism: env.IS_OPTIMISM,
|
||||
};
|
||||
|
||||
unconfirmedBundles = new Set<Bundle>();
|
||||
@@ -156,14 +160,16 @@ export default class BundleService {
|
||||
|
||||
const failures: TransactionFailure[] = [];
|
||||
|
||||
for (const walletAddr of walletAddresses) {
|
||||
const signedCorrectly = this.blsWalletSigner.verify(bundle, walletAddr);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: `invalid signature for wallet address ${walletAddr}`,
|
||||
});
|
||||
}
|
||||
const signedCorrectly = this.blsWalletSigner.verify(
|
||||
bundle,
|
||||
walletAddresses,
|
||||
);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description:
|
||||
`invalid bundle signature for signature ${bundle.signature}`,
|
||||
});
|
||||
}
|
||||
|
||||
failures.push(...await this.ethereumService.checkNonces(bundle));
|
||||
@@ -173,7 +179,7 @@ export default class BundleService {
|
||||
}
|
||||
|
||||
return await this.runQueryGroup(async () => {
|
||||
const hash = makeHash();
|
||||
const hash = await this.hashBundle(bundle);
|
||||
|
||||
this.bundleTable.add({
|
||||
status: "pending",
|
||||
@@ -201,15 +207,21 @@ export default class BundleService {
|
||||
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 } = bundle;
|
||||
const { receipt, hash, aggregateHash } = bundle;
|
||||
|
||||
return {
|
||||
bundleHash: hash,
|
||||
aggregateBundleHash: aggregateHash,
|
||||
to: receipt.to,
|
||||
from: receipt.from,
|
||||
contractAddress: receipt.contractAddress,
|
||||
@@ -230,6 +242,44 @@ export default class BundleService {
|
||||
};
|
||||
}
|
||||
|
||||
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++;
|
||||
|
||||
@@ -257,6 +307,7 @@ export default class BundleService {
|
||||
aggregateBundle,
|
||||
includedRows,
|
||||
bundleOverheadCost,
|
||||
bundleOverheadLen,
|
||||
expectedFee,
|
||||
expectedMaxCost,
|
||||
failedRows,
|
||||
@@ -268,11 +319,19 @@ export default class BundleService {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
for (const failedRow of failedRows) {
|
||||
this.emit({
|
||||
type: "failed-row",
|
||||
@@ -361,7 +420,16 @@ export default class BundleService {
|
||||
const profit = balanceAfter.sub(balanceBefore);
|
||||
|
||||
/** What we paid to process the bundle */
|
||||
const cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
|
||||
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);
|
||||
|
||||
@@ -30,6 +30,7 @@ type RawRow = {
|
||||
nextEligibilityDelay: string;
|
||||
submitError: string | null;
|
||||
receipt: string | null;
|
||||
aggregateHash: string | null;
|
||||
};
|
||||
|
||||
const BundleStatuses = ["pending", "confirmed", "failed"] as const;
|
||||
@@ -44,30 +45,26 @@ type Row = {
|
||||
nextEligibilityDelay: BigNumber;
|
||||
submitError?: string;
|
||||
receipt?: ethers.ContractReceipt;
|
||||
aggregateHash?: string;
|
||||
};
|
||||
|
||||
type InsertRow = Omit<Row, "id">;
|
||||
type InsertRawRow = Omit<RawRow, "id">;
|
||||
|
||||
export function makeHash() {
|
||||
const buf = new Uint8Array(32);
|
||||
crypto.getRandomValues(buf);
|
||||
return ethers.utils.hexlify(buf);
|
||||
}
|
||||
|
||||
export type BundleRow = Row;
|
||||
|
||||
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
if (Array.isArray(rawRow)) {
|
||||
rawRow = {
|
||||
id: rawRow[0],
|
||||
status: rawRow[1],
|
||||
hash: rawRow[2],
|
||||
bundle: rawRow[3],
|
||||
eligibleAfter: rawRow[4],
|
||||
nextEligibilityDelay: rawRow[5],
|
||||
submitError: rawRow[6],
|
||||
receipt: rawRow[7],
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -99,6 +96,7 @@ function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
|
||||
nextEligibilityDelay: BigNumber.from(rawRow.nextEligibilityDelay),
|
||||
submitError: rawRow.submitError ?? nil,
|
||||
receipt,
|
||||
aggregateHash: rawRow.aggregateHash ?? nil,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -109,6 +107,7 @@ function toInsertRawRow(row: InsertRow): InsertRawRow {
|
||||
bundle: JSON.stringify(bundleToDto(row.bundle)),
|
||||
eligibleAfter: toUint256Hex(row.eligibleAfter),
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
};
|
||||
}
|
||||
@@ -123,6 +122,7 @@ function toRawRow(row: Row): RawRow {
|
||||
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
|
||||
submitError: row.submitError ?? null,
|
||||
receipt: JSON.stringify(row.receipt),
|
||||
aggregateHash: row.aggregateHash ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -140,10 +140,11 @@ export default class BundleTable {
|
||||
eligibleAfter TEXT NOT NULL,
|
||||
nextEligibilityDelay TEXT NOT NULL,
|
||||
submitError TEXT,
|
||||
receipt TEXT
|
||||
receipt TEXT,
|
||||
aggregateHash TEXT
|
||||
)
|
||||
`);
|
||||
}
|
||||
}
|
||||
|
||||
dbQuery(sql: string, params?: sqlite.QueryParameterSet) {
|
||||
this.onQuery(sql, params);
|
||||
@@ -164,7 +165,8 @@ export default class BundleTable {
|
||||
eligibleAfter,
|
||||
nextEligibilityDelay,
|
||||
submitError,
|
||||
receipt
|
||||
receipt,
|
||||
aggregateHash
|
||||
) VALUES (
|
||||
:id,
|
||||
:status,
|
||||
@@ -173,7 +175,8 @@ export default class BundleTable {
|
||||
:eligibleAfter,
|
||||
:nextEligibilityDelay,
|
||||
:submitError,
|
||||
:receipt
|
||||
:receipt,
|
||||
:aggregateHash
|
||||
)
|
||||
`,
|
||||
{
|
||||
@@ -184,6 +187,7 @@ export default class BundleTable {
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -202,7 +206,8 @@ export default class BundleTable {
|
||||
eligibleAfter = :eligibleAfter,
|
||||
nextEligibilityDelay = :nextEligibilityDelay,
|
||||
submitError = :submitError,
|
||||
receipt = :receipt
|
||||
receipt = :receipt,
|
||||
aggregateHash = :aggregateHash
|
||||
WHERE
|
||||
id = :id
|
||||
`,
|
||||
@@ -215,6 +220,7 @@ export default class BundleTable {
|
||||
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
|
||||
":submitError": rawRow.submitError,
|
||||
":receipt": rawRow.receipt,
|
||||
":aggregateHash": rawRow.aggregateHash,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -255,6 +261,21 @@ export default class BundleTable {
|
||||
return rows.map(fromRawRow)[0];
|
||||
}
|
||||
|
||||
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");
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import {
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory,
|
||||
BaseContract,
|
||||
BigNumber,
|
||||
BlsRegistrationCompressor,
|
||||
BlsWalletSigner,
|
||||
BlsWalletWrapper,
|
||||
Bundle,
|
||||
BundleCompressor,
|
||||
BytesLike,
|
||||
ContractsConnector,
|
||||
delay,
|
||||
Erc20Compressor,
|
||||
ethers,
|
||||
FallbackCompressor,
|
||||
initBlsWalletSigner,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
Wallet,
|
||||
} from "../../deps.ts";
|
||||
|
||||
@@ -23,6 +26,8 @@ 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[];
|
||||
@@ -64,30 +69,24 @@ type DecodeReturnType<
|
||||
Method extends keyof Contract["callStatic"],
|
||||
> = EnforceArray<AsyncReturnType<Contract["callStatic"][Method]>>;
|
||||
|
||||
export default class EthereumService {
|
||||
verificationGateway: VerificationGateway;
|
||||
utilities: AggregatorUtilities;
|
||||
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,
|
||||
utilitiesAddress: string,
|
||||
public verificationGateway: VerificationGateway,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
public expanderEntryPoint: ExpanderEntryPoint,
|
||||
public bundleCompressor: BundleCompressor,
|
||||
public nextNonce: BigNumber,
|
||||
) {
|
||||
this.verificationGateway = VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
this.wallet,
|
||||
);
|
||||
|
||||
this.utilities = AggregatorUtilities__factory.connect(
|
||||
utilitiesAddress,
|
||||
this.wallet,
|
||||
);
|
||||
}
|
||||
) {}
|
||||
|
||||
NextNonce() {
|
||||
const result = this.nextNonce;
|
||||
@@ -97,17 +96,35 @@ export default class EthereumService {
|
||||
|
||||
static async create(
|
||||
emit: (evt: AppEvent) => void,
|
||||
verificationGatewayAddress: string,
|
||||
utilitiesAddress: string,
|
||||
aggPrivateKey: string,
|
||||
): Promise<EthereumService> {
|
||||
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,
|
||||
verificationGatewayAddress,
|
||||
verificationGateway.address,
|
||||
provider,
|
||||
);
|
||||
|
||||
@@ -122,13 +139,12 @@ export default class EthereumService {
|
||||
].join(" "));
|
||||
}
|
||||
|
||||
await (await VerificationGateway__factory.connect(
|
||||
verificationGatewayAddress,
|
||||
wallet,
|
||||
).processBundle(blsWalletWrapper.sign({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}))).wait();
|
||||
await (await verificationGateway.processBundle(
|
||||
await blsWalletWrapper.signWithGasEstimate({
|
||||
nonce: 0,
|
||||
actions: [],
|
||||
}),
|
||||
)).wait();
|
||||
}
|
||||
|
||||
const nextNonce = BigNumber.from(await wallet.getTransactionCount());
|
||||
@@ -136,16 +152,33 @@ export default class EthereumService {
|
||||
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,
|
||||
utilitiesAddress,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
expanderEntryPoint,
|
||||
bundleCompressor,
|
||||
nextNonce,
|
||||
);
|
||||
}
|
||||
@@ -218,9 +251,10 @@ export default class EthereumService {
|
||||
async callStaticSequence<Calls extends CallHelper<unknown>[]>(
|
||||
...calls: Calls
|
||||
): Promise<MapCallHelperReturns<Calls>> {
|
||||
const rawResults = await this.utilities.callStatic.performSequence(
|
||||
calls.map((c) => c.value),
|
||||
);
|
||||
const rawResults = await this.aggregatorUtilities.callStatic
|
||||
.performSequence(
|
||||
calls.map((c) => c.value),
|
||||
);
|
||||
|
||||
const results: CallResult<unknown>[] = rawResults.map(
|
||||
([success, result], i) => {
|
||||
@@ -288,22 +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(),
|
||||
...await this.GasConfig(),
|
||||
},
|
||||
];
|
||||
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
|
||||
@@ -319,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 };
|
||||
}
|
||||
@@ -330,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;
|
||||
}
|
||||
|
||||
@@ -364,8 +412,44 @@ export default class EthereumService {
|
||||
throw new Error("Expected return or throw from attempt loop");
|
||||
}
|
||||
|
||||
async GasConfig() {
|
||||
const block = await this.provider.getBlock("latest");
|
||||
/**
|
||||
* 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);
|
||||
|
||||
@@ -392,6 +476,66 @@ export default class EthereumService {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,15 @@ import errorHandler from "./errorHandler.ts";
|
||||
import notFoundHandler from "./notFoundHandler.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 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();
|
||||
|
||||
@@ -37,8 +38,6 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
|
||||
const ethereumService = await EthereumService.create(
|
||||
emit,
|
||||
addresses.verificationGateway,
|
||||
addresses.utilities,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
@@ -64,10 +63,13 @@ export default async function app(emit: (evt: AppEvent) => void) {
|
||||
bundleTable,
|
||||
);
|
||||
|
||||
const healthService = new HealthService();
|
||||
|
||||
const routers = [
|
||||
BundleRouter(bundleService),
|
||||
AdminRouter(adminService),
|
||||
AggregationStrategyRouter(aggregationStrategy),
|
||||
HealthRouter(healthService),
|
||||
];
|
||||
|
||||
const app = new Application();
|
||||
|
||||
@@ -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,4 +1,6 @@
|
||||
import assert from "./helpers/assert.ts";
|
||||
import {
|
||||
optionalEnv,
|
||||
optionalNumberEnv,
|
||||
requireBigNumberEnv,
|
||||
requireBoolEnv,
|
||||
@@ -95,3 +97,38 @@ export const PREVIOUS_BASE_FEE_PERCENT_INCREASE = requireNumberEnv(
|
||||
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",
|
||||
);
|
||||
}
|
||||
|
||||
50
aggregator/src/helpers/getOptimismL1Fee.ts
Normal file
50
aggregator/src/helpers/getOptimismL1Fee.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { BigNumber, ethers } from "../../deps.ts";
|
||||
import OptimismGasPriceOracle from "../app/OptimismGasPriceOracle.ts";
|
||||
import assert from "./assert.ts";
|
||||
import getRawTransaction from "./getRawTransaction.ts";
|
||||
import hexToUint8Array from "./hexToUint8Array.ts";
|
||||
import nil from "./nil.ts";
|
||||
|
||||
export default async function getOptimismL1Fee(
|
||||
provider: ethers.providers.Provider,
|
||||
txResponseOrHash: string | ethers.providers.TransactionResponse,
|
||||
) {
|
||||
const tx = typeof txResponseOrHash === "string"
|
||||
? await provider.getTransaction(txResponseOrHash)
|
||||
: txResponseOrHash;
|
||||
|
||||
const rawTx = await getRawTransaction(provider, tx);
|
||||
|
||||
let l1Gas = 0;
|
||||
|
||||
for (const byte of hexToUint8Array(rawTx)) {
|
||||
if (byte === 0) {
|
||||
l1Gas += 4;
|
||||
} else {
|
||||
l1Gas += 16;
|
||||
}
|
||||
}
|
||||
|
||||
const gasOracle = new OptimismGasPriceOracle(provider);
|
||||
|
||||
assert(tx.blockNumber !== nil);
|
||||
|
||||
const {
|
||||
l1BaseFee,
|
||||
overhead,
|
||||
scalar,
|
||||
decimals,
|
||||
} = await gasOracle.getAllParams(tx.blockNumber);
|
||||
|
||||
l1Gas = l1Gas += overhead.toNumber();
|
||||
|
||||
const l1Fee = BigNumber
|
||||
.from(l1Gas)
|
||||
.mul(l1BaseFee)
|
||||
.mul(scalar)
|
||||
.div(
|
||||
BigNumber.from(10).pow(decimals),
|
||||
);
|
||||
|
||||
return l1Fee;
|
||||
}
|
||||
49
aggregator/src/helpers/getRawTransaction.ts
Normal file
49
aggregator/src/helpers/getRawTransaction.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { ethers, pick } from "../../deps.ts";
|
||||
import assert from "./assert.ts";
|
||||
import nil from "./nil.ts";
|
||||
|
||||
export default async function getRawTransaction(
|
||||
provider: ethers.providers.Provider,
|
||||
txResponseOrHash: string | ethers.providers.TransactionResponse,
|
||||
) {
|
||||
const tx = typeof txResponseOrHash === "string"
|
||||
? await provider.getTransaction(txResponseOrHash)
|
||||
: txResponseOrHash;
|
||||
|
||||
const txHash = typeof txResponseOrHash === "string"
|
||||
? txResponseOrHash
|
||||
: tx.hash;
|
||||
|
||||
assert(typeof txHash === "string");
|
||||
|
||||
const { v, r, s } = tx;
|
||||
assert(r !== nil);
|
||||
|
||||
const txBytes = ethers.utils.serializeTransaction(
|
||||
pick(
|
||||
tx,
|
||||
"to",
|
||||
"nonce",
|
||||
"gasLimit",
|
||||
...(tx.type === 2 ? [] : ["gasPrice"] as const),
|
||||
"data",
|
||||
"value",
|
||||
"chainId",
|
||||
"type",
|
||||
...(tx.type !== 2 ? [] : [
|
||||
"accessList",
|
||||
"maxPriorityFeePerGas",
|
||||
"maxFeePerGas",
|
||||
] as const),
|
||||
),
|
||||
{ v, r, s },
|
||||
);
|
||||
|
||||
const reconstructedHash = ethers.utils.keccak256(txBytes);
|
||||
|
||||
if (reconstructedHash !== txHash) {
|
||||
throw new Error("Reconstructed hash did not match original hash");
|
||||
}
|
||||
|
||||
return txBytes;
|
||||
}
|
||||
16
aggregator/src/helpers/hexToUint8Array.ts
Normal file
16
aggregator/src/helpers/hexToUint8Array.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import assert from "./assert.ts";
|
||||
|
||||
export default function hexToUint8Array(hex: string) {
|
||||
assert(hex.startsWith("0x"));
|
||||
assert(hex.length % 2 === 0);
|
||||
|
||||
const len = (hex.length - 2) / 2;
|
||||
const result = new Uint8Array(len);
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const hexPos = 2 * i + 2;
|
||||
result[i] = parseInt(hex.slice(hexPos, hexPos + 2), 16);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -9,7 +9,7 @@ import Fixture from "./helpers/Fixture.ts";
|
||||
Fixture.test("nonzero fee estimate from default test config", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -51,7 +51,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
let bundle = wallet.sign({
|
||||
let bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
@@ -84,7 +84,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
|
||||
assertEquals(feeEstimation.feeDetected, BigNumber.from(1));
|
||||
|
||||
// Redefine bundle using the estimated fee
|
||||
bundle = wallet.sign({
|
||||
bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
@@ -135,7 +135,7 @@ Fixture.test("includes submitError on failed row when bundle callStaticSequence
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { assertBundleSucceeds, assertEquals, Operation } from "./deps.ts";
|
||||
|
||||
import { BigNumber, Operation, VerificationGatewayFactory, assertBundleSucceeds, assertEquals, ethers } from "./deps.ts";
|
||||
import ExplicitAny from "../src/helpers/ExplicitAny.ts";
|
||||
import Fixture from "./helpers/Fixture.ts";
|
||||
|
||||
Fixture.test("adds valid bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const tx = wallet.sign({
|
||||
const tx = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -20,7 +20,7 @@ Fixture.test("adds valid bundle", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
@@ -33,6 +33,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: await wallet.Nonce(),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -53,7 +54,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
// sig test)
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -62,7 +63,47 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with valid signature but invalid public key", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet, otherWallet] = await fx.setupWallets(2);
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: await wallet.Nonce(),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const tx = wallet.sign(operation);
|
||||
const otherTx = otherWallet.sign(operation);
|
||||
|
||||
// Make the signature invalid
|
||||
// Note: Bug in bls prevents just corrupting the signature (see other invalid
|
||||
// sig test)
|
||||
tx.senderPublicKeys[0] = otherTx.senderPublicKeys[0];
|
||||
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
throw new Error("expected bundle to fail");
|
||||
}
|
||||
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
|
||||
assertEquals(res.failures.map((f) => f.description), [`invalid bundle signature for signature ${tx.signature}`]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
@@ -71,6 +112,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
|
||||
const tx = wallet.sign({
|
||||
nonce: (await wallet.Nonce()).sub(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -83,7 +125,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -92,7 +134,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
|
||||
assertEquals(res.failures.map((f) => f.type), ["duplicate-nonce"]);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
});
|
||||
|
||||
Fixture.test(
|
||||
@@ -103,6 +145,7 @@ Fixture.test(
|
||||
|
||||
const operation: Operation = {
|
||||
nonce: (await wallet.Nonce()).sub(1),
|
||||
gas: 0,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -125,7 +168,7 @@ Fixture.test(
|
||||
// https://github.com/thehubbleproject/hubble-bls/pull/20
|
||||
tx.signature = otherTx.signature;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
const res = await bundleService.add(tx);
|
||||
if ("hash" in res) {
|
||||
@@ -138,7 +181,7 @@ Fixture.test(
|
||||
);
|
||||
|
||||
// Bundle table remains empty
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -148,6 +191,7 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
|
||||
const tx = wallet.sign({
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -160,11 +204,232 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
|
||||
],
|
||||
});
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 0);
|
||||
assertEquals(bundleService.bundleTable.count(), 0);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(tx));
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
assertEquals(bundleService.bundleTable.count(), 1);
|
||||
});
|
||||
|
||||
Fixture.test("Same bundle produces same hash", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const firstBundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const secondBundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 999999,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const firstBundleHash = await bundleService.hashBundle(firstBundle);
|
||||
const secondBundleHash = await bundleService.hashBundle(secondBundle);
|
||||
|
||||
assertEquals(firstBundleHash, secondBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes bundle with single operation", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
nonce,
|
||||
gas: 100000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, "3"],
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.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 bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes bundle with multiple operations", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const nonce = await wallet.Nonce();
|
||||
|
||||
const bundle = fx.blsWalletSigner.aggregate([
|
||||
wallet.sign({
|
||||
nonce,
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 3],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
wallet.sign({
|
||||
nonce: nonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 5],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
]);
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.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 bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
Fixture.test("hashes empty bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService();
|
||||
const bundle = fx.blsWalletSigner.aggregate([]);
|
||||
|
||||
const operationsWithZeroGas = bundle.operations.map((operation) => {
|
||||
return {
|
||||
...operation,
|
||||
gas: BigNumber.from(0),
|
||||
};
|
||||
});
|
||||
|
||||
const bundleType = VerificationGatewayFactory.abi.find(
|
||||
(entry) => "name" in entry && entry.name === "verify",
|
||||
)?.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 bundleService.ethereumService.provider.getNetwork()).chainId;
|
||||
|
||||
const bundleAndChainIdEncoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
const expectedBundleHash = ethers.utils.keccak256(bundleAndChainIdEncoding);
|
||||
|
||||
const hash = await bundleService.hashBundle(bundle);
|
||||
|
||||
assertEquals(hash, expectedBundleHash);
|
||||
});
|
||||
|
||||
// TODO (merge-ok): Add a mechanism for limiting the number of stored
|
||||
|
||||
@@ -37,7 +37,7 @@ function approveAndSendTokensToOrigin(
|
||||
fx: Fixture,
|
||||
nonce: BigNumber,
|
||||
amount: BigNumber,
|
||||
): Operation {
|
||||
): Omit<Operation, "gas"> {
|
||||
const es = fx.ethereumService;
|
||||
|
||||
return {
|
||||
@@ -48,13 +48,13 @@ function approveAndSendTokensToOrigin(
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"approve",
|
||||
[es.utilities.address, amount],
|
||||
[es.aggregatorUtilities.address, amount],
|
||||
),
|
||||
},
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendTokenToTxOrigin",
|
||||
[fx.testErc20.address, amount],
|
||||
),
|
||||
@@ -68,7 +68,7 @@ Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -108,8 +108,9 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
tokenBalance: oneToken,
|
||||
});
|
||||
|
||||
const bundle = wallet.sign(
|
||||
const bundle = await wallet.signWithGasEstimate(
|
||||
approveAndSendTokensToOrigin(fx, await wallet.Nonce(), oneToken),
|
||||
0.1,
|
||||
);
|
||||
|
||||
const bundleResponse = await bundleService.add(bundle);
|
||||
@@ -120,7 +121,7 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
oneToken,
|
||||
);
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 1);
|
||||
assertEquals(bundleService.bundleTable.count(), 1);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
@@ -129,7 +130,7 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
|
||||
if ("failures" in bundleResponse) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
const bundleRow = await bundleService.bundleTable.findBundle(
|
||||
const bundleRow = bundleService.bundleTable.findBundle(
|
||||
bundleResponse.hash,
|
||||
);
|
||||
|
||||
@@ -158,13 +159,13 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
})).wait();
|
||||
|
||||
const estimation = await bundleService.aggregationStrategy.estimateFee(
|
||||
wallet.sign({
|
||||
await wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 1,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
@@ -183,13 +184,13 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
|
||||
.sub(1), // Already sent 1 wei before
|
||||
})).wait();
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
ethValue: fee,
|
||||
contractAddress: es.utilities.address,
|
||||
encodedFunction: es.utilities.interface.encodeFunctionData(
|
||||
contractAddress: es.aggregatorUtilities.address,
|
||||
encodedFunction: es.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
),
|
||||
},
|
||||
@@ -245,8 +246,9 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
|
||||
wallet: BlsWalletWrapper,
|
||||
fee: BigNumber,
|
||||
) {
|
||||
const bundle = wallet.sign(
|
||||
const bundle = await wallet.signWithGasEstimate(
|
||||
approveAndSendTokensToOrigin(fx, nonce, fee),
|
||||
0.1,
|
||||
);
|
||||
|
||||
assertBundleSucceeds(await bundleService.add(bundle));
|
||||
@@ -275,7 +277,7 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
|
||||
// Restore this value now that all the bundles are added together
|
||||
bundleService.config.breakevenOperationCount = breakevenOperationCount;
|
||||
|
||||
assertEquals(await bundleService.bundleTable.count(), 10);
|
||||
assertEquals(bundleService.bundleTable.count(), 10);
|
||||
|
||||
fx.clock.advance(5000);
|
||||
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
|
||||
|
||||
@@ -10,7 +10,7 @@ const bundleServiceConfig = {
|
||||
};
|
||||
|
||||
const aggregationStrategyConfig: AggregationStrategyConfig = {
|
||||
maxGasPerBundle: 900000,
|
||||
maxGasPerBundle: 1_000_000,
|
||||
fees: nil,
|
||||
bundleCheckingConcurrency: 8,
|
||||
};
|
||||
@@ -23,7 +23,7 @@ Fixture.test("submits a single action in a timed submission", async (fx) => {
|
||||
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -78,20 +78,22 @@ Fixture.test("submits a full submission without delay", async (fx) => {
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
for (const b of bundles) {
|
||||
@@ -121,20 +123,22 @@ Fixture.test(
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = wallets.map((wallet) =>
|
||||
wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
// Prevent submission from triggering on max aggregation size.
|
||||
@@ -185,6 +189,7 @@ Fixture.test(
|
||||
const bundles = Range(3).reverse().map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -273,6 +278,7 @@ Fixture.test("retains failing bundle when its eligibility delay is smaller than
|
||||
const bundle = wallet.sign({
|
||||
// Future nonce makes this a failing bundle
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -313,6 +319,7 @@ Fixture.test("updates status of failing bundle when its eligibility delay is lar
|
||||
const bundle = wallet.sign({
|
||||
// Future nonce makes this a failing bundle
|
||||
nonce: (await wallet.Nonce()).add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -351,3 +358,57 @@ Fixture.test("updates status of failing bundle when its eligibility delay is lar
|
||||
const failedBundleRow = await bundleService.bundleTable.findBundle(res.hash);
|
||||
assertEquals(failedBundleRow?.status, "failed");
|
||||
});
|
||||
|
||||
Fixture.test("Retrieves all sub bundles included in a submitted bundle from single a sub bundle", async (fx) => {
|
||||
const bundleService = fx.createBundleService(
|
||||
bundleServiceConfig,
|
||||
aggregationStrategyConfig,
|
||||
);
|
||||
|
||||
const wallets = await fx.setupWallets(3);
|
||||
const firstWallet = wallets[0];
|
||||
const nonce = await firstWallet.Nonce();
|
||||
|
||||
const bundles = await Promise.all(
|
||||
wallets.map((wallet) =>
|
||||
wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[firstWallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
);
|
||||
|
||||
const subBundleHashes = await Promise.all(bundles.map(async (bundle) => {
|
||||
const res = await bundleService.add(bundle);
|
||||
|
||||
if ("failures" in res) {
|
||||
throw new Error("Bundle failed to be created");
|
||||
}
|
||||
|
||||
return res.hash;
|
||||
}));
|
||||
|
||||
await bundleService.submissionTimer.trigger();
|
||||
await bundleService.waitForConfirmations();
|
||||
const firstSubBundle = bundleService.lookupBundle(subBundleHashes[0]);
|
||||
const secondSubBundle = bundleService.lookupBundle(subBundleHashes[1]);
|
||||
const thirdSubBundle = bundleService.lookupBundle(subBundleHashes[2]);
|
||||
|
||||
const orderedSubBundles = [firstSubBundle, secondSubBundle, thirdSubBundle].sort((a, b) => a!.id - b!.id);
|
||||
|
||||
for (const subBundleHash of subBundleHashes) {
|
||||
const aggregateBundle = bundleService.lookupAggregateBundle(subBundleHash);
|
||||
assertEquals(aggregateBundle?.[0], orderedSubBundles[0]);
|
||||
assertEquals(aggregateBundle?.[1], orderedSubBundles[1]);
|
||||
assertEquals(aggregateBundle?.[2], orderedSubBundles[2]);
|
||||
}
|
||||
});
|
||||
@@ -13,6 +13,7 @@ const sampleRows: BundleRow[] = [
|
||||
operations: [
|
||||
{
|
||||
nonce: "0x01",
|
||||
gas: "0x01",
|
||||
actions: [
|
||||
{
|
||||
ethValue: "0x00",
|
||||
@@ -28,6 +29,7 @@ const sampleRows: BundleRow[] = [
|
||||
nextEligibilityDelay: BigNumber.from(1),
|
||||
submitError: nil,
|
||||
receipt: nil,
|
||||
aggregateHash: nil,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Fixture.test("EthereumService submits mint action", async (fx) => {
|
||||
const [wallet] = await fx.setupWallets(1);
|
||||
const startBalance = await fx.testErc20.balanceOf(wallet.address);
|
||||
|
||||
const bundle = wallet.sign({
|
||||
const bundle = await wallet.signWithGasEstimate({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -32,7 +32,7 @@ Fixture.test("EthereumService submits mint action", async (fx) => {
|
||||
Fixture.test("EthereumService submits transfer action", async (fx) => {
|
||||
const wallets = await fx.setupWallets(2);
|
||||
|
||||
const bundle = wallets[0].sign({
|
||||
const bundle = await wallets[0].signWithGasEstimate({
|
||||
nonce: await wallets[0].Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -62,6 +62,7 @@ Fixture.test("EthereumService submits aggregated bundle", async (fx) => {
|
||||
const bundle = fx.blsWalletSigner.aggregate([
|
||||
wallet.sign({
|
||||
nonce: walletNonce,
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -75,6 +76,7 @@ Fixture.test("EthereumService submits aggregated bundle", async (fx) => {
|
||||
}),
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(1),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -102,21 +104,24 @@ Fixture.test("EthereumService submits large aggregate mint bundle", async (fx) =
|
||||
const size = 11;
|
||||
|
||||
const bundle = fx.blsWalletSigner.aggregate(
|
||||
Range(size).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
actions: [
|
||||
// TODO (merge-ok): Add single operation multi-action variation of this test
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
await Promise.all(
|
||||
Range(size).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
// TODO (merge-ok): Add single operation multi-action variation of this test
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: fx.testErc20.address,
|
||||
encodedFunction: fx.testErc20.interface.encodeFunctionData(
|
||||
"mint",
|
||||
[wallet.address, 1],
|
||||
),
|
||||
},
|
||||
],
|
||||
})
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -137,6 +142,7 @@ Fixture.test("EthereumService sends large aggregate transfer bundle", async (fx)
|
||||
Range(size).map((i) =>
|
||||
sendWallet.sign({
|
||||
nonce: sendWalletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -170,6 +176,7 @@ Fixture.test(
|
||||
Range(5).map((i) =>
|
||||
wallet.sign({
|
||||
nonce: walletNonce.add(i),
|
||||
gas: 1_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: 0,
|
||||
@@ -238,7 +245,7 @@ Fixture.test("callStaticSequence - correctly measures transfer", async (fx) => {
|
||||
value: transferAmount,
|
||||
})).wait();
|
||||
|
||||
const bundle = sendWallet.sign({
|
||||
const bundle = await sendWallet.signWithGasEstimate({
|
||||
nonce: await sendWallet.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -253,9 +260,9 @@ Fixture.test("callStaticSequence - correctly measures transfer", async (fx) => {
|
||||
const es = fx.ethereumService;
|
||||
|
||||
const results = await es.callStaticSequence(
|
||||
es.Call(es.utilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.aggregatorUtilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.verificationGateway, "processBundle", [bundle]),
|
||||
es.Call(es.utilities, "ethBalanceOf", [recvWallet.address]),
|
||||
es.Call(es.aggregatorUtilities, "ethBalanceOf", [recvWallet.address]),
|
||||
);
|
||||
|
||||
const [balanceResultBefore, , balanceResultAfter] = results;
|
||||
|
||||
10
aggregator/test/HealthService.test.ts
Normal file
10
aggregator/test/HealthService.test.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { assertEquals } from "./deps.ts";
|
||||
|
||||
import Fixture from "./helpers/Fixture.ts";
|
||||
|
||||
Fixture.test("HealthService returns healthy", async (fx) => {
|
||||
const healthCheckService = fx.createHealthCheckService()
|
||||
const healthStatus = await healthCheckService.getHealth();
|
||||
const expected = {"status":"healthy"};
|
||||
assertEquals(JSON.stringify(healthStatus), JSON.stringify(expected));
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
MockERC20,
|
||||
MockERC20__factory,
|
||||
MockERC20Factory,
|
||||
NetworkConfig,
|
||||
sqlite,
|
||||
} from "../../deps.ts";
|
||||
@@ -25,6 +25,7 @@ import BundleTable, { BundleRow } from "../../src/app/BundleTable.ts";
|
||||
import AggregationStrategy, {
|
||||
AggregationStrategyConfig,
|
||||
} from "../../src/app/AggregationStrategy.ts";
|
||||
import HealthService from "../../src/app/HealthService.ts";
|
||||
|
||||
// deno-lint-ignore no-explicit-any
|
||||
type ExplicitAny = any;
|
||||
@@ -36,6 +37,7 @@ export const bundleServiceDefaultTestConfig:
|
||||
maxAggregationDelayMillis: 5000,
|
||||
maxUnconfirmedAggregations: 3,
|
||||
maxEligibilityDelay: 300,
|
||||
isOptimism: false,
|
||||
};
|
||||
|
||||
export const aggregationStrategyDefaultTestConfig: AggregationStrategyConfig = {
|
||||
@@ -74,11 +76,10 @@ export default class Fixture {
|
||||
static async create(testName: string): Promise<Fixture> {
|
||||
const netCfg = await getNetworkConfig();
|
||||
const rng = testRng.seed(testName);
|
||||
const emit = (evt: AppEvent) => fx.emit(evt);
|
||||
|
||||
const ethereumService = await EthereumService.create(
|
||||
(evt) => fx.emit(evt),
|
||||
netCfg.addresses.verificationGateway,
|
||||
netCfg.addresses.utilities,
|
||||
emit,
|
||||
env.PRIVATE_KEY_AGG,
|
||||
);
|
||||
|
||||
@@ -95,6 +96,7 @@ export default class Fixture {
|
||||
ethereumService.blsWalletSigner,
|
||||
ethereumService,
|
||||
aggregationStrategyDefaultTestConfig,
|
||||
emit,
|
||||
),
|
||||
netCfg,
|
||||
);
|
||||
@@ -131,7 +133,7 @@ export default class Fixture {
|
||||
public aggregationStrategy: AggregationStrategy,
|
||||
public networkConfig: NetworkConfig,
|
||||
) {
|
||||
this.testErc20 = MockERC20__factory.connect(
|
||||
this.testErc20 = MockERC20Factory.connect(
|
||||
this.networkConfig.addresses.testToken,
|
||||
this.ethereumService.wallet.provider,
|
||||
);
|
||||
@@ -171,6 +173,7 @@ export default class Fixture {
|
||||
this.blsWalletSigner,
|
||||
this.ethereumService,
|
||||
aggregationStrategyConfig,
|
||||
this.emit,
|
||||
);
|
||||
|
||||
const bundleService = new BundleService(
|
||||
@@ -245,7 +248,7 @@ export default class Fixture {
|
||||
const topUp = BigNumber.from(tokenBalance).sub(balance);
|
||||
|
||||
if (topUp.gt(0)) {
|
||||
return wallet.sign({
|
||||
return await wallet.signWithGasEstimate({
|
||||
nonce: (await wallet.Nonce()).add(i),
|
||||
actions: [
|
||||
{
|
||||
@@ -261,7 +264,7 @@ export default class Fixture {
|
||||
}
|
||||
|
||||
if (topUp.lt(0)) {
|
||||
return wallet.sign({
|
||||
return await wallet.signWithGasEstimate({
|
||||
nonce: (await wallet.Nonce()).add(i),
|
||||
actions: [
|
||||
{
|
||||
@@ -293,6 +296,12 @@ export default class Fixture {
|
||||
return wallets;
|
||||
}
|
||||
|
||||
createHealthCheckService() {
|
||||
const healthCheckService = new HealthService();
|
||||
|
||||
return healthCheckService;
|
||||
}
|
||||
|
||||
async cleanup() {
|
||||
for (const job of this.cleanupJobs) {
|
||||
await job();
|
||||
|
||||
@@ -29,6 +29,7 @@ Deno.test("parseBundleDto accepts dummy values", () => {
|
||||
"operations": [
|
||||
{
|
||||
"nonce": "0x01",
|
||||
"gas": "0x01",
|
||||
"actions": [
|
||||
{
|
||||
"ethValue": "0x00",
|
||||
|
||||
@@ -8,12 +8,6 @@ OPTIMISM_TESETNET_URL=https://kovan.optimism.io
|
||||
OPTIMISM_URL=https://mainnet.optimism.io
|
||||
OPTIMISM_GOERLI_URL=https://goerli.optimism.io
|
||||
|
||||
# Only used for deploying the deployer contract at the same address on each evm network
|
||||
DEPLOYER_MNEMONIC="sock poet alone around radar forum quiz session observe rebel another choice"
|
||||
DEPLOYER_SET_INDEX=1
|
||||
DEPLOYER_ACCOUNT=0x6435e511f8908D5C733898C81831a4A3aFE31D07
|
||||
DEPLOYER_CONTRACT_ADDRESS=0x036d996D6855B83cd80142f2933d8C2617dA5617
|
||||
|
||||
# Used for deploying contracts via the deployment contract, and testing
|
||||
MAIN_MNEMONIC="test test test test test test test test test test test junk"
|
||||
|
||||
|
||||
@@ -116,7 +116,7 @@ For each network, the deployer contract can be deployed with the following scrip
|
||||
|
||||
To run integration tests:
|
||||
|
||||
1. cd into `./contracts` and run `yarn start-hardhat`
|
||||
1. cd into `./contracts` and run `yarn start`
|
||||
2. cd into `./aggregator` and run `./programs/aggregator.ts`
|
||||
3. from `./contracts`, run `yarn test-integration`.
|
||||
|
||||
|
||||
11
contracts/audits/README.md
Normal file
11
contracts/audits/README.md
Normal file
@@ -0,0 +1,11 @@
|
||||
# Audits
|
||||
|
||||
## 2021
|
||||
|
||||
[Hubble contracts, including core BLS contracts](https://github.com/thehubbleproject/hubble-contracts/blob/master/audits/2021-03-17%20Igor%20Gulamov.md)
|
||||
|
||||
## 2022
|
||||
|
||||
[BLS Wallet](./Sigma_Prime_-_Ethereum_Foundation_-_BLS_Wallet_Smart_Contract_Security_Assessment_Report_-_v1.0.pdf)
|
||||
|
||||
All critical & high issues were addressed, but re-audit was not conducted.
|
||||
Binary file not shown.
@@ -142,7 +142,7 @@ const bundle = wallet.sign({
|
||||
|
||||
User bundles must pay fees to compensate the aggregator. Fees can be paid by adding an additional action to the users bundle that pays tx.origin. For more info on how fees work, see [aggregator fees](../../aggregator/README.md#fees).
|
||||
|
||||
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
|
||||
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero wei, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
|
||||
|
||||
### Paying aggregator fees with native currency (ETH)
|
||||
|
||||
@@ -198,21 +198,6 @@ const bundle = wallet.sign({
|
||||
});
|
||||
```
|
||||
|
||||
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
|
||||
|
||||
```ts
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
{
|
||||
ethValue: safeFee, // fee amount
|
||||
contractAddress: aggregatorAddress,
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
### Paying aggregator fees with custom currency (ERC20)
|
||||
|
||||
The aggregator must be set up to accept ERC20 tokens in order for this to work.
|
||||
@@ -288,25 +273,6 @@ const bundle = wallet.sign({
|
||||
});
|
||||
```
|
||||
|
||||
Since the aggregator detects that fees have been paid by observing the effect of a bundle on its own balance, you can also tranfer the required fee directly to the aggregator. Following the same example as above, you can add an action that transfers the fee amount to the aggregator address, instead of the action that calls sendEthToTxOrigin. Ensure you modify the estimateFeeBundle to use this action instead.
|
||||
|
||||
```ts
|
||||
const bundle = wallet.sign({
|
||||
nonce: await wallet.Nonce(),
|
||||
actions: [
|
||||
...actions, // ... add your user actions here (approve, transfer, etc.)
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: tokenContract.address,
|
||||
encodedFunction: tokenContract.interface.encodeFunctionData("transfer", [
|
||||
aggregatorAddress,
|
||||
safeFee, // fee amount
|
||||
]),
|
||||
},
|
||||
],
|
||||
});
|
||||
```
|
||||
|
||||
## VerificationGateway
|
||||
|
||||
Exposes `VerificationGateway` and `VerificationGateway__factory` generated by
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bls-wallet-clients",
|
||||
"version": "0.8.2-1452ef5",
|
||||
"version": "0.9.0",
|
||||
"description": "Client libraries for interacting with BLS Wallet components",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
|
||||
116
contracts/clients/src/AddressRegistryWrapper.ts
Normal file
116
contracts/clients/src/AddressRegistryWrapper.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { BigNumber, BigNumberish, ethers, Signer } from "ethers";
|
||||
import { AddressRegistry } from "../typechain-types/contracts/AddressRegistry";
|
||||
import { AddressRegistry__factory as AddressRegistryFactory } from "../typechain-types/factories/contracts/AddressRegistry__factory";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
|
||||
/**
|
||||
* A wrapper around the `AddressRegistry` contract to provide a more ergonomic
|
||||
* interface, especially for `reverseLookup`.
|
||||
*/
|
||||
export default class AddressRegistryWrapper {
|
||||
constructor(public registry: AddressRegistry) {}
|
||||
|
||||
/**
|
||||
* Deploys a new `AddressRegistry` contract the traditional way.
|
||||
*/
|
||||
static async deployNew(signer: Signer): Promise<AddressRegistryWrapper> {
|
||||
const factory = new AddressRegistryFactory(signer);
|
||||
|
||||
return new AddressRegistryWrapper(await factory.deploy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `AddressRegistry` contract at a
|
||||
* predetermined address. Deploys if it doesn't already exist.
|
||||
*/
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<AddressRegistryWrapper> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const registry = await factory.connectOrDeploy(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new AddressRegistryWrapper(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `AddressRegistry` contract at a
|
||||
* predetermined address. Returns undefined if it doesn't exist.
|
||||
*/
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<AddressRegistryWrapper | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const registry = await factoryViewer.connectIfDeployed(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return registry ? new AddressRegistryWrapper(registry) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an id to lookup an address, the same way that happens on chain.
|
||||
*/
|
||||
async lookup(id: BigNumberish): Promise<string | undefined> {
|
||||
const address = await this.registry.addresses(id);
|
||||
|
||||
return address === ethers.constants.AddressZero ? undefined : address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an address to lookup an id - the reverse of what happens on chain, by
|
||||
* making use of the indexed `AddressRegistered` event.
|
||||
*/
|
||||
async reverseLookup(address: string): Promise<BigNumber | undefined> {
|
||||
const events = await this.registry.queryFilter(
|
||||
this.registry.filters.AddressRegistered(null, address),
|
||||
);
|
||||
|
||||
const id = events.at(-1)?.args?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an address and returns the id that was assigned to it.
|
||||
*/
|
||||
async register(address: string): Promise<BigNumber> {
|
||||
await (await this.registry.register(address)).wait();
|
||||
|
||||
const id = await this.reverseLookup(address);
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Registration completed but couldn't find id");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers an address if it hasn't already been registered, and returns the
|
||||
* id that was assigned to it.
|
||||
*/
|
||||
async registerIfNeeded(address: string): Promise<BigNumber> {
|
||||
let id = await this.reverseLookup(address);
|
||||
|
||||
if (id === undefined) {
|
||||
id = await this.register(address);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,7 @@ export type BundleReceiptError = {
|
||||
*/
|
||||
export type BlsBundleReceipt = {
|
||||
bundleHash: string;
|
||||
aggregateBundleHash: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -99,7 +100,7 @@ export default class Aggregator {
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimates the fee required for a bundle by the aggreagtor to submit it.
|
||||
* Estimates the fee required for a bundle by the aggregator to submit it.
|
||||
*
|
||||
* @param bundle Bundle to estimates the fee for
|
||||
* @returns Estimate of the fee needed to submit the bundle
|
||||
@@ -125,6 +126,21 @@ export default class Aggregator {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the aggregate bundle that a sub bundle was a part of.
|
||||
* This will return undefined if the bundle does not exist or does not have an aggregate bundle.
|
||||
*
|
||||
* @param hash Hash of the bundle to find the aggregate bundle for.
|
||||
* @returns The aggregate bundle, or undefined if either the sub bundle or aggregate bundle were not found.
|
||||
*/
|
||||
async getAggregateBundleFromSubBundle(
|
||||
subBundleHash: string,
|
||||
): Promise<Bundle | undefined> {
|
||||
return this.jsonGet<Bundle>(
|
||||
`${this.origin}/aggregateBundle/${subBundleHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Note: This should be private instead of exposed. Leaving as is for compatibility.
|
||||
async jsonPost(path: string, body: unknown): Promise<unknown> {
|
||||
const resp = await this.fetchImpl(`${this.origin}${path}`, {
|
||||
|
||||
@@ -2,10 +2,13 @@
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { Deferrable } from "ethers/lib/utils";
|
||||
|
||||
import { ActionData, Bundle } from "./signer/types";
|
||||
import { ActionData, Bundle, PublicKey } from "./signer/types";
|
||||
import Aggregator, { BundleReceipt } from "./Aggregator";
|
||||
import BlsSigner, {
|
||||
TransactionBatchResponse,
|
||||
// Used for sendTransactionBatch TSdoc comment
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
TransactionBatch,
|
||||
UncheckedBlsSigner,
|
||||
_constructorGuard,
|
||||
} from "./BlsSigner";
|
||||
@@ -14,15 +17,28 @@ import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import {
|
||||
AggregatorUtilities__factory,
|
||||
BLSWallet__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "../typechain-types";
|
||||
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
|
||||
|
||||
/** Public key linked to actions parsed from a bundle */
|
||||
export type PublicKeyLinkedToActions = {
|
||||
publicKey: PublicKey;
|
||||
actions: Array<ActionData>;
|
||||
};
|
||||
|
||||
export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
readonly aggregator: Aggregator;
|
||||
readonly verificationGatewayAddress: string;
|
||||
readonly aggregatorUtilitiesAddress: string;
|
||||
signer!: BlsSigner;
|
||||
|
||||
/**
|
||||
* @param aggregatorUrl The url for an aggregator instance
|
||||
* @param verificationGatewayAddress Verification gateway contract address
|
||||
* @param aggregatorUtilitiesAddress Aggregator utilities contract address
|
||||
* @param url Rpc url
|
||||
* @param network The network the provider should connect to
|
||||
*/
|
||||
constructor(
|
||||
aggregatorUrl: string,
|
||||
verificationGatewayAddress: string,
|
||||
@@ -36,56 +52,70 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
this.aggregatorUtilitiesAddress = aggregatorUtilitiesAddress;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transaction Transaction request object
|
||||
* @returns An estimate of the amount of gas that would be required to submit the transaction to the network
|
||||
*/
|
||||
override async estimateGas(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<BigNumber> {
|
||||
if (!transaction.to) {
|
||||
const resolvedTransaction = await ethers.utils.resolveProperties(
|
||||
transaction,
|
||||
);
|
||||
|
||||
if (!resolvedTransaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
|
||||
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
|
||||
// We do not want to pass the private key to this method.
|
||||
if (!this.signer) {
|
||||
throw new Error("Call provider.getSigner first");
|
||||
if (!resolvedTransaction.from) {
|
||||
throw new TypeError("Transaction.from should be defined");
|
||||
}
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
ethValue: resolvedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: resolvedTransaction.to.toString(),
|
||||
encodedFunction: resolvedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
this.signer.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
// set to zero at all times as an error will be thrown. If the
|
||||
// nonce of the actual wallet is more than 0, there will be a
|
||||
// nonce mistmatch as signWithGasEstimate with check the operation
|
||||
// nonce against the throwawayBlsWalletWrapper nonce, which is always zero
|
||||
const nonce = 0;
|
||||
|
||||
const actionWithFeePaymentAction =
|
||||
this._addFeePaymentActionForFeeEstimation([action]);
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(
|
||||
this.signer.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
}),
|
||||
// TODO: (merge-ok) bls-wallet #560 Estimate fee without requiring a signed bundle
|
||||
// There is no way to estimate the cost of a bundle without signing a bundle. The
|
||||
// alternative would be to use a signer instance in this method which is undesirable,
|
||||
// as this would result in tight coupling between a provider and a signer.
|
||||
const throwawayPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
const throwawayBlsWalletWrapper = await BlsWalletWrapper.connect(
|
||||
throwawayPrivateKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
const bundle = await throwawayBlsWalletWrapper.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.aggregator.estimateFee(bundle);
|
||||
|
||||
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
|
||||
return addSafetyPremiumToFee(feeRequired);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends transaction to be executed. Adds the signed bundle to the aggregator
|
||||
*
|
||||
* @param signedTransaction A signed bundle
|
||||
* @returns A transaction response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
override async sendTransaction(
|
||||
signedTransaction: string | Promise<string>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
|
||||
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
|
||||
// We do not want to pass the private key to this method.
|
||||
if (!this.signer) {
|
||||
throw new Error("Call provider.getSigner first");
|
||||
}
|
||||
|
||||
const resolvedTransaction = await signedTransaction;
|
||||
const bundle: Bundle = JSON.parse(resolvedTransaction);
|
||||
|
||||
@@ -107,23 +137,20 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
encodedFunction: bundle.operations[0].actions[0].encodedFunction,
|
||||
};
|
||||
|
||||
return this.signer.constructTransactionResponse(
|
||||
return await this._constructTransactionResponse(
|
||||
actionData,
|
||||
bundle.senderPublicKeys[0],
|
||||
result.hash,
|
||||
this.signer.wallet.address,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param signedTransactionBatch A signed {@link TransactionBatch}
|
||||
* @returns A transaction batch response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
async sendTransactionBatch(
|
||||
signedTransactionBatch: string,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
// TODO: bls-wallet #413 Move references to private key outside of BlsSigner.
|
||||
// Without doing this, we would have to call `const signer = this.getSigner(privateKey)`.
|
||||
// We do not want to pass the private key to this method.
|
||||
if (!this.signer) {
|
||||
throw new Error("Call provider.getSigner first");
|
||||
}
|
||||
|
||||
const bundle: Bundle = JSON.parse(signedTransactionBatch);
|
||||
|
||||
const result = await this.aggregator.add(bundle);
|
||||
@@ -132,35 +159,40 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
const actionData: Array<ActionData> = bundle.operations
|
||||
.map((operation) => operation.actions)
|
||||
.flat();
|
||||
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
|
||||
bundle.senderPublicKeys.map((publicKey, i) => {
|
||||
const operation = bundle.operations[i];
|
||||
const actions = operation.actions;
|
||||
|
||||
return this.signer.constructTransactionBatchResponse(
|
||||
actionData,
|
||||
return {
|
||||
publicKey,
|
||||
actions,
|
||||
};
|
||||
});
|
||||
|
||||
return await this._constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions,
|
||||
result.hash,
|
||||
this.signer.wallet.address,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param privateKey Private key for the account the signer represents
|
||||
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
|
||||
* @returns A new BlsSigner instance
|
||||
*/
|
||||
override getSigner(
|
||||
privateKey: string,
|
||||
addressOrIndex?: string | number,
|
||||
): BlsSigner {
|
||||
if (this.signer) {
|
||||
return this.signer;
|
||||
}
|
||||
|
||||
const signer = new BlsSigner(
|
||||
_constructorGuard,
|
||||
this,
|
||||
privateKey,
|
||||
addressOrIndex,
|
||||
);
|
||||
this.signer = signer;
|
||||
return signer;
|
||||
return new BlsSigner(_constructorGuard, this, privateKey, addressOrIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param privateKey Private key for the account the signer represents
|
||||
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
|
||||
* @returns A new UncheckedBlsSigner instance
|
||||
*/
|
||||
override getUncheckedSigner(
|
||||
privateKey: string,
|
||||
addressOrIndex?: string,
|
||||
@@ -168,6 +200,15 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return this.getSigner(privateKey, addressOrIndex).connectUnchecked();
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transaction receipt associated with the transaction (bundle) hash
|
||||
*
|
||||
* @remarks The transaction hash argument corresponds to a bundle hash and cannot be used on a block explorer.
|
||||
* Instead, the transaction hash returned in the transaction receipt from this method can be used in a block explorer.
|
||||
*
|
||||
* @param transactionHash The transaction hash returned from the BlsProvider and BlsSigner sendTransaction methods. This is technically a bundle hash
|
||||
* @returns The transaction receipt that corressponds to the transaction hash (bundle hash)
|
||||
*/
|
||||
override async getTransactionReceipt(
|
||||
transactionHash: string | Promise<string>,
|
||||
): Promise<ethers.providers.TransactionReceipt> {
|
||||
@@ -175,6 +216,17 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return this._getTransactionReceipt(resolvedTransactionHash, 1, 20);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the transaction receipt associated with the transaction (bundle) hash
|
||||
*
|
||||
* @remarks The transaction hash argument cannot be used on a block explorer. It instead corresponds to a bundle hash.
|
||||
* The transaction hash returned in the transaction receipt from this method can be used in a block explorer.
|
||||
*
|
||||
* @param transactionHash The transaction hash returned from sending a transaction. This is technically a bundle hash
|
||||
* @param confirmations (Not used) the number of confirmations to wait for before returning the transaction receipt
|
||||
* @param retries The number of retries to poll the receipt for
|
||||
* @returns
|
||||
*/
|
||||
override async waitForTransaction(
|
||||
transactionHash: string,
|
||||
confirmations?: number,
|
||||
@@ -187,6 +239,11 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param address The address that the method gets the transaction count from
|
||||
* @param blockTag The specific block tag to get the transaction count from
|
||||
* @returns The number of transactions an account has sent
|
||||
*/
|
||||
override async getTransactionCount(
|
||||
address: string | Promise<string>,
|
||||
blockTag?:
|
||||
@@ -286,7 +343,7 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
return [
|
||||
...actions,
|
||||
{
|
||||
ethValue: fee,
|
||||
ethValue: fee.toHexString(),
|
||||
contractAddress: this.aggregatorUtilitiesAddress,
|
||||
encodedFunction:
|
||||
aggregatorUtilitiesContract.interface.encodeFunctionData(
|
||||
@@ -295,4 +352,104 @@ export default class BlsProvider extends ethers.providers.JsonRpcProvider {
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async _constructTransactionResponse(
|
||||
action: ActionData,
|
||||
publicKey: PublicKey,
|
||||
hash: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
const chainId = await this.send("eth_chainId", []);
|
||||
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
publicKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
const from = await BlsWalletWrapper.AddressFromPublicKey(
|
||||
publicKey,
|
||||
verificationGateway,
|
||||
);
|
||||
|
||||
return {
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId: parseInt(chainId, 16),
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async _constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions: Array<PublicKeyLinkedToActions>,
|
||||
hash: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
const chainId = await this.send("eth_chainId", []);
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
|
||||
const transactions: Array<ethers.providers.TransactionResponse> = [];
|
||||
|
||||
for (const publicKeyLinkedToActions of publicKeysLinkedToActions) {
|
||||
const from = await BlsWalletWrapper.AddressFromPublicKey(
|
||||
publicKeyLinkedToActions.publicKey,
|
||||
verificationGateway,
|
||||
);
|
||||
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
publicKeyLinkedToActions.publicKey,
|
||||
this.verificationGatewayAddress,
|
||||
this,
|
||||
);
|
||||
}
|
||||
|
||||
for (const action of publicKeyLinkedToActions.actions) {
|
||||
if (action.contractAddress === this.aggregatorUtilitiesAddress) {
|
||||
break;
|
||||
}
|
||||
|
||||
transactions.push({
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce!.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId: parseInt(chainId, 16),
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
transactions,
|
||||
awaitBatchReceipt: (confirmations?: number) => {
|
||||
return this.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
131
contracts/clients/src/BlsPublicKeyRegistryWrapper.ts
Normal file
131
contracts/clients/src/BlsPublicKeyRegistryWrapper.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { BigNumber, BigNumberish, ethers, Signer } from "ethers";
|
||||
import { solidityKeccak256 } from "ethers/lib/utils";
|
||||
import {
|
||||
BLSPublicKeyRegistry,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
} from "../typechain-types";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { PublicKey } from "./signer";
|
||||
|
||||
/**
|
||||
* A wrapper around the `BLSPublicKeyRegistry` contract to provide a more
|
||||
* ergonomic interface, especially for `reverseLookup`.
|
||||
*/
|
||||
export default class BlsPublicKeyRegistryWrapper {
|
||||
constructor(public registry: BLSPublicKeyRegistry) {}
|
||||
|
||||
/**
|
||||
* Deploys a new `BLSPublicKeyRegistry` contract the traditional way.
|
||||
*/
|
||||
static async deployNew(signer: Signer): Promise<BlsPublicKeyRegistryWrapper> {
|
||||
const factory = new BLSPublicKeyRegistryFactory(signer);
|
||||
|
||||
return new BlsPublicKeyRegistryWrapper(await factory.deploy());
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `BLSPublicKeyRegistry` contract at a
|
||||
* predetermined address. Deploys if it doesn't already exist.
|
||||
*/
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsPublicKeyRegistryWrapper> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const registry = await factory.connectOrDeploy(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new BlsPublicKeyRegistryWrapper(registry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses Gnosis Safe's factory to get an `BLSPublicKeyRegistry` contract at a
|
||||
* predetermined address. Returns undefined if it doesn't exist.
|
||||
*/
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsPublicKeyRegistryWrapper | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const registry = await factoryViewer.connectIfDeployed(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
return registry ? new BlsPublicKeyRegistryWrapper(registry) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses an id to lookup a public key, the same way that happens on chain.
|
||||
*/
|
||||
async lookup(id: BigNumberish): Promise<PublicKey | undefined> {
|
||||
const blsPublicKey = await Promise.all([
|
||||
this.registry.blsPublicKeys(id, 0),
|
||||
this.registry.blsPublicKeys(id, 1),
|
||||
this.registry.blsPublicKeys(id, 2),
|
||||
this.registry.blsPublicKeys(id, 3),
|
||||
]);
|
||||
|
||||
if (blsPublicKey.every((x) => x.eq(0))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return blsPublicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses a public key to lookup an id - the reverse of what happens on chain,
|
||||
* by making use of the indexed `BLSPublicKeyRegistered` event.
|
||||
*/
|
||||
async reverseLookup(blsPublicKey: PublicKey): Promise<BigNumber | undefined> {
|
||||
const blsPublicKeyHash = solidityKeccak256(["uint256[4]"], [blsPublicKey]);
|
||||
|
||||
const events = await this.registry.queryFilter(
|
||||
this.registry.filters.BLSPublicKeyRegistered(null, blsPublicKeyHash),
|
||||
);
|
||||
|
||||
const id = events.at(-1)?.args?.id;
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a public key and returns the id.
|
||||
*/
|
||||
async register(blsPublicKey: PublicKey): Promise<BigNumber> {
|
||||
await (await this.registry.register(blsPublicKey)).wait();
|
||||
|
||||
const id = await this.reverseLookup(blsPublicKey);
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Registration completed but couldn't find id");
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a public key if it hasn't already been registered, and returns
|
||||
* the id that was assigned to it.
|
||||
*/
|
||||
async registerIfNeeded(blsPublicKey: PublicKey): Promise<BigNumber> {
|
||||
let id = await this.reverseLookup(blsPublicKey);
|
||||
|
||||
if (id === undefined) {
|
||||
id = await this.register(blsPublicKey);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
}
|
||||
216
contracts/clients/src/BlsRegistrationCompressor.ts
Normal file
216
contracts/clients/src/BlsRegistrationCompressor.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import { BigNumber, ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
BLSRegistration,
|
||||
BLSRegistration__factory as BLSRegistrationFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import { encodePseudoFloat, encodeVLQ, hexJoin } from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class BlsRegistrationCompressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public blsRegistration: BLSRegistration,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(
|
||||
blsRegistration: BLSRegistration,
|
||||
): Promise<BlsRegistrationCompressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
blsRegistration.blsPublicKeyRegistry(),
|
||||
blsRegistration.addressRegistry(),
|
||||
blsRegistration.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistration,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
blsRegistration.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<BlsRegistrationCompressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const blsRegistrationFactory = new BLSRegistrationFactory(signer);
|
||||
|
||||
const blsRegistrationContract = await blsRegistrationFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistrationContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsRegistrationCompressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const blsRegistrationContract = await factory.connectOrDeploy(
|
||||
BLSRegistrationFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new BlsRegistrationCompressor(
|
||||
blsRegistrationContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<BlsRegistrationCompressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const blsPublicKeyRegistryAddress = factoryViewer.calculateAddress(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const addressRegistryAddress = factoryViewer.calculateAddress(
|
||||
AddressRegistryFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const aggregatorUtilitiesAddress = factoryViewer.calculateAddress(
|
||||
AggregatorUtilitiesFactory,
|
||||
[],
|
||||
salt,
|
||||
);
|
||||
|
||||
const blsRegistration = await factoryViewer.connectIfDeployed(
|
||||
BLSRegistrationFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!blsRegistration) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await BlsRegistrationCompressor.wrap(blsRegistration);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.blsRegistration.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
if (operation.actions.length > 2) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Must be a non-paying call to blsRegistration.register with the user's
|
||||
// blsPublicKey
|
||||
const firstAction = operation.actions.at(0);
|
||||
|
||||
if (
|
||||
firstAction === undefined ||
|
||||
!BigNumber.from(firstAction.ethValue).isZero() ||
|
||||
firstAction.contractAddress !== this.blsRegistration.address ||
|
||||
ethers.utils.hexlify(firstAction.encodedFunction) !==
|
||||
this.blsRegistration.interface.encodeFunctionData("register", [
|
||||
blsPublicKey,
|
||||
])
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Must be absent or a non-zero payment to tx.origin
|
||||
const secondAction = operation.actions.at(1);
|
||||
|
||||
if (secondAction !== undefined) {
|
||||
if (
|
||||
BigNumber.from(secondAction.ethValue).isZero() ||
|
||||
secondAction.contractAddress !== this.aggregatorUtilities.address ||
|
||||
ethers.utils.hexlify(secondAction.encodedFunction) !==
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
return hexJoin([
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
encodeVLQ(operation.nonce),
|
||||
encodePseudoFloat(operation.gas),
|
||||
encodePseudoFloat(secondAction?.ethValue ?? 0),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,22 @@
|
||||
/* eslint-disable camelcase */
|
||||
import { ethers, BigNumber, Signer, Bytes, BigNumberish } from "ethers";
|
||||
import {
|
||||
AccessListish,
|
||||
Deferrable,
|
||||
hexlify,
|
||||
isBytes,
|
||||
RLP,
|
||||
} from "ethers/lib/utils";
|
||||
import { AccessListish, Deferrable, hexlify, isBytes } from "ethers/lib/utils";
|
||||
|
||||
import BlsProvider from "./BlsProvider";
|
||||
import BlsProvider, { PublicKeyLinkedToActions } from "./BlsProvider";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
|
||||
import { ActionData, bundleToDto } from "./signer";
|
||||
import { ActionData, Signature, bundleToDto } from "./signer";
|
||||
|
||||
export const _constructorGuard = {};
|
||||
|
||||
/**
|
||||
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) transaction gas limit
|
||||
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) miner tip aka priority fee
|
||||
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) the maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
|
||||
* @property nonce - integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
|
||||
* @property chainId - chain ID that this transaction is valid on
|
||||
* Based on draft wallet_batchTransactions rpc proposal https://hackmd.io/HFHohGDbRSGgUFI2rk22bA?view
|
||||
*
|
||||
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Transaction gas limit
|
||||
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Miner tip aka priority fee
|
||||
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) The maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
|
||||
* @property nonce - Integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
|
||||
* @property chainId - Chain ID that this transaction is valid on
|
||||
* @property accessList - (THIS PROPERTY IS NOT USED BY BLS WALLET) EIP-2930 access list
|
||||
*/
|
||||
export type BatchOptions = {
|
||||
@@ -33,14 +29,18 @@ export type BatchOptions = {
|
||||
};
|
||||
|
||||
/**
|
||||
* @property transactions - an array of transaction objects
|
||||
* @property batchOptions - optional batch options taken into account by smart contract wallets
|
||||
* @property transactions - An array of Ethers transaction objects
|
||||
* @property batchOptions - Optional batch options taken into account by smart contract wallets. See {@link BatchOptions}
|
||||
*/
|
||||
export type TransactionBatch = {
|
||||
transactions: Array<ethers.providers.TransactionRequest>;
|
||||
batchOptions?: BatchOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
* @property transactions - An array of Ethers transaction response objects
|
||||
* @property awaitBatchReceipt - A function that returns a promise that resolves to a transaction receipt
|
||||
*/
|
||||
export interface TransactionBatchResponse {
|
||||
transactions: Array<ethers.providers.TransactionResponse>;
|
||||
awaitBatchReceipt: (
|
||||
@@ -58,10 +58,16 @@ export default class BlsSigner extends Signer {
|
||||
|
||||
readonly initPromise: Promise<void>;
|
||||
|
||||
/**
|
||||
* @param constructorGuard Prevents BlsSigner constructor being called directly
|
||||
* @param provider BlsProvider accociated with this signer
|
||||
* @param privateKey Private key for the account this signer represents
|
||||
* @param addressOrIndex (Not used) Address or index of this account, managed by the connected Ethereum node
|
||||
*/
|
||||
constructor(
|
||||
constructorGuard: Record<string, unknown>,
|
||||
provider: BlsProvider,
|
||||
privateKey: string,
|
||||
privateKey: string | Promise<string>,
|
||||
readonly addressOrIndex?: string | number,
|
||||
) {
|
||||
super();
|
||||
@@ -92,22 +98,41 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
}
|
||||
|
||||
private async initializeWallet(privateKey: string) {
|
||||
/** Instantiates a BLS Wallet and then connects the signer to it */
|
||||
private async initializeWallet(privateKey: string | Promise<string>) {
|
||||
const resolvedPrivateKey = await privateKey;
|
||||
this.wallet = await BlsWalletWrapper.connect(
|
||||
privateKey,
|
||||
resolvedPrivateKey,
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns a random BLS private key
|
||||
*/
|
||||
static async getRandomBlsPrivateKey(): Promise<string> {
|
||||
return await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends transactions to be executed. Converts the TransactionRequest
|
||||
* to a bundle and adds it to the aggregator
|
||||
*
|
||||
* @remarks The transaction hash returned in the transaction response does
|
||||
* NOT correspond to a transaction hash that can be viewed on a block
|
||||
* explorer. It instead represents the bundle hash, which can be used to
|
||||
* get a transaction receipt that has a hash that can be used on a block explorer
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns A transaction response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
override async sendTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
await this.initPromise;
|
||||
|
||||
if (!transaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -116,18 +141,18 @@ export default class BlsSigner extends Signer {
|
||||
);
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
ethValue: validatedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: validatedTransaction.to!.toString(),
|
||||
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const feeEstimate = await this.provider.estimateGas(transaction);
|
||||
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
[action],
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -137,26 +162,30 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
return this.constructTransactionResponse(
|
||||
return await this.provider._constructTransactionResponse(
|
||||
action,
|
||||
bundle.senderPublicKeys[0],
|
||||
result.hash,
|
||||
this.wallet.address,
|
||||
nonce,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transactionBatch A transaction batch object
|
||||
* @returns A transaction batch response object that can be awaited to get the transaction receipt
|
||||
*/
|
||||
async sendTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
await this.initPromise;
|
||||
|
||||
const validatedTransactionBatch = await this._validateTransactionBatch(
|
||||
transactionBatch,
|
||||
);
|
||||
|
||||
let nonce: BigNumber;
|
||||
if (transactionBatch.batchOptions) {
|
||||
const validatedBatchOptions = await this._validateBatchOptions(
|
||||
transactionBatch.batchOptions,
|
||||
);
|
||||
|
||||
nonce = validatedBatchOptions.nonce as BigNumber;
|
||||
nonce = BigNumber.from(validatedTransactionBatch.batchOptions!.nonce);
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -166,11 +195,7 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
const actions: Array<ActionData> = transactionBatch.transactions.map(
|
||||
(transaction, i) => {
|
||||
if (!transaction.to) {
|
||||
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
|
||||
}
|
||||
|
||||
(transaction) => {
|
||||
return {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to!.toString(),
|
||||
@@ -182,11 +207,13 @@ export default class BlsSigner extends Signer {
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const bundleWithFeePaymentAction = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
bundleWithFeePaymentAction,
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
@@ -198,7 +225,7 @@ export default class BlsSigner extends Signer {
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -208,14 +235,27 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error(JSON.stringify(result.failures));
|
||||
}
|
||||
|
||||
return this.constructTransactionBatchResponse(
|
||||
actions,
|
||||
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
|
||||
bundle.senderPublicKeys.map((publicKey, i) => {
|
||||
const operation = bundle.operations[i];
|
||||
const actions = operation.actions;
|
||||
|
||||
return {
|
||||
publicKey,
|
||||
actions,
|
||||
};
|
||||
});
|
||||
|
||||
return await this.provider._constructTransactionBatchResponse(
|
||||
publicKeysLinkedToActions,
|
||||
result.hash,
|
||||
this.wallet.address,
|
||||
nonce,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns The address associated with the BlsSigner
|
||||
*/
|
||||
async getAddress(): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (this._address) {
|
||||
@@ -226,83 +266,6 @@ export default class BlsSigner extends Signer {
|
||||
return this._address;
|
||||
}
|
||||
|
||||
// Construct a response that follows the ethers TransactionResponse type
|
||||
async constructTransactionResponse(
|
||||
action: ActionData,
|
||||
hash: string,
|
||||
from: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
await this.initPromise;
|
||||
const chainId = await this.getChainId();
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId,
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.provider.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async constructTransactionBatchResponse(
|
||||
actions: Array<ActionData>,
|
||||
hash: string,
|
||||
from: string,
|
||||
nonce?: BigNumber,
|
||||
): Promise<TransactionBatchResponse> {
|
||||
await this.initPromise;
|
||||
const chainId = await this.getChainId();
|
||||
if (!nonce) {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
this.verificationGatewayAddress,
|
||||
this.provider,
|
||||
);
|
||||
}
|
||||
|
||||
const transactions: Array<ethers.providers.TransactionResponse> =
|
||||
actions.map((action) => {
|
||||
return {
|
||||
hash,
|
||||
to: action.contractAddress,
|
||||
from,
|
||||
nonce: nonce!.toNumber(),
|
||||
gasLimit: BigNumber.from("0x0"),
|
||||
data: action.encodedFunction.toString(),
|
||||
value: BigNumber.from(action.ethValue),
|
||||
chainId,
|
||||
type: 2,
|
||||
confirmations: 1,
|
||||
wait: (confirmations?: number) => {
|
||||
return this.provider.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
transactions,
|
||||
awaitBatchReceipt: (confirmations?: number) => {
|
||||
return this.provider.waitForTransaction(hash, confirmations);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* This method passes calls through to the underlying node and allows users to unlock EOA accounts through this provider.
|
||||
* The personal namespace is used to manage keys for ECDSA signing. BLS keys are not supported natively by execution clients.
|
||||
@@ -319,19 +282,23 @@ export default class BlsSigner extends Signer {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @remarks Signs a transaction that can be executed by the BlsProvider
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns A signed bundle as a string
|
||||
*/
|
||||
override async signTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<string> {
|
||||
await this.initPromise;
|
||||
|
||||
if (!transaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
const validatedTransaction = await this._validateTransaction(transaction);
|
||||
|
||||
const action: ActionData = {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to.toString(),
|
||||
encodedFunction: transaction.data?.toString() ?? "0x",
|
||||
ethValue: validatedTransaction.value?.toString() ?? "0",
|
||||
contractAddress: validatedTransaction.to!.toString(),
|
||||
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
|
||||
};
|
||||
|
||||
const nonce = await BlsWalletWrapper.Nonce(
|
||||
@@ -340,14 +307,14 @@ export default class BlsSigner extends Signer {
|
||||
this.provider,
|
||||
);
|
||||
|
||||
const feeEstimate = await this.provider.estimateGas(transaction);
|
||||
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
|
||||
|
||||
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
|
||||
[action],
|
||||
feeEstimate,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -355,18 +322,24 @@ export default class BlsSigner extends Signer {
|
||||
return JSON.stringify(bundleToDto(bundle));
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a transaction batch that can be executed by the BlsProvider
|
||||
*
|
||||
* @param transactionBatch A transaction batch object
|
||||
* @returns A signed bundle containing all transactions from the transaction batch as a string
|
||||
*/
|
||||
async signTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<string> {
|
||||
await this.initPromise;
|
||||
|
||||
const validatedTransactionBatch = await this._validateTransactionBatch(
|
||||
transactionBatch,
|
||||
);
|
||||
|
||||
let nonce: BigNumber;
|
||||
if (transactionBatch.batchOptions) {
|
||||
const validatedBatchOptions = await this._validateBatchOptions(
|
||||
transactionBatch.batchOptions,
|
||||
);
|
||||
|
||||
nonce = validatedBatchOptions.nonce as BigNumber;
|
||||
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
|
||||
} else {
|
||||
nonce = await BlsWalletWrapper.Nonce(
|
||||
this.wallet.PublicKey(),
|
||||
@@ -376,11 +349,7 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
const actions: Array<ActionData> = transactionBatch.transactions.map(
|
||||
(transaction, i) => {
|
||||
if (!transaction.to) {
|
||||
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
|
||||
}
|
||||
|
||||
(transaction) => {
|
||||
return {
|
||||
ethValue: transaction.value?.toString() ?? "0",
|
||||
contractAddress: transaction.to!.toString(),
|
||||
@@ -392,11 +361,13 @@ export default class BlsSigner extends Signer {
|
||||
const actionsWithFeePaymentAction =
|
||||
this.provider._addFeePaymentActionForFeeEstimation(actions);
|
||||
|
||||
const bundleWithFeePaymentAction = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
});
|
||||
|
||||
const feeEstimate = await this.provider.aggregator.estimateFee(
|
||||
this.wallet.sign({
|
||||
nonce,
|
||||
actions: [...actionsWithFeePaymentAction],
|
||||
}),
|
||||
bundleWithFeePaymentAction,
|
||||
);
|
||||
|
||||
const safeFee = addSafetyPremiumToFee(
|
||||
@@ -408,7 +379,7 @@ export default class BlsSigner extends Signer {
|
||||
safeFee,
|
||||
);
|
||||
|
||||
const bundle = this.wallet.sign({
|
||||
const bundle = await this.wallet.signWithGasEstimate({
|
||||
nonce,
|
||||
actions: [...actionsWithSafeFee],
|
||||
});
|
||||
@@ -416,8 +387,15 @@ export default class BlsSigner extends Signer {
|
||||
return JSON.stringify(bundleToDto(bundle));
|
||||
}
|
||||
|
||||
/** Sign a message */
|
||||
// TODO: Come back to this once we support EIP-1271
|
||||
/**
|
||||
* Signs a message. Because of the function signature enforced by ethers, we cannot return the signature
|
||||
* in it's default type. Instead, we return a concatenated string of the signature.
|
||||
*
|
||||
* Use BlsSigner.signedMessageToSignature to convert the concatenated signature string into a BLS Signature type.
|
||||
*
|
||||
* @param message the message to be signed
|
||||
* @returns a concatenated string of the signature
|
||||
*/
|
||||
override async signMessage(message: Bytes | string): Promise<string> {
|
||||
await this.initPromise;
|
||||
if (isBytes(message)) {
|
||||
@@ -425,7 +403,18 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
const signedMessage = this.wallet.signMessage(message);
|
||||
return RLP.encode(signedMessage);
|
||||
return (
|
||||
ethers.utils.hexlify(signedMessage[0]) +
|
||||
ethers.utils.hexlify(signedMessage[1]).substring(2)
|
||||
);
|
||||
}
|
||||
|
||||
/** helper method to convert blsSigner.signMessage concatenated signature string into BLS Signature type */
|
||||
static signedMessageToSignature(signedMessage: string): Signature {
|
||||
return [
|
||||
ethers.utils.hexlify(signedMessage.substring(0, 66)),
|
||||
ethers.utils.hexlify("0x" + signedMessage.substring(66, 130)),
|
||||
];
|
||||
}
|
||||
|
||||
override connect(provider: ethers.providers.Provider): BlsSigner {
|
||||
@@ -440,6 +429,9 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error("_signTypedData() is not implemented");
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns A new Signer object which does not perform additional checks when sending a transaction
|
||||
*/
|
||||
connectUnchecked(): BlsSigner {
|
||||
return new UncheckedBlsSigner(
|
||||
_constructorGuard,
|
||||
@@ -453,6 +445,10 @@ export default class BlsSigner extends Signer {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param transaction Transaction request object
|
||||
* @returns Transaction hash for the transaction, corresponds to a bundle hash
|
||||
*/
|
||||
async sendUncheckedTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<string> {
|
||||
@@ -464,6 +460,54 @@ export default class BlsSigner extends Signer {
|
||||
throw new Error("_legacySignMessage() is not implemented");
|
||||
}
|
||||
|
||||
async _validateTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionRequest> {
|
||||
const resolvedTransaction = await ethers.utils.resolveProperties(
|
||||
transaction,
|
||||
);
|
||||
|
||||
if (!resolvedTransaction.to) {
|
||||
throw new TypeError("Transaction.to should be defined");
|
||||
}
|
||||
|
||||
if (!resolvedTransaction.from) {
|
||||
resolvedTransaction.from = await this.getAddress();
|
||||
}
|
||||
|
||||
return resolvedTransaction;
|
||||
}
|
||||
|
||||
async _validateTransactionBatch(
|
||||
transactionBatch: TransactionBatch,
|
||||
): Promise<TransactionBatch> {
|
||||
const signerAddress = await this.getAddress();
|
||||
|
||||
const validatedTransactions: Array<ethers.providers.TransactionRequest> =
|
||||
transactionBatch.transactions.map((transaction, i) => {
|
||||
if (!transaction.to) {
|
||||
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
|
||||
}
|
||||
|
||||
if (!transaction.from) {
|
||||
transaction.from = signerAddress;
|
||||
}
|
||||
|
||||
return {
|
||||
...transaction,
|
||||
};
|
||||
});
|
||||
|
||||
const validatedBatchOptions = transactionBatch.batchOptions
|
||||
? await this._validateBatchOptions(transactionBatch.batchOptions)
|
||||
: transactionBatch.batchOptions;
|
||||
|
||||
return {
|
||||
transactions: validatedTransactions,
|
||||
batchOptions: validatedBatchOptions,
|
||||
};
|
||||
}
|
||||
|
||||
async _validateBatchOptions(
|
||||
batchOptions: BatchOptions,
|
||||
): Promise<BatchOptions> {
|
||||
@@ -481,6 +525,12 @@ export default class BlsSigner extends Signer {
|
||||
}
|
||||
|
||||
export class UncheckedBlsSigner extends BlsSigner {
|
||||
/**
|
||||
* As with other transaction methods, the transaction hash returned represents the bundle hash, NOT a transaction hash you can use on a block explorer
|
||||
*
|
||||
* @param transaction Transaction request object
|
||||
* @returns The transaction response object with only the transaction hash property populated with a valid value
|
||||
*/
|
||||
override async sendTransaction(
|
||||
transaction: Deferrable<ethers.providers.TransactionRequest>,
|
||||
): Promise<ethers.providers.TransactionResponse> {
|
||||
@@ -489,7 +539,7 @@ export class UncheckedBlsSigner extends BlsSigner {
|
||||
const transactionResponse = await super.sendTransaction(transaction);
|
||||
return {
|
||||
hash: transactionResponse.hash,
|
||||
nonce: 1,
|
||||
nonce: NaN,
|
||||
gasLimit: BigNumber.from(0),
|
||||
gasPrice: BigNumber.from(0),
|
||||
data: "",
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { providers } from "ethers";
|
||||
import {
|
||||
BNPairingPrecompileCostEstimator,
|
||||
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
|
||||
Create2Deployer,
|
||||
Create2Deployer__factory as Create2DeployerFactory,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
BLSOpen,
|
||||
BLSOpen__factory as BLSOpenFactory,
|
||||
BLSExpander,
|
||||
BLSExpander__factory as BLSExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
@@ -21,10 +15,7 @@ import { NetworkConfig } from "./NetworkConfig";
|
||||
* BLS Wallet Contracts
|
||||
*/
|
||||
export type BlsWalletContracts = Readonly<{
|
||||
create2Deployer: Create2Deployer;
|
||||
precompileCostEstimator: BNPairingPrecompileCostEstimator;
|
||||
verificationGateway: VerificationGateway;
|
||||
blsLibrary: BLSOpen;
|
||||
blsExpander: BLSExpander;
|
||||
aggregatorUtilities: AggregatorUtilities;
|
||||
testToken: MockERC20;
|
||||
@@ -41,32 +32,19 @@ export const connectToContracts = async (
|
||||
provider: providers.Provider,
|
||||
{ addresses }: NetworkConfig,
|
||||
): Promise<BlsWalletContracts> => {
|
||||
const [
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
] = await Promise.all([
|
||||
Create2DeployerFactory.connect(addresses.create2Deployer, provider),
|
||||
BNPairingPrecompileCostEstimatorFactory.connect(
|
||||
addresses.create2Deployer,
|
||||
provider,
|
||||
),
|
||||
VerificationGatewayFactory.connect(addresses.verificationGateway, provider),
|
||||
BLSOpenFactory.connect(addresses.blsLibrary, provider),
|
||||
BLSExpanderFactory.connect(addresses.blsExpander, provider),
|
||||
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
|
||||
MockERC20Factory.connect(addresses.testToken, provider),
|
||||
]);
|
||||
const [verificationGateway, blsExpander, aggregatorUtilities, testToken] =
|
||||
await Promise.all([
|
||||
VerificationGatewayFactory.connect(
|
||||
addresses.verificationGateway,
|
||||
provider,
|
||||
),
|
||||
BLSExpanderFactory.connect(addresses.blsExpander, provider),
|
||||
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
|
||||
MockERC20Factory.connect(addresses.testToken, provider),
|
||||
]);
|
||||
|
||||
return {
|
||||
create2Deployer,
|
||||
precompileCostEstimator,
|
||||
verificationGateway,
|
||||
blsLibrary,
|
||||
blsExpander,
|
||||
aggregatorUtilities,
|
||||
testToken,
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { keccak256, solidityKeccak256, solidityPack } from "ethers/lib/utils";
|
||||
import {
|
||||
@@ -13,10 +11,10 @@ import {
|
||||
|
||||
import {
|
||||
BLSWallet,
|
||||
BLSWallet__factory,
|
||||
TransparentUpgradeableProxy__factory,
|
||||
BLSWallet__factory as BLSWalletFactory,
|
||||
TransparentUpgradeableProxy__factory as TransparentUpgradeableProxyFactory,
|
||||
VerificationGateway,
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
} from "../typechain-types";
|
||||
|
||||
import getRandomBlsPrivateKey from "./signer/getRandomBlsPrivateKey";
|
||||
@@ -28,9 +26,11 @@ type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
|
||||
*/
|
||||
export default class BlsWalletWrapper {
|
||||
public address: string;
|
||||
public blockGasLimit: BigNumber = BigNumber.from(0);
|
||||
private constructor(
|
||||
public blsWalletSigner: BlsWalletSigner,
|
||||
public walletContract: BLSWallet,
|
||||
public defaultGatewayAddress: string,
|
||||
) {
|
||||
this.address = walletContract.address;
|
||||
}
|
||||
@@ -45,7 +45,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
return BLSWallet__factory.connect(
|
||||
return BLSWalletFactory.connect(
|
||||
contractAddress,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
@@ -74,9 +74,10 @@ export default class BlsWalletWrapper {
|
||||
const blsWalletSigner = await this.#BlsWalletSigner(
|
||||
signerOrProvider,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
);
|
||||
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -140,19 +141,24 @@ export default class BlsWalletWrapper {
|
||||
verificationGatewayAddress: string,
|
||||
provider: ethers.providers.Provider,
|
||||
): Promise<BlsWalletWrapper> {
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
provider,
|
||||
);
|
||||
const blsWalletSigner = await initBlsWalletSigner({
|
||||
chainId: (await verificationGateway.provider.getNetwork()).chainId,
|
||||
verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const blsWalletWrapper = new BlsWalletWrapper(
|
||||
blsWalletSigner,
|
||||
await BlsWalletWrapper.BLSWallet(privateKey, verificationGateway),
|
||||
verificationGateway.address,
|
||||
);
|
||||
blsWalletWrapper.blockGasLimit = (
|
||||
await provider.getBlock("latest")
|
||||
).gasLimit;
|
||||
|
||||
return blsWalletWrapper;
|
||||
}
|
||||
@@ -164,7 +170,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
this.walletContract = BLSWallet__factory.connect(
|
||||
this.walletContract = BLSWalletFactory.connect(
|
||||
this.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
@@ -194,7 +200,7 @@ export default class BlsWalletWrapper {
|
||||
verificationGatewayAddress: string,
|
||||
signerOrProvider: SignerOrProvider,
|
||||
): Promise<BigNumber> {
|
||||
const verificationGateway = VerificationGateway__factory.connect(
|
||||
const verificationGateway = VerificationGatewayFactory.connect(
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -208,7 +214,7 @@ export default class BlsWalletWrapper {
|
||||
publicKeyHash,
|
||||
);
|
||||
|
||||
const walletContract = BLSWallet__factory.connect(
|
||||
const walletContract = BLSWalletFactory.connect(
|
||||
contractAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
@@ -225,6 +231,46 @@ export default class BlsWalletWrapper {
|
||||
return await walletContract.nonce();
|
||||
}
|
||||
|
||||
/** Sign an operation with an estimate of the gas required. */
|
||||
async signWithGasEstimate(
|
||||
operation: Omit<Operation, "gas">,
|
||||
|
||||
/**
|
||||
* Optional: Multiply estimate by `(1+overhead)` to account for uncertainty.
|
||||
* Reduces the chance of running out of gas.
|
||||
*/
|
||||
overhead = 0,
|
||||
): Promise<Bundle> {
|
||||
let gas = await this.estimateGas(operation);
|
||||
gas = gas.add(gas.div(10000).mul(Math.ceil(overhead * 10000)));
|
||||
|
||||
return this.sign({ ...operation, gas });
|
||||
}
|
||||
|
||||
/** Estimate the gas needed for an operation. */
|
||||
async estimateGas(operation: Omit<Operation, "gas">): Promise<BigNumber> {
|
||||
const exists =
|
||||
(await this.walletContract.provider.getCode(this.address)) !== "0x";
|
||||
|
||||
const gatewayAddress = exists
|
||||
? await this.walletContract.trustedBLSGateway()
|
||||
: this.defaultGatewayAddress;
|
||||
|
||||
const gateway = VerificationGatewayFactory.connect(
|
||||
gatewayAddress,
|
||||
this.walletContract.provider,
|
||||
);
|
||||
|
||||
const gas = await gateway
|
||||
.connect(ethers.constants.AddressZero)
|
||||
.callStatic.measureOperationGas(this.PublicKey(), {
|
||||
...operation,
|
||||
gas: this.blockGasLimit,
|
||||
});
|
||||
|
||||
return gas;
|
||||
}
|
||||
|
||||
/** Sign an operation, producing a `Bundle` object suitable for use with an aggregator. */
|
||||
sign(operation: Operation): Bundle {
|
||||
return this.blsWalletSigner.sign(operation, this.walletContract.address);
|
||||
@@ -264,7 +310,7 @@ export default class BlsWalletWrapper {
|
||||
[recoverWalletAddress, walletHash, saltHash],
|
||||
);
|
||||
|
||||
return this.sign({
|
||||
return await this.signWithGasEstimate({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -284,13 +330,17 @@ export default class BlsWalletWrapper {
|
||||
newPrivateKey: string,
|
||||
recoverySalt: string,
|
||||
verificationGateway: VerificationGateway,
|
||||
signatureExpiryTimestamp: number,
|
||||
): Promise<Bundle> {
|
||||
const updatedWallet = await BlsWalletWrapper.connect(
|
||||
newPrivateKey,
|
||||
verificationGateway.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
const addressMessage = solidityPack(["address"], [recoveryAddress]);
|
||||
const addressMessage = solidityPack(
|
||||
["address", "uint256"],
|
||||
[recoveryAddress, signatureExpiryTimestamp],
|
||||
);
|
||||
const addressSignature = updatedWallet.signMessage(addressMessage);
|
||||
|
||||
const recoveryWalletHash = await verificationGateway.hashFromWallet(
|
||||
@@ -298,7 +348,7 @@ export default class BlsWalletWrapper {
|
||||
);
|
||||
const saltHash = ethers.utils.formatBytes32String(recoverySalt);
|
||||
|
||||
return this.sign({
|
||||
return await this.signWithGasEstimate({
|
||||
nonce: await this.Nonce(),
|
||||
actions: [
|
||||
{
|
||||
@@ -311,6 +361,7 @@ export default class BlsWalletWrapper {
|
||||
recoveryWalletHash,
|
||||
saltHash,
|
||||
updatedWallet.PublicKey(),
|
||||
signatureExpiryTimestamp,
|
||||
],
|
||||
),
|
||||
},
|
||||
@@ -321,36 +372,18 @@ export default class BlsWalletWrapper {
|
||||
static async #BlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
verificationGatewayAddress: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
return await initBlsWalletSigner({ chainId, privateKey });
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the BlsWalletSigner instance to a new private key and chainId
|
||||
*
|
||||
* @returns The updated BlsWalletSigner object
|
||||
*/
|
||||
async setBlsWalletSigner(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
privateKey: string,
|
||||
): Promise<BlsWalletSigner> {
|
||||
const chainId =
|
||||
"getChainId" in signerOrProvider
|
||||
? await signerOrProvider.getChainId()
|
||||
: (await signerOrProvider.getNetwork()).chainId;
|
||||
|
||||
const newBlsWalletSigner = await initBlsWalletSigner({
|
||||
return await initBlsWalletSigner({
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
});
|
||||
|
||||
this.blsWalletSigner = newBlsWalletSigner;
|
||||
return newBlsWalletSigner;
|
||||
}
|
||||
|
||||
// Calculates the expected address the wallet will be created at
|
||||
@@ -364,7 +397,7 @@ export default class BlsWalletWrapper {
|
||||
]);
|
||||
|
||||
const initFunctionParams =
|
||||
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
|
||||
BLSWalletFactory.createInterface().encodeFunctionData("initialize", [
|
||||
verificationGateway.address,
|
||||
]);
|
||||
|
||||
@@ -374,7 +407,7 @@ export default class BlsWalletWrapper {
|
||||
ethers.utils.solidityKeccak256(
|
||||
["bytes", "bytes"],
|
||||
[
|
||||
TransparentUpgradeableProxy__factory.bytecode,
|
||||
TransparentUpgradeableProxyFactory.bytecode,
|
||||
ethers.utils.defaultAbiCoder.encode(
|
||||
["address", "address", "bytes"],
|
||||
[blsWalletLogicAddress, proxyAdminAddress, initFunctionParams],
|
||||
|
||||
98
contracts/clients/src/BundleCompressor.ts
Normal file
98
contracts/clients/src/BundleCompressor.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { ethers } from "ethers";
|
||||
import { encodeVLQ, hexJoin } from "./encodeUtils";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import { Bundle, Operation, PublicKey } from "./signer";
|
||||
import Range from "./helpers/Range";
|
||||
import { BLSExpanderDelegator } from "../typechain-types";
|
||||
|
||||
/**
|
||||
* Produces compressed bundles that can be passed to `BLSExpanderDelegator.run`
|
||||
* instead of `VerificationGateway.processBundle`.
|
||||
*
|
||||
* The compression of operations is delegated to other compressors that you
|
||||
* inject using `.addCompressor`. For each operation of the bundle, these
|
||||
* compressors are tried in the order they were added, and the first one that
|
||||
* succeeds is used. Note that `expanderIndex` is unrelated to this order - it
|
||||
* just needs to match the index that the corresponding expander contract is
|
||||
* registered at in BLSExpanderDelegator.
|
||||
*/
|
||||
export default class BundleCompressor {
|
||||
compressors: [number, IOperationCompressor][] = [];
|
||||
|
||||
constructor(public blsExpanderDelegator: BLSExpanderDelegator) {}
|
||||
|
||||
/** Add an operation compressor. */
|
||||
async addCompressor(compressor: IOperationCompressor) {
|
||||
const registrations = await this.blsExpanderDelegator.queryFilter(
|
||||
this.blsExpanderDelegator.filters.ExpanderRegistered(
|
||||
null,
|
||||
compressor.getExpanderAddress(),
|
||||
),
|
||||
);
|
||||
|
||||
const id = registrations.at(0)?.args?.id;
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error("Expander not registered");
|
||||
}
|
||||
|
||||
this.compressors.push([id.toNumber(), compressor]);
|
||||
}
|
||||
|
||||
/** Compresses a single operation. */
|
||||
async compressOperation(
|
||||
blsPublicKey: PublicKey,
|
||||
operation: Operation,
|
||||
): Promise<string> {
|
||||
let expanderIndexAndData: [number, string] | undefined;
|
||||
|
||||
for (const [expanderIndex, compressor] of this.compressors) {
|
||||
let data: string | undefined;
|
||||
|
||||
try {
|
||||
data = await compressor.compress(blsPublicKey, operation);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (data === undefined) {
|
||||
continue;
|
||||
}
|
||||
|
||||
expanderIndexAndData = [expanderIndex, data];
|
||||
break;
|
||||
}
|
||||
|
||||
if (expanderIndexAndData === undefined) {
|
||||
throw new Error("Failed to compress operation");
|
||||
}
|
||||
|
||||
const [expanderIndex, data] = expanderIndexAndData;
|
||||
|
||||
return hexJoin([encodeVLQ(expanderIndex), data]);
|
||||
}
|
||||
|
||||
/** Compresses a bundle. */
|
||||
async compress(bundle: Bundle): Promise<string> {
|
||||
const len = bundle.operations.length;
|
||||
|
||||
if (bundle.senderPublicKeys.length !== len) {
|
||||
throw new Error("ops vs keys length mismatch");
|
||||
}
|
||||
|
||||
const compressedOperations = await Promise.all(
|
||||
Range(len).map((i) =>
|
||||
this.compressOperation(
|
||||
bundle.senderPublicKeys[i],
|
||||
bundle.operations[i],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return hexJoin([
|
||||
encodeVLQ(bundle.operations.length),
|
||||
...compressedOperations,
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[2]"], [bundle.signature]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
165
contracts/clients/src/ContractsConnector.ts
Normal file
165
contracts/clients/src/ContractsConnector.ts
Normal file
@@ -0,0 +1,165 @@
|
||||
import { ethers } from "ethers";
|
||||
import { SafeSingletonFactoryViewer } from "./SafeSingletonFactory";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import assert from "./helpers/assert";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
BLSExpanderDelegator__factory as BLSExpanderDelegatorFactory,
|
||||
BLSExpander__factory as BLSExpanderFactory,
|
||||
BLSOpen__factory as BLSOpenFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
BLSRegistration__factory as BLSRegistrationFactory,
|
||||
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
|
||||
ERC20Expander__factory as ERC20ExpanderFactory,
|
||||
ExpanderEntryPoint__factory as ExpanderEntryPointFactory,
|
||||
FallbackExpander__factory as FallbackExpanderFactory,
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
} from "../typechain-types";
|
||||
|
||||
export default class ContractsConnector {
|
||||
constructor(
|
||||
public factoryViewer: SafeSingletonFactoryViewer,
|
||||
public salt: ethers.utils.BytesLike = ethers.utils.solidityPack(
|
||||
["uint256"],
|
||||
[0],
|
||||
),
|
||||
) {}
|
||||
|
||||
static async create(signerOrProvider: SignerOrProvider) {
|
||||
let provider: ethers.providers.Provider;
|
||||
|
||||
if ("getNetwork" in signerOrProvider) {
|
||||
provider = signerOrProvider;
|
||||
} else {
|
||||
assert(
|
||||
signerOrProvider.provider !== undefined,
|
||||
"When using a signer, it's required to have a provider",
|
||||
);
|
||||
|
||||
provider = signerOrProvider.provider;
|
||||
}
|
||||
|
||||
const chainId = (await provider.getNetwork()).chainId;
|
||||
|
||||
const factoryViewer = new SafeSingletonFactoryViewer(
|
||||
signerOrProvider,
|
||||
chainId,
|
||||
);
|
||||
|
||||
return new ContractsConnector(factoryViewer);
|
||||
}
|
||||
|
||||
BNPairingPrecompileCostEstimator = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BNPairingPrecompileCostEstimatorFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSOpen = once(() =>
|
||||
this.factoryViewer.connectOrThrow(BLSOpenFactory, [], this.salt),
|
||||
);
|
||||
|
||||
VerificationGateway = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
VerificationGatewayFactory,
|
||||
[(await this.BLSOpen()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
AggregatorUtilities = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
AggregatorUtilitiesFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSExpander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSExpanderFactory,
|
||||
[(await this.VerificationGateway()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSExpanderDelegator = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSExpanderDelegatorFactory,
|
||||
[(await this.VerificationGateway()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSPublicKeyRegistry = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSPublicKeyRegistryFactory,
|
||||
[],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
AddressRegistry = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(AddressRegistryFactory, [], this.salt),
|
||||
);
|
||||
|
||||
FallbackExpander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
FallbackExpanderFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
BLSRegistration = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
BLSRegistrationFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
ERC20Expander = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
ERC20ExpanderFactory,
|
||||
await Promise.all([
|
||||
this.BLSPublicKeyRegistry().then((c) => c.address),
|
||||
this.AddressRegistry().then((c) => c.address),
|
||||
this.AggregatorUtilities().then((c) => c.address),
|
||||
]),
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
|
||||
ExpanderEntryPoint = once(async () =>
|
||||
this.factoryViewer.connectOrThrow(
|
||||
ExpanderEntryPointFactory,
|
||||
[(await this.BLSExpanderDelegator()).address],
|
||||
this.salt,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function once<T extends {}>(fn: () => T): () => T {
|
||||
let result: T | undefined;
|
||||
|
||||
return () => {
|
||||
if (result === undefined) {
|
||||
result = fn();
|
||||
(fn as unknown as undefined) = undefined;
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
}
|
||||
394
contracts/clients/src/Erc20Compressor.ts
Normal file
394
contracts/clients/src/Erc20Compressor.ts
Normal file
@@ -0,0 +1,394 @@
|
||||
import { BigNumber, ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
ERC20Expander,
|
||||
ERC20Expander__factory as ERC20ExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import {
|
||||
encodeBitStream,
|
||||
encodePseudoFloat,
|
||||
encodeRegIndex,
|
||||
encodeVLQ,
|
||||
hexJoin,
|
||||
} from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { ActionData, Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class Erc20Compressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public erc20Expander: ERC20Expander,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(erc20Expander: ERC20Expander): Promise<Erc20Compressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
erc20Expander.blsPublicKeyRegistry(),
|
||||
erc20Expander.addressRegistry(),
|
||||
erc20Expander.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20Expander,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
erc20Expander.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<Erc20Compressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const erc20ExpanderFactory = new ERC20ExpanderFactory(signer);
|
||||
|
||||
const erc20ExpanderContract = await erc20ExpanderFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20ExpanderContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<Erc20Compressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const erc20ExpanderContract = await factory.connectOrDeploy(
|
||||
ERC20ExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new Erc20Compressor(
|
||||
erc20ExpanderContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<Erc20Compressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
factoryViewer.calculateAddress(BLSPublicKeyRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AddressRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const erc20Expander = await factoryViewer.connectIfDeployed(
|
||||
ERC20ExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!erc20Expander) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await Erc20Compressor.wrap(erc20Expander);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.erc20Expander.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
const result: string[] = [];
|
||||
|
||||
const resultIndexForRegUsageBitStream = result.length;
|
||||
const bitStream: boolean[] = [];
|
||||
result.push("0x"); // Placeholder to overwrite
|
||||
|
||||
const blsPublicKeyId = await this.blsPublicKeyRegistry.reverseLookup(
|
||||
blsPublicKey,
|
||||
);
|
||||
|
||||
if (blsPublicKeyId === undefined) {
|
||||
bitStream.push(false);
|
||||
|
||||
result.push(
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(blsPublicKeyId));
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(operation.nonce));
|
||||
result.push(encodePseudoFloat(operation.gas));
|
||||
|
||||
result.push(encodeVLQ(operation.actions.length));
|
||||
|
||||
const lastAction = operation.actions.at(-1);
|
||||
let txOriginPaymentAction: ActionData | undefined;
|
||||
|
||||
let regularActions: ActionData[];
|
||||
|
||||
if (
|
||||
lastAction !== undefined &&
|
||||
lastAction.contractAddress === this.aggregatorUtilities.address &&
|
||||
ethers.utils.hexlify(lastAction.encodedFunction) ===
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
bitStream.push(true);
|
||||
txOriginPaymentAction = lastAction;
|
||||
regularActions = operation.actions.slice(0, -1);
|
||||
} else {
|
||||
bitStream.push(false);
|
||||
regularActions = operation.actions;
|
||||
}
|
||||
|
||||
for (const action of regularActions) {
|
||||
const addressId = await this.addressRegistry.reverseLookup(
|
||||
action.contractAddress,
|
||||
);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(action.contractAddress);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
|
||||
const success = await this.compressFunctionCall(
|
||||
action.encodedFunction,
|
||||
result,
|
||||
bitStream,
|
||||
);
|
||||
|
||||
if (!success) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (txOriginPaymentAction !== undefined) {
|
||||
result.push(encodePseudoFloat(txOriginPaymentAction.ethValue));
|
||||
}
|
||||
|
||||
result[resultIndexForRegUsageBitStream] = encodeBitStream(bitStream);
|
||||
|
||||
return hexJoin(result);
|
||||
}
|
||||
|
||||
private async compressFunctionCall(
|
||||
encodedFunction: ethers.utils.BytesLike,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
const encodedFunctionHex = ethers.utils.hexlify(encodedFunction);
|
||||
const parametersHex = ethers.utils.hexDataSlice(encodedFunctionHex, 4);
|
||||
|
||||
if (isMethod("transfer(address,uint256)", encodedFunction)) {
|
||||
return this.compressTransfer(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("transferFrom(address,address,uint256)", encodedFunction)) {
|
||||
return this.compressTransferFrom(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("approve(address,uint256)", encodedFunction)) {
|
||||
return this.compressApprove(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
if (isMethod("mint(address,uint256)", encodedFunction)) {
|
||||
return this.compressMint(parametersHex, result, bitStream);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private async compressTransfer(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(0));
|
||||
|
||||
const [to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressTransferFrom(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 3 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(1));
|
||||
|
||||
const [from, to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, string, BigNumber];
|
||||
|
||||
await this.compressAddress(from, result, bitStream);
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressApprove(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const [spender, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
if (value.eq(ethers.constants.MaxUint256)) {
|
||||
result.push(encodeVLQ(3));
|
||||
await this.compressAddress(spender, result, bitStream);
|
||||
} else {
|
||||
result.push(encodeVLQ(2));
|
||||
await this.compressAddress(spender, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressMint(
|
||||
parametersHex: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
): Promise<boolean> {
|
||||
if (ethers.utils.hexDataLength(parametersHex) !== 2 * 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(4));
|
||||
|
||||
const [to, value] = ethers.utils.defaultAbiCoder.decode(
|
||||
["address", "uint256"],
|
||||
parametersHex,
|
||||
) as [string, BigNumber];
|
||||
|
||||
await this.compressAddress(to, result, bitStream);
|
||||
result.push(encodePseudoFloat(value));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private async compressAddress(
|
||||
address: string,
|
||||
result: string[],
|
||||
bitStream: boolean[],
|
||||
) {
|
||||
const addressId = await this.addressRegistry.reverseLookup(address);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(address);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isMethod(
|
||||
signature: string,
|
||||
encodedFunction: ethers.utils.BytesLike,
|
||||
): boolean {
|
||||
const methodId = ethers.utils
|
||||
.keccak256(ethers.utils.toUtf8Bytes(signature))
|
||||
.slice(0, 10);
|
||||
|
||||
return ethers.utils.hexlify(encodedFunction).startsWith(methodId);
|
||||
}
|
||||
7
contracts/clients/src/Experimental.ts
Normal file
7
contracts/clients/src/Experimental.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* The Experimental namespace exposes APIs that are unstable.
|
||||
* Unstable in the sense that the APIs will be less functional, less well-tested, and/or are expected to change.
|
||||
*/
|
||||
|
||||
export { default as BlsProvider } from "./BlsProvider";
|
||||
export { default as BlsSigner } from "./BlsSigner";
|
||||
247
contracts/clients/src/FallbackCompressor.ts
Normal file
247
contracts/clients/src/FallbackCompressor.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
import { ethers, Signer } from "ethers";
|
||||
import {
|
||||
AddressRegistry__factory as AddressRegistryFactory,
|
||||
BLSPublicKeyRegistry__factory as BLSPublicKeyRegistryFactory,
|
||||
FallbackExpander,
|
||||
FallbackExpander__factory as FallbackExpanderFactory,
|
||||
AggregatorUtilities,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
} from "../typechain-types";
|
||||
import AddressRegistryWrapper from "./AddressRegistryWrapper";
|
||||
import BlsPublicKeyRegistryWrapper from "./BlsPublicKeyRegistryWrapper";
|
||||
import {
|
||||
encodeBitStream,
|
||||
encodePseudoFloat,
|
||||
encodeRegIndex,
|
||||
encodeVLQ,
|
||||
hexJoin,
|
||||
} from "./encodeUtils";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
import IOperationCompressor from "./IOperationCompressor";
|
||||
import SafeSingletonFactory, {
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
import { ActionData, Operation, PublicKey } from "./signer/types";
|
||||
|
||||
export default class FallbackCompressor implements IOperationCompressor {
|
||||
private constructor(
|
||||
public fallbackExpander: FallbackExpander,
|
||||
public blsPublicKeyRegistry: BlsPublicKeyRegistryWrapper,
|
||||
public addressRegistry: AddressRegistryWrapper,
|
||||
public aggregatorUtilities: AggregatorUtilities,
|
||||
) {}
|
||||
|
||||
static async wrap(
|
||||
fallbackExpander: FallbackExpander,
|
||||
): Promise<FallbackCompressor> {
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
fallbackExpander.blsPublicKeyRegistry(),
|
||||
fallbackExpander.addressRegistry(),
|
||||
fallbackExpander.aggregatorUtilities(),
|
||||
]);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpander,
|
||||
new BlsPublicKeyRegistryWrapper(
|
||||
BLSPublicKeyRegistryFactory.connect(
|
||||
blsPublicKeyRegistryAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
),
|
||||
new AddressRegistryWrapper(
|
||||
AddressRegistryFactory.connect(
|
||||
addressRegistryAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
),
|
||||
AggregatorUtilitiesFactory.connect(
|
||||
aggregatorUtilitiesAddress,
|
||||
fallbackExpander.signer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
static async deployNew(signer: Signer): Promise<FallbackCompressor> {
|
||||
const blsPublicKeyRegistryFactory = new BLSPublicKeyRegistryFactory(signer);
|
||||
const addressRegistryFactory = new AddressRegistryFactory(signer);
|
||||
const aggregatorUtilitiesFactory = new AggregatorUtilitiesFactory(signer);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryContract,
|
||||
addressRegistryContract,
|
||||
aggregatorUtilitiesContract,
|
||||
] = await Promise.all([
|
||||
blsPublicKeyRegistryFactory.deploy(),
|
||||
addressRegistryFactory.deploy(),
|
||||
aggregatorUtilitiesFactory.deploy(),
|
||||
]);
|
||||
|
||||
const fallbackExpanderFactory = new FallbackExpanderFactory(signer);
|
||||
|
||||
const fallbackExpanderContract = await fallbackExpanderFactory.deploy(
|
||||
blsPublicKeyRegistryContract.address,
|
||||
addressRegistryContract.address,
|
||||
aggregatorUtilitiesContract.address,
|
||||
);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpanderContract,
|
||||
new BlsPublicKeyRegistryWrapper(blsPublicKeyRegistryContract),
|
||||
new AddressRegistryWrapper(addressRegistryContract),
|
||||
aggregatorUtilitiesContract,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectOrDeploy(
|
||||
signerOrFactory: Signer | SafeSingletonFactory,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<FallbackCompressor> {
|
||||
const factory = await SafeSingletonFactory.from(signerOrFactory);
|
||||
|
||||
const [blsPublicKeyRegistry, addressRegistry, aggregatorUtilities] =
|
||||
await Promise.all([
|
||||
BlsPublicKeyRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
AddressRegistryWrapper.connectOrDeploy(factory, salt),
|
||||
factory.connectOrDeploy(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const fallbackExpanderContract = await factory.connectOrDeploy(
|
||||
FallbackExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistry.registry.address,
|
||||
addressRegistry.registry.address,
|
||||
aggregatorUtilities.address,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
return new FallbackCompressor(
|
||||
fallbackExpanderContract,
|
||||
blsPublicKeyRegistry,
|
||||
addressRegistry,
|
||||
aggregatorUtilities,
|
||||
);
|
||||
}
|
||||
|
||||
static async connectIfDeployed(
|
||||
signerOrProvider: SignerOrProvider,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<FallbackCompressor | undefined> {
|
||||
const factoryViewer = await SafeSingletonFactoryViewer.from(
|
||||
signerOrProvider,
|
||||
);
|
||||
|
||||
const [
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
] = await Promise.all([
|
||||
factoryViewer.calculateAddress(BLSPublicKeyRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AddressRegistryFactory, [], salt),
|
||||
factoryViewer.calculateAddress(AggregatorUtilitiesFactory, [], salt),
|
||||
]);
|
||||
|
||||
const fallbackExpander = await factoryViewer.connectIfDeployed(
|
||||
FallbackExpanderFactory,
|
||||
[
|
||||
blsPublicKeyRegistryAddress,
|
||||
addressRegistryAddress,
|
||||
aggregatorUtilitiesAddress,
|
||||
],
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!fallbackExpander) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return await FallbackCompressor.wrap(fallbackExpander);
|
||||
}
|
||||
|
||||
getExpanderAddress(): string {
|
||||
return this.fallbackExpander.address;
|
||||
}
|
||||
|
||||
async compress(blsPublicKey: PublicKey, operation: Operation) {
|
||||
const result: string[] = [];
|
||||
|
||||
const resultIndexForRegUsageBitStream = result.length;
|
||||
const bitStream: boolean[] = [];
|
||||
result.push("0x"); // Placeholder to overwrite
|
||||
|
||||
const blsPublicKeyId = await this.blsPublicKeyRegistry.reverseLookup(
|
||||
blsPublicKey,
|
||||
);
|
||||
|
||||
if (blsPublicKeyId === undefined) {
|
||||
bitStream.push(false);
|
||||
|
||||
result.push(
|
||||
ethers.utils.defaultAbiCoder.encode(["uint256[4]"], [blsPublicKey]),
|
||||
);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(blsPublicKeyId));
|
||||
}
|
||||
|
||||
result.push(encodeVLQ(operation.nonce));
|
||||
result.push(encodePseudoFloat(operation.gas));
|
||||
|
||||
result.push(encodeVLQ(operation.actions.length));
|
||||
|
||||
const lastAction = operation.actions.at(-1);
|
||||
let txOriginPaymentAction: ActionData | undefined;
|
||||
|
||||
let regularActions: ActionData[];
|
||||
|
||||
if (
|
||||
lastAction !== undefined &&
|
||||
lastAction.contractAddress === this.aggregatorUtilities.address &&
|
||||
ethers.utils.hexlify(lastAction.encodedFunction) ===
|
||||
this.aggregatorUtilities.interface.encodeFunctionData(
|
||||
"sendEthToTxOrigin",
|
||||
)
|
||||
) {
|
||||
bitStream.push(true);
|
||||
txOriginPaymentAction = lastAction;
|
||||
regularActions = operation.actions.slice(0, -1);
|
||||
} else {
|
||||
bitStream.push(false);
|
||||
regularActions = operation.actions;
|
||||
}
|
||||
|
||||
for (const action of regularActions) {
|
||||
result.push(encodePseudoFloat(action.ethValue));
|
||||
|
||||
const addressId = await this.addressRegistry.reverseLookup(
|
||||
action.contractAddress,
|
||||
);
|
||||
|
||||
if (addressId === undefined) {
|
||||
bitStream.push(false);
|
||||
result.push(action.contractAddress);
|
||||
} else {
|
||||
bitStream.push(true);
|
||||
result.push(encodeRegIndex(addressId));
|
||||
}
|
||||
|
||||
const fnHex = ethers.utils.hexlify(action.encodedFunction);
|
||||
const fnLen = (fnHex.length - 2) / 2;
|
||||
|
||||
result.push(encodeVLQ(fnLen));
|
||||
result.push(fnHex);
|
||||
}
|
||||
|
||||
if (txOriginPaymentAction !== undefined) {
|
||||
result.push(encodePseudoFloat(txOriginPaymentAction.ethValue));
|
||||
}
|
||||
|
||||
result[resultIndexForRegUsageBitStream] = encodeBitStream(bitStream);
|
||||
|
||||
return hexJoin(result);
|
||||
}
|
||||
}
|
||||
12
contracts/clients/src/IOperationCompressor.ts
Normal file
12
contracts/clients/src/IOperationCompressor.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Operation, PublicKey } from "./signer";
|
||||
|
||||
type IOperationCompressor = {
|
||||
getExpanderAddress(): string;
|
||||
|
||||
compress(
|
||||
blsPublicKey: PublicKey,
|
||||
operation: Operation,
|
||||
): Promise<string | undefined>;
|
||||
};
|
||||
|
||||
export default IOperationCompressor;
|
||||
@@ -10,10 +10,9 @@ export type NetworkConfig = {
|
||||
* Contract addresses
|
||||
*/
|
||||
addresses: {
|
||||
create2Deployer: string;
|
||||
safeSingletonFactory: string;
|
||||
precompileCostEstimator: string;
|
||||
verificationGateway: string;
|
||||
blsLibrary: string;
|
||||
blsExpander: string;
|
||||
utilities: string;
|
||||
testToken: string;
|
||||
@@ -24,11 +23,15 @@ export type NetworkConfig = {
|
||||
auxiliary: {
|
||||
chainid: number;
|
||||
/**
|
||||
* Domain used for BLS signing
|
||||
* Domain used for signing BLS Proof of Possession messages
|
||||
*/
|
||||
domain: string;
|
||||
walletDomain: string;
|
||||
/**
|
||||
* Starting block contracts began dpeloyment at
|
||||
* Domain used for signing BLS Bundle messages
|
||||
*/
|
||||
bundleDomain: string;
|
||||
/**
|
||||
* Starting block contracts began deployment at
|
||||
*/
|
||||
genesisBlock: number;
|
||||
/**
|
||||
@@ -54,19 +57,19 @@ export function validateConfig(cfg: UnvalidatedConfig): NetworkConfig {
|
||||
return {
|
||||
parameters: assertUnknownRecord(cfg.parameters),
|
||||
addresses: {
|
||||
create2Deployer: assertString(cfg.addresses.create2Deployer),
|
||||
safeSingletonFactory: assertString(cfg.addresses.safeSingletonFactory),
|
||||
precompileCostEstimator: assertString(
|
||||
cfg.addresses.precompileCostEstimator,
|
||||
),
|
||||
verificationGateway: assertString(cfg.addresses.verificationGateway),
|
||||
blsLibrary: assertString(cfg.addresses.blsLibrary),
|
||||
blsExpander: assertString(cfg.addresses.blsExpander),
|
||||
utilities: assertString(cfg.addresses.utilities),
|
||||
testToken: assertString(cfg.addresses.testToken),
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: assertNumber(cfg.auxiliary.chainid),
|
||||
domain: assertString(cfg.auxiliary.domain),
|
||||
walletDomain: assertString(cfg.auxiliary.walletDomain),
|
||||
bundleDomain: assertString(cfg.auxiliary.bundleDomain),
|
||||
genesisBlock: assertNumber(cfg.auxiliary.genesisBlock),
|
||||
deployedBy: assertString(cfg.auxiliary.deployedBy),
|
||||
version: assertString(cfg.auxiliary.version),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { BigNumber, ContractReceipt, utils } from "ethers";
|
||||
import assert from "./helpers/assert";
|
||||
import { ActionData } from "./signer";
|
||||
import { Result } from "ethers/lib/utils";
|
||||
import { VerificationGateway__factory as VerificationGatewayFactory } from "../typechain-types";
|
||||
|
||||
export const errorSelectors = {
|
||||
Error: calculateAndCheckSelector("Error(string)", "0x08c379a0"),
|
||||
@@ -117,22 +119,45 @@ const getError = (
|
||||
export const getOperationResults = (
|
||||
txnReceipt: ContractReceipt,
|
||||
): OperationResult[] => {
|
||||
const walletOpProcessedEvents = txnReceipt.events?.filter(
|
||||
(e) => e.event === "WalletOperationProcessed",
|
||||
);
|
||||
if (!walletOpProcessedEvents?.length) {
|
||||
let walletOpProcessedEvents: Result[] = (txnReceipt.events ?? [])
|
||||
.filter((e) => e.event === "WalletOperationProcessed")
|
||||
.map(({ args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
|
||||
return args;
|
||||
});
|
||||
|
||||
if (walletOpProcessedEvents.length === 0 && txnReceipt.logs !== undefined) {
|
||||
const vgInterface = VerificationGatewayFactory.createInterface();
|
||||
|
||||
const WalletOperationProcessed = vgInterface.getEvent(
|
||||
"WalletOperationProcessed",
|
||||
);
|
||||
|
||||
walletOpProcessedEvents = txnReceipt.logs
|
||||
.filter(
|
||||
(log) =>
|
||||
log.topics[0] === vgInterface.getEventTopic(WalletOperationProcessed),
|
||||
)
|
||||
.map((log) =>
|
||||
vgInterface.decodeEventLog(
|
||||
WalletOperationProcessed,
|
||||
log.data,
|
||||
log.topics,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (walletOpProcessedEvents.length === 0) {
|
||||
throw new Error(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
return walletOpProcessedEvents.reduce<OperationResult[]>(
|
||||
(opResults, { args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
const { wallet, nonce, actions: rawActions, success, results } = args;
|
||||
|
||||
(opResults, { wallet, nonce, actions: rawActions, success, results }) => {
|
||||
const actions = rawActions.map(
|
||||
({
|
||||
ethValue,
|
||||
@@ -150,7 +175,7 @@ export const getOperationResults = (
|
||||
);
|
||||
const error = getError(success, results);
|
||||
|
||||
return [
|
||||
const ret = [
|
||||
...opResults,
|
||||
{
|
||||
walletAddress: wallet,
|
||||
@@ -161,6 +186,8 @@ export const getOperationResults = (
|
||||
error,
|
||||
},
|
||||
];
|
||||
|
||||
return ret;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
380
contracts/clients/src/SafeSingletonFactory.ts
Normal file
380
contracts/clients/src/SafeSingletonFactory.ts
Normal file
@@ -0,0 +1,380 @@
|
||||
import assert from "assert";
|
||||
import { ethers, Signer } from "ethers";
|
||||
import SignerOrProvider from "./helpers/SignerOrProvider";
|
||||
|
||||
/**
|
||||
* Filters out the optional elements of an array type because an optional
|
||||
* element isn't considered to match First in [infer First, ...].
|
||||
*/
|
||||
type NonOptionalElementsOf<A extends unknown[]> = A extends [
|
||||
infer First,
|
||||
...infer Tail,
|
||||
]
|
||||
? [First, ...NonOptionalElementsOf<Tail>]
|
||||
: A extends [opt?: unknown]
|
||||
? []
|
||||
: never;
|
||||
|
||||
export type ContractFactoryConstructor = {
|
||||
new (): ethers.ContractFactory;
|
||||
};
|
||||
|
||||
export type DeployParams<CFC extends ContractFactoryConstructor> =
|
||||
NonOptionalElementsOf<Parameters<InstanceType<CFC>["deploy"]>>;
|
||||
|
||||
type Deployment = {
|
||||
gasPrice: number;
|
||||
gasLimit: number;
|
||||
signerAddress: string;
|
||||
transaction: string;
|
||||
address: string;
|
||||
};
|
||||
|
||||
export default class SafeSingletonFactory {
|
||||
static sharedAddress = "0x914d7Fec6aaC8cd542e72Bca78B30650d45643d7";
|
||||
|
||||
static deployments: Record<number, Deployment | undefined> = {
|
||||
1337: {
|
||||
gasPrice: 100000000000,
|
||||
gasLimit: 100000,
|
||||
signerAddress: "0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37",
|
||||
transaction: [
|
||||
"0x",
|
||||
"f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff",
|
||||
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081",
|
||||
"602082378035828234f58015156039578182fd5b8082525050506014600cf3820a96a",
|
||||
"0460c6ea9b8f791e5d9e67fbf2c70aba92bf88591c39ac3747ea1bedc2ef1750ca04b",
|
||||
"08a4b5cea15a56276513da7a0c0b34f16e89811d5dd911efba5f8625a921cc",
|
||||
].join(""),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
31337: {
|
||||
gasPrice: 100000000000,
|
||||
gasLimit: 100000,
|
||||
signerAddress: "0xE1CB04A0fA36DdD16a06ea828007E35e1a3cBC37",
|
||||
transaction: [
|
||||
"0x",
|
||||
"f8a78085174876e800830186a08080b853604580600e600039806000f350fe7ffffff",
|
||||
"fffffffffffffffffffffffffffffffffffffffffffffffffffffffffe03601600081",
|
||||
"602082378035828234f58015156039578182fd5b8082525050506014600cf382f4f5a",
|
||||
"00dc4d1d21b308094a30f5f93da35e4d72e99115378f135f2295bea47301a3165a063",
|
||||
"6b822daad40aa8c52dd5132f378c0c0e6d83b4898228c7e21c84e631a0b891",
|
||||
].join(""),
|
||||
address: SafeSingletonFactory.sharedAddress,
|
||||
},
|
||||
};
|
||||
|
||||
provider: ethers.providers.Provider;
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
viewer: SafeSingletonFactoryViewer;
|
||||
|
||||
private constructor(
|
||||
public signer: ethers.Signer,
|
||||
public chainId: number,
|
||||
public address: string,
|
||||
) {
|
||||
if (signer.provider === undefined) {
|
||||
throw new Error("Expected signer with provider");
|
||||
}
|
||||
|
||||
this.provider = signer.provider;
|
||||
|
||||
this.viewer = new SafeSingletonFactoryViewer(signer, chainId);
|
||||
}
|
||||
|
||||
static async init(signer: ethers.Signer): Promise<SafeSingletonFactory> {
|
||||
assert(signer.provider !== undefined, "Expected signer with provider");
|
||||
|
||||
const chainId = await signer.getChainId();
|
||||
|
||||
const address =
|
||||
SafeSingletonFactory.deployments[chainId]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
const existingCode = await signer.provider.getCode(address);
|
||||
|
||||
if (existingCode !== "0x") {
|
||||
return new SafeSingletonFactory(signer, chainId, address);
|
||||
}
|
||||
|
||||
const deployment = SafeSingletonFactory.deployments[chainId];
|
||||
|
||||
if (!deployment) {
|
||||
throw new Error(
|
||||
[
|
||||
"Cannot get deployment for SafeSingletonFactory (check",
|
||||
"https://github.com/safe-global/safe-singleton-factory/tree/main/artifacts",
|
||||
`for chain id ${chainId})`,
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
// Fund the eoa account for the presigned transaction
|
||||
await (
|
||||
await signer.sendTransaction({
|
||||
to: deployment.signerAddress,
|
||||
value: ethers.BigNumber.from(deployment.gasPrice).mul(
|
||||
deployment.gasLimit,
|
||||
),
|
||||
})
|
||||
).wait();
|
||||
|
||||
await (
|
||||
await signer.provider.sendTransaction(deployment.transaction)
|
||||
).wait();
|
||||
|
||||
const deployedCode = await signer.provider.getCode(deployment.address);
|
||||
assert(deployedCode !== "0x", "Failed to deploy safe singleton factory");
|
||||
|
||||
return new SafeSingletonFactory(signer, chainId, deployment.address);
|
||||
}
|
||||
|
||||
static async from(signerOrFactory: ethers.Signer | SafeSingletonFactory) {
|
||||
if (signerOrFactory instanceof SafeSingletonFactory) {
|
||||
return signerOrFactory;
|
||||
}
|
||||
|
||||
return await SafeSingletonFactory.init(signerOrFactory);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
return this.viewer.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<boolean> {
|
||||
return this.viewer.isDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]> | undefined> {
|
||||
let contract = await this.viewer.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
if (contract !== undefined) {
|
||||
contract = contract.connect(this.signer) as typeof contract;
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrDeploy<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]>> {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
if (existingCode !== "0x") {
|
||||
return contractFactory.attach(address).connect(this.signer) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
}
|
||||
|
||||
const deployTx = {
|
||||
to: this.address,
|
||||
data: ethers.utils.solidityPack(["uint256", "bytes"], [salt, initCode]),
|
||||
};
|
||||
|
||||
try {
|
||||
await (await this.signer.sendTransaction(deployTx)).wait();
|
||||
} catch (error) {
|
||||
if ((error as { code: string }).code !== "INSUFFICIENT_FUNDS") {
|
||||
throw error;
|
||||
}
|
||||
|
||||
const gasEstimate = await this.provider.estimateGas(deployTx);
|
||||
const gasPrice = await this.provider.getGasPrice();
|
||||
|
||||
const balance = await this.provider.getBalance(this.signer.getAddress());
|
||||
|
||||
throw new Error(
|
||||
[
|
||||
"Account",
|
||||
await this.signer.getAddress(),
|
||||
"has insufficient funds:",
|
||||
ethers.utils.formatEther(balance),
|
||||
"ETH, need (approx):",
|
||||
ethers.utils.formatEther(gasEstimate.mul(gasPrice)),
|
||||
"ETH",
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
const deployedCode = await this.provider.getCode(address);
|
||||
|
||||
assert(deployedCode !== "0x", "Failed to deploy to expected address");
|
||||
|
||||
return contractFactory.attach(address).connect(this.signer) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
}
|
||||
}
|
||||
|
||||
export class SafeSingletonFactoryViewer {
|
||||
safeSingletonFactoryAddress: string;
|
||||
signer?: Signer;
|
||||
provider: ethers.providers.Provider;
|
||||
|
||||
constructor(
|
||||
public signerOrProvider: SignerOrProvider,
|
||||
public chainId: number,
|
||||
) {
|
||||
this.safeSingletonFactoryAddress =
|
||||
SafeSingletonFactory.deployments[chainId]?.address ??
|
||||
SafeSingletonFactory.sharedAddress;
|
||||
|
||||
let provider: ethers.providers.Provider | undefined;
|
||||
|
||||
if (ethers.providers.Provider.isProvider(signerOrProvider)) {
|
||||
provider = signerOrProvider;
|
||||
} else {
|
||||
provider = signerOrProvider.provider;
|
||||
this.signer = signerOrProvider;
|
||||
}
|
||||
|
||||
if (!provider) {
|
||||
throw new Error("No provider found");
|
||||
}
|
||||
|
||||
this.provider = provider;
|
||||
}
|
||||
|
||||
static async from(signerOrProvider: SignerOrProvider) {
|
||||
const provider = ethers.providers.Provider.isProvider(signerOrProvider)
|
||||
? signerOrProvider
|
||||
: signerOrProvider.provider;
|
||||
|
||||
if (!provider) {
|
||||
throw new Error("No provider found");
|
||||
}
|
||||
|
||||
const network = await provider.getNetwork();
|
||||
|
||||
return new SafeSingletonFactoryViewer(signerOrProvider, network.chainId);
|
||||
}
|
||||
|
||||
calculateAddress<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
const initCode =
|
||||
contractFactory.bytecode +
|
||||
contractFactory.interface.encodeDeploy(deployParams).slice(2);
|
||||
|
||||
return ethers.utils.getCreate2Address(
|
||||
this.safeSingletonFactoryAddress,
|
||||
salt,
|
||||
ethers.utils.keccak256(initCode),
|
||||
);
|
||||
}
|
||||
|
||||
async isDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
) {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
return existingCode !== "0x";
|
||||
}
|
||||
|
||||
async connectIfDeployed<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]> | undefined> {
|
||||
const address = this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
const existingCode = await this.provider.getCode(address);
|
||||
|
||||
if (existingCode === "0x") {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const contractFactory = new ContractFactoryConstructor();
|
||||
|
||||
let contract = contractFactory.attach(address) as ReturnType<
|
||||
InstanceType<CFC>["attach"]
|
||||
>;
|
||||
|
||||
if (this.signer) {
|
||||
contract = contract.connect(this.signer) as typeof contract;
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
|
||||
async connectOrThrow<CFC extends ContractFactoryConstructor>(
|
||||
ContractFactoryConstructor: CFC,
|
||||
deployParams: DeployParams<CFC>,
|
||||
salt: ethers.utils.BytesLike = ethers.utils.solidityPack(["uint256"], [0]),
|
||||
): Promise<ReturnType<InstanceType<CFC>["attach"]>> {
|
||||
const contract = await this.connectIfDeployed(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
);
|
||||
|
||||
if (!contract) {
|
||||
throw new Error(
|
||||
`Contract ${
|
||||
ContractFactoryConstructor.name
|
||||
} not deployed at ${this.calculateAddress(
|
||||
ContractFactoryConstructor,
|
||||
deployParams,
|
||||
salt,
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return contract;
|
||||
}
|
||||
}
|
||||
119
contracts/clients/src/encodeUtils.ts
Normal file
119
contracts/clients/src/encodeUtils.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
import { BigNumber, BigNumberish, ethers } from "ethers";
|
||||
|
||||
export function hexJoin(hexStrings: string[]) {
|
||||
return "0x" + hexStrings.map(remove0x).join("");
|
||||
}
|
||||
|
||||
export function remove0x(hexString: string) {
|
||||
if (!hexString.startsWith("0x")) {
|
||||
throw new Error("Expected 0x prefix");
|
||||
}
|
||||
|
||||
return hexString.slice(2);
|
||||
}
|
||||
|
||||
export function encodeVLQ(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
const segments: number[] = [];
|
||||
|
||||
while (true) {
|
||||
const segment = x.mod(128);
|
||||
segments.unshift(segment.toNumber());
|
||||
x = x.sub(segment);
|
||||
x = x.div(128);
|
||||
|
||||
if (x.eq(0)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let result = "0x";
|
||||
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
const keepGoing = i !== segments.length - 1;
|
||||
|
||||
const byte = (keepGoing ? 128 : 0) + segments[i];
|
||||
result += byte.toString(16).padStart(2, "0");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function encodePseudoFloat(x: BigNumberish) {
|
||||
x = BigNumber.from(x);
|
||||
|
||||
if (x.eq(0)) {
|
||||
return "0x00";
|
||||
}
|
||||
|
||||
let exponent = 0;
|
||||
|
||||
while (x.mod(10).eq(0) && exponent < 30) {
|
||||
x = x.div(10);
|
||||
exponent++;
|
||||
}
|
||||
|
||||
const exponentBits = (exponent + 1).toString(2).padStart(5, "0");
|
||||
const lowest3Bits = x.mod(8).toNumber().toString(2).padStart(3, "0");
|
||||
|
||||
const firstByte = parseInt(`${exponentBits}${lowest3Bits}`, 2)
|
||||
.toString(16)
|
||||
.padStart(2, "0");
|
||||
|
||||
return hexJoin([`0x${firstByte}`, encodeVLQ(x.div(8))]);
|
||||
}
|
||||
|
||||
export function encodeRegIndex(regIndex: BigNumberish) {
|
||||
regIndex = BigNumber.from(regIndex);
|
||||
|
||||
const vlqValue = regIndex.div(0x010000);
|
||||
const fixedValue = regIndex.mod(0x010000).toNumber();
|
||||
|
||||
return hexJoin([
|
||||
encodeVLQ(vlqValue),
|
||||
`0x${fixedValue.toString(16).padStart(4, "0")}`,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bit streams are just the bits of a uint256 encoded as a VLQ.
|
||||
* (Technically the encoding is unbounded, but 256 booleans is a lot and it's
|
||||
* much easier to just decode the VLQ into a uint256 in the EVM.)
|
||||
*
|
||||
* Notably, the bits are little endian - the first bit is the *lowest* bit. This
|
||||
* is because the lowest bit is clearly the 1-valued bit, but the highest valued
|
||||
* bit could be anywhere - there's infinitely many zero-bits to choose from.
|
||||
*
|
||||
* If it wasn't for this need to be little endian, we'd definitely use big
|
||||
* endian (like our other encodings generally do), since that's preferred by the
|
||||
* EVM and the ecosystem:
|
||||
*
|
||||
* ```ts
|
||||
* const abi = new ethers.utils.AbiCoder():
|
||||
* console.log(abi.encode(["uint"], [0xff]));
|
||||
* // 0x00000000000000000000000000000000000000000000000000000000000000ff
|
||||
*
|
||||
* // If Ethereum used little endian (like x86), it would instead be:
|
||||
* // 0xff00000000000000000000000000000000000000000000000000000000000000
|
||||
* ```
|
||||
*/
|
||||
export function encodeBitStream(bitStream: boolean[]) {
|
||||
let stream = 0;
|
||||
let bitValue = 1;
|
||||
|
||||
const abi = new ethers.utils.AbiCoder();
|
||||
abi.encode(["uint"], [0xff]);
|
||||
|
||||
for (const bit of bitStream) {
|
||||
if (bit) {
|
||||
stream += bitValue;
|
||||
}
|
||||
|
||||
bitValue *= 2;
|
||||
}
|
||||
|
||||
const streamVLQ = encodeVLQ(stream);
|
||||
|
||||
return streamVLQ;
|
||||
}
|
||||
5
contracts/clients/src/helpers/SignerOrProvider.ts
Normal file
5
contracts/clients/src/helpers/SignerOrProvider.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { ethers } from "ethers";
|
||||
|
||||
type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
|
||||
|
||||
export default SignerOrProvider;
|
||||
@@ -6,7 +6,7 @@ import { BigNumber } from "ethers";
|
||||
* the chance that bundles get accepted during aggregation.
|
||||
*
|
||||
* @param feeEstimate fee required for bundle
|
||||
* @param safetyDivisor optional safety divisor. Default is 5
|
||||
* @param safetyDivisor optional safety divisor. Default is 5 (adds a 20% safety margin)
|
||||
* @returns fee estimate with added safety premium
|
||||
*/
|
||||
export default function addSafetyPremiumToFee(
|
||||
|
||||
58
contracts/clients/src/helpers/hashBundle.ts
Normal file
58
contracts/clients/src/helpers/hashBundle.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { Bundle } from "../signer";
|
||||
import { VerificationGatewayFactory } from "../index";
|
||||
|
||||
/**
|
||||
* Generates a deterministic hash of a bundle. Because the signature of the bundle could change, along with the gas property on operations,
|
||||
* those values are set to 0 before hashing. This leads to a more consistent hash for variations of the same bundle.
|
||||
*
|
||||
* @remarks the hash output is senstive to the internal types of the bundle. For example, an identical bundle with a
|
||||
* BigNumber value for one of the properties, vs the same bundle with a hex string value for one of the properties, will
|
||||
* generate different hashes, even though the underlying value may be the same.
|
||||
*
|
||||
* @param bundle the signed bundle to generate the hash for
|
||||
* @param chainId the chain id of the network the bundle is being submitted to
|
||||
* @returns a deterministic hash of the bundle
|
||||
*/
|
||||
export default function hashBundle(bundle: Bundle, chainId: number): string {
|
||||
if (bundle.operations.length !== bundle.senderPublicKeys.length) {
|
||||
throw new Error(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
}
|
||||
|
||||
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 any],
|
||||
[
|
||||
{
|
||||
...validatedBundle,
|
||||
signature: [BigNumber.from(0), BigNumber.from(0)],
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
const bundleHash = ethers.utils.keccak256(encodedBundleWithZeroSignature);
|
||||
|
||||
const encoding = ethers.utils.defaultAbiCoder.encode(
|
||||
["bytes32", "uint256"],
|
||||
[bundleHash, chainId],
|
||||
);
|
||||
return ethers.utils.keccak256(encoding);
|
||||
}
|
||||
@@ -1,81 +1,48 @@
|
||||
import Aggregator from "./Aggregator";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
import BlsProvider from "./BlsProvider";
|
||||
import BlsSigner from "./BlsSigner";
|
||||
export { default as Aggregator } from "./Aggregator";
|
||||
export { default as BlsWalletWrapper } from "./BlsWalletWrapper";
|
||||
export { default as BlsProvider } from "./BlsProvider";
|
||||
export { default as BlsSigner } from "./BlsSigner";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { VerificationGateway__factory } from "../typechain-types/factories/contracts/VerificationGateway__factory";
|
||||
import type { VerificationGateway } from "../typechain-types/contracts/VerificationGateway";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { AggregatorUtilities__factory } from "../typechain-types/factories/contracts/AggregatorUtilities__factory";
|
||||
import type { AggregatorUtilities } from "../typechain-types/contracts/AggregatorUtilities";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { ERC20__factory } from "../typechain-types/factories/@openzeppelin/contracts/token/ERC20/ERC20__factory";
|
||||
import type { ERC20 } from "../typechain-types/@openzeppelin/contracts/token/ERC20/ERC20";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { MockERC20__factory } from "../typechain-types/factories/contracts/mock/MockERC20__factory";
|
||||
import type { MockERC20 } from "../typechain-types/contracts/mock/MockERC20";
|
||||
|
||||
import { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
import {
|
||||
export { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
export {
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
} from "./MultiNetworkConfig";
|
||||
export { BlsWalletContracts, connectToContracts } from "./BlsWalletContracts";
|
||||
|
||||
import {
|
||||
export {
|
||||
OperationResult,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
OperationResultError,
|
||||
} from "./OperationResults";
|
||||
import { BlsWalletContracts, connectToContracts } from "./BlsWalletContracts";
|
||||
|
||||
export { default as hashBundle } from "./helpers/hashBundle";
|
||||
|
||||
export {
|
||||
VerificationGateway__factory as VerificationGatewayFactory,
|
||||
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
|
||||
ERC20__factory as ERC20Factory,
|
||||
MockERC20__factory as MockERC20Factory,
|
||||
type VerificationGateway,
|
||||
type AggregatorUtilities,
|
||||
type ERC20,
|
||||
type MockERC20,
|
||||
} from "../typechain-types";
|
||||
|
||||
export * from "./signer";
|
||||
|
||||
const Experimental_ = {
|
||||
BlsProvider,
|
||||
BlsSigner,
|
||||
};
|
||||
|
||||
/**
|
||||
* The Experimental namespace exposes APIs that are unstable.
|
||||
* Unstable in the sense that the APIs will be less functional, less well-tested, and/or are expected to change.
|
||||
*/
|
||||
namespace Experimental {
|
||||
export const BlsProvider = Experimental_.BlsProvider;
|
||||
export const BlsSigner = Experimental_.BlsSigner;
|
||||
}
|
||||
|
||||
export {
|
||||
Aggregator,
|
||||
BlsWalletWrapper,
|
||||
NetworkConfig,
|
||||
getConfig,
|
||||
validateConfig,
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
OperationResult,
|
||||
OperationResultError,
|
||||
getOperationResults,
|
||||
decodeError,
|
||||
// eslint-disable-next-line camelcase
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway,
|
||||
// eslint-disable-next-line camelcase
|
||||
AggregatorUtilities__factory,
|
||||
AggregatorUtilities,
|
||||
// eslint-disable-next-line camelcase
|
||||
ERC20__factory,
|
||||
ERC20,
|
||||
// eslint-disable-next-line camelcase
|
||||
MockERC20__factory,
|
||||
MockERC20,
|
||||
BlsWalletContracts,
|
||||
connectToContracts,
|
||||
Experimental,
|
||||
};
|
||||
default as SafeSingletonFactory,
|
||||
SafeSingletonFactoryViewer,
|
||||
} from "./SafeSingletonFactory";
|
||||
|
||||
export { default as AddressRegistryWrapper } from "./AddressRegistryWrapper";
|
||||
export { default as BlsPublicKeyRegistryWrapper } from "./BlsPublicKeyRegistryWrapper";
|
||||
export { default as FallbackCompressor } from "./FallbackCompressor";
|
||||
export { default as Erc20Compressor } from "./Erc20Compressor";
|
||||
export { default as BlsRegistrationCompressor } from "./BlsRegistrationCompressor";
|
||||
export { default as BundleCompressor } from "./BundleCompressor";
|
||||
export { default as ContractsConnector } from "./ContractsConnector";
|
||||
export * from "./encodeUtils";
|
||||
|
||||
@@ -12,6 +12,7 @@ export function bundleToDto(bundle: Bundle): BundleDto {
|
||||
]),
|
||||
operations: bundle.operations.map((op) => ({
|
||||
nonce: BigNumber.from(op.nonce).toHexString(),
|
||||
gas: BigNumber.from(op.gas).toHexString(),
|
||||
actions: op.actions.map((a) => ({
|
||||
ethValue: BigNumber.from(a.ethValue).toHexString(),
|
||||
contractAddress: a.contractAddress,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { keccak256, solidityPack } from "ethers/lib/utils";
|
||||
import { Operation } from "./types";
|
||||
|
||||
export default (chainId: number) =>
|
||||
export default () =>
|
||||
(operation: Operation, walletAddress: string): string => {
|
||||
let encodedActionData = "0x";
|
||||
|
||||
@@ -18,7 +18,7 @@ export default (chainId: number) =>
|
||||
}
|
||||
|
||||
return solidityPack(
|
||||
["uint256", "address", "uint256", "bytes32"],
|
||||
[chainId, walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
["address", "uint256", "bytes32"],
|
||||
[walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
);
|
||||
};
|
||||
|
||||
17
contracts/clients/src/signer/getDomain.ts
Normal file
17
contracts/clients/src/signer/getDomain.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { arrayify, solidityPack } from "ethers/lib/utils";
|
||||
import { utils } from "ethers";
|
||||
|
||||
export default (
|
||||
name: string,
|
||||
version: string,
|
||||
chainId: number,
|
||||
verificationGatewayAddress: string,
|
||||
type: string,
|
||||
): Uint8Array => {
|
||||
const encoded = solidityPack(
|
||||
["string", "string", "uint256", "address", "string"],
|
||||
[name, version, chainId, verificationGatewayAddress, type],
|
||||
);
|
||||
|
||||
return arrayify(utils.keccak256(encoded));
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { signer } from "@thehubbleproject/bls";
|
||||
|
||||
import aggregate from "./aggregate";
|
||||
import defaultDomain from "./defaultDomain";
|
||||
import getDomain from "./getDomain";
|
||||
import getPublicKey from "./getPublicKey";
|
||||
import getPublicKeyHash from "./getPublicKeyHash";
|
||||
import getPublicKeyStr from "./getPublicKeyStr";
|
||||
@@ -16,13 +16,13 @@ export * from "./conversions";
|
||||
export type BlsWalletSigner = AsyncReturnType<typeof initBlsWalletSigner>;
|
||||
|
||||
export async function initBlsWalletSigner({
|
||||
domain = defaultDomain,
|
||||
chainId,
|
||||
privateKey,
|
||||
verificationGatewayAddress,
|
||||
}: {
|
||||
domain?: Uint8Array;
|
||||
chainId: number;
|
||||
privateKey: string;
|
||||
verificationGatewayAddress: string;
|
||||
}) {
|
||||
// Note: Getting signers via this factory ensures that mcl-wasm's underlying
|
||||
// init() has been called when signing. However, other operations such as
|
||||
@@ -32,14 +32,32 @@ export async function initBlsWalletSigner({
|
||||
// properly initialized for all use cases, not just signing.
|
||||
const signerFactory = await signer.BlsSignerFactory.new();
|
||||
|
||||
const domainName = "BLS_WALLET";
|
||||
const domainVersion = "1";
|
||||
|
||||
const bundleDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
chainId,
|
||||
verificationGatewayAddress,
|
||||
"Bundle",
|
||||
);
|
||||
const walletDomain = getDomain(
|
||||
domainName,
|
||||
domainVersion,
|
||||
chainId,
|
||||
verificationGatewayAddress,
|
||||
"Wallet",
|
||||
);
|
||||
|
||||
return {
|
||||
aggregate,
|
||||
getPublicKey: getPublicKey(signerFactory, domain, privateKey),
|
||||
getPublicKeyHash: getPublicKeyHash(signerFactory, domain, privateKey),
|
||||
getPublicKeyStr: getPublicKeyStr(signerFactory, domain, privateKey),
|
||||
sign: sign(signerFactory, domain, chainId, privateKey),
|
||||
signMessage: signMessage(signerFactory, domain, privateKey),
|
||||
verify: verify(domain, chainId),
|
||||
getPublicKey: getPublicKey(signerFactory, bundleDomain, privateKey),
|
||||
getPublicKeyHash: getPublicKeyHash(signerFactory, bundleDomain, privateKey),
|
||||
getPublicKeyStr: getPublicKeyStr(signerFactory, bundleDomain, privateKey),
|
||||
sign: sign(signerFactory, bundleDomain, privateKey),
|
||||
signMessage: signMessage(signerFactory, walletDomain, privateKey),
|
||||
verify: verify(bundleDomain),
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,12 +6,11 @@ import { Bundle, Operation } from "./types";
|
||||
export default (
|
||||
signerFactory: signer.BlsSignerFactory,
|
||||
domain: Uint8Array,
|
||||
chainId: number,
|
||||
privateKey: string,
|
||||
) =>
|
||||
(operation: Operation, walletAddress: string): Bundle => {
|
||||
const signer = signerFactory.getSigner(domain, privateKey);
|
||||
const message = encodeMessageForSigning(chainId)(operation, walletAddress);
|
||||
const message = encodeMessageForSigning()(operation, walletAddress);
|
||||
const signature = signer.sign(message);
|
||||
|
||||
return {
|
||||
|
||||
@@ -8,6 +8,7 @@ export type ActionData = {
|
||||
|
||||
export type Operation = {
|
||||
nonce: BigNumberish;
|
||||
gas: BigNumberish;
|
||||
actions: ActionData[];
|
||||
};
|
||||
|
||||
@@ -28,6 +29,7 @@ export type ActionDataDto = {
|
||||
|
||||
export type OperationDto = {
|
||||
nonce: string;
|
||||
gas: string;
|
||||
actions: ActionDataDto[];
|
||||
};
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import encodeMessageForSigning from "./encodeMessageForSigning";
|
||||
import type { Bundle } from "./types";
|
||||
import isValidEmptyBundle from "./isValidEmptyBundle";
|
||||
|
||||
export default (domain: Uint8Array, chainId: number) =>
|
||||
(bundle: Bundle, walletAddress: string): boolean => {
|
||||
export default (domain: Uint8Array) =>
|
||||
(bundle: Bundle, walletAddresses: Array<string>): boolean => {
|
||||
// hubbleBls verifier incorrectly rejects empty bundles
|
||||
if (isValidEmptyBundle(bundle)) {
|
||||
return true;
|
||||
@@ -25,8 +25,8 @@ export default (domain: Uint8Array, chainId: number) =>
|
||||
BigNumber.from(n2).toHexString(),
|
||||
BigNumber.from(n3).toHexString(),
|
||||
]),
|
||||
bundle.operations.map((op) =>
|
||||
encodeMessageForSigning(chainId)(op, walletAddress),
|
||||
bundle.operations.map((op, i) =>
|
||||
encodeMessageForSigning()(op, walletAddresses[i]),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "ethers";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
import { parseEther } from "ethers/lib/utils";
|
||||
|
||||
import { Experimental, BlsWalletWrapper } from "../src";
|
||||
import BlsSigner, { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
import { BlsProvider, BlsSigner, bundleToDto } from "../src";
|
||||
import { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
import { initBlsWalletSigner } from "../src/signer";
|
||||
|
||||
let aggregatorUrl: string;
|
||||
let verificationGateway: string;
|
||||
@@ -12,25 +13,25 @@ let rpcUrl: string;
|
||||
let network: ethers.providers.Networkish;
|
||||
|
||||
let privateKey: string;
|
||||
let blsProvider: InstanceType<typeof Experimental.BlsProvider>;
|
||||
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
|
||||
let blsProvider: BlsProvider;
|
||||
let blsSigner: BlsSigner;
|
||||
|
||||
let regularProvider: ethers.providers.JsonRpcProvider;
|
||||
|
||||
describe("BlsProvider", () => {
|
||||
beforeEach(async () => {
|
||||
aggregatorUrl = "http://localhost:3000";
|
||||
verificationGateway = "mockVerificationGatewayAddress";
|
||||
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
|
||||
verificationGateway = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
aggregatorUtilities = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
rpcUrl = "http://localhost:8545";
|
||||
network = {
|
||||
name: "localhost",
|
||||
chainId: 0x7a69,
|
||||
chainId: 0x539, // 1337
|
||||
};
|
||||
|
||||
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
privateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
blsProvider = new Experimental.BlsProvider(
|
||||
blsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
@@ -60,51 +61,29 @@ describe("BlsProvider", () => {
|
||||
expect(uncheckedBlsSigner).to.be.instanceOf(UncheckedBlsSigner);
|
||||
});
|
||||
|
||||
it("should return a new signer if one has not been instantiated", async () => {
|
||||
it("should return a new signer", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
const newVerificationGateway = "newMockVerificationGatewayAddress";
|
||||
const newBlsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
newVerificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
);
|
||||
|
||||
// Act
|
||||
const newPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
const newPrivateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
const newBlsSigner = newBlsProvider.getSigner(newPrivateKey);
|
||||
|
||||
// Assert
|
||||
expect(newBlsSigner).to.not.equal(blsSigner);
|
||||
expect(newBlsSigner).to.equal(newBlsProvider.getSigner(newPrivateKey));
|
||||
});
|
||||
|
||||
it("should throw an error when this.signer has not been assigned", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
expect(newBlsSigner.provider.verificationGatewayAddress).to.not.equal(
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
);
|
||||
|
||||
const recipient = ethers.Wallet.createRandom().address;
|
||||
const value = parseEther("1");
|
||||
const transactionRequest = {
|
||||
to: recipient,
|
||||
value,
|
||||
};
|
||||
|
||||
// Act
|
||||
const gasEstimate = async () =>
|
||||
await newBlsProvider.estimateGas(transactionRequest);
|
||||
|
||||
// Assert
|
||||
await expect(gasEstimate()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Call provider.getSigner first",
|
||||
expect(newBlsSigner.provider.verificationGatewayAddress).to.equal(
|
||||
newVerificationGateway,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -112,6 +91,7 @@ describe("BlsProvider", () => {
|
||||
// Arrange
|
||||
const transaction = {
|
||||
value: parseEther("1"),
|
||||
// Explicitly omit 'to'
|
||||
};
|
||||
|
||||
// Act
|
||||
@@ -124,28 +104,21 @@ describe("BlsProvider", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw an error sending a transaction when this.signer is not defined", async () => {
|
||||
it("should throw an error estimating gas when 'transaction.from' has not been defined", async () => {
|
||||
// Arrange
|
||||
const newBlsProvider = new Experimental.BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
rpcUrl,
|
||||
network,
|
||||
);
|
||||
const signedTransaction = blsSigner.signTransaction({
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
const transaction = {
|
||||
value: parseEther("1"),
|
||||
});
|
||||
to: ethers.Wallet.createRandom().address,
|
||||
// Explicitly omit 'from'
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await newBlsProvider.sendTransaction(signedTransaction);
|
||||
const result = async () => await blsProvider.estimateGas(transaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Call provider.getSigner first",
|
||||
TypeError,
|
||||
"Transaction.from should be defined",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -159,4 +132,155 @@ describe("BlsProvider", () => {
|
||||
// Assert
|
||||
expect(connection).to.deep.equal(expectedConnection);
|
||||
});
|
||||
|
||||
it("should throw an error when sending invalid signed transactions", async () => {
|
||||
// Arrange
|
||||
const invalidTransaction = "Invalid signed transaction";
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsProvider.sendTransaction(invalidTransaction);
|
||||
const batchResult = async () =>
|
||||
await blsProvider.sendTransaction(invalidTransaction);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Unexpected token I in JSON at position 0",
|
||||
);
|
||||
await expect(batchResult()).to.be.rejectedWith(
|
||||
Error,
|
||||
"Unexpected token I in JSON at position 0",
|
||||
);
|
||||
});
|
||||
|
||||
it("should get the polling interval", async () => {
|
||||
// Arrange
|
||||
const expectedpollingInterval = 4000; // default
|
||||
const updatedInterval = 1000;
|
||||
|
||||
// Act
|
||||
const pollingInterval = blsProvider.pollingInterval;
|
||||
blsProvider.pollingInterval = updatedInterval;
|
||||
const updatedPollingInterval = blsProvider.pollingInterval;
|
||||
|
||||
// Assert
|
||||
expect(pollingInterval).to.equal(expectedpollingInterval);
|
||||
expect(updatedPollingInterval).to.equal(updatedInterval);
|
||||
});
|
||||
|
||||
it("should get the event listener count and remove all listeners", async () => {
|
||||
blsProvider.on("block", () => {});
|
||||
blsProvider.on("error", () => {});
|
||||
expect(blsProvider.listenerCount("block")).to.equal(1);
|
||||
expect(blsProvider.listenerCount("error")).to.equal(1);
|
||||
expect(blsProvider.listenerCount()).to.equal(2);
|
||||
|
||||
blsProvider.removeAllListeners();
|
||||
expect(blsProvider.listenerCount("block")).to.equal(0);
|
||||
expect(blsProvider.listenerCount("error")).to.equal(0);
|
||||
expect(blsProvider.listenerCount()).to.equal(0);
|
||||
});
|
||||
|
||||
it("should return true and an array of listeners if polling", async () => {
|
||||
// Arrange
|
||||
const expectedListener = () => {};
|
||||
|
||||
// Act
|
||||
blsProvider.on("block", expectedListener);
|
||||
const listeners = blsProvider.listeners("block");
|
||||
const isPolling = blsProvider.polling;
|
||||
blsProvider.removeAllListeners();
|
||||
|
||||
// Assert
|
||||
expect(listeners).to.deep.equal([expectedListener]);
|
||||
expect(isPolling).to.be.true;
|
||||
});
|
||||
|
||||
it("should be a provider", async () => {
|
||||
// Arrange & Act
|
||||
const isProvider = BlsProvider.isProvider(blsProvider);
|
||||
const isProviderWithInvalidProvider = BlsProvider.isProvider(blsSigner);
|
||||
|
||||
// Assert
|
||||
expect(isProvider).to.equal(true);
|
||||
expect(isProviderWithInvalidProvider).to.equal(false);
|
||||
});
|
||||
|
||||
it("should a return a promise which will stall until the network has heen established", async () => {
|
||||
// Arrange
|
||||
const expectedReady = { name: "localhost", chainId: 1337 };
|
||||
|
||||
// Act
|
||||
const ready = await blsProvider.ready;
|
||||
|
||||
// Assert
|
||||
expect(ready).to.deep.equal(expectedReady);
|
||||
});
|
||||
|
||||
it("should throw an error when sending multiple signed operations to sendTransaction", async () => {
|
||||
// Arrange
|
||||
const mockWalletAddress = "0x1337AF0f4b693fd1c36d7059a0798Ff05a60DFFE";
|
||||
const { sign, aggregate } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress: verificationGateway,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const expectedAmount = parseEther("1");
|
||||
const verySafeFee = parseEther("0.1");
|
||||
const firstRecipient = ethers.Wallet.createRandom().address;
|
||||
const secondRecipient = ethers.Wallet.createRandom().address;
|
||||
|
||||
const firstActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
[
|
||||
{
|
||||
ethValue: expectedAmount,
|
||||
contractAddress: firstRecipient,
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
verySafeFee,
|
||||
);
|
||||
const secondActionWithSafeFee = blsProvider._addFeePaymentActionWithSafeFee(
|
||||
[
|
||||
{
|
||||
ethValue: expectedAmount,
|
||||
contractAddress: secondRecipient,
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
verySafeFee,
|
||||
);
|
||||
|
||||
const nonce = BigNumber.from(0);
|
||||
|
||||
const firstOperation = {
|
||||
nonce,
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [...firstActionWithSafeFee],
|
||||
};
|
||||
const secondOperation = {
|
||||
nonce,
|
||||
gas: BigNumber.from(30_000_000),
|
||||
actions: [...secondActionWithSafeFee],
|
||||
};
|
||||
|
||||
const firstBundle = sign(firstOperation, mockWalletAddress);
|
||||
const secondBundle = sign(secondOperation, mockWalletAddress);
|
||||
|
||||
const aggregatedBundle = aggregate([firstBundle, secondBundle]);
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsProvider.sendTransaction(
|
||||
JSON.stringify(bundleToDto(aggregatedBundle)),
|
||||
);
|
||||
|
||||
// Assert
|
||||
await expect(result()).to.rejectedWith(
|
||||
Error,
|
||||
"Can only operate on single operations. Call provider.sendTransactionBatch instead",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { expect } from "chai";
|
||||
import { ethers } from "ethers";
|
||||
import { BigNumber, ethers } from "ethers";
|
||||
|
||||
import { Experimental, BlsWalletWrapper } from "../src";
|
||||
import { BlsProvider, BlsSigner } from "../src";
|
||||
import { UncheckedBlsSigner } from "../src/BlsSigner";
|
||||
|
||||
let aggregatorUrl: string;
|
||||
@@ -11,23 +11,23 @@ let rpcUrl: string;
|
||||
let network: ethers.providers.Networkish;
|
||||
|
||||
let privateKey: string;
|
||||
let blsProvider: InstanceType<typeof Experimental.BlsProvider>;
|
||||
let blsSigner: InstanceType<typeof Experimental.BlsSigner>;
|
||||
let blsProvider: BlsProvider;
|
||||
let blsSigner: BlsSigner;
|
||||
|
||||
describe("BlsSigner", () => {
|
||||
beforeEach(async () => {
|
||||
aggregatorUrl = "http://localhost:3000";
|
||||
verificationGateway = "mockVerificationGatewayAddress";
|
||||
aggregatorUtilities = "mockAggregatorUtilitiesAddress";
|
||||
verificationGateway = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
aggregatorUtilities = "0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
rpcUrl = "http://localhost:8545";
|
||||
network = {
|
||||
name: "localhost",
|
||||
chainId: 0x7a69,
|
||||
chainId: 0x539,
|
||||
};
|
||||
|
||||
privateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
|
||||
privateKey = await BlsSigner.getRandomBlsPrivateKey();
|
||||
|
||||
blsProvider = new Experimental.BlsProvider(
|
||||
blsProvider = new BlsProvider(
|
||||
aggregatorUrl,
|
||||
verificationGateway,
|
||||
aggregatorUtilities,
|
||||
@@ -74,13 +74,13 @@ describe("BlsSigner", () => {
|
||||
|
||||
// Assert
|
||||
expect(provider._isProvider).to.be.true;
|
||||
expect(provider).to.be.instanceOf(Experimental.BlsProvider);
|
||||
expect(provider).to.be.instanceOf(BlsProvider);
|
||||
});
|
||||
|
||||
it("should detect whether an object is a valid signer", async () => {
|
||||
// Arrange & Act
|
||||
const validSigner = Experimental.BlsSigner.isSigner(blsSigner);
|
||||
const invalidSigner = Experimental.BlsSigner.isSigner({});
|
||||
const validSigner = BlsSigner.isSigner(blsSigner);
|
||||
const invalidSigner = BlsSigner.isSigner({});
|
||||
|
||||
// Assert
|
||||
expect(validSigner).to.be.true;
|
||||
@@ -94,4 +94,27 @@ describe("BlsSigner", () => {
|
||||
// Assert
|
||||
expect(connect).to.throw(Error, "cannot alter JSON-RPC Signer connection");
|
||||
});
|
||||
|
||||
it("should throw error for wrong chain id when validating batch options", async () => {
|
||||
// Arrange
|
||||
const invalidChainId = 123;
|
||||
const batchOptions = {
|
||||
gas: BigNumber.from("40000"),
|
||||
maxPriorityFeePerGas: ethers.utils.parseUnits("0.5", "gwei"),
|
||||
maxFeePerGas: ethers.utils.parseUnits("23", "gwei"),
|
||||
nonce: 1,
|
||||
chainId: invalidChainId,
|
||||
accessList: [],
|
||||
};
|
||||
|
||||
// Act
|
||||
const result = async () =>
|
||||
await blsSigner._validateBatchOptions(batchOptions);
|
||||
|
||||
// Assert
|
||||
expect(result()).to.be.rejectedWith(
|
||||
Error,
|
||||
`Supplied chain ID ${invalidChainId} does not match the expected chain ID 1337`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -112,16 +112,6 @@ describe("OperationResults", () => {
|
||||
});
|
||||
|
||||
describe("getOperationResults", () => {
|
||||
it("fails if no events are in transaction", () => {
|
||||
const txnReceipt = {
|
||||
transactionHash: "0x111111",
|
||||
} as ContractReceipt;
|
||||
|
||||
expect(() => getOperationResults(txnReceipt)).to.throw(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("fails if no WalletOperationProcessed events are in transaction", () => {
|
||||
const event = { event: "Other" };
|
||||
const txnReceipt = {
|
||||
@@ -222,5 +212,159 @@ describe("OperationResults", () => {
|
||||
expect(r2.results).to.deep.equal(successfulEvent.args.results);
|
||||
expect(r2.error).to.eql(undefined);
|
||||
});
|
||||
|
||||
it("decodes WalletOperationProcessed logs", () => {
|
||||
const receipt = {
|
||||
// Sometimes you get .events but WalletOperationProcessed are not
|
||||
// decoded, this helps cover that case.
|
||||
// Eg: calling blsExpanderDelegator.run
|
||||
events: [] as unknown[],
|
||||
|
||||
logs: [
|
||||
// Unrelated log
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
topics: [
|
||||
"0xbc7cd75a20ee27fd9adebab32041f755214dbc6bffa90cc0225b39da2e5c2d3b",
|
||||
"0x000000000000000000000000e619cf09e1f0eb1f9172431ec49dbef4747f8fe7",
|
||||
],
|
||||
data: "0x",
|
||||
logIndex: 0,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for successful transfer
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x00000000000000000000000000f8cc7bb32b7ee91c346640d203ddc57204a977",
|
||||
],
|
||||
data: "0x000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000016345785d8a0000000000000000000000000000e9d90fb095c18ce6dd2acee68684503b7837ed4200000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 4,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for failing transfer (insufficient funds)
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x000000000000000000000000e9d90fb095c18ce6dd2acee68684503b7837ed42",
|
||||
],
|
||||
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000001bc16d674ec800000000000000000000000000007c7cace58eccaac75021a2da4f5fc5cdc095e411000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000645c66760100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 9,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
|
||||
// WalletOperationProcessed for successful transfer
|
||||
{
|
||||
transactionIndex: 0,
|
||||
blockNumber: 28,
|
||||
transactionHash:
|
||||
"0x34385907a3bfb358cefdecd12071f12617a8f81d3a7c37ab52b7444f56856728",
|
||||
address: "0xE25229F29BAD62B1198F05F32169B70a9edc84b8",
|
||||
topics: [
|
||||
"0x9872451083cef0fc4232916d3eef8f2267edb3d496db39434a0d3142a27df456",
|
||||
"0x0000000000000000000000007c7cace58eccaac75021a2da4f5fc5cdc095e411",
|
||||
],
|
||||
data: "0x0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000429d069189e000000000000000000000000000000f8cc7bb32b7ee91c346640d203ddc57204a97700000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000000",
|
||||
logIndex: 14,
|
||||
blockHash:
|
||||
"0x728208c8902b362abc7c6c7496e41d9a4825204788125d7e76038775851fc27e",
|
||||
},
|
||||
],
|
||||
} as ContractReceipt;
|
||||
|
||||
const results = getOperationResults(receipt);
|
||||
|
||||
// Helps to smooth out oddities like `error: undefined` which we don't
|
||||
// care about
|
||||
const normalizedResults = JSON.parse(JSON.stringify(results));
|
||||
|
||||
expect(normalizedResults).to.deep.eq([
|
||||
{
|
||||
walletAddress: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x016345785d8a0000",
|
||||
},
|
||||
contractAddress: "0xE9d90fB095c18ce6Dd2AcEe68684503b7837eD42",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: true,
|
||||
results: ["0x"],
|
||||
},
|
||||
{
|
||||
walletAddress: "0xE9d90fB095c18ce6Dd2AcEe68684503b7837eD42",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x1bc16d674ec80000",
|
||||
},
|
||||
contractAddress: "0x7c7CAce58eCCAac75021a2dA4F5fc5cDc095E411",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: false,
|
||||
results: [
|
||||
"0x5c667601000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000",
|
||||
],
|
||||
error: {
|
||||
actionIndex: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
message: "Unexpected action error data: 0x",
|
||||
},
|
||||
},
|
||||
{
|
||||
walletAddress: "0x7c7CAce58eCCAac75021a2dA4F5fc5cDc095E411",
|
||||
nonce: {
|
||||
type: "BigNumber",
|
||||
hex: "0x00",
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
ethValue: {
|
||||
type: "BigNumber",
|
||||
hex: "0x0429d069189e0000",
|
||||
},
|
||||
contractAddress: "0x00f8CC7Bb32B7ee91c346640D203DdC57204a977",
|
||||
encodedFunction: "0x",
|
||||
},
|
||||
],
|
||||
success: true,
|
||||
results: ["0x"],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { expect } from "chai";
|
||||
import { NetworkConfig } from "../src";
|
||||
import {
|
||||
UnvalidatedMultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
@@ -7,20 +8,20 @@ import {
|
||||
const getValue = (networkKey: string, propName: string) =>
|
||||
`${networkKey}-${propName}`;
|
||||
|
||||
const getSingleConfig = (networkKey: string) => ({
|
||||
const getSingleConfig = (networkKey: string): NetworkConfig => ({
|
||||
parameters: {},
|
||||
addresses: {
|
||||
create2Deployer: getValue(networkKey, "create2Deployer"),
|
||||
safeSingletonFactory: getValue(networkKey, "safeSingletonFactory"),
|
||||
precompileCostEstimator: getValue(networkKey, "precompileCostEstimator"),
|
||||
verificationGateway: getValue(networkKey, "verificationGateway"),
|
||||
blsLibrary: getValue(networkKey, "blsLibrary"),
|
||||
blsExpander: getValue(networkKey, "blsExpander"),
|
||||
utilities: getValue(networkKey, "utilities"),
|
||||
testToken: getValue(networkKey, "testToken"),
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: 123,
|
||||
domain: getValue(networkKey, "domain"),
|
||||
walletDomain: getValue(networkKey, "walletDomain"),
|
||||
bundleDomain: getValue(networkKey, "bundleDomain"),
|
||||
genesisBlock: 456,
|
||||
deployedBy: getValue(networkKey, "deployedBy"),
|
||||
version: getValue(networkKey, "version"),
|
||||
@@ -71,13 +72,6 @@ describe("MultiNetworkConfig", () => {
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network1}.addresses.blsLibrary is set to a number`, async () => {
|
||||
validConfig[network1].addresses.blsLibrary = 1337;
|
||||
|
||||
await expect(getMultiConfig("", async () => JSON.stringify(validConfig)))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network2}.auxiliary.chainid is set to a string`, async () => {
|
||||
validConfig[network1].auxiliary.chainid = "off-the-chain";
|
||||
|
||||
|
||||
65
contracts/clients/test/hashBundle.test.ts
Normal file
65
contracts/clients/test/hashBundle.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { expect } from "chai";
|
||||
import hashBundle from "../src/helpers/hashBundle";
|
||||
import { Bundle } from "../src";
|
||||
import { BigNumber } from "ethers";
|
||||
|
||||
describe("hashBundle", () => {
|
||||
it("should return a valid hash when provided with a valid bundle and chainId", () => {
|
||||
// Arrange
|
||||
const operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const bundle: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation, operation],
|
||||
senderPublicKeys: [
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
],
|
||||
};
|
||||
const chainId = 1;
|
||||
|
||||
// Act
|
||||
const result = hashBundle(bundle, chainId);
|
||||
|
||||
// Assert
|
||||
expect(result).to.be.a("string");
|
||||
expect(result.length).to.equal(66);
|
||||
});
|
||||
|
||||
it("should throw an error when the number of operations does not match the number of public keys", () => {
|
||||
// Arrange
|
||||
const operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [],
|
||||
};
|
||||
|
||||
const bundle1: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation, operation],
|
||||
senderPublicKeys: [["0x4321", "0x4321", "0x4321", "0x4321"]],
|
||||
};
|
||||
const bundle2: Bundle = {
|
||||
signature: ["0x1234", "0x1234"],
|
||||
operations: [operation],
|
||||
senderPublicKeys: [
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
["0x4321", "0x4321", "0x4321", "0x4321"],
|
||||
],
|
||||
};
|
||||
const chainId = 1;
|
||||
|
||||
// Act & Assert
|
||||
expect(() => hashBundle(bundle1, chainId)).to.throw(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
|
||||
expect(() => hashBundle(bundle2, chainId)).to.throw(
|
||||
"number of operations does not match number of public keys",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,10 @@
|
||||
import { BigNumber } from "ethers";
|
||||
import { keccak256, arrayify } from "ethers/lib/utils";
|
||||
import { expect } from "chai";
|
||||
|
||||
import { initBlsWalletSigner, Bundle, Operation } from "../src/signer";
|
||||
|
||||
import Range from "./helpers/Range";
|
||||
|
||||
const domain = arrayify(keccak256("0xfeedbee5"));
|
||||
const weiPerToken = BigNumber.from(10).pow(18);
|
||||
|
||||
const samples = (() => {
|
||||
@@ -15,9 +13,12 @@ const samples = (() => {
|
||||
// Random addresses
|
||||
const walletAddress = "0x1337AF0f4b693fd1c36d7059a0798Ff05a60DFFE";
|
||||
const otherWalletAddress = "0x42C8157D539825daFD6586B119db53761a2a91CD";
|
||||
const verificationGatewayAddress =
|
||||
"0xC8CD2BE653759aed7B0996315821AAe71e1FEAdF";
|
||||
|
||||
const bundleTemplate: Operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
gas: 30_000_000,
|
||||
actions: [
|
||||
{
|
||||
ethValue: BigNumber.from(0),
|
||||
@@ -42,6 +43,7 @@ const samples = (() => {
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
verificationGatewayAddress,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -52,22 +54,22 @@ describe("index", () => {
|
||||
|
||||
const { sign, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const bundle = sign(bundleTemplate, walletAddress);
|
||||
|
||||
expect(bundle.signature).to.deep.equal([
|
||||
"0x2c1b0dc6643375e05a6f2ba3d23b1ce941253010b13a127e22f5db647dc37952",
|
||||
"0x0338f96fc67ce194a74a459791865ac2eb304fc214fd0962775078d12aea5b7e",
|
||||
"0x117171c1a4af03133390b454989658d9c6ae7a7fe1c3958ad545e584e63ab5e3",
|
||||
"0x2f90b24bbc03de665816b3a632e0c7b5fb837c87541d9337480671613cf1359c",
|
||||
]);
|
||||
|
||||
expect(verify(bundle, walletAddress)).to.equal(true);
|
||||
expect(verify(bundle, [walletAddress])).to.equal(true);
|
||||
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -77,7 +79,7 @@ describe("index", () => {
|
||||
.signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadSig, walletAddress)).to.equal(false);
|
||||
expect(verify(bundleBadSig, [walletAddress])).to.equal(false);
|
||||
|
||||
const bundleBadMessage: Bundle = {
|
||||
senderPublicKeys: bundle.senderPublicKeys,
|
||||
@@ -97,7 +99,7 @@ describe("index", () => {
|
||||
signature: bundle.signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadMessage, walletAddress)).to.equal(false);
|
||||
expect(verify(bundleBadMessage, [walletAddress])).to.equal(false);
|
||||
});
|
||||
|
||||
it("aggregates transactions", async () => {
|
||||
@@ -111,12 +113,12 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
@@ -125,15 +127,19 @@ describe("index", () => {
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(aggBundle.signature).to.deep.equal([
|
||||
"0x2319fc81d339dce4678c73429dfd2f11766742ed1e41df5a2ba2bf4863d877b5",
|
||||
"0x1bb25c15ad1f2f967a80a7a65c7593fcd66b59bf092669707baf2db726e8e714",
|
||||
"0x18b917c1f52155d9748025bd94aa07c0017af31dd2ef2a00289931f660e88ec9",
|
||||
"0x0235a99bcd1f0793efb7f3307cd349f211a433f60cfab795f5f976298f17a768",
|
||||
]);
|
||||
|
||||
expect(verify(bundle1, walletAddress)).to.equal(true);
|
||||
expect(verify(bundle2, otherWalletAddress)).to.equal(true);
|
||||
expect(verify(bundle1, [walletAddress])).to.equal(true);
|
||||
expect(verify(bundle2, [otherWalletAddress])).to.equal(true);
|
||||
|
||||
expect(verify(bundle1, otherWalletAddress)).to.equal(false);
|
||||
expect(verify(bundle2, walletAddress)).to.equal(false);
|
||||
expect(verify(bundle1, [otherWalletAddress])).to.equal(false);
|
||||
expect(verify(bundle2, [walletAddress])).to.equal(false);
|
||||
|
||||
expect(verify(aggBundle, [walletAddress, otherWalletAddress])).to.equal(
|
||||
true,
|
||||
);
|
||||
|
||||
const aggBundleBadMessage: Bundle = {
|
||||
...aggBundle,
|
||||
@@ -154,8 +160,12 @@ describe("index", () => {
|
||||
],
|
||||
};
|
||||
|
||||
expect(verify(aggBundleBadMessage, walletAddress)).to.equal(false);
|
||||
expect(verify(aggBundleBadMessage, otherWalletAddress)).to.equal(false);
|
||||
expect(
|
||||
verify(aggBundleBadMessage, [walletAddress, otherWalletAddress]),
|
||||
).to.equal(false);
|
||||
expect(
|
||||
verify(aggBundleBadMessage, [otherWalletAddress, walletAddress]),
|
||||
).to.equal(false);
|
||||
});
|
||||
|
||||
it("can aggregate transactions which already have multiple subTransactions", async () => {
|
||||
@@ -163,7 +173,7 @@ describe("index", () => {
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -186,8 +196,39 @@ describe("index", () => {
|
||||
const aggBundle2 = aggregate(bundles.slice(2, 4));
|
||||
|
||||
const aggAggBundle = aggregate([aggBundle1, aggBundle2]);
|
||||
const walletAddresses = new Array(4).fill(walletAddress);
|
||||
|
||||
expect(verify(aggAggBundle, walletAddress)).to.equal(true);
|
||||
expect(verify(aggAggBundle, walletAddresses)).to.equal(true);
|
||||
});
|
||||
|
||||
it("should fail to verify bundle with wallet address mismatches", async () => {
|
||||
const {
|
||||
bundleTemplate,
|
||||
privateKey,
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
verificationGatewayAddress,
|
||||
} = samples;
|
||||
|
||||
const { sign, aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
const { sign: signWithOtherPrivateKey } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
verificationGatewayAddress,
|
||||
privateKey: otherPrivateKey,
|
||||
});
|
||||
|
||||
const bundle1 = sign(bundleTemplate, walletAddress);
|
||||
const bundle2 = signWithOtherPrivateKey(bundleTemplate, otherWalletAddress);
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(verify(aggBundle, [otherWalletAddress, walletAddress])).to.equal(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it("generates expected publicKeyStr", async () => {
|
||||
@@ -195,7 +236,7 @@ describe("index", () => {
|
||||
|
||||
const { getPublicKeyStr } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -215,7 +256,7 @@ describe("index", () => {
|
||||
|
||||
const { aggregate } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
@@ -230,12 +271,12 @@ describe("index", () => {
|
||||
|
||||
const { aggregate, verify } = await initBlsWalletSigner({
|
||||
chainId: 123,
|
||||
domain,
|
||||
verificationGatewayAddress: samples.verificationGatewayAddress,
|
||||
privateKey,
|
||||
});
|
||||
|
||||
const emptyBundle = aggregate([]);
|
||||
|
||||
expect(verify(emptyBundle, samples.walletAddress)).to.equal(true);
|
||||
expect(verify(emptyBundle, [samples.walletAddress])).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
25
contracts/contracts/AddressRegistry.sol
Normal file
25
contracts/contracts/AddressRegistry.sol
Normal file
@@ -0,0 +1,25 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
contract AddressRegistry {
|
||||
mapping(uint256 => address) public addresses;
|
||||
uint256 public nextId = 0;
|
||||
|
||||
event AddressRegistered(uint256 id, address indexed addr);
|
||||
|
||||
function register(address addr) external {
|
||||
uint256 id = nextId;
|
||||
nextId += 1;
|
||||
addresses[id] = addr;
|
||||
|
||||
emit AddressRegistered(id, addr);
|
||||
}
|
||||
|
||||
function lookup(uint256 id) external view returns (address) {
|
||||
address addr = addresses[id];
|
||||
require(addr != address(0), "AddressRegistry: Address not found");
|
||||
|
||||
return addr;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ contract BLSExpander {
|
||||
function blsCallMultiCheckRewardIncrease(
|
||||
IERC20 tokenRewardAddress,
|
||||
uint256 tokenRewardAmount,
|
||||
VerificationGateway.Bundle calldata bundle
|
||||
IWallet.Bundle calldata bundle
|
||||
// uint256[4][] calldata publicKeys,
|
||||
// uint256[2] memory signature,
|
||||
// VerificationGateway.TxSet[] calldata txs
|
||||
@@ -102,6 +102,7 @@ contract BLSExpander {
|
||||
function blsCallMultiSameCallerContractFunction(
|
||||
uint256[4] calldata publicKey,
|
||||
uint256 nonce,
|
||||
uint256 gas,
|
||||
uint256[2] calldata signature,
|
||||
address contractAddress,
|
||||
bytes4 methodId,
|
||||
@@ -109,7 +110,7 @@ contract BLSExpander {
|
||||
) external {
|
||||
uint256 length = encodedParamSets.length;
|
||||
|
||||
VerificationGateway.Bundle memory bundle;
|
||||
IWallet.Bundle memory bundle;
|
||||
bundle.signature = signature;
|
||||
|
||||
bundle.senderPublicKeys = new uint256[4][](1);
|
||||
@@ -117,6 +118,7 @@ contract BLSExpander {
|
||||
|
||||
bundle.operations = new IWallet.Operation[](1);
|
||||
bundle.operations[0].nonce = nonce;
|
||||
bundle.operations[0].gas = gas;
|
||||
bundle.operations[0].actions = new IWallet.ActionData[](length);
|
||||
for (uint256 i=0; i<length; i++) {
|
||||
bundle.operations[0].actions[i].ethValue = 0;
|
||||
@@ -185,7 +187,7 @@ contract BLSExpander {
|
||||
}
|
||||
|
||||
// Use them to re-create bundle
|
||||
VerificationGateway.Bundle memory bundle;
|
||||
IWallet.Bundle memory bundle;
|
||||
bundle.signature = addressBundle.signature;
|
||||
bundle.senderPublicKeys = senderPublicKeys;
|
||||
bundle.operations = addressBundle.operations;
|
||||
|
||||
94
contracts/contracts/BLSExpanderDelegator.sol
Normal file
94
contracts/contracts/BLSExpanderDelegator.sol
Normal file
@@ -0,0 +1,94 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./lib/VLQ.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
|
||||
// Aka gateway, but we only care about processBundle here
|
||||
interface IBundleProcessor {
|
||||
function processBundle(
|
||||
IWallet.Bundle memory bundle
|
||||
) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
);
|
||||
}
|
||||
|
||||
contract BLSExpanderDelegator {
|
||||
uint8 constant BLS_KEY_LEN = 4;
|
||||
|
||||
IBundleProcessor gateway;
|
||||
|
||||
uint256 public nextExpanderId = 0;
|
||||
mapping(uint256 => IExpander) public expanders;
|
||||
event ExpanderRegistered(uint256 id, IExpander indexed expanderAddress);
|
||||
|
||||
constructor(IBundleProcessor gatewayParam) {
|
||||
gateway = gatewayParam;
|
||||
}
|
||||
|
||||
function run(bytes calldata stream) external returns (
|
||||
bool[] memory successes,
|
||||
bytes[][] memory results
|
||||
) {
|
||||
IWallet.Bundle memory bundle;
|
||||
|
||||
// The signature is just the last 64 bytes.
|
||||
uint256 opsByteLen = stream.length - 64;
|
||||
bundle.signature = abi.decode(stream[opsByteLen:], (uint256[2]));
|
||||
stream = stream[:opsByteLen]; // Only keep what we still need
|
||||
|
||||
uint256 vlqValue;
|
||||
|
||||
// Get the number of operations upfront so that we can allocate the
|
||||
// memory. This information is technically redundant but extracting it
|
||||
// by decoding everything before we have a place to put it would add a
|
||||
// lot of complexity. For <=127 operations it's only one extra byte.
|
||||
// Otherwise 2 bytes.
|
||||
(vlqValue, stream) = VLQ.decode(stream);
|
||||
|
||||
bundle.senderPublicKeys = new uint256[BLS_KEY_LEN][](vlqValue);
|
||||
bundle.operations = new IWallet.Operation[](vlqValue);
|
||||
|
||||
uint256 opsDecoded = 0;
|
||||
|
||||
while (stream.length > 0) {
|
||||
// First figure out which expander to use.
|
||||
(vlqValue, stream) = VLQ.decode(stream);
|
||||
IExpander expander = expanders[vlqValue];
|
||||
require(expander != IExpander(address(0)), "expander not found");
|
||||
|
||||
// Then use it to expand the operation.
|
||||
(
|
||||
uint256[BLS_KEY_LEN] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 expanderBytesRead
|
||||
) = expander.expand(stream);
|
||||
|
||||
// It would be more consistent to have .expand above return the new
|
||||
// stream, but there appears to be a bug in solidity that prevents
|
||||
// this.
|
||||
stream = stream[expanderBytesRead:];
|
||||
|
||||
bundle.senderPublicKeys[opsDecoded] = senderPublicKey;
|
||||
bundle.operations[opsDecoded] = operation;
|
||||
|
||||
opsDecoded++;
|
||||
}
|
||||
|
||||
// Finished expanding. Now just return the call.
|
||||
return gateway.processBundle(bundle);
|
||||
}
|
||||
|
||||
function registerExpander(
|
||||
IExpander expander
|
||||
) external {
|
||||
uint256 expanderId = nextExpanderId;
|
||||
nextExpanderId += 1;
|
||||
expanders[expanderId] = expander;
|
||||
|
||||
emit ExpanderRegistered(expanderId, expander);
|
||||
}
|
||||
}
|
||||
35
contracts/contracts/BLSPublicKeyRegistry.sol
Normal file
35
contracts/contracts/BLSPublicKeyRegistry.sol
Normal file
@@ -0,0 +1,35 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
contract BLSPublicKeyRegistry {
|
||||
mapping(uint256 => uint256[4]) public blsPublicKeys;
|
||||
uint256 public nextId = 0;
|
||||
|
||||
event BLSPublicKeyRegistered(uint256 id, bytes32 indexed blsPublicKeyHash);
|
||||
|
||||
function register(uint256[4] memory blsPublicKey) external {
|
||||
uint256 id = nextId;
|
||||
nextId += 1;
|
||||
blsPublicKeys[id] = blsPublicKey;
|
||||
|
||||
emit BLSPublicKeyRegistered(id, keccak256(abi.encode(blsPublicKey)));
|
||||
}
|
||||
|
||||
function lookup(uint256 id) external view returns (uint256[4] memory) {
|
||||
uint256[4] memory blsPublicKey = blsPublicKeys[id];
|
||||
require(!isZeroBLSPublicKey(blsPublicKey), "BLSPublicKeyRegistry: BLS public key not found");
|
||||
|
||||
return blsPublicKey;
|
||||
}
|
||||
|
||||
function isZeroBLSPublicKey(uint256[4] memory blsPublicKey) internal pure returns (bool) {
|
||||
for (uint i = 0; i < 4; i++) {
|
||||
if (blsPublicKey[i] != 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
81
contracts/contracts/BLSRegistration.sol
Normal file
81
contracts/contracts/BLSRegistration.sol
Normal file
@@ -0,0 +1,81 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AddressRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
|
||||
contract BLSRegistration is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
|
||||
uint256 nonce;
|
||||
(nonce, stream) = VLQ.decode(stream);
|
||||
|
||||
uint256 gas;
|
||||
(gas, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
uint256 txOriginPayment;
|
||||
(txOriginPayment, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
uint256 actionLen = 1;
|
||||
|
||||
if (txOriginPayment > 0) {
|
||||
actionLen += 1;
|
||||
}
|
||||
|
||||
operation = IWallet.Operation({
|
||||
nonce: nonce,
|
||||
gas: gas,
|
||||
actions: new IWallet.ActionData[](actionLen)
|
||||
});
|
||||
|
||||
operation.actions[0] = IWallet.ActionData({
|
||||
ethValue: 0,
|
||||
contractAddress: address(this),
|
||||
encodedFunction: abi.encodeWithSignature(
|
||||
"register(uint256[4])",
|
||||
senderPublicKey
|
||||
)
|
||||
});
|
||||
|
||||
if (txOriginPayment > 0) {
|
||||
operation.actions[1] = IWallet.ActionData({
|
||||
ethValue: txOriginPayment,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function register(uint256[4] memory blsPublicKey) external {
|
||||
blsPublicKeyRegistry.register(blsPublicKey);
|
||||
addressRegistry.register(msg.sender);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
//SPDX-License-Identifier: Unlicense
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
@@ -7,6 +7,9 @@ pragma abicoder v2;
|
||||
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
interface IVerificationGateway {
|
||||
function isValidSignature(bytes32 _hash, bytes memory _signature) external view returns (bool);
|
||||
}
|
||||
|
||||
/** Minimal upgradable smart contract wallet.
|
||||
Generic calls can only be requested by its trusted gateway.
|
||||
@@ -194,6 +197,22 @@ contract BLSWallet is Initializable, IWallet
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dev ERC-1271 signature validation
|
||||
*/
|
||||
function isValidSignature(
|
||||
bytes32 hash,
|
||||
bytes memory signature
|
||||
) public view returns (bytes4 magicValue) {
|
||||
bool verified = IVerificationGateway(trustedBLSGateway).isValidSignature(hash, signature);
|
||||
|
||||
if (verified) {
|
||||
magicValue = 0x1626ba7e;
|
||||
} else {
|
||||
magicValue = 0xffffffff;
|
||||
}
|
||||
}
|
||||
|
||||
function stripMethodId(bytes memory encodedFunction) pure private returns(bytes memory) {
|
||||
bytes memory params = new bytes(encodedFunction.length - 4);
|
||||
for (uint256 i=0; i<params.length; i++) {
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.8.4 <0.9.0;
|
||||
|
||||
contract Create2Deployer {
|
||||
event Deployed(address sender, uint256 salt, address addr);
|
||||
|
||||
function addressFrom(
|
||||
address sender,
|
||||
uint256 salt,
|
||||
bytes calldata code
|
||||
) public pure returns (
|
||||
address predictedAddress
|
||||
) {
|
||||
predictedAddress = address(uint160(uint(keccak256(abi.encodePacked(
|
||||
bytes1(0xff),
|
||||
sender,
|
||||
salt,
|
||||
keccak256(code)
|
||||
)))));
|
||||
}
|
||||
|
||||
function deploy(
|
||||
uint256 salt,
|
||||
bytes memory code
|
||||
) public {
|
||||
address addr;
|
||||
assembly {
|
||||
addr := create2(0, add(code, 0x20), mload(code), salt)
|
||||
if iszero(extcodesize(addr)) {
|
||||
revert(0, 0)
|
||||
}
|
||||
}
|
||||
emit Deployed(msg.sender, salt, addr);
|
||||
}
|
||||
|
||||
}
|
||||
316
contracts/contracts/ERC20Expander.sol
Normal file
316
contracts/contracts/ERC20Expander.sol
Normal file
@@ -0,0 +1,316 @@
|
||||
//SPDX-License-Identifier: MIT
|
||||
pragma solidity >=0.7.0 <0.9.0;
|
||||
pragma abicoder v2;
|
||||
|
||||
import "./AddressRegistry.sol";
|
||||
import "./BLSPublicKeyRegistry.sol";
|
||||
import "./AggregatorUtilities.sol";
|
||||
import "./lib/RegIndex.sol";
|
||||
import "./lib/VLQ.sol";
|
||||
import "./lib/PseudoFloat.sol";
|
||||
import "./interfaces/IExpander.sol";
|
||||
import "./interfaces/IWallet.sol";
|
||||
|
||||
/**
|
||||
* An expander that supports ERC20 operations.
|
||||
*
|
||||
* This is a bit of a first pass, it could be more compact:
|
||||
* - Optimize specifically for transfer
|
||||
* - Require registries, removing the need for a bit stream
|
||||
* - Use a fixed gas value
|
||||
* - Use a dedicated ERC20 registry, allowing a single byte for popular
|
||||
* currencies (and 2 bytes for all but the most obscure currencies)
|
||||
* - Use a fixed tx.origin reward (or call contract that computes an appropriate
|
||||
* reward)
|
||||
*
|
||||
* This would get us down to about 11 bytes. (Current example is 20 bytes.)
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* 0f - 0x0f = 0b1111 bit stream:
|
||||
* - 1: Use registry for BLS key
|
||||
* - 1: Include a tx.origin payment
|
||||
* - 1: Use registry for ERC20 address
|
||||
* - 1: Use registry for recipient address
|
||||
* 000000 - Registry index for sendWallet's public key
|
||||
* 00 - nonce: 0
|
||||
* 0bda28 - gas: 92,483
|
||||
* 02 - two actions
|
||||
* 000000 - Registry index for ERC20 address
|
||||
* 00 - transfer
|
||||
* 000002 - Registry index for recipient address
|
||||
* 9100 - 0.1 MCK (amount/value)
|
||||
* 6d00 - Pay 0.000005 ETH to tx.origin
|
||||
*/
|
||||
contract ERC20Expander is IExpander {
|
||||
BLSPublicKeyRegistry public blsPublicKeyRegistry;
|
||||
AddressRegistry public addressRegistry;
|
||||
AggregatorUtilities public aggregatorUtilities;
|
||||
|
||||
constructor(
|
||||
BLSPublicKeyRegistry blsPublicKeyRegistryParam,
|
||||
AddressRegistry addressRegistryParam,
|
||||
AggregatorUtilities aggregatorUtilitiesParam
|
||||
) {
|
||||
blsPublicKeyRegistry = blsPublicKeyRegistryParam;
|
||||
addressRegistry = addressRegistryParam;
|
||||
aggregatorUtilities = aggregatorUtilitiesParam;
|
||||
}
|
||||
|
||||
function expand(bytes calldata stream) external view returns (
|
||||
uint256[4] memory senderPublicKey,
|
||||
IWallet.Operation memory operation,
|
||||
uint256 bytesRead
|
||||
) {
|
||||
uint256 originalStreamLen = stream.length;
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
uint256 bitStream;
|
||||
|
||||
(bitStream, stream) = VLQ.decode(stream);
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(
|
||||
bitStream
|
||||
);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
senderPublicKey = blsPublicKeyRegistry.lookup(decodedValue);
|
||||
} else {
|
||||
senderPublicKey = abi.decode(stream[:128], (uint256[4]));
|
||||
stream = stream[128:];
|
||||
}
|
||||
|
||||
(decodedValue, stream) = VLQ.decode(stream);
|
||||
operation.nonce = decodedValue;
|
||||
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
operation.gas = decodedValue;
|
||||
|
||||
uint256 actionLen;
|
||||
(actionLen, stream) = VLQ.decode(stream);
|
||||
operation.actions = new IWallet.ActionData[](actionLen);
|
||||
|
||||
// hasTxOriginPayment
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
// We would use a separate variable for this, but the solidity
|
||||
// compiler makes it important to minimize local variables.
|
||||
actionLen -= 1;
|
||||
}
|
||||
|
||||
for (uint256 i = 0; i < actionLen; i++) {
|
||||
(
|
||||
operation.actions[i].contractAddress,
|
||||
stream,
|
||||
bitStream
|
||||
) = decodeAddress(
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
|
||||
(
|
||||
operation.actions[i].encodedFunction,
|
||||
stream,
|
||||
bitStream
|
||||
) = decodeFunctionCall(
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
if (actionLen < operation.actions.length) {
|
||||
(decodedValue, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
operation.actions[actionLen] = IWallet.ActionData({
|
||||
ethValue: decodedValue,
|
||||
contractAddress: address(aggregatorUtilities),
|
||||
encodedFunction: abi.encodeWithSignature("sendEthToTxOrigin()")
|
||||
});
|
||||
}
|
||||
|
||||
bytesRead = originalStreamLen - stream.length;
|
||||
}
|
||||
|
||||
function decodeBit(uint256 bitStream) internal pure returns (bool, uint256) {
|
||||
return ((bitStream & 1) == 1, bitStream >> 1);
|
||||
}
|
||||
|
||||
// Following the naming convention this would be called
|
||||
// decodeEncodedFunction, but that's pretty confusing.
|
||||
function decodeFunctionCall(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
uint256 methodIndex;
|
||||
(methodIndex, stream) = VLQ.decode(stream);
|
||||
|
||||
if (methodIndex == 0) {
|
||||
return decodeTransfer(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 1) {
|
||||
return decodeTransferFrom(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 2) {
|
||||
return decodeApprove(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 3) {
|
||||
// Not a real method, but uint256Max is common for approve, and is
|
||||
// not represented efficiently by PseudoFloat.
|
||||
return decodeApproveMax(stream, bitStream);
|
||||
}
|
||||
|
||||
if (methodIndex == 4) {
|
||||
return decodeMint(stream, bitStream);
|
||||
}
|
||||
|
||||
revert("Unrecognized ERC20 method index");
|
||||
}
|
||||
|
||||
function decodeTransfer(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature("transfer(address,uint256)", to, value),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeTransferFrom(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address from;
|
||||
(from, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"transferFrom(address,address,uint256)",
|
||||
from,
|
||||
to,
|
||||
value
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeApprove(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address spender;
|
||||
(spender, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"approve(address,uint256)",
|
||||
spender,
|
||||
value
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeApproveMax(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address spender;
|
||||
(spender, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature(
|
||||
"approve(address,uint256)",
|
||||
spender,
|
||||
type(uint256).max
|
||||
),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeMint(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
bytes memory,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
address to;
|
||||
(to, stream, bitStream) = decodeAddress(stream, bitStream);
|
||||
|
||||
uint256 value;
|
||||
(value, stream) = PseudoFloat.decode(stream);
|
||||
|
||||
return (
|
||||
abi.encodeWithSignature("mint(address,uint256)", to, value),
|
||||
stream,
|
||||
bitStream
|
||||
);
|
||||
}
|
||||
|
||||
function decodeAddress(
|
||||
bytes calldata stream,
|
||||
uint256 bitStream
|
||||
) internal view returns (
|
||||
address,
|
||||
bytes calldata,
|
||||
uint256
|
||||
) {
|
||||
uint256 decodedValue;
|
||||
bool decodedBit;
|
||||
|
||||
(decodedBit, bitStream) = decodeBit(bitStream);
|
||||
|
||||
if (decodedBit) {
|
||||
(decodedValue, stream) = RegIndex.decode(stream);
|
||||
return (addressRegistry.lookup(decodedValue), stream, bitStream);
|
||||
}
|
||||
|
||||
return (address(bytes20(stream[:20])), stream[20:], bitStream);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user