614 Commits

Author SHA1 Message Date
Blake Duncan
cdecab26ee Reorder workflow steps 2023-03-29 14:12:22 +01:00
Blake Duncan
e2b6e4f1ee Debug session 2023-03-29 14:05:40 +01:00
Blake Duncan
7c2901f243 update test verification gateway address 2023-03-29 13:26:26 +01:00
Blake Duncan
4a7331c4a7 Publish experimental bls-wallet-clients 2023-03-28 15:32:06 +01:00
Blake Duncan
1fb4a557ab Update param naming 2023-03-28 15:24:22 +01:00
Blake Duncan
7862b5278a Merge branch 'main' into auditIssue7 2023-03-28 15:16:40 +01:00
Blake Duncan
e2ef1ed62e Fix TS issue in aggregator 2023-03-28 14:42:46 +01:00
John Guilding
96bfb32e5b Merge pull request #562 from web3well/549-add-documentation-for-bls-provider-and-bls-signer
549 add documentation for bls provider and bls signer
2023-03-28 14:36:57 +01:00
Blake Duncan
2f9a3442f8 Fix linting issues 2023-03-28 14:27:40 +01:00
Blake Duncan
8833c5f0af Rename variables and code cleanup 2023-03-28 14:11:47 +01:00
Blake Duncan
9505ed425b Using seperate domains for proof of possession messages and for bundles 2023-03-28 13:11:08 +01:00
JohnGuilding
7435d9976e Fix typescript errors in BundleTable 2023-03-28 13:04:58 +01:00
JohnGuilding
2dae355817 Update root readme to include provider link 2023-03-28 11:07:20 +01:00
JohnGuilding
e734209df0 Remove BlsWalletWrapper in provider docs where possible 2023-03-28 11:06:50 +01:00
Blake Duncan
fedf05cc7e Fix test by signing message with correct verification gateway 2023-03-27 17:28:43 +01:00
Blake Duncan
7dc7bb4a5e use chainId and verification gateway in bls domain 2023-03-27 13:35:59 +01:00
JohnGuilding
d2c6cff629 Add missing documentation following rebase 2023-03-27 12:48:35 +01:00
JohnGuilding
25469d50e4 Remove directly paying aggregator fees docs example 2023-03-27 11:09:26 +01:00
JohnGuilding
548301d32d Add bls provider guide and ts doc comments 2023-03-27 11:09:00 +01:00
John Guilding
ac7cd956a8 Merge pull request #548 from web3well/413-improve-private-key-management-in-bls-provider-and-signer
413 improve private key management in bls provider and signer
2023-03-22 19:20:06 +00:00
JohnGuilding
5423f65503 Add comment explaining throwaway BlsWalletWrapper 2023-03-22 12:03:28 +00:00
JohnGuilding
3c0f36f444 re-add accidentally deleted tests 2023-03-16 17:19:36 +00:00
JohnGuilding
32c6b13e7d Cleanup test code & add contract interaction gas estimate tests 2023-03-16 16:59:28 +00:00
JohnGuilding
e3bbd393d8 Remove reference to signer from provider 2023-03-16 16:12:19 +00:00
Jacob Caban-Tomski
b203072196 Merge pull request #540 from web3well/375-add-multi-action-transactions-to-bls-provider
Add multi-action send transaction method to bls signer
2023-03-14 17:50:02 -06:00
JohnGuilding
ee17357e03 Fix intermittent timing issue with test 2023-03-14 14:07:22 +00:00
JohnGuilding
f6770cf2d4 Handle batch options 2023-03-14 14:07:22 +00:00
JohnGuilding
cd8065be16 Fix intermittent signature mismatch 2023-03-14 14:07:22 +00:00
JohnGuilding
94d6bc8280 Include txn index in errors when "to" has not been defined 2023-03-14 14:07:22 +00:00
JohnGuilding
24a871814e Adding missing transaction batch signing test case 2023-03-14 14:07:22 +00:00
JohnGuilding
3567167ccb Unskip batched tx test 2023-03-14 14:07:22 +00:00
JohnGuilding
b4e6be0f98 Use mint instead of safeMint to fix failing test 2023-03-14 14:07:22 +00:00
JohnGuilding
e86db6f72e Move common fee estimate logic to helper functions 2023-03-14 14:07:22 +00:00
JohnGuilding
3a8446fda1 Add contract interaction tests for provider batched transactions 2023-03-14 14:07:22 +00:00
JohnGuilding
c1c518e922 Move bls signer test to integration tests 2023-03-14 14:07:22 +00:00
JohnGuilding
bcc16373f9 Add multi-action send transaction method to bls provider 2023-03-14 14:07:08 +00:00
JohnGuilding
0c65c78d7e Add multi-action send transaction method to bls signer 2023-03-14 14:04:16 +00:00
Andrew Morris
fbb0fe3a1a Merge pull request #541 from web3well/499-Improve-private-key-management-in-BlsWalletWrapper
Improve private key management in BlsWalletWrapper
2023-03-14 17:29:50 +11:00
JohnGuilding
2f8df2fe3a Publish experimental release, update extension & aggregator 2023-03-06 18:06:26 +00:00
JohnGuilding
1452ef5f4a Update clients readme 2023-03-06 17:19:12 +00:00
JohnGuilding
c963d622a1 Remove private key param from syncWallet method 2023-03-06 12:05:29 +00:00
JohnGuilding
ca307ef52c Rename test vars and clean up 2023-03-03 17:24:37 +00:00
JohnGuilding
597920944a Remove optional blsWalletSigner param 2023-03-03 16:44:53 +00:00
JohnGuilding
aa21c603fe Move BlsWalletWrapper private key references to blsWalletSigner 2023-03-03 15:02:21 +00:00
John Guilding
af44452199 Merge pull request #512 from web3well/bls-provider-accounts-blocks-and-network-status-methods
Add bls provider accounts, blocks, & network status method tests
2023-02-21 10:42:03 +00:00
JohnGuilding
c9c03a699d Add getTransactionCount method and update tests 2023-02-21 09:55:13 +00:00
JohnGuilding
cbbf32225a Update chainIds to reflect geth integration test changes 2023-02-21 09:52:15 +00:00
JohnGuilding
5dfa46f126 Add accounts, blocks, & network status method tests
Remove .only
2023-02-21 09:52:03 +00:00
John Guilding
f5f6f139a3 Merge pull request #535 from web3well/provider-pays-aggregator-fees
Provider pays aggregator fees
2023-02-20 18:18:31 +00:00
JohnGuilding
46987d1002 Ensure signer is funded for bls provider contract tests 2023-02-20 15:56:56 +00:00
JohnGuilding
334816adda Update fee estimate examples 2023-02-20 12:12:30 +00:00
JohnGuilding
d3d4541b55 Update tests for invalid signed transaction 2023-02-20 12:12:30 +00:00
JohnGuilding
0927798ea0 Append action to pay tx.origin when estimating fee & update docs 2023-02-20 12:12:30 +00:00
JohnGuilding
d00f59173f Update env.test to closer match .env.local.example & update signer sign tx test 2023-02-20 12:12:30 +00:00
JohnGuilding
7fc77523cc Set require fees to true in test env 2023-02-20 12:12:30 +00:00
JohnGuilding
0aa0f4efa2 Move gas estimate to signer when sending txs via provider
Update test comment on account nonce
2023-02-20 12:12:30 +00:00
JohnGuilding
8db993a76f Use estimateFee and pay fees
Add aggregator utils address to clients tests setup
2023-02-20 12:12:30 +00:00
John Guilding
a1892cb9a1 Merge pull request #538 from web3well/384-bls-provider-base-provider-event-emitter-logs-and-inspection-methods
Add provider tests for base provider, event emitter, logs, inspection methods
2023-02-20 12:08:53 +00:00
JohnGuilding
f2c428871f Generate test private keys with BlsWalletWrapper helper method 2023-02-20 11:19:47 +00:00
JohnGuilding
d621f982d6 Add provider tests for base provider, event emitter, logs, inspection methods 2023-02-17 16:34:55 +00:00
Andrew Morris
db968c34fd Merge pull request #530 from web3well/bw-434-fix-bignum-tests 2023-02-14 23:10:41 +11:00
Andrew Morris
f535662407 Merge pull request #529 from web3well/bw-498-aggregator-devx 2023-02-14 23:09:28 +11:00
Andrew Morris
e5a426a991 Fix intermittent test failure by using normalized hex strings 2023-02-14 14:20:42 +11:00
Andrew Morris
d5422da19d Update hosting guide to use dockerhub images 2023-02-14 13:42:00 +11:00
Andrew Morris
12286389b1 Document easier ways of running aggregator 2023-02-14 12:40:18 +11:00
Andrew Morris
6968940f0c Merge pull request #520 from web3well/fee-tests
Additional tests for estimateGas for different Tx types
2023-02-14 09:20:29 +11:00
Andrew Morris
ea12f8fc42 Update contracts/test-integration/BlsSignerContractInteraction.test.ts
Co-authored-by: John Guilding <54913924+JohnGuilding@users.noreply.github.com>
2023-02-14 09:14:25 +11:00
John Guilding
c72a843c85 Merge pull request #513 from web3well/use-geth-in-client-integration-tests
Use geth in client integration tests
2023-02-10 13:58:15 +00:00
JohnGuilding
15f28dfe76 separate geth and hardhat contract deploys 2023-02-10 12:06:41 +00:00
JohnGuilding
0e8588e819 Run integration tests with geth in github workflow 2023-02-10 12:04:54 +00:00
JohnGuilding
080568cb93 Modify integration tests to pass against geth node 2023-02-10 10:11:18 +00:00
kautukkundan
6b4f6ccd96 added additional fee checks for each tx type 2023-02-10 15:29:18 +08:00
Andrew Morris
3bf2d78215 Merge pull request #516 from web3well/bw-507-followup
Sqlite followup
2023-02-10 10:29:31 +11:00
Andrew Morris
76ea1cabcd Remove more --unstable 2023-02-10 10:15:48 +11:00
Andrew Morris
f4e1c9b250 Remove unnecessary --allow-write 2023-02-10 10:14:02 +11:00
Andrew Morris
5d34594bfd Add --allow-write to aggregator.ts 2023-02-10 10:14:02 +11:00
Andrew Morris
b6e62ed303 Add --allow-write to showTables.ts 2023-02-10 10:14:02 +11:00
Andrew Morris
771335002b aggregator.db -> aggregator.sqlite 2023-02-10 10:13:45 +11:00
Jacob Caban-Tomski
c9d88ce73b Merge pull request #341 from web3well/feature/update-gas-measurements
Update gas measurement script for Arbitrum Nitro
2023-02-09 16:01:45 -07:00
Jacob Caban-Tomski
39e346f057 Update contracts/scripts/measure_gas/README.md
Co-authored-by: Andrew Morris <voltrevo@gmail.com>
2023-02-09 15:53:11 -07:00
omahs
5769d08a0e Fix: typos (#517)
Fix docs typos
2023-02-09 18:41:15 +00:00
Andrew Morris
99ed78bf49 Merge pull request #504 from web3well/bw-91-publish-agg-image
Publish aggregator images using CI
2023-02-09 12:51:41 +11:00
Andrew Morris
ef97000344 Ignore aggregator.db 2023-02-09 11:16:50 +11:00
Andrew Morris
55ad789c3a Remove --unstable 2023-02-09 11:16:45 +11:00
Andrew Morris
2a59616b2f Merge pull request #510 from web3well/bw-507-sqlite
Switch to sqlite
2023-02-09 10:54:19 +11:00
Jacob Caban-Tomski
54f3fbcf41 Infer net cfg name for hardhat network.
Remove arbitrum measurements if no l1Gas prop found.
2023-02-08 11:29:40 -07:00
Andrew Morris
a31e53871a Replace sleep with wait-for-rpc 2023-02-08 12:38:25 +11:00
Andrew Morris
ceaa69adeb Fix local-aggregator-deploy 2023-02-08 12:10:30 +11:00
Andrew Morris
ba617b0a75 Remove unnecessary test wrapper in BundleTable.test.ts 2023-02-08 11:48:42 +11:00
Andrew Morris
aa16947628 Fix query logging in runQueryGroup 2023-02-08 11:44:11 +11:00
Andrew Morris
80efc04784 Remove postquery from deps 2023-02-08 11:39:01 +11:00
Andrew Morris
7dd04ee62e Remove remaining postgres things 2023-02-08 11:34:00 +11:00
Andrew Morris
3f7db13dba Remove/update postgres in readme 2023-02-08 11:33:01 +11:00
Andrew Morris
37ead8f0c9 Update environment variables 2023-02-08 11:30:38 +11:00
Andrew Morris
98ae7ec6c7 Fix tests, cleanup 2023-02-08 11:21:47 +11:00
Andrew Morris
131f84b7fc Updates for sync and rollbacks 2023-02-08 11:21:47 +11:00
Andrew Morris
c8f454fd90 Fix BundleTable.test.ts 2023-02-08 11:21:47 +11:00
Andrew Morris
58dff2f0ce First pass conversion in BundleTable 2023-02-08 11:21:47 +11:00
Jacob Caban-Tomski
8ef89face7 Merge pull request #497 from web3well/integrations-documentation-improvements
Integrations documentation improvements
2023-02-03 19:07:35 -07:00
JohnGuilding
ad0a019ca1 Add WSL2 recommendation 2023-02-03 17:56:08 +00:00
JohnGuilding
c7b245fed9 Tweak BLS Wallet description 2023-02-03 14:53:56 +00:00
JohnGuilding
b715a7f491 Edit contracts production use statement & fix typos 2023-02-03 14:53:56 +00:00
JohnGuilding
c620f3cd7e docs typos and add additional context 2023-02-03 14:53:56 +00:00
JohnGuilding
7640415ab6 Format docs 2023-02-03 14:53:56 +00:00
JohnGuilding
d01df873e6 Add troubleshooting tips & fee info to client docs 2023-02-03 14:53:56 +00:00
JohnGuilding
639cf4c26d Remove redudent extension env instructions 2023-02-03 14:53:56 +00:00
JohnGuilding
f964136a6e Update aggregator docs 2023-02-03 14:53:56 +00:00
JohnGuilding
506d376b36 Add statement on production use for contracts 2023-02-03 14:53:56 +00:00
JohnGuilding
9835886fb1 Add additional examples to clients readme 2023-02-03 14:53:56 +00:00
JohnGuilding
54f2c7d1fe Reference agg env table in dev setup instructions 2023-02-03 14:53:56 +00:00
JohnGuilding
4a3d9615a9 Remove stale extension .env instructions 2023-02-03 14:53:56 +00:00
JohnGuilding
fcd629c20d Add more descriptive intro to root readme 2023-02-03 14:53:56 +00:00
Andrew Morris
4eba0baabf Clarify cli args 2023-02-03 18:09:43 +11:00
Andrew Morris
387e478249 Add aggregator-dockerhub workflow 2023-02-03 18:03:01 +11:00
Andrew Morris
b61af4acd2 Use latest deno 2023-02-03 15:23:31 +11:00
Andrew Morris
ff4ff05c83 buildName -> tag 2023-02-03 15:22:56 +11:00
Andrew Morris
dfe73e4684 also-tag-latest 2023-02-03 14:48:49 +11:00
Andrew Morris
746038be5b Add --push 2023-02-03 14:38:21 +11:00
Andrew Morris
99f08967ab Allow customizing image name 2023-02-03 14:32:37 +11:00
Andrew Morris
916b7fbb5f Log build outputs 2023-02-03 14:29:16 +11:00
Andrew Morris
f1d16bbb88 Add --image-only for skipping the save step 2023-02-03 14:22:48 +11:00
Blake Duncan
6af93bde8c Add recovery methods to bls-wallet-clients module(#486) 2023-02-01 19:32:34 +00:00
Jacob Caban-Tomski
d7f21c96a5 Further optimize gas measurement script.
Add BLSWalletContracts and connectToContracts to clients to make it easier to bootstrap contracts.
Simplify gas measurement config to rely on netowrk config.
Use token minting instead of transfers.
Add processBundle utility with error checking.
Use a custom ethers.js provider and signer to better handle RPC throttling/rate limiting.
Remove sleep between transaction config runs.
2023-01-31 17:29:28 -07:00
Jacob Caban-Tomski
5934cf19bd Add documentation, addresses to gas measurements
Add gas measurement README.
Add BLS Wallet & EOA signer addresses to results.
Split gas measurement index into run & measurement files.
Minor code cleanup & bug fixes.
2023-01-30 20:10:32 -07:00
Jacob Caban-Tomski
7c242ad985 Update gas script for Arbitrum Nitro
Add alea lib for deterministic random values
Move gas scripts to new folder
Move to config based approach for measurement script
Fix bls wallet lazy creation in fixture so nonce will be correct for larger amount of wallets
Fix fixture initialization of bls wallet contract from swallowing errors & attempting to initialize when not needed.
Get arbitrum specific gas field
Add address indexing optimization to expander, better typing on contracts fixture
Use @ethereumjs/tx to get raw txn size
Add sleep between measurements
Add local Ci test to make sure measurement script is working
Update measurements to be able to handle multiple transactions
Update gethDev to allow for longer http timeouts on longer running operations
2023-01-30 18:22:46 -07:00
John Guilding
016c1e59d8 Merge pull request #480 from web3well/update-bundle-receipt-to-match-ethers-transaction-response
Update bundle receipt to match ethers transaction response
2023-01-26 14:14:06 +01:00
JohnGuilding
fe9eacd061 Remove extra level of nesting in BundleReceipt 2023-01-26 12:56:16 +00:00
JohnGuilding
d39c16bfdb Update experimental release 2023-01-26 10:20:00 +00:00
JohnGuilding
4f9ddac4b8 Rename BlsWallet type tp BlsBundleReceipt 2023-01-26 09:50:24 +00:00
JohnGuilding
fee9d200c0 Update repo to same ethers version and add experimental client module to proxy agg 2023-01-24 16:25:00 +00:00
JohnGuilding
e214c6ef35 Add experimental client module version 2023-01-24 16:05:56 +00:00
JohnGuilding
6dbee9fad2 Update todo 2023-01-24 15:51:15 +00:00
JohnGuilding
2fb5cefa5b Update bundle receipt in client and provider 2023-01-24 15:51:15 +00:00
JohnGuilding
a92a39481b Update bundle receipt in aggregator 2023-01-24 15:51:15 +00:00
Jacob Caban-Tomski
b6fc2164a3 Merge pull request #483 from web3well/docs/optimism-goerli-deploy
Add Optimism Goerli deploy to README
2023-01-24 08:33:54 -07:00
Jacob Caban-Tomski
772e44dd60 Add Optimism Goerli deploy to README
Add Optimism Goerli network config to contracts deploy in main README.
Add Optimism Goerli to extension networks.
Remove or update rinkeby, kovan, and older arbitrum and optimism test networks.
2023-01-23 17:29:17 -07:00
Blake Duncan
009e3762f7 Add missing slash in the docker run command (#479) 2023-01-23 14:31:54 +00:00
Andrew Morris
3b79d553b9 Merge pull request #477 from web3well/updateStartDocker
Change port to --net=host in startup script
2023-01-23 15:30:15 +11:00
Blake Duncan
f48979b4da Change port to --net=host 2023-01-20 16:39:12 +00:00
Andrew Morris
b49964287a Merge pull request #473 from web3well/optimism-goerli-aggregator
Add optimism goerli network config
2023-01-20 12:19:54 +11:00
John Guilding
5e29d4e4e7 Merge pull request #467 from web3well/bls-provider-and-signer-contracts-interaction
Bls provider and signer contracts interaction
2023-01-19 13:54:30 +01:00
JohnGuilding
e5e8fa187a Update issue for todo comments 2023-01-19 11:24:17 +00:00
JohnGuilding
92bc376ba6 Increase receipt polling retries 2023-01-19 10:58:06 +00:00
JohnGuilding
13ff3155df Update readme & failing signer contract test 2023-01-19 10:23:08 +00:00
JohnGuilding
91b88cd603 Update receipt handling and test 2023-01-19 10:21:33 +00:00
JohnGuilding
264491a5b0 Update receipt test 2023-01-19 10:12:06 +00:00
JohnGuilding
8b5bfcafba Move non-integration tests to clients directory 2023-01-19 10:12:06 +00:00
JohnGuilding
850bde5f46 Fix false positive test 2023-01-19 10:12:06 +00:00
JohnGuilding
c95de82ee3 Add contract factory tests 2023-01-19 10:12:05 +00:00
JohnGuilding
5f5f039a1c Add provider contract tests & update signer tests 2023-01-19 10:12:05 +00:00
JohnGuilding
d740fdc594 Use bls signer for contract interaction tests 2023-01-19 10:12:05 +00:00
JohnGuilding
b37555fcb4 debug transfer() call test WIP 2023-01-19 10:12:05 +00:00
Jacob Caban-Tomski
4d6b836961 Use gethDev network for client integration tests 2023-01-19 10:12:05 +00:00
JohnGuilding
9e386dd784 Use blsSigner for contracts tests WIP 2023-01-19 10:12:05 +00:00
Jacob Caban-Tomski
ebfa843bf8 Merge pull request #472 from web3well/bw-470-artifact-followup
Restore aggregatorLogs.txt artifact
2023-01-18 19:41:38 -07:00
Andrew Morris
283bc365d6 Restore aggregatorLogs.txt artifact 2023-01-19 13:33:09 +11:00
Jacob Caban-Tomski
26617c96d3 Merge pull request #471 from web3well/bw-470-fix-test-integration
Fix test integration
2023-01-18 19:29:48 -07:00
Andrew Morris
27bc22d755 Remove unused step 2023-01-19 13:02:13 +11:00
Andrew Morris
95bf1dfba3 Merge remote-tracking branch 'origin/main' into bw-470-fix-test-integration 2023-01-19 12:47:31 +11:00
Andrew Morris
212daabf85 Fix typo 2023-01-19 12:38:51 +11:00
Andrew Morris
3b0d3536e7 Fix access on maybe-nil bundleRow 2023-01-19 12:29:05 +11:00
Andrew Morris
636f60af2f Handle bundleService.lookupBundle returning nil 2023-01-19 12:24:05 +11:00
Jacob Caban-Tomski
4945f48a43 Merge pull request #468 from web3well/bw-416-followup
Followup suggestions from #443
2023-01-18 18:14:05 -07:00
Andrew Morris
c96c835966 Merge branch 'bw-416-followup' into bw-470-fix-test-integration 2023-01-19 11:52:35 +11:00
Andrew Morris
7747e353cc Log normally instead of using artifact 2023-01-19 11:46:07 +11:00
Andrew Morris
c46e063f0f Split out integration.yml so that test-integration can run more often and not on contract-updates 2023-01-19 11:23:33 +11:00
Andrew Morris
824b9a90ac Improve BUNDLE_CHECKING_CONCURRENCY description 2023-01-19 10:30:01 +11:00
Andrew Morris
533c81e5c6 MAX_GAS -> MAX_GAS_PER_BUNDLE 2023-01-19 10:28:10 +11:00
Andrew Morris
6a5ea24873 Merge pull request #443 from web3well/bw-416-limit-bundles-by-gas
Limit bundles by gas instead of actions
2023-01-19 10:19:27 +11:00
Jacob Caban-Tomski
dc3d4c0edb Merge pull request #454 from web3well/bw-453-agg-config-outside-image
Move config outside docker image
2023-01-18 11:42:56 -07:00
Andrew Morris
dd513f8261 Merge remote-tracking branch 'origin/main' into bw-416-limit-bundles-by-gas 2023-01-18 16:24:56 +11:00
Andrew Morris
369df16cae Update README 2023-01-18 16:17:39 +11:00
Andrew Morris
f6eb73c43a Use last entry in env file instead of first 2023-01-18 16:03:24 +11:00
Andrew Morris
147ff4aaad Simplify start-docker.sh, avoid overrides 2023-01-18 15:53:53 +11:00
Andrew Morris
7b7777bf20 Merge remote-tracking branch 'origin/main' into bw-453-agg-config-outside-image 2023-01-18 14:55:32 +11:00
Andrew Morris
85dcdc10e5 Merge pull request #452 from web3well/use-geth
Use geth
2023-01-18 14:42:14 +11:00
Andrew Morris
f34ccaceed Merge remote-tracking branch 'origin/main' into use-geth 2023-01-18 14:38:53 +11:00
Andrew Morris
9451501f20 Use a longer delay 2023-01-18 14:38:15 +11:00
Blake Duncan
92c69de7ef Persist bundles in the bundles table (#440) 2023-01-17 16:26:33 +00:00
Blake Duncan
1b95f66431 Add optimism goerli network config 2023-01-17 14:45:38 +00:00
Andrew Morris
4facaba47f Move config outside docker image 2023-01-12 18:34:21 +11:00
Andrew Morris
7111fa621b Run node in background 2023-01-12 14:09:49 +11:00
Andrew Morris
8c884186a6 Remove unnecessary flags 2023-01-12 14:04:52 +11:00
Andrew Morris
4a61371e0a Pull container beforehand so that we don't hit the node too quickly 2023-01-12 14:01:40 +11:00
Andrew Morris
b698c7f3a9 Use geth on CI 2023-01-12 13:56:36 +11:00
Andrew Morris
bcc460bf34 Use faster polling when running locally 2023-01-12 13:53:47 +11:00
Andrew Morris
b0635d9f6a Simplify test / fix race condition 2023-01-12 13:49:05 +11:00
Andrew Morris
0eeddb376d Add hardhat option 2023-01-12 12:54:23 +11:00
Andrew Morris
bcc74926dd Replace evm_mine 2023-01-12 12:45:24 +11:00
Andrew Morris
cffd316c89 Merge branch 'main' into bw-416-limit-bundles-by-gas 2023-01-12 12:40:05 +11:00
Andrew Morris
472ac28d80 Use geth instead of hardhat for local node + automate setup 2023-01-12 12:38:08 +11:00
Jacob Caban-Tomski
1d35f4e80a Merge pull request #451 from web3well/fix-agg-test-stalls
Fix aggregator test stalls
2023-01-11 18:00:52 -07:00
Andrew Morris
7b5ca4e88d Remove gasPrice:0 2023-01-12 11:30:29 +11:00
Andrew Morris
140b606152 On stop(), stop listening for blocks 2023-01-12 10:59:21 +11:00
Jacob Caban-Tomski
61c5c43572 Merge pull request #450 from web3well/better-split-local-and-remote-docs
Better split local and remote docs
2023-01-11 14:40:17 -07:00
JohnGuilding
32d508fb6f Remove whitespace 2023-01-11 21:32:34 +01:00
JohnGuilding
e77f9bd887 Better split of local and remote docs 2023-01-11 21:28:39 +01:00
Andrew Morris
319f2d1a95 Update .env.local.example 2023-01-11 10:56:23 +11:00
John Guilding
a4d90f109c Merge pull request #439 from web3well/bls-signer-json-rpc-signer-and-signer-methods
Add bls signer json rpc signer and default signer methods
2023-01-10 14:05:19 +01:00
JohnGuilding
d8fca1bf0c Add error test for signer.unlock 2023-01-10 13:06:54 +01:00
Andrew Morris
8246c53d29 Update README 2023-01-10 17:25:09 +11:00
Andrew Morris
2448bdea4d Remove MAX_AGGREGATION_SIZE 2023-01-10 17:13:09 +11:00
Andrew Morris
25813f118b Bypass timer on profit rather than the aggregation being 'full' 2023-01-10 17:06:50 +11:00
Andrew Morris
1aa00a8848 Limit bundles by gas 2023-01-10 16:20:53 +11:00
JohnGuilding
c208296f8c Update test env to match aggregator fee changes 2023-01-09 15:33:55 +01:00
JohnGuilding
763ede3cb4 Remove unnecessary comment 2023-01-09 13:22:44 +01:00
JohnGuilding
671cec495c Add tests for signer.provider, signer.isSigner & signer.connect 2023-01-09 13:22:44 +01:00
Andrew Morris
2e2b4f3cf2 Charge accurate fees -- followup (#438)
* Split out calculation for baseFeeIncrease

* Add comments explaining maxFeePerGas calculation

* Add docstring for `#pickBest`

* Add docstring for #augmentAggregateBundle

* Update environment variables table in README

* Document fee system
2023-01-06 10:18:46 +00:00
Andrew Morris
7423b28f89 Merge pull request #418 from web3well/bw-415-charge-accurate-fees
Charge accurate fees
2023-01-06 15:17:32 +11:00
Andrew Morris
44fa3102b9 Merge remote-tracking branch 'origin/main' into bw-415-charge-accurate-fees 2023-01-06 14:50:59 +11:00
Jacob Caban-Tomski
852b676f87 Merge pull request #366 from web3well/feature/decode-errors-from-action-results
Add tests to decode error results
2023-01-05 11:08:08 -07:00
Jacob Caban-Tomski
9f4064eef9 Split client unit & intergration test CI runs 2023-01-04 13:38:31 -07:00
Jacob Caban-Tomski
2f4d91e336 Address additional PR feedback
Remove unnecessary unknown casts.
Split test error encoding functions into one for strings and one for abi encoded messages.
Remove magic numbers/slicing to extract error action data and properly generate instead.
2023-01-04 13:17:53 -07:00
Jacob Caban-Tomski
ee5f044028 Finish test coverage of OperationResults
Add html output to nyc test coverage to more easily find gaps.
2023-01-04 12:39:39 -07:00
Jacob Caban-Tomski
1b95731106 Add nyc test coverage tool 2023-01-04 12:39:39 -07:00
Jacob Caban-Tomski
9c552dc3c9 Address PR feedback
Remove Typemoq
Fix encoding error for test
Remove number of  events check in getOperationResults
Remove other event test case
2023-01-04 12:39:39 -07:00
Jacob Caban-Tomski
f66fa370e9 Add getOperationResults to clients to make it easier to decode operation errors.
Change existing contract test case to use getOperationResults.
Add typemoq to allow typed mocking.
Bump clients patch version.
2023-01-04 12:39:38 -07:00
John Guilding
8180d365f2 Add provider tests to github workflows (#425)
* Add workflow for spinning up env & running tests

* Rename provider tests directory

* Remove periods from error messages

* Move chai spies to hardhat config

* Print agg error logs to artifact

* Use fresh state between tests

* Fix nonce too low error
2023-01-04 11:21:53 +00:00
Jacob Caban-Tomski
2b8f3c2ead Merge pull request #432 from web3well/pin-clients-0.8.2
Pin bls-wallet-clients@0.8.2
2023-01-03 08:27:04 -07:00
Jacob Caban-Tomski
99847b4ab2 Pin bls-wallet-clients@0.8.2 2022-12-23 12:17:18 -07:00
Jacob Caban-Tomski
8fa6b50b61 Merge pull request #431 from web3well/bug/agg-client-fetch-error
Fix issue with fetch in client in browser
2022-12-23 12:10:03 -07:00
Jacob Caban-Tomski
5911285f8d Pin bls-wallet-clients@0.8.2-add1351 2022-12-23 11:52:22 -07:00
Blake Duncan
e1e985a82c Signer tests for the BLS Signer (#399) 2022-12-20 11:26:29 +00:00
Jacob Caban-Tomski
add135161f Parse text response 2022-12-19 19:36:45 -07:00
Jacob Caban-Tomski
a3a7cc6fe7 Properly bind fetch 2022-12-19 19:29:31 -07:00
Blake Duncan
9078f6189a Wallet recovery (#342)
* Add addRecoveryWallet function to the keyring controller

* Get wallet hash from the recovery address so user no longer needs to copy that over

* Update types for addRecoveryWallet method

* Fixing the add recovery wallet function

* Use aggregator url from currently selected network

* Adding a param for the signers private key and saving the new wallet in the keyring

* added UI for entering recovery hash (#355)

* added UI for entering recovery hash

* added ui for salt

* completed workflow for wallet recovery

* lint: fix

* updated hex conversion of salt

* using temp wallet for recovery

* replace address after recovery

* Lint fix

* Adding randFr() for PK generation

* Add back button to recovery modal

* Missed some files to commit

* Fix address issues using randFr

* Fix randFr function by initing the mcl

* Fix lint issue

Co-authored-by: Kautuk Kundan <kautukkundan@gmail.com>
2022-12-15 15:58:53 -05:00
Jacob Caban-Tomski
46dfe7b08d Merge pull request #429 from web3well/bug/submit-error
Correctly bubble up bundle submit errors
2022-12-15 11:36:17 -05:00
Jacob Caban-Tomski
ce5d8b3a01 Add error path on submit error to extension 2022-12-14 20:01:44 -05:00
Jacob Caban-Tomski
19f6cbdc6f Pin bls-wallet-clients0.8.1-758c7c4 2022-12-14 19:47:16 -05:00
Jacob Caban-Tomski
758c7c4fe2 Revert node-fetch to 2.6.7 2022-12-14 19:43:56 -05:00
Jacob Caban-Tomski
68dbaa25c8 Add/update documentation for Aggregator client 2022-12-14 19:30:52 -05:00
Jacob Caban-Tomski
cc3da2bc5d Pin bls-wallet-clients@0.8.1-1995bbc 2022-12-14 19:11:17 -05:00
Jacob Caban-Tomski
1995bbcba0 Correctly bubble up bundle submit errors
Do not swallow submit errors from aggregator on 404.
Switch AggregatorClient fetch to use globalThis implementation if it exists.
Update client deps.
Bump next verison to 0.8.1.
2022-12-14 19:00:37 -05:00
Jacob Caban-Tomski
da03830e04 Merge pull request #422 from web3well/bls-provider-json-rpc-provider-methods
Bls provider json rpc provider methods
2022-12-14 17:54:22 -05:00
Jacob Caban-Tomski
9a6612bda2 Add return typing on anon func 2022-12-14 14:40:01 -05:00
Jacob Caban-Tomski
a266bb2432 Update unchecked signer test, typing, and comments.
Update BlsProvider constructor to accept a private key promise.
Remove unneeded comment on connectUnchecked.
Update and skip test for using a unchecked signer from a new provider, add follow up issue.
2022-12-14 14:34:33 -05:00
JohnGuilding
29240dd288 Merge branch 'main' into bls-provider-json-rpc-provider-methods 2022-12-14 18:00:30 +00:00
JohnGuilding
7d8d72e285 Drop privateKey param & use ethers method signature 2022-12-14 15:29:27 +00:00
Jacob Caban-Tomski
4d5c8d9ecc Merge pull request #419 from web3well/bls-signer-blockchain-methods
Bls signer blockchain methods
2022-12-12 11:02:02 -07:00
JohnGuilding
71c9e63ae6 Add bls signer blockchain methods
- Add tests for getBalance
- Add test for getChainId
- Add test for getGasPrice
- Add test for getTransactionCount
- Add test for call()
- Add test for estimateGas & add todo to revisit fn
- Update todo issue number
- Add error test cases for resolveName
- Tidy up & assert against regular provider chain id
2022-12-12 17:49:24 +00:00
JohnGuilding
00b668a87c Add test for provider.send 2022-12-09 13:30:05 +00:00
James Zaki
4f5b7c75d9 Test recovery of blswallet via blswallet call (#420) 2022-12-09 14:55:42 +02:00
JohnGuilding
1a13d8dcb8 Add test for listAccounts 2022-12-08 17:22:30 +00:00
JohnGuilding
aa8a969857 Add unchecked bls signer 2022-12-08 17:00:38 +00:00
JohnGuilding
e5897786d7 Add test for provider.connection 2022-12-07 12:50:12 +00:00
Andrew Morris
d5f977b657 Log bundleOverheadCost 2022-12-07 16:39:36 +11:00
Andrew Morris
0969715701 Fix AUTO_CREATE_INTERNAL_BLS_WALLET in examples 2022-12-07 16:16:15 +11:00
Andrew Morris
d95404ec6b Add config comments 2022-12-07 15:37:13 +11:00
Andrew Morris
ac03d4d165 Update example configs 2022-12-07 15:29:40 +11:00
Andrew Morris
969dd2b605 Get accurate fee estimates by using custom gas prices 2022-12-07 15:27:22 +11:00
Andrew Morris
bc18c8be1a Fix BundleServiceSubmitting tests 2022-12-06 13:46:53 +11:00
Andrew Morris
30fc05cbd5 Fix 9/10 bundle test 2022-12-06 13:35:19 +11:00
Andrew Morris
29566d50f0 Include errorReason in estimateFee, fix test 2022-12-06 13:20:30 +11:00
Andrew Morris
8064eb04d0 Return failedRows correctly 2022-12-06 12:25:58 +11:00
Andrew Morris
ef1b175f8b Use +20% fee to ensure tx gets processed during test (lots of gas fluctuation in hardhat) 2022-12-06 12:23:08 +11:00
Andrew Morris
27d939556a Remove log 2022-12-06 12:13:13 +11:00
Andrew Morris
f2d3aada78 Update test 2022-12-06 12:12:30 +11:00
Andrew Morris
f0f02f6921 Use an internal bls wallet to avoid including wallet creation in bundle overhead 2022-12-06 12:09:40 +11:00
Andrew Morris
0bfaf98dfa Fix fee configs in tests, remove obsolete tests 2022-12-05 16:22:22 +11:00
Andrew Morris
e0250ca502 Remove todo 2022-12-05 16:10:37 +11:00
Andrew Morris
27b936863b Factor out #preventLosses 2022-12-05 14:45:22 +11:00
Andrew Morris
e20b2eea52 Handle unprofitable-despite-breakeven-operations 2022-12-05 14:33:15 +11:00
Andrew Morris
a8e9f89e8f Merge remote-tracking branch 'origin/main' into bw-415-charge-accurate-fees 2022-12-05 13:44:20 +11:00
Andrew Morris
1cf7159003 Merge pull request #377 from web3well/bundleErrorLogging
Bundle error logging
2022-12-05 13:04:59 +11:00
Blake Duncan
ab1209afbd Merge branch 'main' into bundleErrorLogging
# Conflicts:
#	contracts/clients/src/Aggregator.ts
2022-12-02 11:38:59 +00:00
Blake Duncan
231bf59184 Removing 202 error code 2022-12-02 10:58:09 +00:00
Andrew Morris
294b5d13a6 measure bundle overhead gas by extrapolating backwards 2022-12-02 18:12:42 +11:00
Andrew Morris
4f6172a9a0 Remove redundant unstable flag 2022-12-02 17:46:33 +11:00
Andrew Morris
542c33d983 Update example configs 2022-12-02 17:44:33 +11:00
Andrew Morris
0d1f5c20d7 Check empty bundle before profitability 2022-12-02 17:40:36 +11:00
Andrew Morris
41ef0fe974 Check bundles individually using overhead sharing 2022-12-02 17:35:33 +11:00
Andrew Morris
e8aaf4512f Contribution is per-operation, not per-bundle 2022-12-02 16:40:56 +11:00
Andrew Morris
08d490eef4 Share the bundleOverheadGas according to breakevenOperationCount 2022-12-02 16:34:46 +11:00
Andrew Morris
1e100cbf33 Use new fee parameterization 2022-12-02 15:45:50 +11:00
John Guilding
6aee4db636 Bls provider/signer send transaction (#402)
* Add empty BlsProvider & BlsSigner classes

* Send transactions WIP

* Add init wallet method

* Add empty BlsProvider & BlsSigner classes

* Use node-fetch & update verification gateway

* Fund test bls wallet

* Add:
- provider.sendBlsTransaction
- provider.getTransactionReceipt
- signer.#verifyInit
- signer.signBlsTransaction

* - Add estimate gas function & test case
- Refactor getSigner logic to create singleton signer for each provider
- Add getSigner test cases

* Tidy up

* Update unsupported methods & sendBlsTx logic

* Rename test files

* Extract poll function logic & add retry limit

* Add test case for tx receipt error handling

* Fix incorrect test argument

* Replace use of ActionDataDto with ActionData

* Add & update test cases for transaction return types

* sendTransaction error test case & remove trycatch

* Add estimate gas test & update tests to closer align with AAA pattern

* update sendBlsTransaction error handling

* Add test to check account nonce is retrieved

* Update todos

* transaction.to error handling for & update todos

* Move provider/signer tests to dedicated directory

* provider.call & provider.getTransaction tests

* Add checkTransaction & populateTransaction tests

* Add signBlsMessage method

* Error handling updates

* Tidy up before publishing PR

* Add explicit override

* Provider & signer clean slate for merge

* make index and address non-readonly

* Fix linting errors

* Fix client module export error

* Remove console log from poll function

* Update object test assertions

* Fix failing test

* Turn no-unused-expressions off for test files

* Improve constructorGuard typing & update null/undefined checks

* Store init function as promise to be resolved

* Add BlsProvider & BlsSigner to Experimental namespace

* Remove signBlsMessage, signBlsTransaction & sendBlsTransaction. Convert logic to match ethers methods

* Use consistent private key for tests

* Fix typo & ensure consistent private key for test

* Tidy test assertions

* Update TODOs for future work
2022-12-01 12:38:44 -08:00
Blake Duncan
efa2e062cc Update client bundle receipt response type 2022-12-01 12:22:25 +00:00
Blake Duncan
8f182bc7ba Cleaning up empty string vs null inconsistency 2022-12-01 12:21:19 +00:00
Andrew Morris
e4c9c8dc16 wip remove lower-bound fee system 2022-12-01 16:33:37 +11:00
Blake Duncan
270abf90e4 Fix aggregator proxy build 2022-11-30 13:44:02 +00:00
Blake Duncan
e243212e67 Idk how I'm missing these lint errors 2022-11-30 13:22:41 +00:00
Blake Duncan
916ba7285a Fixing type issue in the extension 2022-11-30 13:16:27 +00:00
Blake Duncan
56f33ecf4e Fix BundleReceipt type and update the extension 2022-11-30 11:28:37 +00:00
Blake Duncan
035891aa37 Merge branch 'main' into bundleErrorLogging 2022-11-30 10:00:21 +00:00
Blake Duncan
69a1a19edb Update bls-wallet-clients to experimental version and update some client types 2022-11-29 17:17:20 +00:00
Blake Duncan
d476556588 Add aggregationStrategy test to check error message 2022-11-28 14:50:25 +00:00
Blake Duncan
5286429503 Add in unknown error reason 2022-11-28 12:58:27 +00:00
Jacob Caban-Tomski
72b546c7a7 Merge pull request #407 from web3well/exposeDecodeFunction
Export decode error function from the client module
2022-11-23 14:32:17 -07:00
Blake Duncan
51ee33f753 PR comments 2022-11-23 13:02:24 +00:00
Blake Duncan
3de61b7a83 Update readme and index.ts for the decodeError function 2022-11-23 10:26:59 +00:00
Blake Duncan
b62d1709dd Standardise the response body 2022-11-22 15:55:21 +00:00
Blake Duncan
0855462220 Some more clean up 2022-11-22 11:18:24 +00:00
Blake Duncan
bb2ebed49d Export decode error function from the client module 2022-11-22 11:14:50 +00:00
Blake Duncan
bd0ff0da7b Revert "Export decodeError from the client module"
This reverts commit 44fec67e6c.
2022-11-22 11:08:29 +00:00
Blake Duncan
44fec67e6c Export decodeError from the client module 2022-11-22 10:24:58 +00:00
Blake Duncan
c163638cc2 Fix lint and test errors 2022-11-22 10:05:17 +00:00
Blake Duncan
ec3c05d822 Add 202 bundle receipt status and add submitError to db 2022-11-21 17:28:24 +00:00
Blake Duncan
ebbe072320 Merge branch 'main' into bundleErrorLogging 2022-11-17 13:36:10 +00:00
Kautuk Kundan
b40d29c82d Merge pull request #400 from web3well/bls-wallet-clients-0.8.0
Pin bls-wallet-clients@0.8.0
2022-11-16 17:14:32 +05:30
Jacob Caban-Tomski
5c6d96f7e5 Pin bls-wallet-clients@0.8.0 2022-11-15 17:48:50 -07:00
Jacob Caban-Tomski
ccc8a59f2f Merge pull request #367 from web3well/contract-updates
Contract Updates & Client Integration
2022-11-15 12:10:10 -07:00
Jacob Caban-Tomski
57e7ec3d24 Merge pull request #397 from web3well/update-client-pins-again
Fix encodedFunction type and update client pins
2022-11-14 10:49:50 -07:00
Andrew Morris
0a00d8606f Update bls-wallet-clients usage to 940dd11 2022-11-14 17:56:47 +11:00
Andrew Morris
940dd11de9 Fix type of ActionData.encodedFunction 2022-11-14 17:46:16 +11:00
Jacob Caban-Tomski
02298b8029 Merge pull request #393 from web3well/update-client-pins
Update clients package (experimental) and pinned versions
2022-11-13 20:04:04 -07:00
Andrew Morris
08c059acb4 Update client pins 2022-11-11 12:33:33 +11:00
Andrew Morris
54e752f503 Deploy to arbitrum goerli 2022-11-11 12:19:53 +11:00
Jacob Caban-Tomski
bc3d1463f1 Merge pull request #391 from web3well/test-prohibit-renounceOwnership
Test prohibit renounce ownership
2022-11-10 15:16:33 -07:00
James Zaki
bc37324932 better handle no admin params 2022-11-10 17:33:47 +00:00
Blake Duncan
e613af7b1e Fix test failure 2022-11-10 15:14:48 +00:00
Blake Duncan
63f230c742 Code clean up 2022-11-10 14:59:21 +00:00
Andrew Morris
a4a0e951a8 Remove redundant disables (#390) 2022-11-10 09:27:55 +00:00
Andrew Morris
f3da0977b8 Use ActionError instead of formatted string, parse Error, Panic in js-land, fallback to reporting bytes 2022-11-10 17:36:28 +11:00
Andrew Morris
b8cefff40e Check selectorId before address (since address is not there for renounceOwnership) 2022-11-10 16:09:46 +11:00
Andrew Morris
59e1a65242 Merge pull request #368 from web3well/bw-160-update-deps
Update dependencies
2022-11-10 12:33:08 +11:00
Blake Duncan
dc4b421bbe Spacing 2022-11-04 10:04:19 +00:00
Blake Duncan
dca0eb3ff6 Log bundle error and add script to test 2022-11-03 16:56:40 +00:00
Blake Duncan
771de1fbab Return txhash from aggregator and display in Quill (#369)
* Add bundle hash and txhash to receipt request

* Save txhash on transaction in local storage

* Fix case issue with filtering in the tx table
2022-11-03 13:37:02 +00:00
Andrew Morris
2b5ad82d5c Don't run aggregator tests for contract-updates 2022-11-02 11:00:26 +11:00
Andrew Morris
c508211f18 Fix eslint issue 2022-11-01 16:42:45 +11:00
Andrew Morris
6e00912981 Fix types 2022-11-01 16:39:05 +11:00
Andrew Morris
d1adf2572a Fix test 2022-11-01 16:13:19 +11:00
Andrew Morris
4888426a4e typechain-types migration 2022-11-01 16:02:02 +11:00
Andrew Morris
cd103faf84 Update deps in clients 2022-11-01 15:52:17 +11:00
Andrew Morris
20fc590cd7 Disable camelcase rule 2022-11-01 15:42:57 +11:00
Andrew Morris
99dbaadda9 Raw update dependencies 2022-11-01 15:42:40 +11:00
Jacob Caban-Tomski
02ffad7f85 Add test cases for new require statements. One still not working. 2022-10-27 17:40:46 -05:00
Jacob Caban-Tomski
84dd0a09d8 Throw exception in client if private key has been recovered from 2022-10-27 16:40:21 -05:00
Jacob Caban-Tomski
e4ab6b53db Move send eth script to hardhat task 2022-10-27 16:24:51 -05:00
Jacob Caban-Tomski
097174b243 Merge branch 'main' of github.com:web3well/bls-wallet into contract-updates 2022-10-27 15:47:54 -05:00
Jacob Caban-Tomski
93c7935dc6 Merge pull request #350 from web3well/contract-audit
Audit fixes 1-4 and BLSWalletWrapper changes
2022-10-27 10:54:26 -05:00
Jacob Caban-Tomski
b06435e3d7 Merge pull request #358 from web3well/docs/add-deployments
Add links to contract deployments to README.
2022-10-26 22:20:46 -05:00
Jacob Caban-Tomski
bb8bd2458e Add links to contract deployments to README.
Co-authored-by: outsider-analytics <outsideranalytics@gmail.com>
2022-10-26 22:19:00 -05:00
Jacob Caban-Tomski
6379006133 Add getOperationResults to cleints
Add getOperationResults to bls-wallet-clients to allow consumers to more easily get at operation errors.
Change existing test case to use getOperationResults.
Add message to require used to prevent ownership changes to proxy admin.
2022-10-25 23:17:06 -05:00
Jacob Caban-Tomski
4bc4701faf Add replay attack test after recovery 2022-10-24 17:23:46 -05:00
Jacob Caban-Tomski
5b7231b980 Merge branch 'main' of github.com:web3well/bls-wallet into contract-audit 2022-10-24 16:09:00 -05:00
Jacob Caban-Tomski
3cfabaa4b1 Merge pull request #353 from web3well/update-input-decode
updated inputDecode to parse params
2022-10-24 15:04:04 -05:00
Jacob Caban-Tomski
b8ae335449 Merge pull request #356 from web3well/existing-seed
Import pre-existing seed phrase during Onboarding
2022-10-24 15:01:23 -05:00
kautukkundan
221b7b94e0 removed stray import and reduced line length 2022-10-23 23:30:33 +05:30
kautukkundan
8f17aa983d added validation to mnemonic 2022-10-23 23:26:32 +05:30
Jacob Caban-Tomski
6a3df55a38 Merge pull request #357 from web3well/docs/agg-env-vars
Add documentation for aggregator env vars
2022-10-20 11:38:28 -05:00
Jacob Caban-Tomski
200718814d Add documentation for aggregator env vars 2022-10-20 11:25:14 -05:00
kautukkundan
0904d8169d lint: fix warnings 2022-10-16 21:05:42 +05:30
kautukkundan
d476a53d0d add existing seed phrase during wallet generation 2022-10-16 21:01:50 +05:30
kautukkundan
5b771c0e8c lint: fixed warnings 2022-10-16 18:41:33 +05:30
kautukkundan
2b16c7367d updated inputDecode to parse params 2022-10-16 18:20:52 +05:30
Jacob Caban-Tomski
0c956188a3 Merge pull request #352 from web3well/update-docs
Update docs
2022-10-07 17:57:28 -05:00
JohnGuilding
6ec971ecac Add ref to scaffold-eth example dapp 2022-10-07 17:55:38 -05:00
JohnGuilding
80e56c1bb5 Add margin to diagram 2022-10-07 17:38:05 -05:00
JohnGuilding
05660dd468 Add proxy aggregator diagrams 2022-10-07 15:50:32 -05:00
JohnGuilding
aa0ba1ad47 Remove "run" section from remote dev doc 2022-10-07 15:47:37 -05:00
JohnGuilding
feaf497c22 Correctly instantiate Web3Provider 2022-10-07 15:46:33 -05:00
JohnGuilding
06a3dd025a Explicit terminal instructions & define HD wallet 2022-10-07 15:46:02 -05:00
JohnGuilding
cac08ec5a6 Update rinkarby instructions to goerli 2022-10-07 15:41:49 -05:00
JohnGuilding
7c93ccea89 Update extension config reference 2022-10-07 15:40:40 -05:00
Jacob Caban-Tomski
647c1bbf4d Fix import string & contract lint 2022-10-05 17:28:41 -06:00
Jacob Caban-Tomski
c2bf4bbc16 Bump bls-wallet-clients version to experimental v0.8.0
Update aggregator BundleService to work with new signature payload which includes wallet address.
2022-10-05 17:21:17 -06:00
Jacob Caban-Tomski
2d28b7b17a Bump bls-wallet-clients to v0.8.0 2022-10-05 17:14:23 -06:00
Jacob Caban-Tomski
92b9b136a7 Add wallet AddressFromPublicKey function 2022-10-05 17:01:38 -06:00
Jacob Caban-Tomski
c3e79a7c8e Remove unneeded arg, expand on any cast. 2022-10-05 16:02:55 -06:00
James Zaki
163af9d49c Remove gateway param to return compatible to server tests 2022-10-05 17:26:34 +01:00
James Zaki
f2a490aaf2 Minor fixes from review 2022-10-05 15:43:51 +01:00
Jacob Caban-Tomski
1481c69dc0 WIP Fix typechain import path 2022-10-04 16:37:08 -06:00
Jacob Caban-Tomski
17052cdaa1 WIP Add build test to client CI process 2022-10-04 16:30:54 -06:00
Jacob Caban-Tomski
def1eaf208 WIP Add back in main restriction for push workflow trigger 2022-10-04 16:23:34 -06:00
Jacob Caban-Tomski
5a33e43945 WIP Remove main branch requirement to run CI steps 2022-10-04 16:16:01 -06:00
Jacob Caban-Tomski
998ee6ba30 WIP Fix client tests, run agg tests on contract changes 2022-10-04 16:06:49 -06:00
James Zaki
f0a3b56a44 Sign wallet address in msg (04) tests 2022-10-04 22:14:40 +01:00
James Zaki
4147726d2a Sign wallet address in msg (04) - WIP 2022-10-04 22:14:40 +01:00
James Zaki
192db97b01 Restrict Owner functions from ProxyAdmin call (03) 2022-10-04 22:14:40 +01:00
James Zaki
e2ee017824 Increment nonce before operation - protect from relpay reentrancy (02) 2022-10-04 22:14:40 +01:00
James Zaki
a485d84364 Sign key hash with chainid and UserOp - protect from rogue key attack (01) 2022-10-04 22:14:40 +01:00
Kautuk Kundan
ae5e1adcc2 Merge pull request #349 from web3well/fix-extension
update config for extension
2022-10-04 01:00:22 +05:30
kautukkundan
1ee24813ed update config for extension 2022-10-03 01:18:13 +05:30
Andrew Morris
4dfa0bdd4a Merge pull request #344 from web3well/bw-330-improve-clients-docs
Improve clients docs
2022-09-29 14:01:27 +10:00
Andrew Morris
de5c288d07 Update docs/use_bls_wallet_clients.md
Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>
2022-09-29 14:01:05 +10:00
Andrew Morris
db0cf3acc8 Merge pull request #282 from web3well/bw-251-currency-switching
Add currency switching and use currency conversions
2022-09-22 10:34:55 +10:00
Jacob Caban-Tomski
035f1872fe Remove unused import 2022-09-21 17:34:56 -06:00
Andrew Morris
60ed167b2d Make example code work 2022-09-21 14:55:33 +10:00
Andrew Morris
1860ecbd18 Remove unnecessary line 2022-09-21 13:59:09 +10:00
Andrew Morris
e830f49714 yarn install -> yarn add 2022-09-21 13:55:24 +10:00
Andrew Morris
8753978719 Just use USD instead of ip-api 2022-09-21 13:43:10 +10:00
Andrew Morris
fec0f12e45 Avoid $ prefix 2022-09-19 15:32:15 +10:00
Jacob Caban-Tomski
dcfd13d8b3 Merge pull request #336 from web3well/contract-operation-logs
Return results in Operation event for visibility of actions
2022-09-16 11:37:41 -06:00
kautukkundan
665ea530fc Update projects to bls-wallet-clients 0.7.3 2022-09-16 22:47:25 +05:30
kautukkundan
b547361d86 Merge branch 'main' of github.com:web3well/bls-wallet into contract-operation-logs 2022-09-16 22:14:57 +05:30
Andrew Morris
b513d9feab Merge remote-tracking branch 'origin/main' into bw-251-currency-switching 2022-09-15 14:13:13 +10:00
Andrew Morris
8d4257aac2 Merge pull request #335 from web3well/clients-0.7.0
Update to bls-wallet-clients 0.7.0
2022-09-15 14:12:29 +10:00
Andrew Morris
e4ad6d317c Update projects to bls-wallet-clients 0.7.1 2022-09-15 13:58:35 +10:00
Andrew Morris
080513ec12 0.7.1 2022-09-15 13:44:16 +10:00
Andrew Morris
3c9490b5a6 Merge commit '1303ffe' into bw-251-currency-switching 2022-09-15 13:33:03 +10:00
Andrew Morris
e7decded4c Merge commit '356b067' into bw-251-currency-switching 2022-09-15 13:32:14 +10:00
Andrew Morris
427d05ce82 Merge remote-tracking branch 'origin/main' into clients-0.7.0 2022-09-15 13:05:25 +10:00
James Zaki
a6e5d6d3d8 Emit action data with Operation event 2022-09-14 14:26:40 +01:00
James Zaki
bcbab0d08a Return index and error message of action in failed Operation 2022-09-14 13:59:18 +01:00
Andrew Morris
1303ffea9b Add network switching (#273)
* Update config system in prep for network switching

* Move builtinNetworks into config

* Move currencyConversionConfig into config

* Select network in ui

* mixtureHasChanged

* Fix issue where ethers Web3Provider assumes network doesn't change, handle addresses changing per network

* Implement per-network information for wallets

* lookupAddress -> pkHashToAddress

* Fix duplication of getting bls network config

* Restore preferred nonce sourcing

* Fix global access of blsNetworksConfig

* Fix global config access

* Fix commented hasChanged

* Fix build failures

* Fix linting issues

* Update extension/config.release.json

Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>

* Update with PR feedback

Switch $preferences to non-$ name.
Add hidden field to networks to hide from end users.
Refactor wallet network data generation. Needs one more pass.

* PR fixes

Fix trailing comma in config json.
Properly inject env vars into config file.

* Move MultiNetowrkConfig to bls-wallet-clients

Add MultiNetworkConfig to clients. Deprecate NetworkConfig.
Update deps in clients.
Add chai-as-promised, ts-node to clients.
Remove need for transpiliation before client tests.
Finish getAndUpdateWalletsNetworkData changes in extension.

* Remove .only from client tests

* Use MultiNetworkConfig from clients lib.

* Fix file misspelling

* Update bls-wallet-clients experimental with main

* Remove empty local.json from CI build

* Update setup script with new extension config

Add troubleshooting section for Deno version

* Update extension & aggregator configs.

Update extension configs to hide all non-deployed networks.
Update aggregator local config to use pks 0 & 1 from main hardhat mnemonic.
Add dangerous command to print private keys from main hardhat mnemonic.

* Default extension network to arbitrum goerli

* Revert changes in aggregator local env

Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>
2022-09-13 20:57:07 -06:00
Jacob Caban-Tomski
356b0674e7 Revert changes in aggregator local env 2022-09-13 20:46:47 -06:00
Jacob Caban-Tomski
90499370b5 Default extension network to arbitrum goerli 2022-09-13 20:15:03 -06:00
Jacob Caban-Tomski
0007a8491d Update extension & aggregator configs.
Update extension configs to hide all non-deployed networks.
Update aggregator local config to use pks 0 & 1 from main hardhat mnemonic.
Add dangerous command to print private keys from main hardhat mnemonic.
2022-09-13 20:01:12 -06:00
Jacob Caban-Tomski
eeb7f506ab Update setup script with new extension config
Add troubleshooting section for Deno version
2022-09-13 18:55:56 -06:00
Jacob Caban-Tomski
8a7187c041 Remove empty local.json from CI build 2022-09-13 18:25:45 -06:00
Jacob Caban-Tomski
68547ca88f Update bls-wallet-clients experimental with main 2022-09-13 18:21:03 -06:00
Jacob Caban-Tomski
576802a79a Merge branch 'main' of github.com:web3well/bls-wallet into bw-252-network-switching 2022-09-13 15:56:54 -06:00
James Zaki
ce8620c978 Return results in Operation event for visibility of failing actions 2022-09-13 22:37:07 +01:00
Andrew Morris
ce9e654815 Use clients 0.7.0 in aggregator 2022-09-13 11:33:27 +10:00
Andrew Morris
19bdc19e8d Use clients 0.7.0 in aggregator-proxy 2022-09-13 11:32:05 +10:00
Andrew Morris
b05525a1b8 Use clients 0.7.0 in extension 2022-09-13 11:30:58 +10:00
Andrew Morris
53e2f518d5 0.7.0 2022-09-13 11:26:23 +10:00
Andrew Morris
9d22092b2c Merge pull request #323 from web3well/default-nitro
updated default env to use arbitrum nitro goerli
2022-09-13 11:15:40 +10:00
Andrew Morris
3936b59e22 Gas adjustment (in test) for new contracts 2022-09-13 10:04:37 +10:00
Andrew Morris
a7d8209664 Use local config when testing aggregator 2022-09-13 09:39:18 +10:00
Andrew Morris
6ed993d951 Remove contracts workaround in aggregator.yml 2022-09-13 09:19:48 +10:00
Andrew Morris
23613b42b8 Merge pull request #333 from web3well/update-contracts
Update contracts
2022-09-13 09:06:44 +10:00
Andrew Morris
92195d1f3e Fix formatting 2022-09-12 16:40:08 +10:00
Andrew Morris
5b2168e262 Revert markdown formatting changes 2022-09-12 15:35:22 +10:00
Andrew Morris
8d5b0867fe Update arbitrum-goerli deployment 2022-09-12 15:02:30 +10:00
Andrew Morris
03ced09fc2 Use updated bls-wallet-clients (contracts compatibility) 2022-09-09 16:54:05 +10:00
Andrew Morris
b267fcd3f3 Merge remote-tracking branch 'origin/default-nitro' 2022-09-09 16:24:04 +10:00
kautukkundan
297fba186b updated network naming 2022-09-09 11:48:13 +05:30
kautukkundan
ebb90e2f9a resolved conflict: pvt_key_agg 2022-09-09 11:44:21 +05:30
Andrew Morris
6608add701 Remove no longer necessary branch switching in setup 2022-09-09 15:54:09 +10:00
Andrew Morris
6d07b754dc Use updated bls-wallet-clients 2022-09-09 15:53:02 +10:00
Andrew Morris
3ac37a0e43 Merge pull request #308 from web3well/new-deployment
New testnet deployments - Arb Rinkeby and Arb Goerli
2022-09-09 15:33:43 +10:00
kautukkundan
b1bac6fa79 updated network name based on chainlist defaults 2022-09-07 21:11:01 +05:30
Andrew Morris
87eb0b53fb Merge pull request #324 from web3well/qr-code
Added QR Code for receive button
2022-09-07 11:25:23 +10:00
kautukkundan
17b7293057 lint fix 2022-09-06 01:59:31 +05:30
kautukkundan
11414de511 Added QR Code for receive button 2022-09-06 01:55:45 +05:30
kautukkundan
6c6972ce5c updated docs 2022-09-06 01:11:20 +05:30
kautukkundan
a8b5e4c9d7 updated extension to use remote aggregator by default 2022-09-06 01:00:01 +05:30
kautukkundan
07c59cd0bd updated default env to use arbitrum nitro goerli 2022-09-06 00:47:27 +05:30
kautukkundan
3512982eff fix: typo for chain Id 2022-09-06 00:39:30 +05:30
Jacob Caban-Tomski
7a37dc89d0 Fix file misspelling 2022-09-03 14:10:14 -06:00
Jacob Caban-Tomski
17654c5042 Use MultiNetworkConfig from clients lib. 2022-09-03 13:59:48 -06:00
Jacob Caban-Tomski
2f62a6da8f Remove .only from client tests 2022-09-03 13:29:59 -06:00
Jacob Caban-Tomski
ee2a533c72 Move MultiNetowrkConfig to bls-wallet-clients
Add MultiNetworkConfig to clients. Deprecate NetworkConfig.
Update deps in clients.
Add chai-as-promised, ts-node to clients.
Remove need for transpiliation before client tests.
Finish getAndUpdateWalletsNetworkData changes in extension.
2022-09-03 13:24:56 -06:00
Andrew Morris
6f45f7942f Merge pull request #313 from web3well/bw-271-fix-aggregator-tests
Switch to supported contracts when testing aggregator
2022-09-02 10:13:25 +10:00
Jacob Caban-Tomski
df6daffcce PR fixes
Fix trailing comma in config json.
Properly inject env vars into config file.
2022-09-01 15:42:26 -06:00
Andrew Morris
287c1c64ee Add comments 2022-09-01 13:52:42 +10:00
Andrew Morris
c13e9800a4 Fetch the commit before checking it out 2022-09-01 13:47:48 +10:00
Andrew Morris
4548bf0a77 Trigger aggregator workflows for yml file 2022-09-01 13:38:57 +10:00
Andrew Morris
e64b637440 Switch to supported contracts when testing aggregator 2022-09-01 13:35:03 +10:00
John Guilding
fd6f60d39c Merge pull request #307 from web3well/update-PRIVATE_KEY_AGG-in–env.example
Update aggregator .env.example private key
2022-08-31 15:41:53 +01:00
kautukkundan
3a519cd6e4 fix: goerli explorer url 2022-08-25 22:51:57 +05:30
kautukkundan
56bdf8a1df created new goerli testnet deployment for git hash 45def92 2022-08-25 22:46:28 +05:30
kautukkundan
600d898fc0 created new testnet deployment for git hash 45def92 2022-08-19 15:07:33 +05:30
Jacob Caban-Tomski
8f5eff131a Merge branch 'main' of github.com:web3well/bls-wallet into bw-252-network-switching 2022-08-18 17:47:06 -06:00
Jacob Caban-Tomski
eebff6d205 Update with PR feedback
Switch $preferences to non-$ name.
Add hidden field to networks to hide from end users.
Refactor wallet network data generation. Needs one more pass.
2022-08-18 17:45:17 -06:00
JohnGuilding
bcf6aef290 Update private key 2022-08-18 13:16:03 +01:00
Jacob Caban-Tomski
ed7bbf5397 Merge pull request #304 from web3well/bw-295-add-note
Add checkout commands to contract deployment and explanation
2022-08-16 14:09:58 +09:00
Andrew Morris
0aacf46234 Add checkout commands to contract deployment and explanation 2022-08-16 14:34:47 +10:00
Andrew Morris
723740348f Merge pull request #296 from web3well/extension-webpack-ignore-node-modules
Ignore watching node_modules during extension dev
2022-08-16 12:04:52 +10:00
Andrew Morris
63d9470cad Merge pull request #298 from web3well/actions-modal
created clickable actions modal
2022-08-16 11:06:49 +10:00
Andrew Morris
c285e8dfaf Merge pull request #293 from web3well/transactions-list
Create Historical Transactions list
2022-08-16 11:00:10 +10:00
kautukkundan
cf46dbf3ff fixed no-unstable-nested-components issue 2022-08-11 22:24:01 +05:30
kautukkundan
427ce33267 change native button to use custom Button 2022-08-11 22:21:54 +05:30
kautukkundan
7a7cc0a9d4 created clickable actions modal 2022-08-11 22:21:54 +05:30
kautukkundan
6d143fcbc8 fixed no-unstable-nested-components issue 2022-08-11 22:20:35 +05:30
Andrew Morris
8c8e840dc6 Update extension/config.release.json
Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>
2022-08-10 22:51:23 +10:00
kautukkundan
d30c692263 lint fix 2022-08-09 20:41:58 +05:30
Jacob Caban-Tomski
014871ee8d Merge pull request #292 from web3well/dapp-guide
Add guide for using BLS Wallet/Quill in a dApp
2022-08-09 19:46:55 +09:00
Jacob Caban-Tomski
20d46b4900 Add guide for using BLS Wallet in a dApp 2022-08-09 19:39:46 +09:00
Jacob Caban-Tomski
268ffe41f8 Ignore watching node_modules during extension dev. 2022-08-09 17:47:35 +09:00
Jacob Caban-Tomski
a0a2ee878b Merge pull request #285 from web3well/bw-284-fix-broken-links
Fix broken links
2022-08-09 17:42:03 +09:00
kautukkundan
3a9c6ce4cd completed tx table ui 2022-08-05 16:34:31 +09:00
kautukkundan
3106e1dd0a crated transactions table 2022-08-05 16:34:31 +09:00
Andrew Morris
8745c185b9 Merge pull request #291 from web3well/bug/extension-release
Switch extension upload to more relevant GH action
2022-08-05 15:00:13 +10:00
Andrew Morris
668362304b Merge branch 'bw-252-network-switching' into bw-251-currency-switching 2022-08-05 13:57:09 +09:00
Andrew Morris
e89a9151e6 Merge remote-tracking branch 'origin/main' into bw-252-network-switching 2022-08-05 13:53:13 +09:00
Jacob Caban-Tomski
c435fe0471 Update presentations list 2022-08-04 15:25:31 +09:00
Jacob Caban-Tomski
956607a479 Switch extension upload to more relevant GH action 2022-08-04 14:51:01 +09:00
Blake Duncan
04af3ae83a Encrypting necessary local storage data (#286)
* First attempt at encrypting the local storage

* Enable encrypted and decrypted storage

* Lint fixes and remove console log

* Update variable naming

* Creating new StorageConfig type

* Removing unused variable
2022-07-29 10:32:45 +01:00
Andrew Morris
27b0864c79 Fix broken links 2022-07-25 07:54:23 +10:00
Andrew Morris
76a7daa0bd Fix linting issues 2022-07-21 16:34:57 +10:00
Andrew Morris
b38e216585 Remove unused import 2022-07-21 15:18:06 +10:00
Andrew Morris
5da15cb16f Styling for eth symbol 2022-07-21 15:08:09 +10:00
Andrew Morris
44b89fd8f3 PreferredCurrencySymbol 2022-07-21 14:59:55 +10:00
Andrew Morris
b8122ced5c Use grid for values in confirm popup 2022-07-21 14:41:57 +10:00
Andrew Morris
7613237f96 Merge remote-tracking branch 'origin/bw-252-network-switching' into bw-251-currency-switching 2022-07-21 11:22:13 +10:00
Andrew Morris
368aa43085 Merge remote-tracking branch 'origin/main' into bw-252-network-switching 2022-07-21 11:20:50 +10:00
Andrew Morris
68b5446a6f Add currency conversion to confirm dialog 2022-07-21 11:17:03 +10:00
Andrew Morris
a0f858dbbb Convert currency in ConnectionSummary and AmountSelector 2022-07-21 10:45:59 +10:00
Andrew Morris
83e30afea1 Put fields into currency conversion, CurrencyDisplay 2022-07-21 10:28:25 +10:00
Andrew Morris
d9616333a4 Add alphabetical option 2022-07-21 08:34:44 +10:00
Jacob Caban-Tomski
0570216fb2 Merge pull request #275 from web3well/one-class-per-line
When wrapping className, use one class per line
2022-07-20 13:32:37 +02:00
Jacob Caban-Tomski
0515caacfc Merge pull request #280 from web3well/bw-279-support-node-18
Support node 18
2022-07-20 13:25:10 +02:00
Andrew Morris
767eafe338 Add TODO 2022-07-20 18:09:36 +10:00
Andrew Morris
d76fb1c227 Add currency selection in general settings 2022-07-20 18:07:03 +10:00
Andrew Morris
ec2ae3eb2d Get default currency from ip api, remove preference identities 2022-07-20 17:40:28 +10:00
Andrew Morris
964b4c7098 Merge branch 'bw-279-support-node-18' into bw-251-currency-switching 2022-07-20 16:56:19 +10:00
Andrew Morris
4259ee5bce Fix linting issues 2022-07-20 08:48:24 +10:00
Andrew Morris
06a1b7318e Specify latest LTS instead of 16 2022-07-20 08:40:27 +10:00
Andrew Morris
3c9cbe6c3e Support node 18 2022-07-20 08:40:17 +10:00
Andrew Morris
6b86b95f3c Merge pull request #276 from web3well/specify-node-version
Specify node version
2022-07-20 08:06:53 +10:00
Blake Duncan
911ffa4865 Update git ignore for webstorm 2022-07-19 10:50:28 +01:00
Blake Duncan
c5f460969f Adding .nvmrc to specify node version 2022-07-19 10:47:50 +01:00
Andrew Morris
a2fb8f7039 When wrapping className, use one class per line 2022-07-19 14:20:55 +10:00
Andrew Morris
87696c3251 Fix build failures 2022-07-19 13:00:06 +10:00
Andrew Morris
a4dbeb00e9 Fix commented hasChanged 2022-07-19 12:05:09 +10:00
Andrew Morris
2a24a15460 Fix global config access 2022-07-19 11:12:56 +10:00
Andrew Morris
9040e90457 Fix global access of blsNetworksConfig 2022-07-19 11:03:56 +10:00
Andrew Morris
3d483d6b76 Restore preferred nonce sourcing 2022-07-19 10:55:28 +10:00
Andrew Morris
7d6aa698ec Fix duplication of getting bls network config 2022-07-19 10:53:44 +10:00
Andrew Morris
7d240daaa5 lookupAddress -> pkHashToAddress 2022-07-19 10:45:12 +10:00
Andrew Morris
5f866991dc Implement per-network information for wallets 2022-07-19 10:13:01 +10:00
Jacob Caban-Tomski
0959db5c4c Merge pull request #269 from web3well/local-testing-fixes
Local testing fixes
2022-07-18 19:24:06 +02:00
Andrew Morris
c6c37ffc47 Merge pull request #267 from web3well/linting-overhaul
Linting overhaul
2022-07-18 09:06:28 +10:00
Kautuk Kundan
1a80eb4a4a Merge pull request #266 from web3well/fix-carousel
Specify deps for carousel correctly
2022-07-17 23:55:13 +05:30
Kautuk Kundan
a7c5de25aa Merge pull request #265 from web3well/bw-145-fix-react-table-typing
Fix react table typing
2022-07-17 23:53:21 +05:30
Kautuk Kundan
a2bd4165b3 Merge pull request #264 from web3well/use-quill-in-confirm
Use QuillContext in Confirm dialog
2022-07-17 23:47:10 +05:30
Andrew Morris
d4bf0b38c4 Fix issue where ethers Web3Provider assumes network doesn't change, handle addresses changing per network 2022-07-15 17:54:09 +10:00
Andrew Morris
64b312e646 mixtureHasChanged 2022-07-15 15:50:04 +10:00
Andrew Morris
1abdcd46df Select network in ui 2022-07-15 15:20:33 +10:00
Andrew Morris
64db0b7eba Move currencyConversionConfig into config 2022-07-15 15:04:57 +10:00
Andrew Morris
b809a22cee Move builtinNetworks into config 2022-07-15 14:48:12 +10:00
Andrew Morris
94ae095acf Update config system in prep for network switching 2022-07-15 13:52:57 +10:00
Andrew Morris
d52d598573 allow env 2022-07-15 11:10:56 +10:00
Andrew Morris
8a4c6e52f1 Fix docker weirdness with localhost by using 127.0.0.1 2022-07-15 11:10:56 +10:00
Andrew Morris
f6473e72af Upgrade deno 2022-07-15 11:10:56 +10:00
Andrew Morris
0f82acf931 Add useful things to the geth dev node 2022-07-15 11:10:56 +10:00
Andrew Morris
bfada65260 Disable rule duplicating prettier and with bad autofix interaction 2022-07-12 14:59:53 +10:00
Andrew Morris
79423ec79f Lints are always warnings 2022-07-12 14:59:53 +10:00
Andrew Morris
5225cb1883 Fix jsxBracketSameLine deprecation warning by upgrading to bracketSameLine 2022-07-12 14:59:53 +10:00
Andrew Morris
f4f5703eb6 Disallow lint warnings in CI 2022-07-12 14:59:53 +10:00
Andrew Morris
36825dbbe6 Enable linting js files, fix .eslintrc.js, tailwind.config.js 2022-07-12 14:59:53 +10:00
Andrew Morris
54e90045fb Render eslint config and re-extend 2022-07-12 14:59:53 +10:00
Andrew Morris
89c8b28d20 Rebuild config for typescript and eslint, lint fixes 2022-07-12 14:59:41 +10:00
Andrew Morris
aa5c2fea6b Specify deps for carousel correctly 2022-07-12 12:55:33 +10:00
Andrew Morris
66c257cacd Fix typing in AssetsTable.tsx 2022-07-12 12:40:39 +10:00
Andrew Morris
501afd7d64 Restore placeholder tables with notice and fix key issues 2022-07-12 12:05:58 +10:00
Andrew Morris
676471b3fe Use QuillContext in Confirm dialog 2022-07-12 11:54:02 +10:00
Andrew Morris
26686cdfe3 Merge pull request #263 from web3well/show-nonce
Show nonce on transactions tab
2022-07-12 09:44:29 +10:00
Kautuk Kundan
7cbd6a7617 Merge pull request #254 from web3well/confirmation-dialog
Confirmation Popup and Transaction Lifecycle
2022-07-11 16:12:43 +05:30
Andrew Morris
1c3b1c76b3 Require confirm dialog for Quill txs, but add todo 2022-07-11 16:08:44 +05:30
Andrew Morris
ed9ab2a103 Restore noUnusedLocals:false 2022-07-11 16:08:44 +05:30
Andrew Morris
b38547126e Restore enum 2022-07-11 16:08:44 +05:30
Andrew Morris
e1aa4de7fb Don't require popup for quill-initiated transactions 2022-07-11 16:08:44 +05:30
Andrew Morris
c0fa1cdb91 Clearer failure when user doesn't approve 2022-07-11 16:08:44 +05:30
Andrew Morris
7f5e5e2f27 Reject invalid transitions, simplify transition table, require valid status in rpc 2022-07-11 16:08:44 +05:30
Andrew Morris
8761d5e7f6 Move transaction typing into QuillStorageCells 2022-07-11 16:08:44 +05:30
Andrew Morris
c62dceb6a9 Improve typing of prompt messaging 2022-07-11 16:08:44 +05:30
Andrew Morris
d8dd145ae8 Use local InternalRpc 2022-07-11 16:08:44 +05:30
Andrew Morris
e43ddbd0fe Prompt user before connecting wallet (since connecting wallet is slow) 2022-07-11 16:08:44 +05:30
Andrew Morris
ce73b22a50 uuid -> RandomId 2022-07-11 16:08:44 +05:30
Andrew Morris
449f5b439a Use <quill> for internal methods 2022-07-11 16:08:44 +05:30
Andrew Morris
3fcddfe2f7 Don't allow unused variables 2022-07-11 16:08:44 +05:30
Andrew Morris
0eee5e34b4 Show nonce on transactions tab 2022-07-11 18:05:20 +10:00
kautukkundan
7fb3a62655 lint fixes 2022-07-10 22:15:44 +05:30
kautukkundan
7c15195aa6 visual changes 2022-07-10 21:49:45 +05:30
kautukkundan
b15c81ed2e added default value and gasPrice to txs 2022-07-10 20:37:14 +05:30
kautukkundan
a2756603bc added formatting for eth and added total actions value 2022-07-10 20:25:25 +05:30
kautukkundan
7fc2b45419 fixed transaction state change bug 2022-07-10 20:06:40 +05:30
kautukkundan
ba83438cf0 completed popup for tx confirmation 2022-07-10 19:52:19 +05:30
kautukkundan
e0d47566db updated UI for confirmation dialog 2022-07-10 17:34:31 +05:30
kautukkundan
39d02cbea0 extension: added transaction controller 2022-07-10 16:42:56 +05:30
Jacob Caban-Tomski
b8ae6e9370 Merge pull request #253 from web3well/bw-230-send-eth
Send eth
2022-07-09 13:38:43 -06:00
Andrew Morris
1b403b9300 Fix routing bug (duplicate route name), fix hiding amount selector 2022-07-08 17:18:10 +10:00
Andrew Morris
6ce54b404d Move selector switching into SendDetailSelectors, move send logic into separate function 2022-07-08 17:12:43 +10:00
Andrew Morris
dbdd56795d Simplify SendDetail cells 2022-07-08 16:52:21 +10:00
Andrew Morris
1358430dbb Use formatCompactAddress (remove shortenAddress duplicate) 2022-07-08 16:38:57 +10:00
Andrew Morris
b200502594 Avoid $ prefix outside of cell definitions 2022-07-08 16:36:36 +10:00
Andrew Morris
5f506f5f14 Basic SendProgress design 2022-07-07 11:33:57 +10:00
Andrew Morris
c982fe7a69 Implement send 2022-07-07 10:33:39 +10:00
Andrew Morris
718be15cac Remove back/cancel when sending 2022-07-07 09:29:17 +10:00
Andrew Morris
ae6db5b3f7 Add AmountSelector 2022-07-07 09:24:52 +10:00
Andrew Morris
d4cfe0e4f6 Add detail to BigSendButton 2022-07-07 08:28:30 +10:00
Andrew Morris
aad71f738f Granular back button 2022-07-07 08:03:38 +10:00
Andrew Morris
1e88e2711f WIP ui for sending eth 2022-07-06 16:34:13 +10:00
Jacob Caban-Tomski
b6e057bed8 Merge pull request #250 from web3well/bw-249-fix-params
Allow omitting params key when empty
2022-07-05 21:39:58 -06:00
Andrew Morris
5c1388e95c Allow omitting params key when empty 2022-07-06 11:50:50 +10:00
Andrew Morris
c3169e93b4 Merge pull request #248 from web3well/bw-232-account-balance
Show account balances
2022-07-06 11:48:26 +10:00
Andrew Morris
847b02d31e Merge branch 'main' into bw-232-account-balance 2022-07-06 11:42:08 +10:00
Andrew Morris
b5c30bedb2 Merge pull request #247 from web3well/bw-229-copy-addresses 2022-07-06 06:22:40 +10:00
Andrew Morris
350a31b66f Show account balances 2022-07-05 16:57:18 +10:00
Andrew Morris
36396a74d9 Make more use of onAction 2022-07-05 15:56:17 +10:00
Andrew Morris
78465e133e Copy address on click 2022-07-05 15:56:02 +10:00
Andrew Morris
363a518acd Fix linting issues 2022-07-05 15:35:22 +10:00
Andrew Morris
565e767d12 Merge pull request #239 from web3well/cells-formula-rename
Use prefixKeys to avoid shadowing
2022-07-05 15:04:59 +10:00
Andrew Morris
49a3661e52 Use prefixKeys to avoid shadowing 2022-07-05 14:59:09 +10:00
Andrew Morris
9cc3db2fce The Big One (#243)
* More stuff wasn't needed

* Quill api is not used

* Remove init and just put that in the constructor (🤯)

* More tidying up

* Cleanup config

* Remove commented location.reload

* Fix address selection

* Replace block tracking

* Remove unused code

* Remove unused code

* Remove unused code

* Delete unused code

* Delete commented code

* Deduplicate currency default config

* Enable strictPropertyInitialization

* Remove commented code

* Remove unused methods

* Remove AccountTrackerController and other unused things

* Remove unnecessary method

* Move CurrencyController setup inside CurrencyController's constructor

* Inline preferences cell

* Move NetworkController initialization inside NetworkController's constructor

* Move block tracking inside NetworkController

* Remove unused interfaces, define defaultCurrencyControllerState inside ICurrencyController

* Controllers set up their own cells

* Fix redundant storage of nativeCurrency

* Remove unnecessary handling of missing config in CurrencyController

* Common definition of QuillCells

* Inline unnecessary ICurrencyController.ts

* Remove unnecessary IKeyringController (but keep docs)

* Remove IPreferencesController (but keep docs)

* Move utils from IPreferencesController into PreferencesController (and delete the empty file)

* Remove redundant call to lookupNetwork

* Remove unused changeProvider method (but capture the idea by watching providerConfig in the networkController), also getSelectedAddress 💥

* currentCurrency -> userCurrency

* nativeCurrency -> networkCurrency

* Replace setDefaultCurrency

* Remove unnecessary methods

* Fix failing assertion

* Use vanilla webextension messaging for private rpc

* Remove no-longer desirable quill_ prefixes (because we're not sharing a namespace anymore)

* Directly implement PrivateRpc in QuillController instead of makePrivateRpc

* Ensure public and private rpc can't overlap

* wip: Replacing public rpc

* Remove commented code

* Don't allow unused vars 😄

* Only pass message to QuillController

* controller -> quillController

* Replace in-page rpc

* Allow public rpc to fallback to the network

* Add debugMe public method

* Add isQuill: true

* Improve settings

* Get settings routes working properly

* Add developer settings

* New setting: breakOnAssertionFailures

* Get breakOnAssertionFailures working

* Don't transpile so heavily

* Make RPC errors expose in useful+safe way

* Combine rpc handlers and simplify using toOkError

* handleRpcMessage -> handleMessage

* Add port handler for quill-events-port

* Move broadcast spec into Rpc.ts, prevent registering unknown events

* Simplify assertion

* Allow broadcasts to be scoped by origin and rename to notifications

* Emit notifications the new way

* QuillInPageProvider -> OldQuillInPageProvider

* QuillProvider -> QuillContextProvider

* QuillProvider

* Fix minor errors/warnings

* Add providerId, move EventsPortInfo into Rpc, handle non-promises in toOkError, simplify decoding EventsPortInfo

* Add assert docs, add softAssert, use softAssert in toOkError

* wip: Connecting events

* Connect events

* RpcResult improvements

* Move eth_requestAccounts into new rpc system

* Simplify implementing rpc

* Stop using the old jrpc-based system

* Add some TODOs

* Move makeEthereumMethods into publicRpc, delete more stuff

* Move TaskQueue into helpers

* Delete more unused code, add more TODOs

* Add note about cells walkthrough

* Remove cells demo page (preserving generic cells components)

* Move contentScript to just a file (not nested in a directory)

* Delete unused code

* Clean up the content script and add some nice docs explaining why it maybe shouldn't exist at all

* Cleanup pageContentScript/index.ts

* Resolve TODO

* Handle async in MemoryCell

* LongPollingCell

* Add some TODOs

* Track provider state using long polling (replacing events)

* Fix infuriating null issue

* Fix FormulaCell bug

* wip

* No more events RPC! 🤩

* Fix emission of disconnect errors

* Add fixme

* Remove unused code

* Replace old eth_chainId middleware

* AggregatorController

* Improve/simplify message passing for PublicRpc

* Remove unused code

* Temporary fix for chain id

* wip: Simplify RPC by consolidating public&private

* Simplify RPC by consolidating public&private

* Remove redundant selectedAddress cell

* Add ICell.update

* Fix redundant update methods in controllers

* Update PreferencesController to use cells properly

* Remove unused method

* Inline simple keyring methods

* Remove unused name fields

* Simplify some things

* Simplify networkController.provider using a cell

* Remove unused code

* Use the internal provider in KeyringController

* Simplify KeyringController

* Inline simple functions

* Fix unnecessary optionality of HDPhrase

* Currency api is not optional

* Simplify CurrencyController

* Don't store currency conversion, userCurrency -> preferredCurrency

* Fix redundant storage of preferred currency

* Replace CurrencyController with a cell

* Remove unused code, simplify getDefaultProviderConfig

* Improve typing of constants

* Move CRYPTO_COMPARE_API_KEY into env.ts

* builtinProviderConfigs

* Awesome docstring for ensureType

* Add TODO

* Temporary fix for 'loading' problem

* Replace unnecessary spaghetti with a simple fetch

* Prefer requestStrict

* Remove unused rpc method

* Delete unused code

* Fix unnecessary directory nesting

* Remove completed TODO

* Add rtti for SendTransactionParams

* Remove unused code, add TODOs

* Add TODOs

* QuillProvider -> QuillEthereumProvider

* pageContentScript/index.ts -> ethereum.ts, and don't remove the tag

* Simplify window.ethereum acquisition in quillPage

* Fix (window as any)

* Remove unused code

* constants -> networks

* Controllers/background.ts -> background/index.ts

* Simplify delegation

* Define keyring rpc

* Add fixme

* Use name of the cell instead of 'state' in controllers

* Move setSelectedAddress into PreferencesController

* Simplify rpc definition

* Use vanilla arguments for createUser

* Inline createHDAccount inside rpc

* Reorganize rpcMap

* Move ethersProvider into QuillController and deprecate it

* Move lookupPrivateKey into rpc

* Move createAccount into rpc

* Move removeAccount into rpc

* Move addAccount inside rpc

* Fix createAccount naming issue

* Add TODO

* Use more entropy in random ids and encode using base58check

* Don't worry about locale for now (just use the browser locale!, also we shouldn't be worried about multiple users right now, just different accounts)

* Move getBuiltinRPCURL into useInputDecode

* Move getDefaultProviderConfig inside QuillCells

* Remove unused code

* utils -> helpers/RandomId

* Fix unnecessary method wrapping in background/index.ts

* Add fixme

* Add MEGAFIX tag

* Fix redundant chainId storage (3 places -> 1 place!)

* Make chainId just a FormulaCell, since we shouldn't be writing it without writing the providerConfig

* Fix duplication of breakOnAssertionFailures cell

* Fix theme cell

* Fix RequestBody typing duplication, expose better type information for request

* forEach.ts

* Make use of forEach

* Implement window.ethereum.rpc

* Provide return type for ethereum.request

* Track breakOnFailures without using window.ethereum

* Capitalize io types

* Output -> Response

* Improve internal rpc calls

* Fix redundant network calls when not finding aggregator receipt

* Track HD indexes instead of using wallets.length

* Simplify network cells

* Reimplement time cell so that it doesn't actively update

* Non-actively updating TimeCell

* QuillCells -> QuillStorageCells

* Cleanup QuillContext cells

* Remove redundant rpc client

* Fix RandomId, isQuillExtensionPage

* Fix line break

* wip: LongPollingController

* Respond 'please-retry' after 5 minutes

* Add longPollCancel

* Fix request id issue

* wip: Lazy long polling cell

* Remove unused dependencies

* Use QuillLongPollingCell for providerState

* Fix incorrect usage of Stoppable

* Remember to break when we have a value

* Show network settings

* Show block number in network settings

* isPermittedOrigin, add origin restriction to longPollingCells

* Add awesome logging

* Improve replayability of logged rpc

* quillPage -> home

* Don't construct QuillEthereumProvider unless window.ethereum is actually accessed

* Only relay rpc if ethereum is accessed

* Fix rpc with home.html

* Improve debug.reset, remove isOnboardingComplete

* Fix onboarding

* Remove unused code

* Improving how loading is handled

* Update TODOs/FIXMEs (descoping + removing completed)

* Improve assertion failures by requiring new Error to be inlined

* Replace all custom throws with asserts

* De-scope disconnect event

* EIP1193 error codes

* GeneralSettings stub (testing currencyConversion cell)

* Test currency polling, implement currency aliases

* Make dev setting defaults good for the team for now (should be configurable)

* Just open the full view

* message -> request

* io.literal('ok') -> io.void

* Docstrings for assertType, assertTypeEcho

* Move debugging and runtime interaction out of QuillController

* Organize background/index.ts, 'quill-rpc-request', deduplicate rtti checking

* Add startup message

* Fix ok:undefined bug

* Post-merge tweaks

* Fix bad substitution in yarn.lock

* Fix double-calculating when using long polling

* Remove unused field

* Use sensible modern targets for babel, upgrade to webpack 5

* Remove unnecessary babel plugins

* Fix sourcemaps

* Update yarn.lock

* Allow /js/*

* Add quillController to Debug.ts

* Fix lint errors

* Fix inaccurate type issue

* Improve error reporting.

Add console.error for button errors so they are not swalloed silently.
Add versions to version mismatch write error.
Duplicate page hash TODO at all component usage sites.

Co-authored-by: Jacob Caban-Tomski <jacque006@users.noreply.github.com>
2022-06-30 17:09:57 -06:00
Jacob Caban-Tomski
17ccc5b00c Merge pull request #240 from web3well/extension-releases
Add extension artifacts on releases
2022-06-22 12:32:46 -04:00
James Zaki
ce7f958fb7 Merge pull request #212 from web3well/feat/one-to-one-hash-wallet
Fixes: one-to-one hash wallet 
- Made room in the VerificationGateway by making proxy admin deployment external (requires setting address in constructor as per bls lib param)
- added a delay between wallet's setting of a new blskey hash (using one-to-one reverse mapping)
- removed bls key from wallet
- updated tests
Closes #104 #155
2022-06-20 03:07:24 +10:00
James Zaki
4dae24f4ec Remove bls key from wallet and update tests. Closes #104 #155 2022-06-19 17:52:04 +01:00
James Zaki
471be6f0e0 Set wallet BLS keys in bls verification gateway (wip) 2022-06-19 16:01:07 +01:00
James Zaki
0ef2799980 Reduce VerificationGateway contract bytes (wip) 2022-06-19 15:59:45 +01:00
Jacob Caban-Tomski
f31ee249b8 Add automation label to labeler 2022-06-17 18:05:34 -04:00
Jacob Caban-Tomski
935bdf16c3 Add workflow for uploading extension on releases
Add workflow for uploading extension artifacts for chrome, opera, and firefox on release.
Add action for building and uploading extension assets.
Remove unused extension env vars.
2022-06-17 18:05:31 -04:00
Jacob Caban-Tomski
94c9060d57 Rename CI workflow files 2022-06-17 15:17:58 -04:00
Andrew Morris
e671e73e0f Cells (#221)
* wip StorageManager

* Fixes, add iteration

* Use default to ensure there's always a value

* Define IReadableCell, split out ReadableCellIterator

* FormulaCell

* Remove unused functions

* Rename to cells/ExtensionLocalStorage.ts

* StorageCell -> ExtensionLocalCell

* Split out ICell

* Split out CellIterator

* MemoryCell.ts

* Split out FormulaCell

* ReadableCellIterator -> CellIterator

* Define IAsyncStorage abstraction and use it to generalize ExtensionLocalStorage

* MemoryCellCollection.ts

* Enable removing cells

* useCell.ts, useReadableCell.ts

* Add cells demo

* CellDisplay

* Add counters

* Allow mixing unknowns in CellCollection

* Quill cells

* Allow async formulas

* Use a cell for CurrencyController.state

* Use versionedType when reading

* QuillContext

* Fix window.QuillController() in WalletWrapper

* Replace internal rpc with public+private

* Move eth_accounts into typed rpc

* Replace KeyringController().getAccounts with rpc.public.eth_accounts

* Replace KeyringController().createHdAccount with rpc.private.quill_createHdAccount

* Fix .KeyringController in WalletPage

* Fix CellIterator undefined bug

* useNewCell

* Avoid useCellWithFallback

* Remove useNewCell

* useCellState

* tmp

* Add events to IAsyncStorage, change extensionLocalStorage to singleton

* Use change events in CellCollection

* Allow cleaning up the change handler

* localCellCollection

* Use extensionLocalCellCollection in demo

* Fix demo page

* Make FormulaCell lazy

* Stop iteration more actively

* Fix cleanup of end handler

* Use elcc

* Add blockNumber cell

* Page selection, ethersProvider

* nitpicks

* Balance demo

* More event cleanup fixes

* Improve label

* Add dark-theme

* Remove obsoleted rpc-based cell collection

* Cleanup

* DemoTable

* Add cells readme

* Fix theme persistence

* Remove regex check

* Avoid casting window

* Use ChangeEvent to avoid cast

* Move cells demo into its own page

* Fix linting issues
2022-06-15 20:44:24 -04:00
kautukkundan
140f5e7094 removed unused dependency 2022-06-15 18:02:04 +05:30
Andrew Morris
1b5e2eaec4 Merge pull request #223 from web3well/ci-pipeline
CI Pipeline
2022-06-15 08:13:01 +10:00
Andrew Morris
3982007d76 Merge pull request #238 from web3well/release-drafter
Add release drafter
2022-06-15 08:05:31 +10:00
Andrew Morris
b39ed6653f Merge pull request #234 from web3well/contributing-info
Add contributing recommendations & guidelines
2022-06-15 07:58:48 +10:00
Andrew Morris
cbdb771f33 Merge pull request #236 from web3well/license
Add MIT license
2022-06-15 07:56:30 +10:00
Jacob Caban-Tomski
dd944ccf54 Add release drafter 2022-06-14 16:58:33 -04:00
Jacob Caban-Tomski
e58903eb9b Add MIT license
Add MIT license to extension.
2022-06-14 15:41:27 -04:00
Jacob Caban-Tomski
b538fa70ec Add contributing recommendations & guidelines.
Add ways to contribute to README.
Add contributing guidelines & information.
Move images dir under docs.
2022-06-14 15:14:10 -04:00
kautukkundan
fdd1c020c6 added test for setExternalWallet 1:1 mapping 2022-06-14 23:44:57 +05:30
Jacob Caban-Tomski
7165a4c2aa Update labeler glob patterns 2022-06-09 18:02:56 -04:00
Jacob Caban-Tomski
0cbe1ceaff Add aggregator CI pipeline
Add workflow for linting, typechecking, and running tests for aggregator.
Break up aggregator premerge program into additional typescript check & todo linting programs.
Update default aggregator private keys in .env.example to hardhat node values.
Add fundDeployer hardhat task to simplify local & CI contract deployment.
Remove steps from local development doc.
Default contracts & clients setup action to use NodeJS 16.x.
2022-06-09 17:16:30 -04:00
Jacob Caban-Tomski
7c8b8383e4 Update CI workflows to target PRs, main branch 2022-06-09 11:55:15 -04:00
Jacob Caban-Tomski
d2b18f3f50 Switch extension eslint-disables to warnings
Switch most eslint-disables in extension to warnings.
Update some existing types to better forms.
2022-06-09 11:31:59 -04:00
Jacob Caban-Tomski
fafc15c897 Add CI pipeline for extension
Add workflow file to lint & build extension.
Switch eth-query provider to unknown type.
Supress or fix existing lint errors.
2022-06-09 11:30:29 -04:00
Jacob Caban-Tomski
b52c8e6e90 Add workflow & engine for aggregator-proxy 2022-06-09 11:28:15 -04:00
Jacob Caban-Tomski
c8e7c7370a Add CI pipeline for contracts & clients
Add action which sets up contracts & clients projects.
Add workflows to lint & test contracts & clients.
Update labeler to include aggregator-proxy & documentation.
Add engine fields to contracts & clients.
Update extension engine field.
2022-06-09 11:28:15 -04:00
Jacob Caban-Tomski
53c8e62cf6 Merge pull request #217 from web3well/bw-173-214-215-fix-rpc
Refactor+Update RPC methods and Add Quill context provider
2022-06-09 10:44:22 -04:00
Jacob Caban-Tomski
436094914f Update TODO with issue 2022-06-09 10:43:02 -04:00
Jacob Caban-Tomski
745aed060f Remove quill provider default export 2022-06-07 22:09:25 -04:00
Jacob Caban-Tomski
df53392ce7 Refactor quill context, improve window types.
Remove async init of React root.
Move window.ethereum extraction to quill context provider.
Improve window.ethereum typing.
2022-06-07 19:07:28 -04:00
Jacob Caban-Tomski
7d82903f69 Merge branch 'main' of github.com:web3well/bls-wallet into bw-173-214-215-fix-rpc 2022-06-06 22:21:00 -04:00
Jacob Caban-Tomski
5e8827b595 Merge pull request #158 from web3well/update-docs
Update repo documentation
2022-06-06 22:17:37 -04:00
Jacob Caban-Tomski
d2c1e6af1e Add clients local dev instructions 2022-06-06 14:48:55 -04:00
Jacob Caban-Tomski
21dd23953d Update repo documentation.
Turn main repo README into landing page with relevant sub folder/component links.
Include aggregator-proxy in setup.ts.
Update aggreagator-proxy npm repo.
Clean up all component READMEs. Add more troubleshooting, removing empty sections.
Add docs folder with guides for local & remote setup, system overview, and client usage.
Add image assets for landing & system overview.
2022-06-03 15:12:09 -04:00
kautukkundan
0b7580a442 removed unused comment block 2022-06-02 20:06:08 +05:30
James Zaki
074d58b8a8 Maintain one-to-one bls key-to-wallet relationship. Fixes #104 2022-05-31 15:40:03 +01:00
James Zaki
114a5fdf6c Add/update dependencies 2022-05-31 15:37:30 +01:00
420 changed files with 59316 additions and 19782 deletions

View File

@@ -0,0 +1,47 @@
name: Build & Upload Extension
description: Builds & uploads extension for a broswer to a Github release
inputs:
node-version:
description: 'NodeJS version to use for setup & build'
required: true
browser:
description: 'Which browser to build the extension for'
required: true
file-name:
description: 'The name of the browser asset to upload'
required: true
tag-name:
description: 'Tag name of the release. Commonly github.ref in an on.release workflow'
required: true
runs:
using: composite
steps:
- uses: actions/setup-node@v3
with:
node-version: ${{ inputs.node-version }}
cache: yarn
cache-dependency-path: extension/yarn.lock
- working-directory: ./extension
shell: bash
run: |
envsubst < config.release.json > config.json
yarn install --frozen-lockfile
- working-directory: ./extension
shell: bash
run: |
NETWORK_CONFIGS_DIR=../contracts/networks \
yarn build:${{ inputs.browser }}
- working-directory: ./extension
shell: bash
run: mv ./extension/${{ inputs.file-name }} ./extension/quill-${{ inputs.file-name }}
- uses: svenstaro/upload-release-action@v2
with:
tag: ${{ inputs.tag-name }}
# Note: This path is from repo root
# working-directory is not applied
file: ./extension/extension/quill-${{ inputs.file-name }}
overwrite: true

View File

@@ -0,0 +1,13 @@
name: Local Aggregator Deploy
description: Runs an aggregator instance
runs:
using: composite
steps:
- working-directory: ./aggregator
shell: bash
run: cp .env.test .env
- working-directory: ./aggregator
shell: bash
run: deno run --allow-read --allow-write --allow-env --allow-net ./programs/aggregator.ts 2>&1 | tee -a aggregatorLogs.txt &

View File

@@ -0,0 +1,9 @@
name: Local Contract Deploy
description: Runs a Hardhat node & deploys contracts
runs:
using: composite
steps:
- working-directory: ./contracts
shell: bash
run: yarn start &

View File

@@ -0,0 +1,17 @@
name: Local Contract Deploy
description: Runs a Hardhat node & deploys contracts
runs:
using: composite
steps:
- working-directory: ./contracts
shell: bash
run: yarn hardhat node &
- working-directory: ./contracts
shell: bash
run: yarn hardhat fundDeployer --network gethDev
- working-directory: ./contracts
shell: bash
run: yarn hardhat run scripts/deploy_all.ts --network gethDev

View File

@@ -0,0 +1,24 @@
name: Setup Contracts & Clients
description: Sets up contracts & clients
runs:
using: composite
steps:
- uses: actions/setup-node@v3
with:
node-version: 16.x
cache: yarn
cache-dependency-path: |
contracts/yarn.lock
contracts/clients/yarn.lock
- working-directory: ./contracts
shell: bash
run: |
cp .env.example .env
yarn install --frozen-lockfile
yarn build
- working-directory: ./contracts/clients
shell: bash
run: yarn install --frozen-lockfile

22
.github/labeler.yml vendored
View File

@@ -1,9 +1,25 @@
aggregator:
- aggregator/*
- aggregator/**/*
aggregator-proxy:
- aggregator-proxy/*
- aggregator-proxy/**/*
automation:
- .github/*
- .github/**/*
extension:
- extension/*
- extension/**/*
contracts:
# Don't label client only changes.
- any: ['contracts/**/*', '!contracts/clients/**/*']
- contracts/*
# Don't label client only changes.
- any: ['contracts/**/*', '!contracts/clients/**/*']
clients:
- contracts/clients/**/*
- 'contracts/clients/*'
- 'contracts/clients/**/*'
documentation:
- 'docs/*'
- 'docs/**/*'
- '*.md'
- '**/*.md'
- '**/**/*.md'

20
.github/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,20 @@
categories:
- title: 'aggregator'
label: 'aggregator'
- title: 'aggregator-proxy'
label: 'aggregator-proxy'
- title: 'contracts'
label: 'contracts'
- title: 'clients'
label: 'clients'
- title: 'docs'
label: 'documentation'
- title: 'extension'
label: 'extension'
version-resolver:
default: minor
prerelease: true
template: |
## Whats Changed
$CHANGES

View File

@@ -0,0 +1,29 @@
name: aggregator-dockerhub
on:
push:
branches:
- 'main'
paths:
- 'aggregator/**'
- '.github/workflows/aggregator-dockerhub.yml'
defaults:
run:
working-directory: ./aggregator
env:
DENO_VERSION: 1.x
jobs:
push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- run: git show HEAD
- run: echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login --username blswalletghactions --password-stdin
- run: ./programs/build.ts --image-name blswallet/aggregator --image-only --also-tag-latest --push

32
.github/workflows/aggregator-proxy.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: aggregator-proxy
on:
push:
branches:
- 'main'
paths:
- 'aggregator-proxy/**'
pull_request:
paths:
- 'aggregator-proxy/**'
defaults:
run:
working-directory: ./aggregator-proxy
env:
NODEJS_VERSION: 16.x
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODEJS_VERSION }}
cache: yarn
cache-dependency-path: aggregator-proxy/yarn.lock
- run: yarn install --frozen-lockfile
- run: yarn build

80
.github/workflows/aggregator.yml vendored Normal file
View File

@@ -0,0 +1,80 @@
name: aggregator
on:
push:
branches:
- 'main'
paths:
- 'aggregator/**'
# Check for breaking changes from contracts
- 'contracts/**'
- '.github/workflows/aggregator.yml'
pull_request:
paths:
- 'aggregator/**'
# Check for breaking changes from contracts
- 'contracts/**'
- '.github/workflows/aggregator.yml'
branches-ignore:
# Changes targeting this branch should be tested+fixed when being merged
# into main
- contract-updates
defaults:
run:
working-directory: ./aggregator
env:
DENO_VERSION: 1.x
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- run: deno lint .
todos-fixmes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- run: ./programs/lintTodos.ts
typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- run: ./programs/checkTs.ts
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- uses: ./.github/actions/setup-contracts-clients
# Setup node & contracts
- working-directory: ./contracts
run: yarn start &
- working-directory: ./contracts
run: ./scripts/wait-for-rpc.sh
- run: cp .env.local.example .env
- run: deno test --allow-net --allow-env --allow-read
- uses: mxschmitt/action-tmate@v3
- run: sleep 3600

28
.github/workflows/clients.yml vendored Normal file
View File

@@ -0,0 +1,28 @@
name: clients
on:
push:
branches:
- 'main'
paths:
- 'contracts/clients/**'
pull_request:
paths:
- 'contracts/clients/**'
defaults:
run:
working-directory: ./contracts/clients
env:
DENO_VERSION: 1.x
jobs:
test-unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-contracts-clients
- run: yarn test

45
.github/workflows/contracts.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: contracts
on:
push:
branches:
- 'main'
paths:
- 'contracts/**'
- '!contracts/clients/**'
pull_request:
paths:
- 'contracts/**'
- '!contracts/clients/**'
defaults:
run:
working-directory: ./contracts
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-contracts-clients
- run: yarn lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-contracts-clients
- run: yarn test
# ensure gas measurement script runs
test-gas-measurements:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-contracts-clients
- uses: ./.github/actions/local-contract-deploy-hardhat
- run: yarn hardhat run ./scripts/measure_gas/run.ts --network gethDev

52
.github/workflows/extension-release.yml vendored Normal file
View File

@@ -0,0 +1,52 @@
name: extension-release
on:
release:
types: [published]
defaults:
run:
working-directory: ./extension
env:
NODEJS_VERSION: 16.x
jobs:
chrome:
runs-on: ubuntu-latest
environment: extension-release
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/build-upload-extension
with:
node-version: ${{ env.NODEJS_VERSION }}
browser: chrome
file-name: chrome.zip
tag-name: ${{ github.ref }}
firefox:
runs-on: ubuntu-latest
environment: extension-release
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/build-upload-extension
with:
node-version: ${{ env.NODEJS_VERSION }}
browser: firefox
file-name: firefox.xpi
tag-name: ${{ github.ref }}
opera:
runs-on: ubuntu-latest
environment: extension-release
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/build-upload-extension
with:
node-version: ${{ env.NODEJS_VERSION }}
browser: opera
file-name: opera.crx
tag-name: ${{ github.ref }}

47
.github/workflows/extension.yml vendored Normal file
View File

@@ -0,0 +1,47 @@
name: extension
on:
push:
branches:
- 'main'
paths:
- 'extension/**'
pull_request:
paths:
- 'extension/**'
defaults:
run:
working-directory: ./extension
env:
NODEJS_VERSION: 16.x
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODEJS_VERSION }}
cache: yarn
cache-dependency-path: extension/yarn.lock
- run: yarn install --frozen-lockfile
- run: yarn lint
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ env.NODEJS_VERSION }}
cache: yarn
cache-dependency-path: extension/yarn.lock
- run: cp config.example.json config.json
- run: yarn install --frozen-lockfile
# For now, just check that chrome builds
- run: yarn build:chrome

69
.github/workflows/integration.yml vendored Normal file
View File

@@ -0,0 +1,69 @@
name: integration
on:
push:
branches:
- 'main'
paths:
- 'aggregator/**'
# Check for breaking changes from contracts
- 'contracts/**'
- '.github/workflows/integration.yml'
pull_request:
paths:
- 'aggregator/**'
# Check for breaking changes from contracts
- 'contracts/**'
- '.github/workflows/integration.yml'
branches-ignore:
# Changes targeting this branch should be tested+fixed when being merged
# into main
- contract-updates
defaults:
run:
working-directory: ./contracts/clients
env:
DENO_VERSION: 1.x
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-contracts-clients
- working-directory: ./contracts/clients
run: yarn build
test-integration:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ./.github/actions/setup-contracts-clients
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
# - name: run geth node and deploy contracts
- uses: ./.github/actions/local-contract-deploy-geth
- working-directory: ./contracts
run: ./scripts/wait-for-contract-deploy.sh
# - name: run aggregator
- uses: ./.github/actions/local-aggregator-deploy
# - name: integration tests
- working-directory: ./contracts
run: yarn test-integration
# - name: upload artifacts
- uses: actions/upload-artifact@v3
if: always()
with:
name: aggregator-logs
path: ./aggregator/aggregatorLogs.txt
retention-days: 5

View File

@@ -6,6 +6,6 @@ jobs:
triage:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@main
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

24
.github/workflows/release-drafter.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Release Drafter
on:
push:
branches:
- main
permissions:
contents: read
jobs:
update_release_draft:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: release-drafter/release-drafter@v5
with:
config-name: release-drafter.yml
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
.data
.DS_Store
.idea

1
.nvmrc Normal file
View File

@@ -0,0 +1 @@
lts/*

61
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,61 @@
# Contribute to BLS Wallet
Thanks for taking the time to contribute to BLS Wallet!
In this guide you will get an overview of the contribution workflow from opening an issue, creating a PR, reviewing, and merging the PR.
## Getting started
To get an overview of the project, see [System Overview](docs/system_overview.md)
To setup the repo for local use, see [Local Development](docs/local_development.md)
## Issues
### Create a new issue
First search for an [existing issue](https://github.com/web3well/bls-wallet/issues). If you find one, add any new insight, helpful context, or some reactions. Otherwise, you can [open a new issue](https://github.com/web3well/bls-wallet/issues/new). Be sure to label it with anything relevant.
### Solve an issue
Search for an [existing issue](https://github.com/github/docs/issues) that is unassigned and interests you. If this is your first time contributing, you may want to choose a [good first issue](https://github.com/web3well/bls-wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22).
## Make Changes
1. [Fork the repo](https://github.com/web3well/bls-wallet/fork)
2. Checkout a new branch
3. Make your changes
### Quality Checks
- You should add new/update test cases for new features or bug fixes to ensure that your changes work properly and will not be broken by other future changes.
- Type checking and code linting should all pass.
- For ambiguous Typescript typing, prefer `unknown` over `any`.
## Commit your update
Commit your changes over one or more commits. It is recommended to format your commit messages as follows:
```
A short summary of what you did
A list or paragraph of more specific details
```
## Pull Request
Create a pull request (PR) from your fork's branch to `main`, filling in the descriptions template including [linking to the issue you are resolving](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue). Feel free to open a draft PR while you are actively working.
Once ready, a BLS Wallet team member will review the PR.
- When run, all Github Actions workflows should succeed.
- All TODO/FIXME comments in code should be resolved, unless marked `merge-ok` with a description/issue link describing how they can be resolved in future work.
- The author of a comment may mark it as [resolved](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/commenting-on-a-pull-request#resolving-conversations) when they are satisfied with a requested change or answer to a question. You are not required to resolve all comments as some may provide good historical information.
## Your PR is merged!
Thanks for your hard work! Accept our heartfelt gratitude and revel in your masterful coding and/or documentational skills.
### Thanks
To [github/docs CONTRIBUTING.md](https://github.com/github/docs/blob/main/CONTRIBUTING.md) for being a great contribution template.

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 BLS Wallet
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

191
README.md
View File

@@ -1,173 +1,58 @@
# bls-wallet
![BLS Wallet](./docs/images/bls-github-banner.svg)
An Ethereum Layer 2 smart contract wallet that uses [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature) and aggregated transactions to reduce gas costs.
## What is BLS Wallet?
You can watch a full end-to-end demo of the project [here](https://www.youtube.com/watch?v=MOQ3sCLP56g)
A set of components to bring lower gas costs to EVM rollups via aggregated [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature). Our smart contract wallet supports recovery, atomic multi-action operations, sponsored transactions and user-controlled upgradability.
You can watch a full end-to-end demo of the project [here](https://www.youtube.com/watch?v=MOQ3sCLP56g).
## Getting Started
- See an [overview](./docs/system_overview.md) of BLS Wallet & how the components work together.
- Use BLS Wallet in [a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md).
- Use BLS Wallet in [your L2 dApp](./docs/use_bls_wallet_dapp.md) for cheaper, multi action transactions.
- Use BLS Wallet components and features with an [ethers.js provider and signer](./use_bls_provider.md)
### Setup your development environment
- [Local development](./docs/local_development.md)
- [Remote development](./docs/remote_development.md)
## Components
See each component's directory `README` for more details.
[contracts](./contracts/)
![System Overview](images/system-overview.svg)
Solidity smart contracts for wallets, BLS signature verification, and deployment/testing tools.
### Aggregator
[aggregator](./aggregator/)
Service which aggregates BLS wallet transactions.
Service which accepts BLS signed transactions and bundles them into one for submission.
### Clients
[aggregator-proxy](./aggregator-proxy/)
TS/JS Client libraries for web apps and services.
npm package for proxying to another aggregator instance.
### Contracts
[bls-wallet-clients](./contracts/clients/)
`bls-wallet` Solidity contracts.
npm package which provides easy to use constructs to interact with the contracts and aggregator.
### Extension
[extension](./extension/)
Quill browser extension used to manage BLS Wallets and sign transactions.
Prototype browser extension used to manage BLS Wallets and sign transactions.
### Signer
## Contract Deployments
TS/JS BLS Signing lib.
See the [networks directory](./contracts/networks/) for a list of all contract deployment (network) manifests. Have an L2/rollup testnet you'd like BLS Wallet deployed on? [Open an issue](https://github.com/web3well/bls-wallet/issues/new) or [Deploy it yourself](./docs/remote_development.md)
## Dependencies
- [Arbitrum Goerli](./contracts/networks/arbitrum-goerli.json)
- [Optimism Goerli](./contracts/networks/optimism-goerli.json)
### Required
## Ways to Contribute
- [NodeJS](https://nodejs.org)
- [Yarn](https://yarnpkg.com/getting-started/install) (`npm install -g yarn`)
- [Deno](https://deno.land/#installation)
- [Work on an open issue](https://github.com/web3well/bls-wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
- [Use BLS Wallet](./docs/use_bls_wallet_clients.md) in your project and [share it with us](https://github.com/web3well/bls-wallet/discussions)
- [Report a bug or request a feature](https://github.com/web3well/bls-wallet/issues/new)
- [Ask a question or answer an existing one](https://github.com/web3well/bls-wallet/discussions)
- [Try or add to our documentation](https://github.com/web3well/bls-wallet/tree/main/docs)
### Optional (Recomended)
- [nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
- [docker-compose](https://docs.docker.com/compose/install/)
- [MetaMask](https://metamask.io/)
## Setup
Run the repo setup script
```sh
./setup.ts
```
Then choose to target either a local Hardhat node or the Arbitrum Testnet.
### Local
Start a local Hardhat node for RPC use.
```sh
cd ./contracts
yarn hardhat node
```
You can use any two of the private keys displayed (PK0 & PK1) to update these values in `./aggregator/.env`.
```
...
PRIVATE_KEY_AGG=PK0
PRIVATE_KEY_ADMIN=PK1
...
```
Set this value in `./contracts/.env` (This mnemonic is special to hardhat and has funds).
```
...
DEPLOYER_MNEMONIC="test test test test test test test test test test test junk"
...
```
Deploy the PrecompileCostEstimator contract.
```sh
yarn hardhat run scripts/0_deploy_precompile_cost_estimator.ts --network gethDev
```
Copy the address that is output.
Update `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s `COST_ESTIMATOR_ADDRESS` to the value of that address;
```solidity
...
address private constant COST_ESTIMATOR_ADDRESS = 0x57047C275bbCb44D85DFA50AD562bA968EEba95A;
...
```
Deploy all remaining `bls-wallet` contracts.
```sh
yarn hardhat run scripts/deploy_all.ts --network gethDev
```
### Arbitrum Testnet (Rinkeby Arbitrum Testnet)
You will need two ETH addresses with Rinkeby ETH and their private keys (PK0 & PK1) for running the aggregator. It is NOT recommended that you use any primary wallets with ETH Mainnet assets.
You can get Rinkeby ETH at https://app.mycrypto.com/faucet, and transfer it into the Arbitrum testnet via https://bridge.arbitrum.io/. Make sure when doing so that your network is set to Rinkeby in MetaMask.
Update these values in `./aggregator/.env`.
```
RPC_URL=https://rinkeby.arbitrum.io/rpc
...
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-testnet.json
PRIVATE_KEY_AGG=PK0
PRIVATE_KEY_ADMIN=PK1
...
```
And then update this value in `./extension/.env`.
```
...
DEFAULT_CHAIN_ID=421611
...
```
## Run
```sh
docker-compose up -d postgres # Or see local postgres instructions in ./aggregator/README.md#PostgreSQL
cd ./aggregator
./programs/aggregator.ts
```
In a seperate terminal/shell instance
```sh
cd ./extension
yarn run dev:chrome # or dev:firefox, dev:opera
```
### Chrome
1. Go to Chrome's [extension page](chrome://extensions).
2. Enable `Developer mode`.
3. Either click `Load unpacked extension...` and select `./extension/extension/chrome` or drag that folder into the page.
### Firefox
1. Go to Firefox's [debugging page](about:debugging#/runtime/this-firefox).
2. Click `Load Temporary Add-on...`.
3. Select `./extension/extension/firefox/manifest.json`.
## Testing/using updates to ./clients
### extension
```sh
cd ./contracts/clients
yarn build
yarn link
cd ../extension
yarn link bls-wallet-clients
```
### aggregator
You will need to push up an `@experimental` version to 'bls-wallet-clients' on npm and update the version in `./aggregtor/src/deps.ts` until a local linking solution for deno is found. See https://github.com/alephjs/esm.sh/discussions/216 for details.
In `./contracts/clients` with your changes:
```
yarn publish-experimental
```
Note the `x.y.z-abc1234` version that was output.
Then in `./aggregtor/deps.ts`, change all `from` references for that package.
```typescript
...
} from "https://esm.sh/bls-wallet-clients@x.y.z-abc1234";
...
```
See our [contribution instructions & guidelines](./CONTRIBUTING.md) for more details.

View File

@@ -1,5 +1,16 @@
# Aggregator Proxy
[![npm version](https://img.shields.io/npm/v/bls-wallet-aggregator-proxy)](https://www.npmjs.com/package/bls-wallet-aggregator-proxy)
This package makes it easy to provide an aggregator by proxying another. The primary use-case is to expose a free aggregator based on one that requires payment by augmenting the bundles with transactions that pay `tx.origin`.
## Setup
```sh
npm install bls-wallet-aggregator-proxy
yarn install bls-wallet-aggregator-proxy
```
## Usage
```ts
@@ -9,20 +20,32 @@ import {
// AggregatorProxyCallback,
// ^ Alternatively, for manual control, import AggregatorProxyCallback to
// just generate the req,res callback for use with http.createServer
} from 'aggregator-proxy';
} from "bls-wallet-aggregator-proxy";
runAggregatorProxy(
'https://arbitrum-testnet.blswallet.org',
async bundle => {
console.log('proxying bundle', JSON.stringify(bundle, null, 2));
"https://arbitrum-goerli.blswallet.org",
async (bundle) => {
console.log("proxying bundle", JSON.stringify(bundle, null, 2));
// Return a different/augmented bundle to send to the upstream aggregator
return bundle;
},
8080,
'0.0.0.0',
"0.0.0.0",
() => {
console.log('Proxying aggregator on port 8080');
},
console.log("Proxying aggregator on port 8080");
}
);
```
## Instant wallet example without dapp-sponsored transaction
![Instant wallet without dapp-sponsored transactions](./../docs/images/system-overview/instant-wallet-without-dapp-sponsored-txs.jpg)
## Instant wallet example with dapp-sponsored transaction
![Instant wallet with dapp-sponsored transaction](./../docs/images/system-overview/instant-wallet-with-dapp-sponsored-txs.jpg)
## Example dApp using a proxy aggregator
- https://github.com/JohnGuilding/single-pool-dex

View File

@@ -1,7 +1,7 @@
import { runAggregatorProxy } from "../src";
runAggregatorProxy(
'https://arbitrum-testnet.blswallet.org',
'https://arbitrum-goerli.blswallet.org',
async b => {
console.log('proxying bundle', JSON.stringify(b, null, 2));
return b;

View File

@@ -2,10 +2,14 @@
"name": "bls-wallet-aggregator-proxy",
"version": "0.1.1",
"main": "dist/src/index.js",
"repository": "https://github.com/web3well/bls-wallet",
"repository": "https://github.com/web3well/bls-wallet/aggregator-proxy",
"author": "Andrew Morris",
"license": "MIT",
"private": false,
"engines": {
"node": ">=16.0.0",
"yarn": ">=1.0.0"
},
"scripts": {
"build": "rm -rf dist && tsc"
},
@@ -17,7 +21,7 @@
"@types/koa__cors": "^3.3.0",
"@types/koa__router": "^8.0.11",
"@types/node-fetch": "^2.6.1",
"bls-wallet-clients": "^0.6.0",
"bls-wallet-clients": "0.8.2-1fb4a55",
"fp-ts": "^2.12.1",
"io-ts": "^2.2.16",
"io-ts-reporters": "^2.0.1",

View File

@@ -2,22 +2,7 @@
# yarn lockfile v1
"@ethersproject/abi@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.5.0.tgz#fb52820e22e50b854ff15ce1647cc508d6660613"
integrity sha512-loW7I4AohP5KycATvc0MgujU6JyCHPqHdeoo9z3Nr9xEiNioxa65ccdm1+fsoJhkuhdRtfcL8cfyGamz2AxZ5w==
dependencies:
"@ethersproject/address" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/constants" "^5.5.0"
"@ethersproject/hash" "^5.5.0"
"@ethersproject/keccak256" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/abi@5.6.1", "@ethersproject/abi@^5.5.0", "@ethersproject/abi@^5.6.0":
"@ethersproject/abi@5.6.1", "@ethersproject/abi@^5.6.0":
version "5.6.1"
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.6.1.tgz#f7de888edeb56b0a657b672bdd1b3a1135cd14f7"
integrity sha512-0cqssYh6FXjlwKWBmLm3+zH2BNARoS5u/hxbz+LpQmcDB3w0W553h2btWui1/uZp2GBM/SI3KniTuMcYyHpA5w==
@@ -32,20 +17,22 @@
"@ethersproject/properties" "^5.6.0"
"@ethersproject/strings" "^5.6.0"
"@ethersproject/abstract-provider@5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.5.1.tgz#2f1f6e8a3ab7d378d8ad0b5718460f85649710c5"
integrity sha512-m+MA/ful6eKbxpr99xUYeRvLkfnlqzrF8SZ46d/xFB1A7ZVknYc/sXJG0RcufF52Qn2jeFj1hhcoQ7IXjNKUqg==
"@ethersproject/abi@5.7.0", "@ethersproject/abi@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/abi/-/abi-5.7.0.tgz#b3f3e045bbbeed1af3947335c247ad625a44e449"
integrity sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==
dependencies:
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/networks" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/transactions" "^5.5.0"
"@ethersproject/web" "^5.5.0"
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/hash" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@ethersproject/abstract-provider@5.6.0", "@ethersproject/abstract-provider@^5.5.0", "@ethersproject/abstract-provider@^5.6.0":
"@ethersproject/abstract-provider@5.6.0", "@ethersproject/abstract-provider@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.6.0.tgz#0c4ac7054650dbd9c476cf5907f588bbb6ef3061"
integrity sha512-oPMFlKLN+g+y7a79cLK3WiLcjWFnZQtXWgnLAbHZcN3s7L4v90UHpTOrLk+m3yr0gt+/h9STTM6zrr7PM8uoRw==
@@ -58,18 +45,20 @@
"@ethersproject/transactions" "^5.6.0"
"@ethersproject/web" "^5.6.0"
"@ethersproject/abstract-signer@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.5.0.tgz#590ff6693370c60ae376bf1c7ada59eb2a8dd08d"
integrity sha512-lj//7r250MXVLKI7sVarXAbZXbv9P50lgmJQGr2/is82EwEb8r7HrxsmMqAjTsztMYy7ohrIhGMIml+Gx4D3mA==
"@ethersproject/abstract-provider@5.7.0", "@ethersproject/abstract-provider@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-provider/-/abstract-provider-5.7.0.tgz#b0a8550f88b6bf9d51f90e4795d48294630cb9ef"
integrity sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==
dependencies:
"@ethersproject/abstract-provider" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/networks" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/transactions" "^5.7.0"
"@ethersproject/web" "^5.7.0"
"@ethersproject/abstract-signer@5.6.0", "@ethersproject/abstract-signer@^5.5.0", "@ethersproject/abstract-signer@^5.6.0":
"@ethersproject/abstract-signer@5.6.0", "@ethersproject/abstract-signer@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.6.0.tgz#9cd7ae9211c2b123a3b29bf47aab17d4d016e3e7"
integrity sha512-WOqnG0NJKtI8n0wWZPReHtaLkDByPL67tn4nBaDAhmVq8sjHTPbCdz4DRhVu/cfTOvfy9w3iq5QZ7BX7zw56BQ==
@@ -80,18 +69,18 @@
"@ethersproject/logger" "^5.6.0"
"@ethersproject/properties" "^5.6.0"
"@ethersproject/address@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.5.0.tgz#bcc6f576a553f21f3dd7ba17248f81b473c9c78f"
integrity sha512-l4Nj0eWlTUh6ro5IbPTgbpT4wRbdH5l8CQf7icF7sb/SI3Nhd9Y9HzhonTSTi6CefI0necIw7LJqQPopPLZyWw==
"@ethersproject/abstract-signer@5.7.0", "@ethersproject/abstract-signer@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/abstract-signer/-/abstract-signer-5.7.0.tgz#13f4f32117868452191a4649723cb086d2b596b2"
integrity sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==
dependencies:
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/keccak256" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/rlp" "^5.5.0"
"@ethersproject/abstract-provider" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/address@5.6.0", "@ethersproject/address@^5.5.0", "@ethersproject/address@^5.6.0":
"@ethersproject/address@5.6.0", "@ethersproject/address@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.6.0.tgz#13c49836d73e7885fc148ad633afad729da25012"
integrity sha512-6nvhYXjbXsHPS+30sHZ+U4VMagFC/9zAk6Gd/h3S21YW4+yfb0WfRtaAIZ4kfM4rrVwqiy284LP0GtL5HXGLxQ==
@@ -102,29 +91,32 @@
"@ethersproject/logger" "^5.6.0"
"@ethersproject/rlp" "^5.6.0"
"@ethersproject/base64@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.5.0.tgz#881e8544e47ed976930836986e5eb8fab259c090"
integrity sha512-tdayUKhU1ljrlHzEWbStXazDpsx4eg1dBXUSI6+mHlYklOXoXF6lZvw8tnD6oVaWfnMxAgRSKROg3cVKtCcppA==
"@ethersproject/address@5.7.0", "@ethersproject/address@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/address/-/address-5.7.0.tgz#19b56c4d74a3b0a46bfdbb6cfcc0a153fc697f37"
integrity sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/base64@5.6.0", "@ethersproject/base64@^5.5.0", "@ethersproject/base64@^5.6.0":
"@ethersproject/base64@5.6.0", "@ethersproject/base64@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.6.0.tgz#a12c4da2a6fb86d88563216b0282308fc15907c9"
integrity sha512-2Neq8wxJ9xHxCF9TUgmKeSh9BXJ6OAxWfeGWvbauPh8FuHEjamgHilllx8KkSd5ErxyHIX7Xv3Fkcud2kY9ezw==
dependencies:
"@ethersproject/bytes" "^5.6.0"
"@ethersproject/basex@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.5.0.tgz#e40a53ae6d6b09ab4d977bd037010d4bed21b4d3"
integrity sha512-ZIodwhHpVJ0Y3hUCfUucmxKsWQA5TMnavp5j/UOuDdzZWzJlRmuOjcTMIGgHCYuZmHt36BfiSyQPSRskPxbfaQ==
"@ethersproject/base64@5.7.0", "@ethersproject/base64@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/base64/-/base64-5.7.0.tgz#ac4ee92aa36c1628173e221d0d01f53692059e1c"
integrity sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/basex@5.6.0", "@ethersproject/basex@^5.5.0", "@ethersproject/basex@^5.6.0":
"@ethersproject/basex@5.6.0", "@ethersproject/basex@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.6.0.tgz#9ea7209bf0a1c3ddc2a90f180c3a7f0d7d2e8a69"
integrity sha512-qN4T+hQd/Md32MoJpc69rOwLYRUXwjTlhHDIeUkUmiN/JyWkkLLMoG0TqvSQKNqZOMgN5stbUYN6ILC+eD7MEQ==
@@ -132,16 +124,15 @@
"@ethersproject/bytes" "^5.6.0"
"@ethersproject/properties" "^5.6.0"
"@ethersproject/bignumber@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.5.0.tgz#875b143f04a216f4f8b96245bde942d42d279527"
integrity sha512-6Xytlwvy6Rn3U3gKEc1vP7nR92frHkv6wtVr95LFR3jREXiCPzdWxKQ1cx4JGQBXxcguAwjA8murlYN2TSiEbg==
"@ethersproject/basex@5.7.0", "@ethersproject/basex@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/basex/-/basex-5.7.0.tgz#97034dc7e8938a8ca943ab20f8a5e492ece4020b"
integrity sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
bn.js "^4.11.9"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@^5.5.0", "@ethersproject/bignumber@^5.6.0":
"@ethersproject/bignumber@5.6.0", "@ethersproject/bignumber@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.6.0.tgz#116c81b075c57fa765a8f3822648cf718a8a0e26"
integrity sha512-VziMaXIUHQlHJmkv1dlcd6GY2PmT0khtAqaMctCIDogxkrarMzA9L94KN1NeXqqOfFD6r0sJT3vCTOFSmZ07DA==
@@ -150,49 +141,42 @@
"@ethersproject/logger" "^5.6.0"
bn.js "^4.11.9"
"@ethersproject/bytes@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.5.0.tgz#cb11c526de657e7b45d2e0f0246fb3b9d29a601c"
integrity sha512-ABvc7BHWhZU9PNM/tANm/Qx4ostPGadAuQzWTr3doklZOhDlmcBqclrQe/ZXUIj3K8wC28oYeuRa+A37tX9kog==
"@ethersproject/bignumber@5.7.0", "@ethersproject/bignumber@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/bignumber/-/bignumber-5.7.0.tgz#e2f03837f268ba655ffba03a57853e18a18dc9c2"
integrity sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==
dependencies:
"@ethersproject/logger" "^5.5.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
bn.js "^5.2.1"
"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.5.0", "@ethersproject/bytes@^5.6.0":
"@ethersproject/bytes@5.6.1", "@ethersproject/bytes@^5.6.0":
version "5.6.1"
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.6.1.tgz#24f916e411f82a8a60412344bf4a813b917eefe7"
integrity sha512-NwQt7cKn5+ZE4uDn+X5RAXLp46E1chXoaMmrxAyA0rblpxz8t58lVkrHXoRIn0lz1joQElQ8410GqhTqMOwc6g==
dependencies:
"@ethersproject/logger" "^5.6.0"
"@ethersproject/constants@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.5.0.tgz#d2a2cd7d94bd1d58377d1d66c4f53c9be4d0a45e"
integrity sha512-2MsRRVChkvMWR+GyMGY4N1sAX9Mt3J9KykCsgUFd/1mwS0UH1qw+Bv9k1UJb3X3YJYFco9H20pjSlOIfCG5HYQ==
"@ethersproject/bytes@5.7.0", "@ethersproject/bytes@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/bytes/-/bytes-5.7.0.tgz#a00f6ea8d7e7534d6d87f47188af1148d71f155d"
integrity sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==
dependencies:
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/constants@5.6.0", "@ethersproject/constants@^5.5.0", "@ethersproject/constants@^5.6.0":
"@ethersproject/constants@5.6.0", "@ethersproject/constants@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.6.0.tgz#55e3eb0918584d3acc0688e9958b0cedef297088"
integrity sha512-SrdaJx2bK0WQl23nSpV/b1aq293Lh0sUaZT/yYKPDKn4tlAbkH96SPJwIhwSwTsoQQZxuh1jnqsKwyymoiBdWA==
dependencies:
"@ethersproject/bignumber" "^5.6.0"
"@ethersproject/contracts@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.5.0.tgz#b735260d4bd61283a670a82d5275e2a38892c197"
integrity sha512-2viY7NzyvJkh+Ug17v7g3/IJC8HqZBDcOjYARZLdzRxrfGlRgmYgl6xPRKVbEzy1dWKw/iv7chDcS83pg6cLxg==
"@ethersproject/constants@5.7.0", "@ethersproject/constants@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/constants/-/constants-5.7.0.tgz#df80a9705a7e08984161f09014ea012d1c75295e"
integrity sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==
dependencies:
"@ethersproject/abi" "^5.5.0"
"@ethersproject/abstract-provider" "^5.5.0"
"@ethersproject/abstract-signer" "^5.5.0"
"@ethersproject/address" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/constants" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/transactions" "^5.5.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/contracts@5.6.0":
version "5.6.0"
@@ -210,21 +194,23 @@
"@ethersproject/properties" "^5.6.0"
"@ethersproject/transactions" "^5.6.0"
"@ethersproject/hash@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.5.0.tgz#7cee76d08f88d1873574c849e0207dcb32380cc9"
integrity sha512-dnGVpK1WtBjmnp3mUT0PlU2MpapnwWI0PibldQEq1408tQBAbZpPidkWoVVuNMOl/lISO3+4hXZWCL3YV7qzfg==
"@ethersproject/contracts@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/contracts/-/contracts-5.7.0.tgz#c305e775abd07e48aa590e1a877ed5c316f8bd1e"
integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==
dependencies:
"@ethersproject/abstract-signer" "^5.5.0"
"@ethersproject/address" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/keccak256" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/abi" "^5.7.0"
"@ethersproject/abstract-provider" "^5.7.0"
"@ethersproject/abstract-signer" "^5.7.0"
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/transactions" "^5.7.0"
"@ethersproject/hash@5.6.0", "@ethersproject/hash@^5.5.0", "@ethersproject/hash@^5.6.0":
"@ethersproject/hash@5.6.0", "@ethersproject/hash@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.6.0.tgz#d24446a5263e02492f9808baa99b6e2b4c3429a2"
integrity sha512-fFd+k9gtczqlr0/BruWLAu7UAOas1uRRJvOR84uDf4lNZ+bTkGl366qvniUZHKtlqxBRU65MkOobkmvmpHU+jA==
@@ -238,25 +224,22 @@
"@ethersproject/properties" "^5.6.0"
"@ethersproject/strings" "^5.6.0"
"@ethersproject/hdnode@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.5.0.tgz#4a04e28f41c546f7c978528ea1575206a200ddf6"
integrity sha512-mcSOo9zeUg1L0CoJH7zmxwUG5ggQHU1UrRf8jyTYy6HxdZV+r0PBoL1bxr+JHIPXRzS6u/UW4mEn43y0tmyF8Q==
"@ethersproject/hash@5.7.0", "@ethersproject/hash@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/hash/-/hash-5.7.0.tgz#eb7aca84a588508369562e16e514b539ba5240a7"
integrity sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==
dependencies:
"@ethersproject/abstract-signer" "^5.5.0"
"@ethersproject/basex" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/pbkdf2" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/sha2" "^5.5.0"
"@ethersproject/signing-key" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/transactions" "^5.5.0"
"@ethersproject/wordlists" "^5.5.0"
"@ethersproject/abstract-signer" "^5.7.0"
"@ethersproject/address" "^5.7.0"
"@ethersproject/base64" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@ethersproject/hdnode@5.6.0", "@ethersproject/hdnode@^5.5.0", "@ethersproject/hdnode@^5.6.0":
"@ethersproject/hdnode@5.6.0", "@ethersproject/hdnode@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.6.0.tgz#9dcbe8d629bbbcf144f2cae476337fe92d320998"
integrity sha512-61g3Jp3nwDqJcL/p4nugSyLrpl/+ChXIOtCEM8UDmWeB3JCAt5FoLdOMXQc3WWkc0oM2C0aAn6GFqqMcS/mHTw==
@@ -274,26 +257,25 @@
"@ethersproject/transactions" "^5.6.0"
"@ethersproject/wordlists" "^5.6.0"
"@ethersproject/json-wallets@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.5.0.tgz#dd522d4297e15bccc8e1427d247ec8376b60e325"
integrity sha512-9lA21XQnCdcS72xlBn1jfQdj2A1VUxZzOzi9UkNdnokNKke/9Ya2xA9aIK1SC3PQyBDLt4C+dfps7ULpkvKikQ==
"@ethersproject/hdnode@5.7.0", "@ethersproject/hdnode@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/hdnode/-/hdnode-5.7.0.tgz#e627ddc6b466bc77aebf1a6b9e47405ca5aef9cf"
integrity sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==
dependencies:
"@ethersproject/abstract-signer" "^5.5.0"
"@ethersproject/address" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/hdnode" "^5.5.0"
"@ethersproject/keccak256" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/pbkdf2" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/random" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/transactions" "^5.5.0"
aes-js "3.0.0"
scrypt-js "3.0.1"
"@ethersproject/abstract-signer" "^5.7.0"
"@ethersproject/basex" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/pbkdf2" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/sha2" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@ethersproject/transactions" "^5.7.0"
"@ethersproject/wordlists" "^5.7.0"
"@ethersproject/json-wallets@5.6.0", "@ethersproject/json-wallets@^5.5.0", "@ethersproject/json-wallets@^5.6.0":
"@ethersproject/json-wallets@5.6.0", "@ethersproject/json-wallets@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.6.0.tgz#4c2fc27f17e36c583e7a252fb938bc46f98891e5"
integrity sha512-fmh86jViB9r0ibWXTQipxpAGMiuxoqUf78oqJDlCAJXgnJF024hOOX7qVgqsjtbeoxmcLwpPsXNU0WEe/16qPQ==
@@ -312,15 +294,26 @@
aes-js "3.0.0"
scrypt-js "3.0.1"
"@ethersproject/keccak256@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.5.0.tgz#e4b1f9d7701da87c564ffe336f86dcee82983492"
integrity sha512-5VoFCTjo2rYbBe1l2f4mccaRFN/4VQEYFwwn04aJV2h7qf4ZvI2wFxUE1XOX+snbwCLRzIeikOqtAoPwMza9kg==
"@ethersproject/json-wallets@5.7.0", "@ethersproject/json-wallets@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/json-wallets/-/json-wallets-5.7.0.tgz#5e3355287b548c32b368d91014919ebebddd5360"
integrity sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==
dependencies:
"@ethersproject/bytes" "^5.5.0"
js-sha3 "0.8.0"
"@ethersproject/abstract-signer" "^5.7.0"
"@ethersproject/address" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/hdnode" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/pbkdf2" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/random" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@ethersproject/transactions" "^5.7.0"
aes-js "3.0.0"
scrypt-js "3.0.1"
"@ethersproject/keccak256@5.6.0", "@ethersproject/keccak256@^5.5.0", "@ethersproject/keccak256@^5.6.0":
"@ethersproject/keccak256@5.6.0", "@ethersproject/keccak256@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.6.0.tgz#fea4bb47dbf8f131c2e1774a1cecbfeb9d606459"
integrity sha512-tk56BJ96mdj/ksi7HWZVWGjCq0WVl/QvfhFQNeL8fxhBlGoP+L80uDCiQcpJPd+2XxkivS3lwRm3E0CXTfol0w==
@@ -328,39 +321,39 @@
"@ethersproject/bytes" "^5.6.0"
js-sha3 "0.8.0"
"@ethersproject/logger@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.5.0.tgz#0c2caebeff98e10aefa5aef27d7441c7fd18cf5d"
integrity sha512-rIY/6WPm7T8n3qS2vuHTUBPdXHl+rGxWxW5okDfo9J4Z0+gRRZT0msvUdIJkE4/HS29GUMziwGaaKO2bWONBrg==
"@ethersproject/keccak256@5.7.0", "@ethersproject/keccak256@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/keccak256/-/keccak256-5.7.0.tgz#3186350c6e1cd6aba7940384ec7d6d9db01f335a"
integrity sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==
dependencies:
"@ethersproject/bytes" "^5.7.0"
js-sha3 "0.8.0"
"@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.5.0", "@ethersproject/logger@^5.6.0":
"@ethersproject/logger@5.6.0", "@ethersproject/logger@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.6.0.tgz#d7db1bfcc22fd2e4ab574cba0bb6ad779a9a3e7a"
integrity sha512-BiBWllUROH9w+P21RzoxJKzqoqpkyM1pRnEKG69bulE9TSQD8SAIvTQqIMZmmCO8pUNkgLP1wndX1gKghSpBmg==
"@ethersproject/networks@5.5.2":
version "5.5.2"
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.5.2.tgz#784c8b1283cd2a931114ab428dae1bd00c07630b"
integrity sha512-NEqPxbGBfy6O3x4ZTISb90SjEDkWYDUbEeIFhJly0F7sZjoQMnj5KYzMSkMkLKZ+1fGpx00EDpHQCy6PrDupkQ==
dependencies:
"@ethersproject/logger" "^5.5.0"
"@ethersproject/logger@5.7.0", "@ethersproject/logger@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/logger/-/logger-5.7.0.tgz#6ce9ae168e74fecf287be17062b590852c311892"
integrity sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==
"@ethersproject/networks@5.6.2", "@ethersproject/networks@^5.5.0", "@ethersproject/networks@^5.6.0":
"@ethersproject/networks@5.6.2", "@ethersproject/networks@^5.6.0":
version "5.6.2"
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.6.2.tgz#2bacda62102c0b1fcee408315f2bed4f6fbdf336"
integrity sha512-9uEzaJY7j5wpYGTojGp8U89mSsgQLc40PCMJLMCnFXTs7nhBveZ0t7dbqWUNrepWTszDbFkYD6WlL8DKx5huHA==
dependencies:
"@ethersproject/logger" "^5.6.0"
"@ethersproject/pbkdf2@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.5.0.tgz#e25032cdf02f31505d47afbf9c3e000d95c4a050"
integrity sha512-SaDvQFvXPnz1QGpzr6/HToLifftSXGoXrbpZ6BvoZhmx4bNLHrxDe8MZisuecyOziP1aVEwzC2Hasj+86TgWVg==
"@ethersproject/networks@5.7.1", "@ethersproject/networks@^5.7.0":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@ethersproject/networks/-/networks-5.7.1.tgz#118e1a981d757d45ccea6bb58d9fd3d9db14ead6"
integrity sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/sha2" "^5.5.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.5.0", "@ethersproject/pbkdf2@^5.6.0":
"@ethersproject/pbkdf2@5.6.0", "@ethersproject/pbkdf2@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.6.0.tgz#04fcc2d7c6bff88393f5b4237d906a192426685a"
integrity sha512-Wu1AxTgJo3T3H6MIu/eejLFok9TYoSdgwRr5oGY1LTLfmGesDoSx05pemsbrPT2gG4cQME+baTSCp5sEo2erZQ==
@@ -368,44 +361,27 @@
"@ethersproject/bytes" "^5.6.0"
"@ethersproject/sha2" "^5.6.0"
"@ethersproject/properties@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.5.0.tgz#61f00f2bb83376d2071baab02245f92070c59995"
integrity sha512-l3zRQg3JkD8EL3CPjNK5g7kMx4qSwiR60/uk5IVjd3oq1MZR5qUg40CNOoEJoX5wc3DyY5bt9EbMk86C7x0DNA==
"@ethersproject/pbkdf2@5.7.0", "@ethersproject/pbkdf2@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/pbkdf2/-/pbkdf2-5.7.0.tgz#d2267d0a1f6e123f3771007338c47cccd83d3102"
integrity sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==
dependencies:
"@ethersproject/logger" "^5.5.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/sha2" "^5.7.0"
"@ethersproject/properties@5.6.0", "@ethersproject/properties@^5.5.0", "@ethersproject/properties@^5.6.0":
"@ethersproject/properties@5.6.0", "@ethersproject/properties@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.6.0.tgz#38904651713bc6bdd5bdd1b0a4287ecda920fa04"
integrity sha512-szoOkHskajKePTJSZ46uHUWWkbv7TzP2ypdEK6jGMqJaEt2sb0jCgfBo0gH0m2HBpRixMuJ6TBRaQCF7a9DoCg==
dependencies:
"@ethersproject/logger" "^5.6.0"
"@ethersproject/providers@5.5.3":
version "5.5.3"
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.5.3.tgz#56c2b070542ac44eb5de2ed3cf6784acd60a3130"
integrity sha512-ZHXxXXXWHuwCQKrgdpIkbzMNJMvs+9YWemanwp1fA7XZEv7QlilseysPvQe0D7Q7DlkJX/w/bGA1MdgK2TbGvA==
"@ethersproject/properties@5.7.0", "@ethersproject/properties@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/properties/-/properties-5.7.0.tgz#a6e12cb0439b878aaf470f1902a176033067ed30"
integrity sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==
dependencies:
"@ethersproject/abstract-provider" "^5.5.0"
"@ethersproject/abstract-signer" "^5.5.0"
"@ethersproject/address" "^5.5.0"
"@ethersproject/basex" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/constants" "^5.5.0"
"@ethersproject/hash" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/networks" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/random" "^5.5.0"
"@ethersproject/rlp" "^5.5.0"
"@ethersproject/sha2" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/transactions" "^5.5.0"
"@ethersproject/web" "^5.5.0"
bech32 "1.1.4"
ws "7.4.6"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/providers@5.6.4":
version "5.6.4"
@@ -432,15 +408,33 @@
bech32 "1.1.4"
ws "7.4.6"
"@ethersproject/random@5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.5.1.tgz#7cdf38ea93dc0b1ed1d8e480ccdaf3535c555415"
integrity sha512-YaU2dQ7DuhL5Au7KbcQLHxcRHfgyNgvFV4sQOo0HrtW3Zkrc9ctWNz8wXQ4uCSfSDsqX2vcjhroxU5RQRV0nqA==
"@ethersproject/providers@5.7.2":
version "5.7.2"
resolved "https://registry.yarnpkg.com/@ethersproject/providers/-/providers-5.7.2.tgz#f8b1a4f275d7ce58cf0a2eec222269a08beb18cb"
integrity sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/abstract-provider" "^5.7.0"
"@ethersproject/abstract-signer" "^5.7.0"
"@ethersproject/address" "^5.7.0"
"@ethersproject/base64" "^5.7.0"
"@ethersproject/basex" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/hash" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/networks" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/random" "^5.7.0"
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/sha2" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@ethersproject/transactions" "^5.7.0"
"@ethersproject/web" "^5.7.0"
bech32 "1.1.4"
ws "7.4.6"
"@ethersproject/random@5.6.0", "@ethersproject/random@^5.5.0", "@ethersproject/random@^5.6.0":
"@ethersproject/random@5.6.0", "@ethersproject/random@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.6.0.tgz#1505d1ab6a250e0ee92f436850fa3314b2cb5ae6"
integrity sha512-si0PLcLjq+NG/XHSZz90asNf+YfKEqJGVdxoEkSukzbnBgC8rydbgbUgBbBGLeHN4kAJwUFEKsu3sCXT93YMsw==
@@ -448,15 +442,15 @@
"@ethersproject/bytes" "^5.6.0"
"@ethersproject/logger" "^5.6.0"
"@ethersproject/rlp@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0"
integrity sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA==
"@ethersproject/random@5.7.0", "@ethersproject/random@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/random/-/random-5.7.0.tgz#af19dcbc2484aae078bb03656ec05df66253280c"
integrity sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.5.0", "@ethersproject/rlp@^5.6.0":
"@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.6.0.tgz#55a7be01c6f5e64d6e6e7edb6061aa120962a717"
integrity sha512-dz9WR1xpcTL+9DtOT/aDO+YyxSSdO8YIS0jyZwHHSlAmnxA6cKU3TrTd4Xc/bHayctxTgGLYNuVVoiXE4tTq1g==
@@ -464,16 +458,15 @@
"@ethersproject/bytes" "^5.6.0"
"@ethersproject/logger" "^5.6.0"
"@ethersproject/sha2@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.5.0.tgz#a40a054c61f98fd9eee99af2c3cc6ff57ec24db7"
integrity sha512-B5UBoglbCiHamRVPLA110J+2uqsifpZaTmid2/7W5rbtYVz6gus6/hSDieIU/6gaKIDcOj12WnOdiymEUHIAOA==
"@ethersproject/rlp@5.7.0", "@ethersproject/rlp@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.7.0.tgz#de39e4d5918b9d74d46de93af80b7685a9c21304"
integrity sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
hash.js "1.1.7"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/sha2@5.6.0", "@ethersproject/sha2@^5.5.0", "@ethersproject/sha2@^5.6.0":
"@ethersproject/sha2@5.6.0", "@ethersproject/sha2@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.6.0.tgz#364c4c11cc753bda36f31f001628706ebadb64d9"
integrity sha512-1tNWCPFLu1n3JM9t4/kytz35DkuF9MxqkGGEHNauEbaARdm2fafnOyw1s0tIQDPKF/7bkP1u3dbrmjpn5CelyA==
@@ -482,19 +475,16 @@
"@ethersproject/logger" "^5.6.0"
hash.js "1.1.7"
"@ethersproject/signing-key@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.5.0.tgz#2aa37169ce7e01e3e80f2c14325f624c29cedbe0"
integrity sha512-5VmseH7qjtNmDdZBswavhotYbWB0bOwKIlOTSlX14rKn5c11QmJwGt4GHeo7NrL/Ycl7uo9AHvEqs5xZgFBTng==
"@ethersproject/sha2@5.7.0", "@ethersproject/sha2@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/sha2/-/sha2-5.7.0.tgz#9a5f7a7824ef784f7f7680984e593a800480c9fb"
integrity sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
bn.js "^4.11.9"
elliptic "6.5.4"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
hash.js "1.1.7"
"@ethersproject/signing-key@5.6.0", "@ethersproject/signing-key@^5.5.0", "@ethersproject/signing-key@^5.6.0":
"@ethersproject/signing-key@5.6.0", "@ethersproject/signing-key@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.6.0.tgz#4f02e3fb09e22b71e2e1d6dc4bcb5dafa69ce042"
integrity sha512-S+njkhowmLeUu/r7ir8n78OUKx63kBdMCPssePS89So1TH4hZqnWFsThEd/GiXYp9qMxVrydf7KdM9MTGPFukA==
@@ -506,17 +496,17 @@
elliptic "6.5.4"
hash.js "1.1.7"
"@ethersproject/solidity@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.5.0.tgz#2662eb3e5da471b85a20531e420054278362f93f"
integrity sha512-9NgZs9LhGMj6aCtHXhtmFQ4AN4sth5HuFXVvAQtzmm0jpSCNOTGtrHZJAeYTh7MBjRR8brylWZxBZR9zDStXbw==
"@ethersproject/signing-key@5.7.0", "@ethersproject/signing-key@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/signing-key/-/signing-key-5.7.0.tgz#06b2df39411b00bc57c7c09b01d1e41cf1b16ab3"
integrity sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==
dependencies:
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/keccak256" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/sha2" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
bn.js "^5.2.1"
elliptic "6.5.4"
hash.js "1.1.7"
"@ethersproject/solidity@5.6.0":
version "5.6.0"
@@ -530,16 +520,19 @@
"@ethersproject/sha2" "^5.6.0"
"@ethersproject/strings" "^5.6.0"
"@ethersproject/strings@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.5.0.tgz#e6784d00ec6c57710755699003bc747e98c5d549"
integrity sha512-9fy3TtF5LrX/wTrBaT8FGE6TDJyVjOvXynXJz5MT5azq+E6D92zuKNx7i29sWW2FjVOaWjAsiZ1ZWznuduTIIQ==
"@ethersproject/solidity@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/solidity/-/solidity-5.7.0.tgz#5e9c911d8a2acce2a5ebb48a5e2e0af20b631cb8"
integrity sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/constants" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/sha2" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@ethersproject/strings@5.6.0", "@ethersproject/strings@^5.5.0", "@ethersproject/strings@^5.6.0":
"@ethersproject/strings@5.6.0", "@ethersproject/strings@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.6.0.tgz#9891b26709153d996bf1303d39a7f4bc047878fd"
integrity sha512-uv10vTtLTZqrJuqBZR862ZQjTIa724wGPWQqZrofaPI/kUsf53TBG0I0D+hQ1qyNtllbNzaW+PDPHHUI6/65Mg==
@@ -548,22 +541,16 @@
"@ethersproject/constants" "^5.6.0"
"@ethersproject/logger" "^5.6.0"
"@ethersproject/transactions@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.5.0.tgz#7e9bf72e97bcdf69db34fe0d59e2f4203c7a2908"
integrity sha512-9RZYSKX26KfzEd/1eqvv8pLauCKzDTub0Ko4LfIgaERvRuwyaNV78mJs7cpIgZaDl6RJui4o49lHwwCM0526zA==
"@ethersproject/strings@5.7.0", "@ethersproject/strings@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/strings/-/strings-5.7.0.tgz#54c9d2a7c57ae8f1205c88a9d3a56471e14d5ed2"
integrity sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==
dependencies:
"@ethersproject/address" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/constants" "^5.5.0"
"@ethersproject/keccak256" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/rlp" "^5.5.0"
"@ethersproject/signing-key" "^5.5.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/transactions@5.6.0", "@ethersproject/transactions@^5.5.0", "@ethersproject/transactions@^5.6.0":
"@ethersproject/transactions@5.6.0", "@ethersproject/transactions@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.6.0.tgz#4b594d73a868ef6e1529a2f8f94a785e6791ae4e"
integrity sha512-4HX+VOhNjXHZyGzER6E/LVI2i6lf9ejYeWD6l4g50AdmimyuStKc39kvKf1bXWQMg7QNVh+uC7dYwtaZ02IXeg==
@@ -578,14 +565,20 @@
"@ethersproject/rlp" "^5.6.0"
"@ethersproject/signing-key" "^5.6.0"
"@ethersproject/units@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.5.0.tgz#104d02db5b5dc42cc672cc4587bafb87a95ee45e"
integrity sha512-7+DpjiZk4v6wrikj+TCyWWa9dXLNU73tSTa7n0TSJDxkYbV3Yf1eRh9ToMLlZtuctNYu9RDNNy2USq3AdqSbag==
"@ethersproject/transactions@5.7.0", "@ethersproject/transactions@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/transactions/-/transactions-5.7.0.tgz#91318fc24063e057885a6af13fdb703e1f993d3b"
integrity sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==
dependencies:
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/constants" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/rlp" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/units@5.6.0":
version "5.6.0"
@@ -596,26 +589,14 @@
"@ethersproject/constants" "^5.6.0"
"@ethersproject/logger" "^5.6.0"
"@ethersproject/wallet@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.5.0.tgz#322a10527a440ece593980dca6182f17d54eae75"
integrity sha512-Mlu13hIctSYaZmUOo7r2PhNSd8eaMPVXe1wxrz4w4FCE4tDYBywDH+bAR1Xz2ADyXGwqYMwstzTrtUVIsKDO0Q==
"@ethersproject/units@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/units/-/units-5.7.0.tgz#637b563d7e14f42deeee39245275d477aae1d8b1"
integrity sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==
dependencies:
"@ethersproject/abstract-provider" "^5.5.0"
"@ethersproject/abstract-signer" "^5.5.0"
"@ethersproject/address" "^5.5.0"
"@ethersproject/bignumber" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/hash" "^5.5.0"
"@ethersproject/hdnode" "^5.5.0"
"@ethersproject/json-wallets" "^5.5.0"
"@ethersproject/keccak256" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/random" "^5.5.0"
"@ethersproject/signing-key" "^5.5.0"
"@ethersproject/transactions" "^5.5.0"
"@ethersproject/wordlists" "^5.5.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/constants" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/wallet@5.6.0":
version "5.6.0"
@@ -638,18 +619,28 @@
"@ethersproject/transactions" "^5.6.0"
"@ethersproject/wordlists" "^5.6.0"
"@ethersproject/web@5.5.1":
version "5.5.1"
resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.5.1.tgz#cfcc4a074a6936c657878ac58917a61341681316"
integrity sha512-olvLvc1CB12sREc1ROPSHTdFCdvMh0J5GSJYiQg2D0hdD4QmJDy8QYDb1CvoqD/bF1c++aeKv2sR5uduuG9dQg==
"@ethersproject/wallet@5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/wallet/-/wallet-5.7.0.tgz#4e5d0790d96fe21d61d38fb40324e6c7ef350b2d"
integrity sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==
dependencies:
"@ethersproject/base64" "^5.5.0"
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/abstract-provider" "^5.7.0"
"@ethersproject/abstract-signer" "^5.7.0"
"@ethersproject/address" "^5.7.0"
"@ethersproject/bignumber" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/hash" "^5.7.0"
"@ethersproject/hdnode" "^5.7.0"
"@ethersproject/json-wallets" "^5.7.0"
"@ethersproject/keccak256" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/random" "^5.7.0"
"@ethersproject/signing-key" "^5.7.0"
"@ethersproject/transactions" "^5.7.0"
"@ethersproject/wordlists" "^5.7.0"
"@ethersproject/web@5.6.0", "@ethersproject/web@^5.5.0", "@ethersproject/web@^5.6.0":
"@ethersproject/web@5.6.0", "@ethersproject/web@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.6.0.tgz#4bf8b3cbc17055027e1a5dd3c357e37474eaaeb8"
integrity sha512-G/XHj0hV1FxI2teHRfCGvfBUHFmU+YOSbCxlAMqJklxSa7QMiHFQfAxvwY2PFqgvdkxEKwRNr/eCjfAPEm2Ctg==
@@ -660,18 +651,18 @@
"@ethersproject/properties" "^5.6.0"
"@ethersproject/strings" "^5.6.0"
"@ethersproject/wordlists@5.5.0":
version "5.5.0"
resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.5.0.tgz#aac74963aa43e643638e5172353d931b347d584f"
integrity sha512-bL0UTReWDiaQJJYOC9sh/XcRu/9i2jMrzf8VLRmPKx58ckSlOJiohODkECCO50dtLZHcGU6MLXQ4OOrgBwP77Q==
"@ethersproject/web@5.7.1", "@ethersproject/web@^5.7.0":
version "5.7.1"
resolved "https://registry.yarnpkg.com/@ethersproject/web/-/web-5.7.1.tgz#de1f285b373149bee5928f4eb7bcb87ee5fbb4ae"
integrity sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==
dependencies:
"@ethersproject/bytes" "^5.5.0"
"@ethersproject/hash" "^5.5.0"
"@ethersproject/logger" "^5.5.0"
"@ethersproject/properties" "^5.5.0"
"@ethersproject/strings" "^5.5.0"
"@ethersproject/base64" "^5.7.0"
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@ethersproject/wordlists@5.6.0", "@ethersproject/wordlists@^5.5.0", "@ethersproject/wordlists@^5.6.0":
"@ethersproject/wordlists@5.6.0", "@ethersproject/wordlists@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.6.0.tgz#79e62c5276e091d8575f6930ba01a29218ded032"
integrity sha512-q0bxNBfIX3fUuAo9OmjlEYxP40IB8ABgb7HjEZCL5IKubzV3j30CWi2rqQbjTS2HfoyQbfINoKcTVWP4ejwR7Q==
@@ -682,6 +673,17 @@
"@ethersproject/properties" "^5.6.0"
"@ethersproject/strings" "^5.6.0"
"@ethersproject/wordlists@5.7.0", "@ethersproject/wordlists@^5.7.0":
version "5.7.0"
resolved "https://registry.yarnpkg.com/@ethersproject/wordlists/-/wordlists-5.7.0.tgz#8fb2c07185d68c3e09eb3bfd6e779ba2774627f5"
integrity sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==
dependencies:
"@ethersproject/bytes" "^5.7.0"
"@ethersproject/hash" "^5.7.0"
"@ethersproject/logger" "^5.7.0"
"@ethersproject/properties" "^5.7.0"
"@ethersproject/strings" "^5.7.0"
"@koa/cors@^3.3.0":
version "3.3.0"
resolved "https://registry.yarnpkg.com/@koa/cors/-/cors-3.3.0.tgz#b4c1c7ee303b7c968c8727f2a638a74675b50bb2"
@@ -885,19 +887,25 @@ bech32@1.1.4:
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
bls-wallet-clients@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.6.0.tgz#9d9b1add69420bbaf807c1442151e487f4ee87a5"
integrity sha512-6EivjMe2uRGIt6Aq5IampqlmsECavLqHGPm6Ki2l3+c+FnwfOQUzNelctVN/vRVxDbDpTX4iAfTIrYYpr1S/vw==
bls-wallet-clients@0.8.2-1fb4a55:
version "0.8.2-1fb4a55"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.2-1fb4a55.tgz#bab40801ee1e60ffbc9c0bc924943c6f90605e7c"
integrity sha512-2tlwOSUGzsOiam0G7GBmsN3W5cHjUwTmHR/DvGRH584zLkCC/8TFdAn2/laSF2baTYvegtIHwQNl1zX5DWivEQ==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "5.5.4"
ethers "^5.7.2"
node-fetch "2.6.7"
bn.js@^4.11.9:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==
bn.js@^5.2.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.1.tgz#0bc527a6a0d18d0aa8d5b0538ce4a77dccfa7b70"
integrity sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==
brorand@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f"
@@ -1036,42 +1044,6 @@ escape-html@^1.0.3:
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
ethers@5.5.4:
version "5.5.4"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.5.4.tgz#e1155b73376a2f5da448e4a33351b57a885f4352"
integrity sha512-N9IAXsF8iKhgHIC6pquzRgPBJEzc9auw3JoRkaKe+y4Wl/LFBtDDunNe7YmdomontECAcC5APaAgWZBiu1kirw==
dependencies:
"@ethersproject/abi" "5.5.0"
"@ethersproject/abstract-provider" "5.5.1"
"@ethersproject/abstract-signer" "5.5.0"
"@ethersproject/address" "5.5.0"
"@ethersproject/base64" "5.5.0"
"@ethersproject/basex" "5.5.0"
"@ethersproject/bignumber" "5.5.0"
"@ethersproject/bytes" "5.5.0"
"@ethersproject/constants" "5.5.0"
"@ethersproject/contracts" "5.5.0"
"@ethersproject/hash" "5.5.0"
"@ethersproject/hdnode" "5.5.0"
"@ethersproject/json-wallets" "5.5.0"
"@ethersproject/keccak256" "5.5.0"
"@ethersproject/logger" "5.5.0"
"@ethersproject/networks" "5.5.2"
"@ethersproject/pbkdf2" "5.5.0"
"@ethersproject/properties" "5.5.0"
"@ethersproject/providers" "5.5.3"
"@ethersproject/random" "5.5.1"
"@ethersproject/rlp" "5.5.0"
"@ethersproject/sha2" "5.5.0"
"@ethersproject/signing-key" "5.5.0"
"@ethersproject/solidity" "5.5.0"
"@ethersproject/strings" "5.5.0"
"@ethersproject/transactions" "5.5.0"
"@ethersproject/units" "5.5.0"
"@ethersproject/wallet" "5.5.0"
"@ethersproject/web" "5.5.1"
"@ethersproject/wordlists" "5.5.0"
ethers@^5.5.3:
version "5.6.4"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.6.4.tgz#23629e9a7d4bc5802dfb53d4da420d738744b53c"
@@ -1108,6 +1080,42 @@ ethers@^5.5.3:
"@ethersproject/web" "5.6.0"
"@ethersproject/wordlists" "5.6.0"
ethers@^5.7.2:
version "5.7.2"
resolved "https://registry.yarnpkg.com/ethers/-/ethers-5.7.2.tgz#3a7deeabbb8c030d4126b24f84e525466145872e"
integrity sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==
dependencies:
"@ethersproject/abi" "5.7.0"
"@ethersproject/abstract-provider" "5.7.0"
"@ethersproject/abstract-signer" "5.7.0"
"@ethersproject/address" "5.7.0"
"@ethersproject/base64" "5.7.0"
"@ethersproject/basex" "5.7.0"
"@ethersproject/bignumber" "5.7.0"
"@ethersproject/bytes" "5.7.0"
"@ethersproject/constants" "5.7.0"
"@ethersproject/contracts" "5.7.0"
"@ethersproject/hash" "5.7.0"
"@ethersproject/hdnode" "5.7.0"
"@ethersproject/json-wallets" "5.7.0"
"@ethersproject/keccak256" "5.7.0"
"@ethersproject/logger" "5.7.0"
"@ethersproject/networks" "5.7.1"
"@ethersproject/pbkdf2" "5.7.0"
"@ethersproject/properties" "5.7.0"
"@ethersproject/providers" "5.7.2"
"@ethersproject/random" "5.7.0"
"@ethersproject/rlp" "5.7.0"
"@ethersproject/sha2" "5.7.0"
"@ethersproject/signing-key" "5.7.0"
"@ethersproject/solidity" "5.7.0"
"@ethersproject/strings" "5.7.0"
"@ethersproject/transactions" "5.7.0"
"@ethersproject/units" "5.7.0"
"@ethersproject/wallet" "5.7.0"
"@ethersproject/web" "5.7.1"
"@ethersproject/wordlists" "5.7.0"
form-data@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/form-data/-/form-data-3.0.1.tgz#ebd53791b78356a99af9a300d4282c4d5eb9755f"
@@ -1352,7 +1360,7 @@ negotiator@0.6.3:
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
node-fetch@2:
node-fetch@2, node-fetch@2.6.7:
version "2.6.7"
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad"
integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==

View File

@@ -1,32 +1,42 @@
RPC_URL=http://localhost:8545
RPC_URL=https://goerli-rollup.arbitrum.io/rpc
RPC_POLLING_INTERVAL=4000
USE_TEST_NET=false
ORIGIN=http://localhost:3000
PORT=3000
NETWORK_CONFIG_PATH=../contracts/networks/local.json
PRIVATE_KEY_AGG=0x0000000000000000000000000000000000000000000000000000000000000a99
PRIVATE_KEY_ADMIN=
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-goerli.json
PRIVATE_KEY_AGG=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
PG_HOST=localhost
PG_PORT=5432
PG_USER=bls
PG_PASSWORD=generate-a-strong-password
PG_DB_NAME=bls_aggregator
DB_PATH=aggregator.sqlite
BUNDLE_TABLE_NAME=bundles
BUNDLE_QUERY_LIMIT=100
MAX_ELIGIBILITY_DELAY=300
MAX_AGGREGATION_SIZE=12
MAX_GAS_PER_BUNDLE=2000000
MAX_AGGREGATION_DELAY_MILLIS=5000
MAX_UNCONFIRMED_AGGREGATIONS=3
LOG_QUERIES=false
TEST_LOGGING=false
REQUIRE_FEES=true
BREAKEVEN_OPERATION_COUNT=4.5
ALLOW_LOSSES=true
FEE_TYPE=ether
FEE_PER_GAS=0
FEE_PER_BYTE=0
# Set this to false in production to avoid an unexpected transaction on startup.
# Use ./programs/createInternalBlsWallet.ts beforehand instead.
AUTO_CREATE_INTERNAL_BLS_WALLET=true
# Arbitrum doesn't seem to use/need priority fees
PRIORITY_FEE_PER_GAS=0
# Arbitrum doesn't change its base fee much, in fact it's usually locked at
# 0.1gwei. They use changes in gasLimit to account for L1 base fee changes.
PREVIOUS_BASE_FEE_PERCENT_INCREASE=2
BUNDLE_CHECKING_CONCURRENCY=8

View File

@@ -0,0 +1,40 @@
RPC_URL=http://localhost:8545
RPC_POLLING_INTERVAL=500
USE_TEST_NET=false
ORIGIN=http://localhost:3000
PORT=3000
NETWORK_CONFIG_PATH=../contracts/networks/local.json
PRIVATE_KEY_AGG=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
DB_PATH=aggregator.sqlite
BUNDLE_QUERY_LIMIT=100
MAX_ELIGIBILITY_DELAY=300
MAX_GAS_PER_BUNDLE=2000000
MAX_AGGREGATION_DELAY_MILLIS=5000
MAX_UNCONFIRMED_AGGREGATIONS=3
LOG_QUERIES=false
TEST_LOGGING=false
REQUIRE_FEES=true
BREAKEVEN_OPERATION_COUNT=2.5
ALLOW_LOSSES=true
FEE_TYPE=ether
# Set this to false in production to avoid an unexpected transaction on startup.
# Use ./programs/createInternalBlsWallet.ts beforehand instead.
AUTO_CREATE_INTERNAL_BLS_WALLET=true
# 0.5 gwei
PRIORITY_FEE_PER_GAS=500000000
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
BUNDLE_CHECKING_CONCURRENCY=8

37
aggregator/.env.test Normal file
View File

@@ -0,0 +1,37 @@
RPC_URL=http://localhost:8545
RPC_POLLING_INTERVAL=500
USE_TEST_NET=false
ORIGIN=http://localhost:3000
PORT=3000
NETWORK_CONFIG_PATH=../contracts/networks/local.json
PRIVATE_KEY_AGG=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
DB_PATH=aggregator.sqlite
BUNDLE_QUERY_LIMIT=100
MAX_ELIGIBILITY_DELAY=300
MAX_GAS_PER_BUNDLE=2000000
MAX_AGGREGATION_DELAY_MILLIS=5000
MAX_UNCONFIRMED_AGGREGATIONS=3
LOG_QUERIES=true
TEST_LOGGING=true
REQUIRE_FEES=true
BREAKEVEN_OPERATION_COUNT=2.5
ALLOW_LOSSES=true
FEE_TYPE=ether
AUTO_CREATE_INTERNAL_BLS_WALLET=true
PRIORITY_FEE_PER_GAS=500000000
PREVIOUS_BASE_FEE_PERCENT_INCREASE=13
BUNDLE_CHECKING_CONCURRENCY=8

View File

@@ -1,4 +1,6 @@
.env*
!.env.example
!.env*.example
!.env*.test
cov_profile*
/build
/aggregator.sqlite

View File

@@ -13,7 +13,6 @@
"runtimeExecutable": "deno",
"runtimeArgs": [
"run",
"--unstable",
"--inspect",
"--allow-all"
],

View File

@@ -1,14 +1,15 @@
FROM denoland/deno:1.20.6
FROM denoland/deno:1.30.1
ADD build /app
WORKDIR /app
RUN deno cache --unstable ts/programs/aggregator.ts
RUN deno cache ts/programs/aggregator.ts
ENV IS_DOCKER="true"
CMD [ \
"deno", \
"run", \
"--unstable", \
"-A", \
"ts/programs/aggregator.ts" \
]

View File

@@ -6,9 +6,66 @@ Accepts transaction bundles (including bundles that contain a single
transaction) and submits aggregations of these bundles to the configured
Verification Gateway.
## Docker Usage
Docker images of the aggregator are
[available on DockerHub](https://hub.docker.com/r/blswallet/aggregator).
If you're targeting a network that
[already has a deployment of the BLSWallet contracts](../contracts/networks),
you can use these images standalone (without this repository) as follows:
```sh
mkdir aggregator
cd aggregator
curl https://raw.githubusercontent.com/web3well/bls-wallet/main/aggregator/.env.example >.env
# Replace CHOSEN_NETWORK below
curl https://raw.githubusercontent.com/web3well/bls-wallet/main/contracts/networks/CHOSEN_NETWORK.json >networkConfig.json
```
In `.env`:
- Change `RPC_URL`
- (If using `localhost`, you probably want `host.docker.internal`)
- Change `PRIVATE_KEY_AGG`
- Ignore `NETWORK_CONFIG_PATH` (it's not used inside docker)
- See [Configuration](#configuration) for more detail and other options
If you're running in production, you might want to set
`AUTO_CREATE_INTERNAL_BLS_WALLET` to `false`. The internal BLS wallet is needed
for user fee estimation. Creating it is a one-time setup that will use
`PRIVATE_KEY_AGG` to pay for gas. You can create it explicitly like this:
```sh
docker run \
--rm \
-it \
--mount type=bind,source="$PWD/.env",target=/app/.env \
--mount type=bind,source="$PWD/networkConfig.json",target=/app/networkConfig.json \
blswallet/aggregator \
./ts/programs/createInternalBlsWallet.ts
```
Finally, start the aggregator:
```sh
docker run \
--name choose-container-name \ # Optional
-d \ # Optional
-p3000:3000 \ # If you chose a different PORT in .env, change it here too
--restart=unless-stopped \ # Optional
--mount type=bind,source="$PWD/.env",target=/app/.env \
--mount type=bind,source="$PWD/networkConfig.json",target=/app/networkConfig.json \
blswallet/aggregator # Tags of the form :git-$VERSION are also available
```
(You may need to remove the comments before pasting into your terminal.)
## Installation
Install [Deno](deno.land).
Install [Deno](deno.land)
### Configuration
@@ -23,74 +80,41 @@ you might have:
```
.env.local
.env.optimistic-kovan
.env.arbitrum-goerli
.env.optimism-goerli
```
If you don't have a `.env`, you will need to append `--env <name>` to all
commands.
### PostgreSQL
#### Environment Variables
#### With docker-compose
```sh
cd .. # root of repo
docker-compose up -d postgres
```
#### Local Install
Install, e.g.:
```sh
sudo apt update
sudo apt install postgresql postgresql-contrib
```
Create a user called `bls`:
```
$ sudo -u postgres createuser --interactive
Enter name of role to add: bls
Shall the new role be a superuser? (y/n) n
Shall the new role be allowed to create databases? (y/n) n
Shall the new role be allowed to create more new roles? (y/n) n
```
Set the user's password:
```
$ sudo -u postgres psql
psql (12.6 (Ubuntu 12.6-0ubuntu0.20.04.1))
Type "help" for help.
postgres=# ALTER USER bls WITH PASSWORD 'generate-a-strong-password';
```
Create a table called `bls_aggregator`:
```sh
sudo -u postgres createdb bls_aggregator
```
On Ubuntu (and probably elsewhere), postgres is configured to offer SSL
connections but with an invalid certificate. However, the deno driver for
postgres doesn't support this.
There are two options here:
1. Set up SSL with a valid certificate
([guide](https://www.postgresql.org/docs/current/ssl-tcp.html)).
2. Turn off SSL in postgres (only for development or if you can ensure the
connection isn't vulnerable to attack).
1. View the config location with
`sudo -u postgres psql -c 'SHOW config_file'`.
2. Turn off ssl in that config.
```diff
-ssl = on
+ssl = off
```
3. Restart postgres `sudo systemctl restart postgresql`.
| Name | Example Value | Description |
| ---------------------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
| RPC_POLLING_INTERVAL | 4000 | How long to wait between retries, when needed (used by ethers when waiting for blocks) |
| USE_TEST_NET | false | Whether to set all transaction's `gasPrice` to 0. Workaround for some networks |
| ORIGIN | http://localhost:3000 | The origin for the aggregator client. Used only in manual tests |
| PORT | 3000 | The port to bind the aggregator to |
| NETWORK_CONFIG_PATH | ../contracts/networks/local.json | Path to the network config file, which contains information on deployed BLS Wallet contracts |
| PRIVATE_KEY_AGG | 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 | Private key for the EOA account used to submit bundles on chain. Transactions are paid by the account linked to PRIVATE_KEY_AGG. By default, bundles must pay for themselves by sending funds to tx.origin or the aggregators onchain address |
| PRIVATE_KEY_ADMIN | 0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d | Private key for the admin EOA account. Used only in tests |
| TEST_BLS_WALLETS_SECRET | test-bls-wallets-secret | Secret used to seed BLS Wallet private keys during tests |
| DB_PATH | aggregator.sqlite | File path of the sqlite db |
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from sqlite |
| MAX_GAS_PER_BUNDLE | 2000000 | Limits the amount of user operations which can be bundled together by using this value as the approximate limit on the amount of gas in an aggregate bundle |
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain. A higher number will allow more time for bundles to fill, but may result in longer periods before submission. A lower number allows more frequent L2 submissions, but may result in smaller bundles |
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain |
| LOG_QUERIES | false | Whether to print sqlite queries in event log. When running tests, `TEST_LOGGING` must also be enabled |
| TEST_LOGGING | false | Whether to print aggregator server events to stdout during tests. Useful for debugging & logging |
| REQUIRE_FEES | true | Whether to require that user bundles pay the aggregator a sufficient fee |
| BREAKEVEN_OPERATION_COUNT | 4.5 | The aggregator must pay an overhead to submit a bundle regardless of how many operations it contains. This parameter determines how much each operation must contribute to this overhead |
| ALLOW_LOSSES | true | Even if each user bundle pays the required fee, the aggregate bundle may not be profitable if it is too small. Setting this to true makes the aggregator submit these bundles anyway |
| FEE_TYPE | ether OR token:0xabcd...1234 | The fee type the aggregator will accept. Either `ether` for ETH/chains native currency or `token:0xabcd...1234` (token contract address) for an ERC20 token |
| AUTO_CREATE_INTERNAL_BLS_WALLET | false | An internal BLS wallet is used to calculate bundle overheads. Setting this to true allows creating this wallet on startup, but might be undesirable in production (see `programs/createInternalBlsWallet.ts` for manual creation) |
| PRIORITY_FEE_PER_GAS | 0 | The priority fee used when submitting bundles (and passed on as a requirement for user bundles) |
| PREVIOUS_BASE_FEE_PERCENT_INCREASE | 2 | Used to determine the max basefee attached to aggregator transaction (and passed on as a requirement for user bundles)s |
| BUNDLE_CHECKING_CONCURRENCY | 8 | The maximum number of bundles that are checked concurrently (getting gas usage, detecting fees, etc) |
## Running
@@ -102,6 +126,20 @@ Can be run locally or hosted.
# ./programs/aggregator.ts --env <name>
```
**Note**: It's also possible to run the aggregator directly from github:
```sh
deno run \
--allow-net \
--allow-env \
--allow-read=. \
--allow-write=. \
https://raw.githubusercontent.com/web3well/bls-wallet/main/aggregator/programs/aggregator.ts
```
(This can be done without a clone of the repository, but you'll still need to
set up `.env` and your network config.)
## Testing
- launch optimism
@@ -110,6 +148,85 @@ Can be run locally or hosted.
NB each test must use unique address(es). (+ init code)
## Fees
### User Guide
User bundles must pay fees to compensate the aggregator (except in testing
situations where the aggregator may be configured to accept bundles which don't
pay fees (see `REQUIRE_FEES`)). The aggregator simply detects fees have been
paid by observing the effect of a user bundle on its balance. This allows
bundles to pay the aggregator using any mechanism of their choosing, and is why
bundles do not have fields for paying fees explicitly.
The simplest way to do this is to include an extra action to pay `tx.origin`.
Use the `POST /estimateFee` API to determine the fee required for a bundle. The
body of this request is the bundle. Response:
```json
{
"feeType": "(See FEE_TYPE enviroment variable)",
"feeDetected": "(The fee that has been detected for the provided bundle)",
"feeRequired": "(Required fee)",
"successes": [
/* Array of bools indicating success of each action */
]
}
```
Note that if you want to pay the aggregator using an additional action, you
should include this additional action with a payment of zero when estimating,
otherwise the additional action will increase the fee that needs to be paid. You
can also use the [aggregator-proxy](../aggregator-proxy/) package as a proxy in
place of an aggregator. This is useful to run more advanced logic such as
inspecting bundles and potentially paying for them, before the proxy aggregator
then sends the bundles to an underlying aggregator.
Also, `feeRequired` is the absolute minimum necessary fee to process the bundle
at the time of estimation, so paying extra is advisable to increase the chance
that the fee is sufficient during submission.
In the case of a malicious aggregator, or if the chosen aggregator service goes
down, an end user can always execute actions themselves, by submitting a bundle
on chain via `VerificationGatewaty.processBundle`.
### Technical Detail
The fees required by the aggregator are designed to prevent it from losing
money. There are two main ways that losses can still happen:
1. Bundles that don't simulate accurately
2. Bundles that make losses are allowed in config (`ALLOW_LOSSES`)
When calculating the required fee, the aggregator needs to account for two
things:
1. The marginal cost of including the user bundle
2. A contribution to the overhead of submitting the aggregate bundle
Remember that the whole point of aggregation is to save on fees using a single
aggregate signature. This means that measuring the fee required to process the
user bundle in isolation won't reflect that saving.
Instead, we measure the overhead using hypothetical operations that contain zero
actions. We make a bundle with one of these, and another with two of these, and
extrapolate backwards to a bundle containing zero operations (see
`measureBundleOverheadGas`).
We can then subtract that overhead from the user's bundle to obtain its marginal
cost.
The user's share of the overhead is then added by multiplying it by
`operationCount / BREAKEVEN_OPERATION_COUNT`. User bundles usually have an
`operationCount` of 1, so if `BREAKEVEN_OPERATION_COUNT` is 4.5, then the bundle
will be required to pay 22% of the overhead.
From the aggregator's perspective, aggregate bundles with fewer operations than
`BREAKEVEN_OPERATION_COUNT` should make a loss, and larger bundles should make a
profit. If `ALLOW_LOSSES` is `false`, bundles which are predicted to make a loss
will not be submitted.
## Development
### Environment
@@ -133,7 +250,7 @@ Tests are defined in `test`. Running them directly is a bit verbose because of
the deno flags you need:
```sh
deno test --allow-net --allow-env --allow-read --unstable
deno test --allow-net --allow-env --allow-read
```
Instead, `./programs/premerge.ts` may be more useful for you. It'll make sure
@@ -165,9 +282,22 @@ TS2300 [ERROR]: Duplicate identifier 'TypedArray'.
You need to reload modules (`-r`):
```sh
deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregator.ts
deno run -r --allow-net --allow-env --allow-read ./programs/aggregator.ts
```
#### Transaction reverted: function call to a non-contract account
- Is `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s
`COST_ESTIMATOR_ADDRESS` set to the right precompile cost estimator's contract
address?
- Are the BLS Wallet contracts deployed on the correct network?
- Is `NETWORK_CONFIG_PATH` in `.env` set to the right config?
#### Deno version
Make sure your Deno version is
[up to date.](https://deno.land/manual/getting_started/installation#updating)
### Notable Components
- **src/chain**: Should contain all of the contract interactions, exposing more
@@ -184,10 +314,9 @@ deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregato
- **`BundleService`**: Keeps track of all stored transactions, as well as
accepting (or rejecting) them and submitting aggregated bundles to
`EthereumService`.
- **`BundleTable`**: Abstraction layer over postgres bundle tables, exposing
typed functions instead of queries. Handles conversions to and from the field
types supported by postgres so that other code can has a uniform js-friendly
interface
- **`BundleTable`**: Abstraction layer over sqlite bundle tables, exposing typed
functions instead of queries. Handles conversions to and from the field types
supported by sqlite so that other code can has a uniform js-friendly interface
([`TransactionData`](https://github.com/jzaki/bls-wallet-signer/blob/673e2ae/src/types.ts#L12)).
- **`Client`**: Provides an abstraction over the external HTTP interface so that
programs talking to the aggregator can do so via regular js functions with
@@ -202,16 +331,9 @@ deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregato
## Hosting Guide
1. Configure your server to allow TCP on ports 80 and 443
2. Follow the [Installation](#Installation) instructions
3. Install docker and nginx:
2. Install docker and nginx:
`sudo apt update && sudo apt install docker.io nginx`
4. Run `./programs/build.ts`
- If you're using a named environment, add `--env <name>`
- If `docker` requires `sudo`, add `--sudo-docker`
5. Configure log rotation in docker by setting `/etc/docker/daemon.json` to
3. Configure log rotation in docker by setting `/etc/docker/daemon.json` to
```json
{
@@ -225,19 +347,9 @@ deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregato
and restart docker `sudo systemctl restart docker`
6. Load the docker image: `sudo docker load <docker-image.tar.gz`
7. Run the aggregator:
```sh
sudo docker run \
--name aggregator \
-d \
--net=host \
--restart=unless-stopped \
aggregator:latest
```
8. Create `/etc/nginx/sites-available/aggregator`
4. Follow the [Docker Usage](#docker-usage) instructions (just use port 3000,
external requests are handled by nginx)
5. Create `/etc/nginx/sites-available/aggregator`
```nginx
server {
@@ -260,7 +372,7 @@ This allows you to add some static content at `/home/aggregator/static-content`.
Adding static content is optional; requests that don't match static content will
be passed to the aggregator.
9. Create a symlink in sites-enabled
6. Create a symlink in sites-enabled
```sh
ln -s /etc/nginx/sites-available/aggregator /etc/nginx/sites-enabled/aggregator
@@ -268,5 +380,5 @@ ln -s /etc/nginx/sites-available/aggregator /etc/nginx/sites-enabled/aggregator
Reload nginx for config to take effect: `sudo nginx -s reload`
10. Set up https for your domain by following the instructions at
https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.
7. Set up https for your domain by following the instructions at
https://certbot.eff.org/lets-encrypt/ubuntufocal-nginx.

View File

@@ -27,16 +27,19 @@ export {
Contract,
ethers,
Wallet,
} from "https://esm.sh/ethers@5.5.4";
} from "https://esm.sh/ethers@5.7.2";
import { ethers } from "https://esm.sh/ethers@5.5.4";
import { ethers } from "https://esm.sh/ethers@5.7.2";
export type {
BaseContract,
BigNumberish,
BytesLike,
} from "https://esm.sh/ethers@5.5.4";
} from "https://esm.sh/ethers@5.7.2";
export const keccak256 = ethers.utils.keccak256;
// Adding more accurate type information here (ethers uses Array<any>)
export const shuffled: <T>(array: T[]) => T[] = ethers.utils.shuffled;
export type {
AggregatorUtilities,
BlsWalletSigner,
@@ -46,39 +49,27 @@ export type {
MockERC20,
NetworkConfig,
Operation,
OperationResultError,
PublicKey,
Signature,
VerificationGateway,
} from "https://esm.sh/bls-wallet-clients@0.6.0";
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
export {
Aggregator as AggregatorClient,
AggregatorUtilities__factory,
BlsWalletWrapper,
decodeError,
ERC20__factory,
getConfig,
MockERC20__factory,
VerificationGateway__factory,
} from "https://esm.sh/bls-wallet-clients@0.6.0";
} from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
// Workaround for esbuild's export-star bug
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.6.0";
const {
bundleFromDto,
bundleToDto,
initBlsWalletSigner,
} = blsWalletClients;
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.2-1fb4a55";
const { bundleFromDto, bundleToDto, initBlsWalletSigner } = blsWalletClients;
export { bundleFromDto, bundleToDto, initBlsWalletSigner };
// Database dependencies
export {
Constraint,
CreateTableMode,
DataType,
OrderByType,
QueryClient,
QueryTable,
unsketchify,
} from "https://deno.land/x/postquery@v0.1.1/mod.ts";
export type { TableOptions } from "https://deno.land/x/postquery@v0.1.1/mod.ts";
export * as sqlite from "https://deno.land/x/sqlite@v3.7.0/mod.ts";
export { Semaphore } from "https://deno.land/x/semaphore@v1.1.2/mod.ts";

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { AggregatorClient } from "../deps.ts";
import * as env from "../src/env.ts";

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { AggregatorClient, BigNumber, Bundle } from "../deps.ts";
import * as env from "../src/env.ts";

View File

@@ -1,16 +1,15 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { ethers } from "../deps.ts";
import * as env from "../src/env.ts";
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
const [wallet] = await TestBlsWallets(
const wallet = await TestBlsWallet(
new ethers.providers.JsonRpcProvider(env.RPC_URL),
1,
);
console.log({
privateKey: wallet.privateKey,
privateKey: wallet.blsWalletSigner.privateKey,
address: wallet.walletContract.address,
});

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import {
AggregatorClient,

View File

@@ -0,0 +1,33 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { AggregatorClient, ethers } from "../deps.ts";
import AdminWallet from "../src/chain/AdminWallet.ts";
import * as env from "../test/env.ts";
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const client = new AggregatorClient(env.ORIGIN);
const wallet = await TestBlsWallet(provider);
const adminWallet = AdminWallet(provider);
await (await adminWallet.sendTransaction({
to: wallet.address,
value: 1,
})).wait();
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [{
ethValue: 1,
contractAddress: adminWallet.address,
encodedFunction: "0x",
}],
});
const feeEstimation = await client.estimateFee(bundle);
console.log({ feeEstimation });

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { ethers } from "../deps.ts";

View File

@@ -2,28 +2,23 @@ import { BlsWalletWrapper, ethers } from "../../deps.ts";
import * as env from "../../test/env.ts";
import AdminWallet from "../../src/chain/AdminWallet.ts";
import Range from "../../src/helpers/Range.ts";
import Rng from "../../src/helpers/Rng.ts";
import getNetworkConfig from "../../src/helpers/getNetworkConfig.ts";
export default async function TestBlsWallets(
export default async function TestBlsWallet(
provider: ethers.providers.Provider,
count: number,
index?: number,
) {
const { addresses } = await getNetworkConfig();
const parent = AdminWallet(provider);
const rng = Rng.root.seed(env.PRIVATE_KEY_ADMIN, env.TEST_BLS_WALLETS_SECRET);
const wallets = await Promise.all(
Range(count).map(async (i) => {
const secret = rng.seed(`${i}`).address();
return await BlsWalletWrapper.connect(
secret,
addresses.verificationGateway,
parent.provider,
);
}),
const secret = rng.seed(`${index}`).address();
return await BlsWalletWrapper.connect(
secret,
addresses.verificationGateway,
parent.provider,
);
return wallets;
}

View File

@@ -1,15 +1,23 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
import {
AggregatorClient,
AggregatorUtilities__factory,
BigNumber,
delay,
ethers,
MockERC20__factory,
} from "../deps.ts";
import AdminWallet from "../src/chain/AdminWallet.ts";
import assert from "../src/helpers/assert.ts";
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
import * as env from "../test/env.ts";
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
const [walletIndexStr = "0"] = Deno.args;
const walletIndex = Number(walletIndexStr);
const { addresses } = await getNetworkConfig();
@@ -17,26 +25,77 @@ const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
const client = new AggregatorClient(env.ORIGIN);
const [wallet] = await TestBlsWallets(provider, 1);
const wallet = await TestBlsWallet(provider, walletIndex);
const nonce = await wallet.Nonce();
const adminWallet = AdminWallet(provider);
console.log("Funding wallet");
await (await adminWallet.sendTransaction({
to: wallet.address,
value: 1,
})).wait();
const startBalance = await testErc20.balanceOf(wallet.address);
const mintAction: ActionData = {
ethValue: 0,
contractAddress: testErc20.address,
encodedFunction: testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, 1],
),
};
const sendEthToTxOrigin = AggregatorUtilities__factory
.createInterface()
.encodeFunctionData("sendEthToTxOrigin");
const feeEstimation = await client.estimateFee(wallet.sign({
nonce,
actions: [
mintAction,
{
ethValue: 1,
contractAddress: addresses.utilities,
encodedFunction: sendEthToTxOrigin,
},
],
}));
console.log({ feeEstimation });
assert(feeEstimation.feeType === "ether");
const feeRequired = BigNumber.from(feeEstimation.feeRequired);
// Add 10% safety margin
const fee = feeRequired.add(feeRequired.div(10));
const balance = await provider.getBalance(wallet.address);
// Ensure wallet can pay the fee
if (balance.lt(fee)) {
console.log("Funding wallet");
await (await adminWallet.sendTransaction({
to: wallet.address,
value: fee.sub(balance),
})).wait();
}
const feeAction: ActionData = {
ethValue: fee,
contractAddress: addresses.utilities,
encodedFunction: sendEthToTxOrigin,
};
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [{
ethValue: 0,
contractAddress: testErc20.address,
encodedFunction: testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, 1],
),
}],
actions: [mintAction, feeAction],
});
// console.log("Calling estimateFee");
// const feeEstimation = await client.estimateFee(bundle);
// console.log({ feeEstimation });
console.log("Sending mint bundle to aggregator");
const res = await client.add(bundle);
@@ -47,7 +106,7 @@ if ("failures" in res) {
console.log("Success response from aggregator", res.hash);
while (true) {
const balance = (await testErc20.balanceOf(wallet.address));
const balance = await testErc20.balanceOf(wallet.address);
console.log({
startBalance: startBalance.toString(),

View File

@@ -1,10 +1,10 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { delay, ethers, MockERC20__factory } from "../deps.ts";
import EthereumService from "../src/app/EthereumService.ts";
import * as env from "../test/env.ts";
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
const { addresses } = await getNetworkConfig();
@@ -20,7 +20,7 @@ const ethereumService = await EthereumService.create(
);
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
const [wallet] = await TestBlsWallets(provider, 1);
const wallet = await TestBlsWallet(provider);
const startBalance = await testErc20.balanceOf(wallet.address);
const bundle = wallet.sign({
@@ -47,7 +47,7 @@ console.log("Sending via ethereumService");
})();
while (true) {
const balance = (await testErc20.balanceOf(wallet.address));
const balance = await testErc20.balanceOf(wallet.address);
console.log({
startBalance: startBalance.toString(),

View File

@@ -0,0 +1,140 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { ActionData } from "https://esm.sh/v99/bls-wallet-clients@0.8.0-efa2e06/dist/src/index.d.ts";
import {
AggregatorClient,
AggregatorUtilities__factory,
BigNumber,
Bundle,
delay,
ethers,
MockERC20__factory,
} from "../deps.ts";
import AdminWallet from "../src/chain/AdminWallet.ts";
import assert from "../src/helpers/assert.ts";
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
import Range from "../src/helpers/Range.ts";
import * as env from "../test/env.ts";
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
const [walletNStr] = Deno.args;
const walletN = Number(walletNStr);
if (!Number.isFinite(walletN)) {
console.error("Usage: ./manualTests/mintNViaAggregator.ts <N>");
Deno.exit(1);
}
const { addresses } = await getNetworkConfig();
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
const client = new AggregatorClient(env.ORIGIN);
const sendEthToTxOrigin = AggregatorUtilities__factory
.createInterface()
.encodeFunctionData("sendEthToTxOrigin");
const adminWallet = AdminWallet(provider);
const wallets = await Promise.all(
Range(walletN).map((i) => TestBlsWallet(provider, i)),
);
const firstWallet = wallets[0];
const mintAction: ActionData = {
ethValue: 0,
contractAddress: testErc20.address,
encodedFunction: testErc20.interface.encodeFunctionData(
"mint",
[wallets[0].address, 1],
),
};
const startBalance = await testErc20.balanceOf(firstWallet.address);
const bundles: Bundle[] = [];
for (const [i, wallet] of wallets.entries()) {
const nonce = await wallet.Nonce();
console.log("Funding wallet", i);
await (await adminWallet.sendTransaction({
to: wallet.address,
value: 1,
})).wait();
const feeEstimation = await client.estimateFee(wallet.sign({
nonce,
actions: [
mintAction,
{
ethValue: 1,
contractAddress: addresses.utilities,
encodedFunction: sendEthToTxOrigin,
},
],
}));
assert(feeEstimation.feeType === "ether");
const feeRequired = BigNumber.from(feeEstimation.feeRequired);
// Add 10% safety margin
const fee = feeRequired.add(feeRequired.div(10));
const balance = await provider.getBalance(wallet.address);
// Ensure wallet can pay the fee
if (balance.lt(fee)) {
console.log("Funding wallet");
await (await adminWallet.sendTransaction({
to: wallet.address,
value: fee.sub(balance),
})).wait();
}
const feeAction: ActionData = {
ethValue: fee,
contractAddress: addresses.utilities,
encodedFunction: sendEthToTxOrigin,
};
bundles.push(wallet.sign({
nonce,
actions: [mintAction, feeAction],
}));
}
console.log("Sending mint bundles to aggregator");
await Promise.all(bundles.map(async (bundle) => {
const res = await client.add(bundle);
if ("failures" in res) {
throw new Error(res.failures.map((f) => f.description).join(", "));
}
console.log("Success response from aggregator", res.hash);
}));
while (true) {
const balance = await testErc20.balanceOf(firstWallet.address);
console.log({
startBalance: startBalance.toString(),
balance: balance.toString(),
});
if (balance.sub(startBalance).gte(walletN)) {
console.log("done");
break;
}
console.log("Mints not completed, waiting 500ms");
await delay(500);
}

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --unstable --allow-net --allow-env --allow-read
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import { ethers } from "../deps.ts";
import * as env from "../src/env.ts";

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import {
AggregatorClient,
@@ -11,8 +11,9 @@ import {
import * as env from "../test/env.ts";
import AdminWallet from "../src/chain/AdminWallet.ts";
import TestBlsWallets from "./helpers/TestBlsWallets.ts";
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
import Range from "../src/helpers/Range.ts";
const logStartTime = Date.now();
@@ -26,7 +27,14 @@ function log(...args: unknown[]) {
console.log(RelativeTimestamp(), ...args);
}
const leadTarget = env.MAX_AGGREGATION_SIZE * env.MAX_UNCONFIRMED_AGGREGATIONS;
// Note: This value is a guess and may require some experimentation for optimal
// throughput. The size of a full aggregation used to be hardcoded in config,
// but now that we use gas to limit the bundle size we don't know this value
// upfront anymore.
const fullAggregationSize = 100;
const leadTarget = fullAggregationSize * env.MAX_UNCONFIRMED_AGGREGATIONS;
const pollingInterval = 400;
const sendWalletCount = 50;
@@ -41,9 +49,8 @@ const client = new AggregatorClient(env.ORIGIN);
log("Connecting/creating test wallets...");
const [recvWallet, ...sendWallets] = await TestBlsWallets(
provider,
sendWalletCount + 1,
const [recvWallet, ...sendWallets] = await Promise.all(
Range(sendWalletCount + 1).map((i) => TestBlsWallet(provider, i)),
);
log("Checking/minting test tokens...");

View File

@@ -0,0 +1,58 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import { AggregatorClient, ethers, MockERC20__factory } from "../deps.ts";
// import EthereumService from "../src/app/EthereumService.ts";
import * as env from "../test/env.ts";
import TestBlsWallet from "./helpers/TestBlsWallet.ts";
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
const { addresses } = await getNetworkConfig();
const client = new AggregatorClient(env.ORIGIN);
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
// const ethereumService = await EthereumService.create(
// (evt) => {
// console.log(evt);
// },
// addresses.verificationGateway,
// addresses.utilities,
// env.PRIVATE_KEY_AGG,
// );
const testErc20 = MockERC20__factory.connect(addresses.testToken, provider);
const wallet = await TestBlsWallet(provider);
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [{
ethValue: 0,
contractAddress: testErc20.address,
encodedFunction: testErc20.interface.encodeFunctionData(
"transferFrom",
[
"0x0000000000000000000000000000000000000000",
wallet.address,
ethers.BigNumber.from(
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
),
],
),
}],
});
console.log("Sending via ethereumService or agg");
(async () => {
try {
// Test directly with ethereum service
// await ethereumService.submitBundle(bundle);
// test by submitting request to the agg
const res = await client.add(bundle);
console.log(res);
} catch (error) {
console.error(error.stack);
Deno.exit(1);
}
})();

View File

@@ -1,4 +1,4 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --unstable
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read --allow-write
import app from "../src/app/app.ts";
import AppEvent from "../src/app/AppEvent.ts";

View File

@@ -1,24 +1,42 @@
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env
import { dirname, parseArgs } from "../deps.ts";
import * as shell from "./helpers/shell.ts";
import repoDir from "../src/helpers/repoDir.ts";
import dotEnvPath, { envName } from "../src/helpers/dotEnvPath.ts";
import nil from "../src/helpers/nil.ts";
const args = parseArgs(Deno.args);
const parseArgsResult = parseArgs(Deno.args);
const args = {
/** Whether to push the image to dockerhub. */
push: parseArgsResult["push"],
/** Override the image name. Default: aggregator. */
imageName: parseArgsResult["image-name"],
/** Only build the image, ie - don't also serialize the image to disk. */
imageOnly: parseArgsResult["image-only"],
/** Prefix all docker commands with sudo. */
sudoDocker: parseArgsResult["sudo-docker"],
/** Tag the image with latest as well as the default git-${sha}. */
alsoTagLatest: parseArgsResult["also-tag-latest"],
};
Deno.chdir(repoDir);
const buildDir = `${repoDir}/build`;
await ensureFreshBuildDir();
await buildEnvironment();
await copyTypescriptFiles();
await buildDockerImage();
await tarballTypescriptFiles();
console.log("Aggregator build complete");
if (args.push) {
await pushDockerImage();
}
console.log("\nAggregator build complete");
async function allFiles() {
return [
@@ -32,13 +50,7 @@ async function allFiles() {
];
}
async function shortContentHash(filePath: string) {
const contentHash = (await shell.Line("shasum", "-a", "256", filePath));
return contentHash.slice(0, 7);
}
async function BuildName() {
async function Tag() {
const commitShort = (await shell.Line("git", "rev-parse", "HEAD")).slice(
0,
7,
@@ -47,15 +59,10 @@ async function BuildName() {
const isDirty =
(await shell.Lines("git", "status", "--porcelain")).length > 0;
const envHashShort = await shortContentHash(`${buildDir}/.env`);
return [
"git",
commitShort,
...(isDirty ? ["dirty"] : []),
"env",
envName,
envHashShort,
].join("-");
}
@@ -74,45 +81,6 @@ async function ensureFreshBuildDir() {
await Deno.mkdir(buildDir);
}
async function buildEnvironment() {
const repoDotEnv = await Deno.readTextFile(dotEnvPath);
let networkConfigPaths: { repo: string; build: string } | nil = nil;
const buildDotEnvLines: string[] = [];
for (const line of repoDotEnv.split("\n")) {
let buildLine = line;
if (line.startsWith("NETWORK_CONFIG_PATH=")) {
const repoNetworkConfigPath = line.slice(
"NETWORK_CONFIG_PATH=".length,
);
const networkConfigHash = await shortContentHash(repoNetworkConfigPath);
networkConfigPaths = {
repo: repoNetworkConfigPath,
build: `networkConfig-${networkConfigHash}.json`,
};
// Need to replace this value with a build location because otherwise
// this file might not be included in the docker image
buildLine = `NETWORK_CONFIG_PATH=${networkConfigPaths.build}`;
}
buildDotEnvLines.push(buildLine);
}
if (networkConfigPaths !== nil) {
await Deno.copyFile(
networkConfigPaths.repo,
`${buildDir}/${networkConfigPaths.build}`,
);
}
await Deno.writeTextFile(`${buildDir}/.env`, buildDotEnvLines.join("\n"));
}
async function copyTypescriptFiles() {
for (const f of await allFiles()) {
if (!f.endsWith(".ts")) {
@@ -138,9 +106,11 @@ async function tarballTypescriptFiles() {
}
async function buildDockerImage() {
const buildName = await BuildName();
const tag = await Tag();
const imageName = args.imageName ?? "aggregator";
const imageNameAndTag = `${imageName}:${tag}`;
const sudoDockerArg = args["sudo-docker"] === true ? ["sudo"] : [];
const sudoDockerArg = args.sudoDocker ? ["sudo"] : [];
await shell.run(
...sudoDockerArg,
@@ -148,18 +118,35 @@ async function buildDockerImage() {
"build",
repoDir,
"-t",
`aggregator:${buildName}`,
imageNameAndTag,
);
const dockerImageName = `aggregator-${buildName}-docker-image`;
if (args.alsoTagLatest) {
await shell.run(
...sudoDockerArg,
"docker",
"tag",
`${imageName}:${tag}`,
`${imageName}:latest`,
);
}
console.log("\nDocker image created:", imageNameAndTag);
if (args.imageName) {
return;
}
const dockerImageFileName = `${imageName}-${tag}-docker-image`;
const tarFilePath = `${repoDir}/build/${dockerImageFileName}.tar`;
await shell.run(
...sudoDockerArg,
"docker",
"save",
"--output",
`${repoDir}/build/${dockerImageName}.tar`,
`aggregator:${buildName}`,
tarFilePath,
imageNameAndTag,
);
if (sudoDockerArg.length > 0) {
@@ -170,12 +157,23 @@ async function buildDockerImage() {
"sudo",
"chown",
username,
`${repoDir}/build/${dockerImageName}.tar`,
tarFilePath,
);
}
await shell.run(
"gzip",
`${repoDir}/build/${dockerImageName}.tar`,
);
await shell.run("gzip", tarFilePath);
console.log(`Docker image saved: ${tarFilePath}.gz`);
}
async function pushDockerImage() {
const tag = await Tag();
const imageName = args.imageName ?? "aggregator";
const imageNameAndTag = `${imageName}:${tag}`;
await shell.run("docker", "push", imageNameAndTag);
if (args.alsoTagLatest) {
await shell.run("docker", "push", `${imageName}:latest`);
}
}

5
aggregator/programs/checkTs.ts Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env
import { checkTs } from "./helpers/typescript.ts";
await checkTs();

View File

@@ -16,10 +16,9 @@ deno test \
--allow-env \
--allow-read \
--coverage=cov_profile \
--unstable \
test/*.test.ts
deno coverage cov_profile --unstable --lcov >cov_profile/profile.lcov
deno coverage cov_profile --lcov >cov_profile/profile.lcov
genhtml -o cov_profile/html cov_profile/profile.lcov

View File

@@ -0,0 +1,41 @@
#!/usr/bin/env -S deno run --allow-net --allow-env --allow-read
import {
BlsWalletWrapper,
ethers,
VerificationGateway__factory,
Wallet,
} from "../deps.ts";
import * as env from "../src/env.ts";
import getNetworkConfig from "../src/helpers/getNetworkConfig.ts";
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
const wallet = new Wallet(env.PRIVATE_KEY_AGG, provider);
const { addresses } = await getNetworkConfig();
const vg = VerificationGateway__factory.connect(
addresses.verificationGateway,
wallet,
);
const internalBlsWallet = await BlsWalletWrapper.connect(
env.PRIVATE_KEY_AGG,
addresses.verificationGateway,
provider,
);
console.log("Connected internal wallet:", internalBlsWallet.address);
const nonce = await internalBlsWallet.Nonce();
if (!nonce.eq(0)) {
console.log("Already exists with nonce", nonce.toNumber());
} else {
await (await vg.processBundle(internalBlsWallet.sign({
nonce: 0,
actions: [],
}))).wait();
console.log("Created successfully");
}

View File

@@ -1,15 +0,0 @@
#!/usr/bin/env -S deno run --unstable --allow-net --allow-read --allow-env
// Useful for when breaking database changes are made.
import createQueryClient from "../src/app/createQueryClient.ts";
import * as env from "../src/env.ts";
import BundleTable from "../src/app/BundleTable.ts";
const queryClient = createQueryClient(() => {});
for (const tableName of [env.BUNDLE_TABLE_NAME]) {
const table = await BundleTable.create(queryClient, tableName);
await table.drop();
console.log(`dropped table ${tableName}`);
}

View File

@@ -0,0 +1,13 @@
import * as shell from "./shell.ts";
export async function allFiles() {
return [
...await shell.Lines("git", "ls-files"),
...await shell.Lines(
"git",
"ls-files",
"--others",
"--exclude-standard",
),
];
}

View File

@@ -0,0 +1,22 @@
import * as shell from "./shell.ts";
import { allFiles } from "./git.ts";
// TODO (merge-ok) Consider turning this into a standard eslint rule
export async function lintTodosFixmes(): Promise<void> { // merge-ok
const searchArgs = [
"egrep",
"--color",
"-ni",
"todo|fixme", // merge-ok
...(await allFiles()),
];
const matches = await shell.Lines(...searchArgs);
const notOkMatches = matches.filter((m) => !m.includes("merge-ok"));
if (notOkMatches.length > 0) {
console.error(notOkMatches.join("\n"));
throw new Error(`${notOkMatches.length} todos/fixmes found`); // merge-ok
}
}

View File

@@ -0,0 +1,25 @@
import { allFiles } from "./git.ts";
import * as shell from "./shell.ts";
import nil from "../../src/helpers/nil.ts";
import repoDir from "../../src/helpers/repoDir.ts";
export async function checkTs(): Promise<void> {
let testFilePath: string | nil = nil;
try {
const tsFiles = (await allFiles()).filter((f) => f.endsWith(".ts"));
testFilePath = await Deno.makeTempFile({ suffix: ".ts" });
await Deno.writeTextFile(
testFilePath,
tsFiles.map((f) => `import "${repoDir}/${f}";`).join("\n"),
);
await shell.run("deno", "check", testFilePath);
} finally {
if (testFilePath !== nil) {
await Deno.remove(testFilePath);
}
}
}

View File

@@ -0,0 +1,7 @@
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-env
// TODO (merge-ok) Consider turning this into a standard eslint rule
import { lintTodosFixmes } from "./helpers/lint.ts"; // merge-ok
await lintTodosFixmes(); // merge-ok

View File

@@ -1,8 +1,9 @@
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write --allow-env
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write --allow-env
import { lintTodosFixmes } from "./helpers/lint.ts"; // merge-ok
import { checkTs } from "./helpers/typescript.ts";
import * as shell from "./helpers/shell.ts";
import repoDir from "../src/helpers/repoDir.ts";
import nil from "../src/helpers/nil.ts";
import { envName } from "../src/helpers/dotEnvPath.ts";
Deno.chdir(repoDir);
@@ -27,44 +28,8 @@ function Checks(): Check[] {
["lint", async () => {
await shell.run("deno", "lint", ".");
}],
["todos and fixmes", async () => { // merge-ok
const searchArgs = [
"egrep",
"--color",
"-ni",
"todo|fixme", // merge-ok
...(await allFiles()),
];
const matches = await shell.Lines(...searchArgs);
const notOkMatches = matches.filter((m) => !m.includes("merge-ok"));
if (notOkMatches.length > 0) {
console.error(notOkMatches.join("\n"));
throw new Error(`${notOkMatches.length} todos/fixmes found`); // merge-ok
}
}],
["typescript", async () => {
let testFilePath: string | nil = nil;
try {
const tsFiles = (await allFiles()).filter((f) => f.endsWith(".ts"));
testFilePath = await Deno.makeTempFile({ suffix: ".ts" });
await Deno.writeTextFile(
testFilePath,
tsFiles.map((f) => `import "${repoDir}/${f}";`).join("\n"),
);
await shell.run("deno", "cache", "--unstable", testFilePath);
} finally {
if (testFilePath !== nil) {
await Deno.remove(testFilePath);
}
}
}],
["todos and fixmes", lintTodosFixmes], // merge-ok
["typescript", checkTs],
["test", async () => {
await shell.run(
"deno",
@@ -77,7 +42,6 @@ function Checks(): Check[] {
"--allow-net",
"--allow-env",
"--allow-read",
"--unstable",
"--",
"--env",
envName,
@@ -85,15 +49,3 @@ function Checks(): Check[] {
}],
];
}
async function allFiles() {
return [
...await shell.Lines("git", "ls-files"),
...await shell.Lines(
"git",
"ls-files",
"--others",
"--exclude-standard",
),
];
}

View File

@@ -1,19 +1,14 @@
#!/usr/bin/env -S deno run --unstable --allow-net --allow-read --unstable
#!/usr/bin/env -S deno run --allow-net --allow-read --allow-write --allow-env
import { BigNumber } from "../deps.ts";
import createQueryClient from "../src/app/createQueryClient.ts";
import { BigNumber, sqlite } from "../deps.ts";
import * as env from "../src/env.ts";
import BundleTable from "../src/app/BundleTable.ts";
const queryClient = createQueryClient(() => {});
const table = new BundleTable(new sqlite.DB(env.DB_PATH));
for (const tableName of [env.BUNDLE_TABLE_NAME]) {
const table = await BundleTable.create(queryClient, tableName);
console.log(tableName, await table.count());
console.log(tableName, (await table.all()).map((bun) => bun.id));
console.log(
tableName,
"findEligible",
(await table.findEligible(BigNumber.from(0), 1000)).map((bun) => bun.id),
);
}
console.log(table.count());
console.log(table.all().map((bun) => bun.id));
console.log(
"findEligible",
table.findEligible(BigNumber.from(0), 1000).map((bun) => bun.id)
);

View File

@@ -0,0 +1,43 @@
#!/bin/bash
set -euo pipefail
if [ -z ${VERSION+x} ]; then
>&2 echo "Missing VERSION. Needs to match the first 7 characters of the git sha used to build the docker image."
>&2 echo "Usage: VERSION=abc1234 start-docker.sh"
exit 1
fi
ENV_PATH="${ENV_PATH:=.env}"
# Normalize ENV_PATH to an absolute path
if [[ $(echo $ENV_PATH | head -c1) != "/" ]]; then
ENV_PATH="$(cd $(dirname $ENV_PATH) && pwd)/$(basename $ENV_PATH)"
fi
echo "Using env" $ENV_PATH
PORT=$(cat $ENV_PATH | grep '^PORT=' | tail -n1 | sed 's/^PORT=//')
NETWORK_CONFIG_PATH=$(cat $ENV_PATH | grep '^NETWORK_CONFIG_PATH=' | tail -n1 | sed 's/^NETWORK_CONFIG_PATH=//')
# Normalize NETWORK_CONFIG_PATH to an absolute path
if [[ $(echo $NETWORK_CONFIG_PATH | head -c1) != "/" ]]; then
NETWORK_CONFIG_PATH="$(cd $(dirname $ENV_PATH) && cd $(dirname $NETWORK_CONFIG_PATH) && pwd)/$(basename $NETWORK_CONFIG_PATH)"
fi
echo "Using network config" $NETWORK_CONFIG_PATH
NETWORK=$(basename $NETWORK_CONFIG_PATH .json)
CONTAINER_NAME="aggregator-$VERSION-$NETWORK"
IMAGE_NAME="aggregator:git-$VERSION"
echo "Creating $CONTAINER_NAME using $IMAGE_NAME"
docker run \
--name "$CONTAINER_NAME" \
-d \
--net=host \
--restart=unless-stopped \
--mount type=bind,source="$ENV_PATH",target=/app/.env \
--mount type=bind,source="$NETWORK_CONFIG_PATH",target=/app/networkConfig.json \
"$IMAGE_NAME"

View File

@@ -5,15 +5,15 @@ import AdminService from "./AdminService.ts";
export default function AdminRouter(adminService: AdminService) {
const router = new Router({ prefix: "/admin/" });
router.get("countTxs", async (ctx) => {
const c = await adminService.bundleCount();
router.get("countTxs", (ctx) => {
const c = adminService.bundleCount();
console.log(`Returning count ${c}\n`);
ctx.response.headers.set("Content-Type", "application/json");
ctx.response.body = c;
});
router.get("resetTxs", async (ctx) => {
await adminService.resetBundles();
router.get("resetTxs", (ctx) => {
adminService.resetBundles();
ctx.response.body = "Transactions reset";
});

View File

@@ -7,11 +7,11 @@ export default class AdminService {
private bundleTable: BundleTable,
) {}
async resetBundles() {
await this.bundleTable.clear();
resetBundles() {
this.bundleTable.clear();
}
async bundleCount(): Promise<bigint> {
return await this.bundleTable.count();
bundleCount(): number {
return this.bundleTable.count();
}
}

View File

@@ -2,211 +2,447 @@ import {
BigNumber,
BlsWalletSigner,
Bundle,
decodeError,
ERC20,
ERC20__factory,
ethers,
OperationResultError,
Semaphore,
shuffled,
} from "../../deps.ts";
import nil from "../helpers/nil.ts";
import Range from "../helpers/Range.ts";
import assert from "../helpers/assert.ts";
import bigSum from "./helpers/bigSum.ts";
import * as env from "../env.ts";
import EthereumService from "./EthereumService.ts";
import { BundleRow } from "./BundleTable.ts";
import countActions from "./helpers/countActions.ts";
import ClientReportableError from "./helpers/ClientReportableError.ts";
import AppEvent from "./AppEvent.ts";
import bigSum from "./helpers/bigSum.ts";
type FeeConfig =
| {
type: "ether";
allowLosses: boolean;
breakevenOperationCount: number;
}
| {
type: "token";
address: string;
ethValueInTokens: number;
allowLosses: boolean;
breakevenOperationCount: number;
}
| nil;
const envFeeConfig = ((): FeeConfig => {
if (!env.REQUIRE_FEES) {
return nil;
}
if (env.FEE_TYPE === "ether") {
return {
type: "ether",
allowLosses: env.ALLOW_LOSSES,
breakevenOperationCount: env.BREAKEVEN_OPERATION_COUNT,
};
}
const feeTypeParts = env.FEE_TYPE.split(":");
assert(feeTypeParts.length === 2);
assert(feeTypeParts[0] === "token");
const address = feeTypeParts[1];
assert(/^0x[0-9a-fA-F]*$/.test(address));
assert(env.ETH_VALUE_IN_TOKENS !== nil);
return {
type: "token",
address,
ethValueInTokens: env.ETH_VALUE_IN_TOKENS,
allowLosses: env.ALLOW_LOSSES,
breakevenOperationCount: env.BREAKEVEN_OPERATION_COUNT,
};
})();
export type AggregationStrategyResult = {
aggregateBundle: Bundle | nil;
includedRows: BundleRow[];
bundleOverheadCost: BigNumber;
expectedFee: BigNumber;
expectedMaxCost: BigNumber;
failedRows: BundleRow[];
};
export type AggregationStrategyConfig =
typeof AggregationStrategy["defaultConfig"];
export default class AggregationStrategy {
static defaultConfig = {
maxAggregationSize: env.MAX_AGGREGATION_SIZE,
fees: {
type: env.FEE_TYPE,
perGas: env.FEE_PER_GAS,
perByte: env.FEE_PER_BYTE,
},
maxGasPerBundle: env.MAX_GAS_PER_BUNDLE,
fees: envFeeConfig,
bundleCheckingConcurrency: env.BUNDLE_CHECKING_CONCURRENCY,
};
#tokenDecimals?: number;
// The concurrency of #checkBundle is limited by this semaphore because it can
// be called on many bundles in parallel
#checkBundleSemaphore: Semaphore;
constructor(
public blsWalletSigner: BlsWalletSigner,
public ethereumService: EthereumService,
public config = AggregationStrategy.defaultConfig,
) {}
async run(eligibleRows: BundleRow[]): (
Promise<{
aggregateBundle: Bundle | nil;
includedRows: BundleRow[];
failedRows: BundleRow[];
}>
public emit: (event: AppEvent) => void = () => {},
) {
this.#checkBundleSemaphore = new Semaphore(
this.config.bundleCheckingConcurrency,
);
}
async run(eligibleRows: BundleRow[]): Promise<AggregationStrategyResult> {
eligibleRows = await this.#filterRows(eligibleRows);
const bundleOverheadGas = await this.measureBundleOverheadGas();
let aggregateBundle = this.blsWalletSigner.aggregate([]);
let aggregateGas = bundleOverheadGas;
const includedRows: BundleRow[] = [];
const failedRows: BundleRow[] = [];
let expectedFee = BigNumber.from(0);
while (eligibleRows.length > 0) {
while (true) {
const {
aggregateBundle: newAggregateBundle,
aggregateGas: newAggregateGas,
includedRows: newIncludedRows,
expectedFees,
failedRows: newFailedRows,
remainingEligibleRows,
} = await this.#augmentAggregateBundle(
aggregateBundle,
aggregateGas,
eligibleRows,
bundleOverheadGas,
);
aggregateBundle = newAggregateBundle;
aggregateGas = newAggregateGas;
includedRows.push(...newIncludedRows);
expectedFee = expectedFee.add(bigSum(expectedFees));
failedRows.push(...newFailedRows);
eligibleRows = remainingEligibleRows;
}
return {
aggregateBundle: aggregateBundle.operations.length > 0
? aggregateBundle
: nil,
includedRows,
failedRows,
};
}
async estimateFee(bundle: Bundle) {
const es = this.ethereumService;
const feeToken = this.#FeeToken();
const balanceCall = feeToken
? es.Call(feeToken, "balanceOf", [es.wallet.address])
: es.Call(es.utilities, "ethBalanceOf", [es.wallet.address]);
const [
balanceResultBefore,
bundleResult,
balanceResultAfter,
] = await es.callStaticSequence(
balanceCall,
es.Call(
es.verificationGateway,
"processBundle",
[bundle],
),
balanceCall,
);
if (
balanceResultBefore.returnValue === undefined ||
balanceResultAfter.returnValue === undefined
) {
throw new ClientReportableError("Failed to get balance");
}
const balanceBefore = balanceResultBefore.returnValue[0];
const balanceAfter = balanceResultAfter.returnValue[0];
const feeDetected = balanceAfter.sub(balanceBefore);
if (bundleResult.returnValue === undefined) {
throw new ClientReportableError("Failed to statically process bundle");
}
const feeRequired = await this.#measureRequiredFee(bundle);
const successes = bundleResult.returnValue.successes;
return {
feeDetected,
feeRequired,
successes,
};
}
async #augmentAggregateBundle(
previousAggregateBundle: Bundle,
eligibleRows: BundleRow[],
): (
Promise<{
aggregateBundle: Bundle;
includedRows: BundleRow[];
failedRows: BundleRow[];
remainingEligibleRows: BundleRow[];
}>
) {
let aggregateBundle: Bundle | nil = nil;
let includedRows: BundleRow[] = [];
const failedRows: BundleRow[] = [];
// TODO (merge-ok): Count gas instead, have idea
// or way to query max gas per txn (submission).
let actionCount = countActions(previousAggregateBundle);
for (const row of eligibleRows) {
const rowActionCount = countActions(row.bundle);
if (actionCount + rowActionCount > this.config.maxAggregationSize) {
if (newIncludedRows.length === 0) {
break;
}
includedRows.push(row);
actionCount += rowActionCount;
}
if (includedRows.length === 0) {
return {
aggregateBundle: previousAggregateBundle,
includedRows,
aggregateBundle: nil,
includedRows: [],
bundleOverheadCost: bundleOverheadGas.mul(
(await this.ethereumService.GasConfig()).maxFeePerGas,
),
expectedFee: BigNumber.from(0),
expectedMaxCost: BigNumber.from(0),
failedRows,
// If we're not able to include anything more, don't consider any rows
// eligible anymore.
remainingEligibleRows: [],
};
}
const [previousFee, ...fees] = (await this.#measureFees([
previousAggregateBundle,
...includedRows.map((r) => r.bundle),
]));
const firstFailureIndex = await this.#findFirstFailureIndex(
previousAggregateBundle,
previousFee,
includedRows.map((r) => r.bundle),
fees,
const aggregateBundleCheck = await this.#checkBundle(
aggregateBundle,
BigNumber.from(0),
);
let remainingEligibleRows: BundleRow[];
if (firstFailureIndex !== nil) {
const failedRow = includedRows[firstFailureIndex];
failedRows.push(failedRow);
includedRows = includedRows.slice(
0,
firstFailureIndex,
);
const eligibleRowIndex = eligibleRows.indexOf(failedRow);
assert(eligibleRowIndex !== -1);
remainingEligibleRows = eligibleRows.slice(includedRows.length + 1);
} else {
remainingEligibleRows = eligibleRows.slice(includedRows.length);
}
aggregateBundle = this.blsWalletSigner.aggregate([
previousAggregateBundle,
...includedRows.map((r) => r.bundle),
]);
return {
let result: AggregationStrategyResult = {
aggregateBundle,
includedRows,
bundleOverheadCost: bundleOverheadGas.mul(
(await this.ethereumService.GasConfig()).maxFeePerGas,
),
expectedFee,
expectedMaxCost: aggregateBundleCheck.expectedMaxCost,
failedRows,
remainingEligibleRows,
};
if (this.config.fees?.allowLosses === false) {
result = this.#preventLosses(
result,
aggregateBundleCheck.errorReason,
this.config.fees.breakevenOperationCount,
);
}
return result;
}
/**
* This is not guaranteed to prevent losses. We cannot 100% know what is going
* to happen until the bundle is actually submitted on chain.
*/
#preventLosses(
result: AggregationStrategyResult,
errorReason: OperationResultError | nil,
breakevenOperationCount: number,
): AggregationStrategyResult {
if (result.aggregateBundle === nil) {
return result;
}
const {
aggregateBundle,
expectedFee,
expectedMaxCost,
includedRows,
failedRows,
} = result;
if (expectedFee.gte(expectedMaxCost)) {
return result;
}
this.emit({
type: "aggregate-bundle-unprofitable",
data: {
reason: errorReason?.message,
},
});
if (aggregateBundle.operations.length < breakevenOperationCount) {
return {
aggregateBundle: nil,
includedRows: [],
bundleOverheadCost: result.bundleOverheadCost,
expectedFee: BigNumber.from(0),
expectedMaxCost: BigNumber.from(0),
failedRows,
};
}
this.emit({ type: "unprofitable-despite-breakeven-operations" });
// This is unexpected: We have enough operations to breakeven, but the
// bundle is unprofitable instead.
//
// This could happen due to small variations on a bundle that is
// borderline, but it could also happen due to an intentional attack.
// In the simplest case, an attacker submits two bundles that both pay
// when simulated in isolation, but the first sets state that prevents
// payment on the second bundle. We need to do something about this
// because it could otherwise put us into a state that might never
// resolve - next time we form a bundle, we'll run into the same issue
// because the bundles will be considered again in the same order.
//
// To fix this, we simply mark half the bundles as failed. We could
// isolate the issue by doing a lot of in-order reprocessing, but
// having this defense in place should prevent the attack in the first
// place, so false-positives here are a minor concern (keeping in mind
// these bundles will still get retried later).
let failureSample = shuffled(includedRows);
failureSample = failureSample.slice(0, failureSample.length / 2);
for (const row of failureSample) {
row.submitError = "Included in failure sample for unprofitable bundle";
}
failedRows.push(...failureSample);
return {
aggregateBundle: nil,
includedRows: [],
bundleOverheadCost: result.bundleOverheadCost,
expectedFee: BigNumber.from(0),
expectedMaxCost: BigNumber.from(0),
failedRows,
};
}
async estimateFee(bundle: Bundle, bundleOverheadGas?: BigNumber) {
const [{ operationStatuses: successes, fee: feeDetected, errorReason }] =
await this
.#measureFees([
bundle,
]);
const feeInfo = await this.#measureFeeInfo(
bundle,
bundleOverheadGas,
);
return {
feeDetected,
feeRequired: feeInfo?.requiredFee ?? BigNumber.from(0),
successes,
errorReason,
};
}
/**
* Removes rows that conflict with each other because they contain an
* operation for the same wallet and the same nonce.
*/
async #filterRows(rows: BundleRow[]) {
const rowsByKeyAndNonce = new Map<string, BundleRow[]>();
for (const row of rows) {
for (let i = 0; i < row.bundle.operations.length; i++) {
const publicKey = row.bundle.senderPublicKeys[i];
const operation = row.bundle.operations[i];
const keyAndNonce = ethers.utils.solidityPack([
"uint256",
"uint256",
"uint256",
"uint256",
"uint256",
], [...publicKey, operation.nonce]);
const entry = rowsByKeyAndNonce.get(keyAndNonce) ?? [];
entry.push(row);
rowsByKeyAndNonce.set(keyAndNonce, entry);
}
}
let filteredRows = rows;
for (let rowGroup of rowsByKeyAndNonce.values()) {
rowGroup = rowGroup.filter((r) => filteredRows.includes(r));
if (rowGroup.length <= 1) {
continue;
}
const bestRow = await this.#pickBest(rowGroup);
const conflictingRows = rowGroup.filter((r) => r !== bestRow);
filteredRows = filteredRows.filter((r) => !conflictingRows.includes(r));
}
return filteredRows;
}
/**
* Returns the row which pays the highest 'excess fee'.
*
* Excess fee is the amount the fee exceeds its requirement.
*/
async #pickBest(rows: BundleRow[]) {
assert(rows.length > 0);
const results = await Promise.all(
rows.map((r) => this.#checkBundle(r.bundle)),
);
const excessFees = results.map((res) =>
res.expectedFee.sub(res.requiredFee)
);
let bestExcessFee = excessFees[0];
let bestExcessFeeIndex = 0;
for (let i = 1; i < excessFees.length; i++) {
if (excessFees[i].gt(bestExcessFee)) {
bestExcessFee = excessFees[i];
bestExcessFeeIndex = i;
}
}
return rows[bestExcessFeeIndex];
}
/**
* Augment the aggregate bundle with more eligible rows.
*
* This is part of an iterative process where the aggregate bundle is
* initially empty and this method is used to accumulate more user bundles
* until we can't add any more.
*/
async #augmentAggregateBundle(
previousAggregateBundle: Bundle,
previousAggregateGas: BigNumber,
eligibleRows: BundleRow[],
bundleOverheadGas: BigNumber,
): Promise<{
aggregateBundle: Bundle;
aggregateGas: BigNumber;
includedRows: BundleRow[];
expectedFees: BigNumber[];
failedRows: BundleRow[];
remainingEligibleRows: BundleRow[];
}> {
const candidateRows = eligibleRows.splice(
0,
this.config.bundleCheckingConcurrency,
);
let aggregateGas = previousAggregateGas;
// Checking in parallel here. We limit the number of candidate rows on each
// round to limit this, and it's also protected by a semaphore.
const rowChecks = await Promise.all(
candidateRows.map((r) => this.#checkBundle(r.bundle, bundleOverheadGas)),
);
const includedRows: BundleRow[] = [];
const expectedFees: BigNumber[] = [];
const failedRows: BundleRow[] = [];
for (
const [
i,
{ success, gasEstimate, expectedFee, errorReason },
] of rowChecks.entries()
) {
const row = candidateRows[i];
if (!success) {
if (errorReason) {
row.submitError = errorReason.message;
}
failedRows.push(row);
continue;
}
const newAggregateGas = (aggregateGas
.add(gasEstimate)
.sub(bundleOverheadGas));
if (newAggregateGas.gt(this.config.maxGasPerBundle)) {
// Bundle would cause us to exceed maxGasPerBundle, so don't include it,
// but also don't mark it as failed.
continue;
}
aggregateGas = newAggregateGas;
includedRows.push(row);
expectedFees.push(expectedFee);
}
return {
aggregateBundle: this.blsWalletSigner.aggregate([
previousAggregateBundle,
...includedRows.map((r) => r.bundle),
]),
aggregateGas,
includedRows,
expectedFees,
failedRows,
remainingEligibleRows: eligibleRows,
};
}
async #measureFees(bundles: Bundle[]): Promise<{
success: boolean;
operationStatuses: boolean[];
fee: BigNumber;
errorReason: OperationResultError | nil;
}[]> {
const es = this.ethereumService;
const feeToken = this.#FeeToken();
@@ -231,216 +467,208 @@ export default class AggregationStrategy {
assert(after.success);
const bundleResult = processBundleResults[i];
let success: boolean;
if (bundleResult.success) {
const [operationResults] = bundleResult.returnValue;
// We require that at least one operation succeeds, even though
// processBundle doesn't revert in this case.
success = operationResults.some((opSuccess) => opSuccess === true);
} else {
success = false;
const fee = after.returnValue[0].sub(before.returnValue[0]);
if (!bundleResult.success) {
const errorReason: OperationResultError = {
message: "Unknown error reason",
};
return {
success: false,
operationStatuses: bundles[i].operations.map(() => false),
fee,
errorReason,
};
}
const fee = after.returnValue[0].sub(before.returnValue[0]);
const [operationStatuses, results] = bundleResult.returnValue;
return { success, fee };
let errorReason: OperationResultError | nil;
// We require that at least one operation succeeds, even though
// processBundle doesn't revert in this case.
const success = operationStatuses.some((opSuccess: boolean) =>
opSuccess === true
);
// If operation is not successful, attempt to decode an error message
if (!success) {
const error = results.map((result: string[]) => {
try {
if (result[0]) {
return decodeError(result[0]);
}
return;
} catch (err) {
console.error(err);
return;
}
});
errorReason = error[0];
}
return { success, operationStatuses, fee, errorReason };
});
}
#FeeToken(): ERC20 | nil {
const feeType = this.config.fees.type;
if (feeType === "ether") {
if (this.config.fees?.type !== "token") {
return nil;
}
return ERC20__factory.connect(
feeType.slice("token:".length),
this.config.fees.address,
this.ethereumService.wallet.provider,
);
}
async #measureRequiredFee(bundle: Bundle) {
const gasEstimate = await this.ethereumService.verificationGateway
.estimateGas
.processBundle(bundle);
const callDataSize = ethers.utils.hexDataLength(
this.ethereumService.verificationGateway.interface
.encodeFunctionData("processBundle", [bundle]),
);
return (
gasEstimate.mul(this.config.fees.perGas).add(
this.config.fees.perByte.mul(callDataSize),
)
);
}
/**
* Get a lower bound for the fee that is required for processing the
* bundle.
*
* This exists because it's a very good lower bound and it's very fast.
* Therefore, when there's an insufficient fee bundle:
* - This lower bound is usually enough to find it
* - Finding it this way is much more efficient
*/
#measureRequiredFeeLowerBound(bundle: Bundle) {
const callDataEmptyBundleSize = ethers.utils.hexDataLength(
this.ethereumService.verificationGateway.interface
.encodeFunctionData("processBundle", [
this.blsWalletSigner.aggregate([]),
]),
);
const callDataSize = ethers.utils.hexDataLength(
this.ethereumService.verificationGateway.interface
.encodeFunctionData("processBundle", [bundle]),
);
// We subtract the size of an empty bundle because it represents the number
// of *additional* bytes added when aggregating. The bundle doesn't
// necessarily have to pay the initial overhead to be viable.
const callDataMarginalSize = callDataSize - callDataEmptyBundleSize;
return this.config.fees.perByte.mul(callDataMarginalSize);
}
async #findFirstFailureIndex(
previousAggregateBundle: Bundle,
previousFee: { success: boolean; fee: BigNumber },
bundles: Bundle[],
fees: { success: boolean; fee: BigNumber }[],
): Promise<number | nil> {
if (bundles.length === 0) {
async #measureFeeInfo(bundle: Bundle, bundleOverheadGas?: BigNumber) {
if (this.config.fees === nil) {
return nil;
}
const len = bundles.length;
assert(fees.length === len);
bundleOverheadGas ??= await this.measureBundleOverheadGas();
const checkFirstN = async (n: number): Promise<{
const gasEstimate = await this.ethereumService.verificationGateway
.estimateGas.processBundle(bundle);
const marginalGasEstimate = gasEstimate.sub(bundleOverheadGas);
const bundleOverheadGasContribution = BigNumber.from(
Math.ceil(
bundleOverheadGas.toNumber() /
this.config.fees.breakevenOperationCount * bundle.operations.length,
),
);
const requiredGas = marginalGasEstimate.add(bundleOverheadGasContribution);
const { maxFeePerGas } = await this.ethereumService.GasConfig();
const ethWeiFee = requiredGas.mul(maxFeePerGas);
const token = this.#FeeToken();
if (!token) {
return {
requiredFee: ethWeiFee,
expectedMaxCost: gasEstimate.mul(maxFeePerGas),
gasEstimate,
bundleOverheadGas,
};
}
const decimals = await this.#TokenDecimals();
const decimalAdj = 10 ** (decimals - 18);
assert(this.config.fees?.type === "token");
const ethWeiOverTokenWei = decimalAdj * this.config.fees.ethValueInTokens;
// Note the use of .toString below. Without it, BigNumber recognizes that
// the float64 number cannot accurately represent integers in this range
// and throws an overflow. However, this number is ultimately an estimation
// with a margin of error, and the rounding caused by float64 is acceptable.
const tokenWeiFee = BigNumber.from(
Math.ceil(ethWeiFee.toNumber() * ethWeiOverTokenWei).toString(),
);
return {
requiredFee: tokenWeiFee,
expectedMaxCost: gasEstimate.mul(maxFeePerGas),
gasEstimate,
bundleOverheadGas,
};
}
async #checkBundle(
bundle: Bundle,
bundleOverheadGas?: BigNumber,
): Promise<
{
success: boolean;
fee: BigNumber;
gasEstimate: BigNumber;
bundleOverheadGas?: BigNumber;
expectedFee: BigNumber;
requiredFee: BigNumber;
}> => {
if (n === 0) {
expectedMaxCost: BigNumber;
errorReason?: OperationResultError;
}
> {
return await this.#checkBundleSemaphore.use(async () => {
const [
feeInfo,
[{ success, fee, errorReason }],
] = await Promise.all([
this.#measureFeeInfo(
bundle,
bundleOverheadGas,
),
this.#measureFees([bundle]),
]);
if (success && feeInfo && fee.lt(feeInfo.requiredFee)) {
return {
success: true,
fee: BigNumber.from(0),
requiredFee: BigNumber.from(0),
success: false,
gasEstimate: feeInfo.gasEstimate,
bundleOverheadGas: feeInfo.bundleOverheadGas,
expectedFee: fee,
requiredFee: feeInfo.requiredFee,
expectedMaxCost: feeInfo.expectedMaxCost,
errorReason: { message: "Insufficient fee" },
};
}
const fee = bigSum([
previousFee.fee,
...fees.slice(0, n).map((r) => r.fee),
]);
const gasEstimate = feeInfo?.gasEstimate ??
await this.ethereumService.verificationGateway
.estimateGas.processBundle(bundle);
const requiredFee = await this.#measureRequiredFee(
this.blsWalletSigner.aggregate([
previousAggregateBundle,
...bundles.slice(0, n),
]),
);
return {
success,
gasEstimate,
expectedFee: fee,
requiredFee: feeInfo?.requiredFee ?? BigNumber.from(0),
expectedMaxCost: feeInfo?.expectedMaxCost ?? BigNumber.from(0),
errorReason,
};
});
}
const success = fee.gte(requiredFee);
async measureBundleOverheadGas() {
// The simple way to do this would be to estimate the gas of an empty
// bundle. However, an empty bundle is a bit of a special case, in
// particular the on-chain BLS library outright refuses to validate it. So
// instead we estimate one operation and two operations and extrapolate
// backwards to zero operations.
return { success, fee, requiredFee };
};
const es = this.ethereumService;
const wallet = es.blsWalletWrapper;
// This calculation is entirely local and cheap. It can find a failing
// bundle, but it might not be the *first* failing bundle.
const fastFailureIndex = (() => {
for (let i = 0; i < len; i++) {
// If the actual call failed then we consider it a failure, even if the
// fee is somehow met (e.g. if zero fee is required).
if (fees[i].success === false) {
return i;
}
const nonce = await wallet.Nonce();
// Because the required fee mostly comes from the calldata size, this
// should find the first insufficient fee most of the time.
const lowerBound = this.#measureRequiredFeeLowerBound(bundles[i]);
// It's a requirement that this wallet has already been created. Otherwise,
// wallet creation would be included in the bundle overhead.
assert(nonce.gt(0));
if (fees[i].fee.lt(lowerBound)) {
return i;
}
}
})();
const bundle1 = wallet.sign({ nonce, actions: [] });
const bundle2 = wallet.sign({ nonce: nonce.add(1), actions: [] });
let left = 0;
let leftRequiredFee = BigNumber.from(0);
let right: number;
let rightRequiredFee: BigNumber;
const [oneOpGasEstimate, twoOpGasEstimate] = await Promise.all([
es.verificationGateway.estimateGas.processBundle(bundle1),
es.verificationGateway.estimateGas.processBundle(
this.blsWalletSigner.aggregate([bundle1, bundle2]),
),
]);
if (fastFailureIndex !== nil) {
// Having a fast failure index is not enough because it might not be the
// first. To establish that it really is the first, we need to ensure that
// all bundles up to that index are ok (indeed, this is the assumption
// that is relied upon outside - that the subset before the first failing
// index can proceed without further checking).
const opMarginalGasEstimate = twoOpGasEstimate.sub(oneOpGasEstimate);
const { success, requiredFee } = await checkFirstN(fastFailureIndex);
return oneOpGasEstimate.sub(opMarginalGasEstimate);
}
if (success) {
return fastFailureIndex;
}
// In case of failure, we now know there as a failing index in a more
// narrow range, so we can at least restrict the bisect to this smaller
// range.
right = fastFailureIndex;
rightRequiredFee = requiredFee;
} else {
// If we don't have a failing index, we still need to establish that there
// is a failing index to be found. This is because it's a requirement of
// the upcoming bisect logic that there is a failing bundle in
// `bundles.slice(left, right)`.
const { success, requiredFee } = await checkFirstN(bundles.length);
if (success) {
return nil;
}
right = bundles.length;
rightRequiredFee = requiredFee;
async #TokenDecimals(): Promise<number> {
if (this.#tokenDecimals === nil) {
const token = this.#FeeToken();
assert(token !== nil);
this.#tokenDecimals = await token.decimals();
}
// Do a bisect to narrow in on the (first) culprit.
while (right - left > 1) {
const mid = Math.floor((left + right) / 2);
const { success, requiredFee } = await checkFirstN(mid);
if (success) {
left = mid;
leftRequiredFee = requiredFee;
} else {
right = mid;
rightRequiredFee = requiredFee;
}
}
assert(right - left === 1, "bisect should identify a single result");
// The bisect procedure maintains that the culprit is a member of
// `bundles.slice(left, right)`. That's now equivalent to `[bundles[left]]`,
// so `left` is our culprit index.
const bundleFee = fees[left].fee;
const bundleRequiredFee = rightRequiredFee.sub(leftRequiredFee);
// Tracking the fees so that we can include this assertion isn't strictly
// necessary. But the cost is negligible and should help troubleshooting a
// lot if something goes wrong.
assert(bundleFee.lt(bundleRequiredFee));
return left;
return this.#tokenDecimals;
}
}

View File

@@ -4,6 +4,8 @@ import BundleHandler from "./helpers/BundleHandler.ts";
import AggregationStrategy from "./AggregationStrategy.ts";
import AsyncReturnType from "../helpers/AsyncReturnType.ts";
import ClientReportableError from "./helpers/ClientReportableError.ts";
import nil from "../helpers/nil.ts";
import never from "./helpers/never.ts";
export default function AggregationStrategyRouter(
aggregationStrategy: AggregationStrategy,
@@ -28,7 +30,19 @@ export default function AggregationStrategyRouter(
}
ctx.response.body = {
feeType: aggregationStrategy.config.fees.type,
feeType: (() => {
const feesConfig = aggregationStrategy.config.fees;
if (feesConfig === nil || feesConfig.type === "ether") {
return "ether";
}
if (feesConfig.type === "token") {
return `token:${feesConfig.address}`;
}
never(feesConfig);
})(),
feeDetected: result.feeDetected.toString(),
feeRequired: result.feeRequired.toString(),
successes: result.successes,

View File

@@ -1,9 +1,38 @@
import { HTTPMethods } from "../../deps.ts";
type AppEvent = (
type AppEvent =
| { type: "listening"; data: { port: number } }
| { type: "db-query"; data: { sql: string; params: unknown[] } }
| { type: "db-query"; data: { sql: string; params: unknown } }
| { type: "waiting-unconfirmed-space" }
| {
type: "running-strategy";
data: {
eligibleRows: number;
};
}
| {
type: "completed-strategy";
data: {
includedRows: number;
bundleOverheadCost: string;
expectedFee: string;
expectedMaxCost: string;
};
}
| {
type: "failed-row";
data: {
publicKeyShorts: string[];
submitError?: string;
};
}
| {
type: "aggregate-bundle-unprofitable";
data: {
reason?: string;
};
}
| { type: "unprofitable-despite-breakeven-operations" }
| {
type: "submission-attempt";
data: { publicKeyShorts: string[]; attemptNumber: number };
@@ -19,7 +48,16 @@ type AppEvent = (
| { type: "submission-sent"; data: { hash: string } }
| {
type: "submission-confirmed";
data: { hash: string; bundleHashes: string[], blockNumber: number };
data: {
hash: string;
bundleHashes: string[];
blockNumber: number;
profit: string;
cost: string;
expectedMaxCost: string;
actualFee: string;
expectedFee: string;
};
}
| { type: "warning"; data: string }
| {
@@ -48,7 +86,6 @@ type AppEvent = (
status: number;
duration: number;
};
}
);
};
export default AppEvent;

View File

@@ -23,15 +23,20 @@ export default function BundleRouter(bundleService: BundleService) {
router.get(
"bundleReceipt/:hash",
async (ctx) => {
const receipt = await bundleService.lookupReceipt(ctx.params.hash!);
(ctx) => {
const bundleRow = bundleService.lookupBundle(ctx.params.hash!);
if (receipt === nil) {
if (bundleRow?.receipt === nil) {
ctx.response.status = 404;
ctx.response.body = {
submitError: bundleRow?.submitError,
};
return;
}
ctx.response.body = receipt;
ctx.response.body = bundleService.receiptFromBundle(bundleRow);
},
);

View File

@@ -1,10 +1,11 @@
import {
BigNumber,
BlsWalletSigner,
BlsWalletWrapper,
Bundle,
delay,
ethers,
QueryClient,
Semaphore,
} from "../../deps.ts";
import { IClock } from "../helpers/Clock.ts";
@@ -18,17 +19,18 @@ import runQueryGroup from "./runQueryGroup.ts";
import EthereumService from "./EthereumService.ts";
import AppEvent from "./AppEvent.ts";
import BundleTable, { BundleRow, makeHash } from "./BundleTable.ts";
import countActions from "./helpers/countActions.ts";
import plus from "./helpers/plus.ts";
import AggregationStrategy from "./AggregationStrategy.ts";
import nil from "../helpers/nil.ts";
export type AddBundleResponse = { hash: string } | { failures: TransactionFailure[] };
export type AddBundleResponse = { hash: string } | {
failures: TransactionFailure[];
};
export default class BundleService {
static defaultConfig = {
bundleQueryLimit: env.BUNDLE_QUERY_LIMIT,
maxAggregationSize: env.MAX_AGGREGATION_SIZE,
breakevenOperationCount: env.BREAKEVEN_OPERATION_COUNT,
maxAggregationDelayMillis: env.MAX_AGGREGATION_DELAY_MILLIS,
maxUnconfirmedAggregations: env.MAX_UNCONFIRMED_AGGREGATIONS,
maxEligibilityDelay: env.MAX_ELIGIBILITY_DELAY,
@@ -38,12 +40,7 @@ export default class BundleService {
unconfirmedActionCount = 0;
unconfirmedRowIds = new Set<number>();
// TODO (merge-ok) use database table in the future to persist
confirmedBundles = new Map<string, {
bundle: Bundle,
receipt: ethers.ContractReceipt,
}>();
submissionSemaphore: Semaphore;
submissionTimer: SubmissionTimer;
submissionsInProgress = 0;
@@ -54,7 +51,6 @@ export default class BundleService {
constructor(
public emit: (evt: AppEvent) => void,
public clock: IClock,
public queryClient: QueryClient,
public bundleTableMutex: Mutex,
public bundleTable: BundleTable,
public blsWalletSigner: BlsWalletSigner,
@@ -62,25 +58,24 @@ export default class BundleService {
public aggregationStrategy: AggregationStrategy,
public config = BundleService.defaultConfig,
) {
this.submissionSemaphore = new Semaphore(config.maxUnconfirmedAggregations);
this.submissionTimer = new SubmissionTimer(
clock,
config.maxAggregationDelayMillis,
() => this.runSubmission(),
);
(async () => {
await delay(100);
while (!this.stopping) {
this.tryAggregating();
// TODO (merge-ok): Stop if there aren't any bundles?
await this.ethereumService.waitForNextBlock();
}
})();
this.ethereumService.provider.on("block", this.handleBlock);
}
handleBlock = () => {
this.addTask(() => this.tryAggregating());
};
async stop() {
this.stopping = true;
this.ethereumService.provider.off("block", this.handleBlock);
await Promise.all(Array.from(this.pendingTaskPromises));
this.stopped = true;
}
@@ -108,19 +103,19 @@ export default class BundleService {
return;
}
const eligibleRows = await this.bundleTable.findEligible(
const eligibleRows = this.bundleTable.findEligible(
await this.ethereumService.BlockNumber(),
this.config.bundleQueryLimit,
);
const actionCount = eligibleRows
const opCount = eligibleRows
.filter((r) => !this.unconfirmedRowIds.has(r.id))
.map((r) => countActions(r.bundle))
.map((r) => r.bundle.operations.length)
.reduce(plus, 0);
if (actionCount >= this.config.maxAggregationSize) {
if (opCount >= this.config.breakevenOperationCount) {
this.submissionTimer.trigger();
} else if (actionCount > 0) {
} else if (opCount > 0) {
this.submissionTimer.notifyActive();
} else {
this.submissionTimer.clear();
@@ -130,8 +125,8 @@ export default class BundleService {
runQueryGroup<T>(body: () => Promise<T>): Promise<T> {
return runQueryGroup(
this.emit,
(sql) => this.bundleTable.dbQuery(sql),
this.bundleTableMutex,
this.queryClient,
body,
);
}
@@ -151,15 +146,24 @@ export default class BundleService {
};
}
const signedCorrectly = this.blsWalletSigner.verify(bundle);
const walletAddresses = await Promise.all(bundle.senderPublicKeys.map(
(pubKey) =>
BlsWalletWrapper.AddressFromPublicKey(
pubKey,
this.ethereumService.verificationGateway,
),
));
const failures: TransactionFailure[] = [];
if (signedCorrectly === false) {
failures.push({
type: "invalid-signature",
description: "invalid signature",
});
for (const walletAddr of walletAddresses) {
const signedCorrectly = this.blsWalletSigner.verify(bundle, walletAddr);
if (!signedCorrectly) {
failures.push({
type: "invalid-signature",
description: `invalid signature for wallet address ${walletAddr}`,
});
}
}
failures.push(...await this.ethereumService.checkNonces(bundle));
@@ -171,7 +175,8 @@ export default class BundleService {
return await this.runQueryGroup(async () => {
const hash = makeHash();
await this.bundleTable.add({
this.bundleTable.add({
status: "pending",
hash,
bundle,
eligibleAfter: await this.ethereumService.BlockNumber(),
@@ -192,31 +197,46 @@ export default class BundleService {
});
}
// TODO (merge-ok) Remove lint ignore when this hits db
// deno-lint-ignore require-await
async lookupReceipt(hash: string) {
const confirmation = this.confirmedBundles.get(hash);
lookupBundle(hash: string) {
return this.bundleTable.findBundle(hash);
}
if (!confirmation) {
receiptFromBundle(bundle: BundleRow) {
if (!bundle.receipt) {
return nil;
}
const receipt = confirmation.receipt;
const { receipt, hash } = bundle;
return {
bundleHash: hash,
to: receipt.to,
from: receipt.from,
contractAddress: receipt.contractAddress,
transactionIndex: receipt.transactionIndex,
root: receipt.root,
gasUsed: receipt.gasUsed,
logsBloom: receipt.logsBloom,
blockHash: receipt.blockHash,
transactionHash: receipt.transactionHash,
logs: receipt.logs,
blockNumber: receipt.blockNumber,
confirmations: receipt.confirmations,
cumulativeGasUsed: receipt.cumulativeGasUsed,
effectiveGasPrice: receipt.effectiveGasPrice,
byzantium: receipt.byzantium,
type: receipt.type,
status: receipt.status,
};
}
async runSubmission() {
this.submissionsInProgress++;
const submissionResult = await this.runQueryGroup(async () => {
const bundleSubmitted = await this.runQueryGroup(async () => {
const currentBlockNumber = await this.ethereumService.BlockNumber();
let eligibleRows = await this.bundleTable.findEligible(
let eligibleRows = this.bundleTable.findEligible(
currentBlockNumber,
this.config.bundleQueryLimit,
);
@@ -226,38 +246,80 @@ export default class BundleService {
(row) => !this.unconfirmedRowIds.has(row.id),
);
const { aggregateBundle, includedRows, failedRows } = await this
this.emit({
type: "running-strategy",
data: {
eligibleRows: eligibleRows.length,
},
});
const {
aggregateBundle,
includedRows,
bundleOverheadCost,
expectedFee,
expectedMaxCost,
failedRows,
} = await this
.aggregationStrategy.run(eligibleRows);
this.emit({
type: "completed-strategy",
data: {
includedRows: includedRows.length,
bundleOverheadCost: ethers.utils.formatEther(bundleOverheadCost),
expectedFee: ethers.utils.formatEther(expectedFee),
expectedMaxCost: ethers.utils.formatEther(expectedMaxCost),
},
});
for (const failedRow of failedRows) {
await this.handleFailedRow(failedRow, currentBlockNumber);
this.emit({
type: "failed-row",
data: {
publicKeyShorts: failedRow.bundle.senderPublicKeys.map(
toShortPublicKey,
),
submitError: failedRow.submitError,
},
});
this.handleFailedRow(failedRow, currentBlockNumber);
}
if (!aggregateBundle || includedRows.length === 0) {
return;
return false;
}
await this.submitAggregateBundle(
aggregateBundle,
includedRows,
expectedFee,
expectedMaxCost,
);
return true;
});
this.submissionsInProgress--;
this.addTask(() => this.tryAggregating());
return submissionResult;
if (bundleSubmitted) {
this.addTask(() => this.tryAggregating());
}
}
async handleFailedRow(row: BundleRow, currentBlockNumber: BigNumber) {
handleFailedRow(row: BundleRow, currentBlockNumber: BigNumber) {
if (row.nextEligibilityDelay.lte(this.config.maxEligibilityDelay)) {
await this.bundleTable.update({
this.bundleTable.update({
...row,
eligibleAfter: currentBlockNumber.add(row.nextEligibilityDelay),
nextEligibilityDelay: row.nextEligibilityDelay.mul(2),
});
} else {
await this.bundleTable.remove(row);
this.bundleTable.update({
...row,
status: "failed",
});
}
this.unconfirmedRowIds.delete(row.id);
@@ -266,23 +328,10 @@ export default class BundleService {
async submitAggregateBundle(
aggregateBundle: Bundle,
includedRows: BundleRow[],
expectedFee: BigNumber,
expectedMaxCost: BigNumber,
) {
const maxUnconfirmedActions = (
this.config.maxUnconfirmedAggregations *
this.config.maxAggregationSize
);
const actionCount = countActions(aggregateBundle);
while (
this.unconfirmedActionCount + actionCount > maxUnconfirmedActions
) {
// FIXME (merge-ok): Polling
this.emit({ type: "waiting-unconfirmed-space" });
await delay(1000);
}
this.unconfirmedActionCount += actionCount;
const releaseSemaphore = await this.submissionSemaphore.acquire();
this.unconfirmedBundles.add(aggregateBundle);
for (const row of includedRows) {
@@ -291,36 +340,53 @@ export default class BundleService {
this.addTask(async () => {
try {
const balanceBefore = await this.ethereumService.wallet.getBalance();
const receipt = await this.ethereumService.submitBundle(
aggregateBundle,
Infinity,
300,
);
const balanceAfter = await this.ethereumService.wallet.getBalance();
for (const row of includedRows) {
this.confirmedBundles.set(row.hash, {
bundle: row.bundle,
this.bundleTable.update({
...row,
receipt,
status: "confirmed",
});
}
const profit = balanceAfter.sub(balanceBefore);
/** What we paid to process the bundle */
const cost = receipt.gasUsed.mul(receipt.effectiveGasPrice);
/** Fees collected from users */
const actualFee = profit.add(cost);
this.emit({
type: "submission-confirmed",
data: {
hash: receipt.transactionHash,
bundleHashes: includedRows.map((row) => row.hash),
blockNumber: receipt.blockNumber,
profit: ethers.utils.formatEther(profit),
cost: ethers.utils.formatEther(cost),
expectedMaxCost: ethers.utils.formatEther(expectedMaxCost),
actualFee: ethers.utils.formatEther(actualFee),
expectedFee: ethers.utils.formatEther(expectedFee),
},
});
await this.bundleTable.remove(...includedRows);
} finally {
this.unconfirmedActionCount -= actionCount;
this.unconfirmedBundles.delete(aggregateBundle);
for (const row of includedRows) {
this.unconfirmedRowIds.delete(row.id);
}
releaseSemaphore();
}
});
}

View File

@@ -3,38 +3,47 @@ import {
Bundle,
bundleFromDto,
bundleToDto,
Constraint,
CreateTableMode,
DataType,
ethers,
QueryClient,
QueryTable,
TableOptions,
unsketchify,
sqlite,
} from "../../deps.ts";
import assertExists from "../helpers/assertExists.ts";
import ExplicitAny from "../helpers/ExplicitAny.ts";
import { parseBundleDto } from "./parsers.ts";
import nil from "../helpers/nil.ts";
import assert from "../helpers/assert.ts";
/**
* Representation used when talking to the database. It's 'raw' in the sense
* that it only uses primitive types, because the database cannot know about
* custom classes like BigNumber.
*
* Note that this isn't as raw as it used to be - sqlite returns each row as an
* array. This is still the raw representation of each field though.
*/
type RawRow = {
id: number;
status: string;
hash: string;
bundle: string;
eligibleAfter: string;
nextEligibilityDelay: string;
submitError: string | null;
receipt: string | null;
};
const BundleStatuses = ["pending", "confirmed", "failed"] as const;
type BundleStatus = typeof BundleStatuses[number];
type Row = {
id: number;
status: BundleStatus;
hash: string;
bundle: Bundle;
eligibleAfter: BigNumber;
nextEligibilityDelay: BigNumber;
submitError?: string;
receipt?: ethers.ContractReceipt;
};
type InsertRow = Omit<Row, "id">;
@@ -48,132 +57,233 @@ export function makeHash() {
export type BundleRow = Row;
const tableOptions: TableOptions = {
id: { type: DataType.Serial, constraint: Constraint.PrimaryKey },
hash: { type: DataType.VarChar },
bundle: { type: DataType.VarChar },
eligibleAfter: { type: DataType.VarChar },
nextEligibilityDelay: { type: DataType.VarChar },
};
function fromRawRow(rawRow: RawRow): Row {
const parseResult = parseBundleDto(JSON.parse(rawRow.bundle));
if ("failures" in parseResult) {
throw new Error(parseResult.failures.join("\n"));
function fromRawRow(rawRow: RawRow | sqlite.Row): Row {
if (Array.isArray(rawRow)) {
rawRow = {
id: rawRow[0] as number,
status: rawRow[1] as string,
hash: rawRow[2] as string,
bundle: rawRow[3] as string,
eligibleAfter: rawRow[4] as string,
nextEligibilityDelay: rawRow[5] as string,
submitError: rawRow[6] as string | null,
receipt: rawRow[7] as string | null,
};
}
const parseBundleResult = parseBundleDto(
JSON.parse(rawRow.bundle),
);
if ("failures" in parseBundleResult) {
throw new Error(parseBundleResult.failures.join("\n"));
}
const status = rawRow.status;
if (!isValidStatus(status)) {
throw new Error(`Not a valid bundle status: ${status}`);
}
const rawReceipt = rawRow.receipt;
const receipt: ethers.ContractReceipt = rawReceipt
? JSON.parse(rawReceipt)
: nil;
return {
...rawRow,
bundle: bundleFromDto(parseResult.success),
id: rawRow.id,
status,
hash: rawRow.hash,
bundle: bundleFromDto(parseBundleResult.success),
eligibleAfter: BigNumber.from(rawRow.eligibleAfter),
nextEligibilityDelay: BigNumber.from(rawRow.nextEligibilityDelay),
submitError: rawRow.submitError ?? nil,
receipt,
};
}
function toInsertRawRow(row: InsertRow): InsertRawRow {
return {
...row,
submitError: row.submitError ?? null,
bundle: JSON.stringify(bundleToDto(row.bundle)),
eligibleAfter: toUint256Hex(row.eligibleAfter),
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
receipt: JSON.stringify(row.receipt),
};
}
function toRawRow(row: Row): RawRow {
return {
...row,
bundle: JSON.stringify(bundleToDto(row.bundle)),
id: row.id,
status: row.status,
hash: row.hash,
bundle: JSON.stringify(row.bundle),
eligibleAfter: toUint256Hex(row.eligibleAfter),
nextEligibilityDelay: toUint256Hex(row.nextEligibilityDelay),
submitError: row.submitError ?? null,
receipt: JSON.stringify(row.receipt),
};
}
export default class BundleTable {
queryTable: QueryTable<RawRow>;
safeName: string;
private constructor(public queryClient: QueryClient, tableName: string) {
this.queryTable = this.queryClient.table<RawRow>(tableName);
this.safeName = unsketchify(this.queryTable.name);
}
static async create(
queryClient: QueryClient,
tableName: string,
): Promise<BundleTable> {
const table = new BundleTable(queryClient, tableName);
await table.queryTable.create(tableOptions, CreateTableMode.IfNotExists);
return table;
}
static async createFresh(
queryClient: QueryClient,
tableName: string,
constructor(
public db: sqlite.DB,
public onQuery = (_sql: string, _params?: sqlite.QueryParameterSet) => {},
) {
const table = new BundleTable(queryClient, tableName);
await table.queryTable.drop(true);
await table.queryTable.create(tableOptions, CreateTableMode.IfNotExists);
return table;
this.dbQuery(`
CREATE TABLE IF NOT EXISTS bundles (
id INTEGER PRIMARY KEY AUTOINCREMENT,
status TEXT NOT NULL,
hash TEXT NOT NULL,
bundle TEXT NOT NULL,
eligibleAfter TEXT NOT NULL,
nextEligibilityDelay TEXT NOT NULL,
submitError TEXT,
receipt TEXT
)
`);
}
async add(...rows: InsertRow[]) {
await this.queryTable.insert(...rows.map(toInsertRawRow));
dbQuery(sql: string, params?: sqlite.QueryParameterSet) {
this.onQuery(sql, params);
return this.db.query(sql, params);
}
async update(row: Row) {
await this.queryTable.where({ id: row.id }).update(toRawRow(row));
add(...rows: InsertRow[]) {
for (const row of rows) {
const rawRow = toInsertRawRow(row);
this.dbQuery(
`
INSERT INTO bundles (
id,
status,
hash,
bundle,
eligibleAfter,
nextEligibilityDelay,
submitError,
receipt
) VALUES (
:id,
:status,
:hash,
:bundle,
:eligibleAfter,
:nextEligibilityDelay,
:submitError,
:receipt
)
`,
{
":status": rawRow.status,
":hash": rawRow.hash,
":bundle": rawRow.bundle,
":eligibleAfter": rawRow.eligibleAfter,
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
":submitError": rawRow.submitError,
":receipt": rawRow.receipt,
},
);
}
}
async remove(...rows: Row[]) {
await Promise.all(rows.map((row) =>
this.queryTable
.where({ id: assertExists(row.id) })
.delete()
));
}
update(row: Row) {
const rawRow = toRawRow(row);
async findEligible(blockNumber: BigNumber, limit: number) {
const rows: RawRow[] = await this.queryClient.query(
this.dbQuery(
`
SELECT * from ${this.safeName}
UPDATE bundles
SET
status = :status,
hash = :hash,
bundle = :bundle,
eligibleAfter = :eligibleAfter,
nextEligibilityDelay = :nextEligibilityDelay,
submitError = :submitError,
receipt = :receipt
WHERE
"eligibleAfter" <= '${toUint256Hex(blockNumber)}'
ORDER BY "id" ASC
LIMIT ${limit}
id = :id
`,
{
":id": rawRow.id,
":status": rawRow.status,
":hash": rawRow.hash,
":bundle": rawRow.bundle,
":eligibleAfter": rawRow.eligibleAfter,
":nextEligibilityDelay": rawRow.nextEligibilityDelay,
":submitError": rawRow.submitError,
":receipt": rawRow.receipt,
},
);
}
remove(...rows: Row[]) {
for (const row of rows) {
this.dbQuery(
"DELETE FROM bundles WHERE id = :id",
{ ":id": assertExists(row.id) },
);
}
}
findEligible(blockNumber: BigNumber, limit: number): Row[] {
const rows = this.dbQuery(
`
SELECT * from bundles
WHERE
eligibleAfter <= '${toUint256Hex(blockNumber)}' AND
status = 'pending'
ORDER BY id ASC
LIMIT :limit
`,
{
":limit": limit,
},
);
return rows.map(fromRawRow);
}
async count(): Promise<bigint> {
const result = await this.queryClient.query(
`SELECT COUNT(*) FROM ${this.queryTable.name}`,
findBundle(hash: string): Row | nil {
const rows = this.dbQuery(
"SELECT * from bundles WHERE hash = :hash",
{ ":hash": hash },
);
return result[0].count as bigint;
return rows.map(fromRawRow)[0];
}
async all(): Promise<Row[]> {
const rawRows: RawRow[] = await this.queryClient.query(
`SELECT * FROM ${this.queryTable.name}`,
count(): number {
const result = this.dbQuery("SELECT COUNT(*) FROM bundles")[0][0];
assert(typeof result === "number");
return result;
}
all(): Row[] {
const rawRows = this.dbQuery(
"SELECT * FROM bundles",
);
return rawRows.map(fromRawRow);
}
async drop() {
await this.queryTable.drop(true);
drop() {
this.dbQuery("DROP TABLE bundles");
}
async clear() {
return await this.queryClient.query(`
DELETE from ${this.safeName}
`);
clear() {
this.dbQuery("DELETE from bundles");
}
}
function toUint256Hex(n: BigNumber) {
return `0x${n.toHexString().slice(2).padStart(64, "0")}`;
}
function isValidStatus(status: unknown): status is BundleStatus {
return typeof status === "string" &&
BundleStatuses.includes(status as ExplicitAny);
}

View File

@@ -22,6 +22,7 @@ import AppEvent from "./AppEvent.ts";
import toPublicKeyShort from "./helpers/toPublicKeyShort.ts";
import AsyncReturnType from "../helpers/AsyncReturnType.ts";
import ExplicitAny from "../helpers/ExplicitAny.ts";
import nil from "../helpers/nil.ts";
export type TxCheckResult = {
failures: TransactionFailure[];
@@ -42,10 +43,9 @@ type CallHelper<T> = {
resultDecoder: (result: BytesLike) => T;
};
type CallResult<T> = (
type CallResult<T> =
| { success: true; returnValue: T }
| { success: false; returnValue: undefined }
);
| { success: false; returnValue: undefined };
type MapCallHelperReturns<T> = T extends CallHelper<unknown>[]
? (T extends [CallHelper<infer First>, ...infer Rest]
@@ -71,6 +71,8 @@ export default class EthereumService {
constructor(
public emit: (evt: AppEvent) => void,
public wallet: Wallet,
public provider: ethers.providers.Provider,
public blsWalletWrapper: BlsWalletWrapper,
public blsWalletSigner: BlsWalletSigner,
verificationGatewayAddress: string,
utilitiesAddress: string,
@@ -99,14 +101,49 @@ export default class EthereumService {
utilitiesAddress: string,
aggPrivateKey: string,
): Promise<EthereumService> {
const wallet = EthereumService.Wallet(aggPrivateKey);
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
provider.pollingInterval = env.RPC_POLLING_INTERVAL;
const wallet = EthereumService.Wallet(provider, aggPrivateKey);
const blsWalletWrapper = await BlsWalletWrapper.connect(
aggPrivateKey,
verificationGatewayAddress,
provider,
);
const blsNonce = await blsWalletWrapper.Nonce();
if (blsNonce.eq(0)) {
if (!env.AUTO_CREATE_INTERNAL_BLS_WALLET) {
throw new Error([
"Required internal bls wallet does not exist. Either enable",
"AUTO_CREATE_INTERNAL_BLS_WALLET or run",
"./programs/createInternalBlsWallet.ts",
].join(" "));
}
await (await VerificationGateway__factory.connect(
verificationGatewayAddress,
wallet,
).processBundle(blsWalletWrapper.sign({
nonce: 0,
actions: [],
}))).wait();
}
const nextNonce = BigNumber.from(await wallet.getTransactionCount());
const chainId = await wallet.getChainId();
const blsWalletSigner = await initBlsWalletSigner({ chainId });
const blsWalletSigner = await initBlsWalletSigner({
chainId,
privateKey: aggPrivateKey,
verificationGatewayAddress,
});
return new EthereumService(
emit,
wallet,
provider,
blsWalletWrapper,
blsWalletSigner,
verificationGatewayAddress,
utilitiesAddress,
@@ -116,16 +153,10 @@ export default class EthereumService {
async BlockNumber(): Promise<BigNumber> {
return BigNumber.from(
await this.wallet.provider.getBlockNumber(),
await this.provider.getBlockNumber(),
);
}
async waitForNextBlock() {
await new Promise((resolve) => {
this.wallet.provider.once("block", resolve);
});
}
// TODO (merge-ok): Consider: We may want to fail operations
// that are not at the next expected nonce, including all
// current pending transactions for that wallet.
@@ -211,10 +242,10 @@ export default class EthereumService {
async callStaticSequenceWithMeasure<Measure, CallReturn>(
measureCall: CallHelper<Measure>,
calls: CallHelper<CallReturn>[],
): (Promise<{
): Promise<{
measureResults: CallResult<Measure>[];
callResults: CallResult<CallReturn>[];
}>) {
}> {
const fullCalls: CallHelper<unknown>[] = [measureCall];
for (const call of calls) {
@@ -261,7 +292,10 @@ export default class EthereumService {
const processBundleArgs: Parameters<VerificationGateway["processBundle"]> =
[
bundle,
{ nonce: this.NextNonce() },
{
nonce: this.NextNonce(),
...await this.GasConfig(),
},
];
const attempt = async () => {
@@ -331,8 +365,38 @@ export default class EthereumService {
throw new Error("Expected return or throw from attempt loop");
}
private static Wallet(privateKey: string) {
const provider = new ethers.providers.JsonRpcProvider(env.RPC_URL);
async GasConfig() {
const block = await this.provider.getBlock("latest");
const previousBaseFee = block.baseFeePerGas;
assert(previousBaseFee !== null && previousBaseFee !== nil);
// Increase the basefee we're willing to pay to improve the chance of our
// transaction getting included. As per EIP-1559, we only pay the actual
// basefee anyway, *but* we also pass this fee onto users which don't have
// this benefit (they'll pay regardless of where basefee lands).
//
// This means there's a tradeoff here - low values risk our transactions not
// being included, high values pass on unnecessary fees to users.
//
const baseFeeIncrease = previousBaseFee.mul(
env.PREVIOUS_BASE_FEE_PERCENT_INCREASE,
).div(100);
return {
maxFeePerGas: previousBaseFee
.add(baseFeeIncrease)
// Remember that basefee is burned, not provided to miners. Miners
// *only* get the priority fee, so they have no reason to care about our
// transaction if the priority fee is zero.
.add(env.PRIORITY_FEE_PER_GAS),
maxPriorityFeePerGas: env.PRIORITY_FEE_PER_GAS,
};
}
private static Wallet(
provider: ethers.providers.Provider,
privateKey: string,
) {
const wallet = new Wallet(privateKey, provider);
if (env.USE_TEST_NET) {

View File

@@ -1,4 +1,4 @@
import { Application, oakCors } from "../../deps.ts";
import { Application, oakCors, sqlite } from "../../deps.ts";
import * as env from "../env.ts";
import EthereumService from "./EthereumService.ts";
@@ -8,7 +8,6 @@ import AdminRouter from "./AdminRouter.ts";
import AdminService from "./AdminService.ts";
import errorHandler from "./errorHandler.ts";
import notFoundHandler from "./notFoundHandler.ts";
import createQueryClient from "./createQueryClient.ts";
import Mutex from "../helpers/Mutex.ts";
import Clock from "../helpers/Clock.ts";
import getNetworkConfig from "../helpers/getNetworkConfig.ts";
@@ -22,11 +21,18 @@ export default async function app(emit: (evt: AppEvent) => void) {
const clock = Clock.create();
const queryClient = createQueryClient(emit);
const bundleTableMutex = new Mutex();
const bundleTable = await BundleTable.create(
queryClient,
env.BUNDLE_TABLE_NAME,
const bundleTable = new BundleTable(
new sqlite.DB(env.DB_PATH),
(sql, params) => {
if (env.LOG_QUERIES) {
emit({
type: "db-query",
data: { sql, params },
});
}
},
);
const ethereumService = await EthereumService.create(
@@ -39,12 +45,13 @@ export default async function app(emit: (evt: AppEvent) => void) {
const aggregationStrategy = new AggregationStrategy(
ethereumService.blsWalletSigner,
ethereumService,
AggregationStrategy.defaultConfig,
emit,
);
const bundleService = new BundleService(
emit,
clock,
queryClient,
bundleTableMutex,
bundleTable,
ethereumService.blsWalletSigner,

View File

@@ -1,39 +0,0 @@
import { QueryClient } from "../../deps.ts";
import * as env from "../env.ts";
import AppEvent from "./AppEvent.ts";
export default function createQueryClient(
emit: (evt: AppEvent) => void,
/**
* Sadly, there appears to be a singleton inside QueryClient, which forces us
* to re-use it during testing.
*/
existingClient?: QueryClient,
): QueryClient {
const client = existingClient ?? new QueryClient({
hostname: env.PG.HOST,
port: env.PG.PORT,
user: env.PG.USER,
password: env.PG.PASSWORD,
database: env.PG.DB_NAME,
tls: {
enforce: false,
},
});
if (env.LOG_QUERIES) {
const originalQuery = client.query.bind(client);
client.query = async (sql, params) => {
emit({
type: "db-query",
data: { sql, params: params ?? [] },
});
return await originalQuery(sql, params);
};
}
return client;
}

View File

@@ -1,6 +0,0 @@
import { Bundle } from "../../../deps.ts";
import plus from "./plus.ts";
export default function countActions(bundle: Bundle) {
return bundle.operations.map((op) => op.actions.length).reduce(plus, 0);
}

View File

@@ -0,0 +1,3 @@
export default function never(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}

View File

@@ -1,18 +1,17 @@
import { QueryClient } from "../../deps.ts";
import Mutex from "../helpers/Mutex.ts";
import AppEvent from "./AppEvent.ts";
export default async function runQueryGroup<T>(
emit: (evt: AppEvent) => void,
query: (sql: string) => void,
mutex: Mutex,
queryClient: QueryClient,
body: () => Promise<T>,
) {
const lock = await mutex.Lock();
let completed = false;
try {
queryClient.query("BEGIN");
query("BEGIN");
const result = await body();
completed = true;
return result;
@@ -25,6 +24,6 @@ export default async function runQueryGroup<T>(
throw error;
} finally {
lock.release();
await queryClient.query(completed ? "COMMIT" : "ROLLBACK");
query(completed ? "COMMIT" : "ROLLBACK");
}
}

View File

@@ -1,42 +1,49 @@
import {
optionalNumberEnv,
requireBigNumberEnv,
requireBoolEnv,
requireEnv,
requireIntEnv,
requireNumberEnv,
} from "./helpers/envTools.ts";
import nil from "./helpers/nil.ts";
export const RPC_URL = requireEnv("RPC_URL");
export const RPC_POLLING_INTERVAL = requireIntEnv("RPC_POLLING_INTERVAL");
export const ORIGIN = requireEnv("ORIGIN");
export const PORT = requireIntEnv("PORT");
export const USE_TEST_NET = requireBoolEnv("USE_TEST_NET");
export const NETWORK_CONFIG_PATH = requireEnv("NETWORK_CONFIG_PATH");
export const NETWORK_CONFIG_PATH = Deno.env.get("IS_DOCKER") === "true"
? "/app/networkConfig.json"
: requireEnv("NETWORK_CONFIG_PATH");
export const PRIVATE_KEY_AGG = requireEnv("PRIVATE_KEY_AGG");
export const PRIVATE_KEY_ADMIN = requireEnv("PRIVATE_KEY_ADMIN");
export const PG = {
HOST: requireEnv("PG_HOST"),
PORT: requireEnv("PG_PORT"),
USER: requireEnv("PG_USER"),
PASSWORD: requireEnv("PG_PASSWORD"),
DB_NAME: requireEnv("PG_DB_NAME"),
};
export const BUNDLE_TABLE_NAME = requireEnv("BUNDLE_TABLE_NAME");
export const DB_PATH = requireEnv("DB_PATH");
/**
* Query limit used when processing potentially large numbers of bundles.
* (Using batching if needed.)
*/
export const BUNDLE_QUERY_LIMIT = requireIntEnv("BUNDLE_QUERY_LIMIT");
/**
* Maximum retry delay in blocks before a failed bundle is discarded.
*/
export const MAX_ELIGIBILITY_DELAY = requireIntEnv("MAX_ELIGIBILITY_DELAY");
export const MAX_AGGREGATION_SIZE = requireIntEnv("MAX_AGGREGATION_SIZE");
/**
* Approximate maximum gas of aggregate bundles.
*
* It's approximate because we use the sum of the marginal gas estimates and add
* the bundle overhead, which is not exactly the same as the gas used when
* putting the bundle together.
*/
export const MAX_GAS_PER_BUNDLE = requireIntEnv("MAX_GAS_PER_BUNDLE");
export const MAX_AGGREGATION_DELAY_MILLIS = requireIntEnv(
"MAX_AGGREGATION_DELAY_MILLIS",
@@ -48,10 +55,43 @@ export const MAX_UNCONFIRMED_AGGREGATIONS = requireIntEnv(
export const LOG_QUERIES = requireBoolEnv("LOG_QUERIES");
export const REQUIRE_FEES = requireBoolEnv("REQUIRE_FEES");
export const BREAKEVEN_OPERATION_COUNT = requireNumberEnv(
"BREAKEVEN_OPERATION_COUNT",
);
export const ALLOW_LOSSES = requireBoolEnv("ALLOW_LOSSES");
export const FEE_TYPE = requireEnv("FEE_TYPE");
export const FEE_PER_GAS = requireBigNumberEnv("FEE_PER_GAS");
export const FEE_PER_BYTE = requireBigNumberEnv("FEE_PER_BYTE");
if (!/^(ether|token:0x[0-9a-fA-F]*)$/.test(FEE_TYPE)) {
throw new Error(`FEE_TYPE has invalid format: "${FEE_TYPE}"`);
}
export const ETH_VALUE_IN_TOKENS = optionalNumberEnv("ETH_VALUE_IN_TOKENS");
if (FEE_TYPE.startsWith("token:") && ETH_VALUE_IN_TOKENS === nil) {
throw new Error([
"Missing ETH_VALUE_IN_TOKENS, which is required because FEE_TYPE is a",
"token",
].join(" "));
}
export const AUTO_CREATE_INTERNAL_BLS_WALLET = requireBoolEnv(
"AUTO_CREATE_INTERNAL_BLS_WALLET",
);
export const PRIORITY_FEE_PER_GAS = requireBigNumberEnv("PRIORITY_FEE_PER_GAS");
/**
* Used to determine the expected basefee when submitting bundles. Note that
* this gets passed onto users.
*/
export const PREVIOUS_BASE_FEE_PERCENT_INCREASE = requireNumberEnv(
"PREVIOUS_BASE_FEE_PERCENT_INCREASE",
);
export const BUNDLE_CHECKING_CONCURRENCY = requireIntEnv(
"BUNDLE_CHECKING_CONCURRENCY",
);

View File

@@ -62,3 +62,19 @@ export function requireNumberEnv(envName: string): number {
return value;
}
export function optionalNumberEnv(envName: string): number | nil {
const strValue = optionalEnv(envName);
if (strValue === nil) {
return nil;
}
const value = Number(strValue);
if (!Number.isFinite(value)) {
throw new Error(`Failed to parse ${envName} as number: ${strValue}`);
}
return value;
}

View File

@@ -1,10 +1,12 @@
import AggregationStrategy from "../src/app/AggregationStrategy.ts";
import { BundleRow } from "../src/app/BundleTable.ts";
import { assertEquals, BigNumber } from "./deps.ts";
import assert from "../src/helpers/assert.ts";
import nil from "../src/helpers/nil.ts";
import { assertEquals, BigNumber, ethers } from "./deps.ts";
import Fixture from "./helpers/Fixture.ts";
Fixture.test("zero fee estimate from default test config", async (fx) => {
Fixture.test("nonzero fee estimate from default test config", async (fx) => {
const [wallet] = await fx.setupWallets(1);
const bundle = wallet.sign({
@@ -23,11 +25,9 @@ Fixture.test("zero fee estimate from default test config", async (fx) => {
const feeEstimation = await fx.aggregationStrategy.estimateFee(bundle);
assertEquals(feeEstimation, {
feeDetected: BigNumber.from(0),
feeRequired: BigNumber.from(0),
successes: [true],
});
assertEquals(feeEstimation.feeDetected, BigNumber.from(0));
assert(feeEstimation.feeRequired.gt(0));
assertEquals(feeEstimation.successes, [true]);
});
Fixture.test("includes bundle in aggregation when estimated fee is provided", async (fx) => {
@@ -37,12 +37,15 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
fx.blsWalletSigner,
fx.ethereumService,
{
maxAggregationSize: 12,
maxGasPerBundle: 1500000,
fees: {
type: `token:${fx.testErc20.address}`,
perGas: BigNumber.from(1000000000),
perByte: BigNumber.from(10000000000000),
type: "token",
address: fx.testErc20.address,
allowLosses: true,
breakevenOperationCount: 4.5,
ethValueInTokens: 1300,
},
bundleCheckingConcurrency: 8,
},
);
@@ -70,7 +73,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
const feeEstimation = await aggregationStrategy.estimateFee(bundle);
const safetyDivisor = 100;
const safetyDivisor = 5;
const safetyPremium = feeEstimation.feeRequired.div(safetyDivisor);
// Due to small fluctuations is gas estimation, we add a little safety premium
@@ -97,6 +100,7 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
const bundleRow: BundleRow = {
id: 0,
status: "pending",
hash: "0x0",
bundle,
eligibleAfter: BigNumber.from(0),
@@ -105,9 +109,69 @@ Fixture.test("includes bundle in aggregation when estimated fee is provided", as
const aggregationResult = await aggregationStrategy.run([bundleRow]);
assertEquals(aggregationResult, {
aggregateBundle: bundle,
includedRows: [bundleRow],
failedRows: [],
});
assertEquals(aggregationResult.aggregateBundle, bundle);
assertEquals(aggregationResult.includedRows, [bundleRow]);
assertEquals(aggregationResult.failedRows, []);
});
Fixture.test("includes submitError on failed row when bundle callStaticSequence fails", async (fx) => {
const [wallet] = await fx.setupWallets(1);
const aggregationStrategy = new AggregationStrategy(
fx.blsWalletSigner,
fx.ethereumService,
{
maxGasPerBundle: 1500000,
fees: {
type: "token",
address: fx.testErc20.address,
allowLosses: true,
breakevenOperationCount: 4.5,
ethValueInTokens: 1300,
},
bundleCheckingConcurrency: 8,
},
);
const nonce = await wallet.Nonce();
const bundle = wallet.sign({
nonce,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"transferFrom",
[
"0x0000000000000000000000000000000000000000",
wallet.address,
ethers.BigNumber.from(
"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
),
],
),
},
],
});
const bundleRow: BundleRow = {
id: 0,
status: "pending",
hash: "0x0",
bundle,
eligibleAfter: BigNumber.from(0),
nextEligibilityDelay: BigNumber.from(1),
};
const aggregationResult = await aggregationStrategy.run([bundleRow]);
const expectedFailedRow = {
...bundleRow,
submitError: "ERC20: insufficient allowance",
};
assertEquals(aggregationResult.aggregateBundle, nil);
assertEquals(aggregationResult.includedRows, []);
assertEquals(aggregationResult.failedRows, [expectedFailedRow]);
});

View File

@@ -1,9 +1,9 @@
import { assertEquals, assertBundleSucceeds, Operation } from "./deps.ts";
import { assertBundleSucceeds, assertEquals, Operation } from "./deps.ts";
import Fixture from "./helpers/Fixture.ts";
Fixture.test("adds valid bundle", async (fx) => {
const bundleService = await fx.createBundleService();
const bundleService = fx.createBundleService();
const [wallet] = await fx.setupWallets(1);
const tx = wallet.sign({
@@ -20,15 +20,15 @@ Fixture.test("adds valid bundle", async (fx) => {
],
});
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
assertBundleSucceeds(await bundleService.add(tx));
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
});
Fixture.test("rejects bundle with invalid signature", async (fx) => {
const bundleService = await fx.createBundleService();
const bundleService = fx.createBundleService();
const [wallet, otherWallet] = await fx.setupWallets(2);
const operation: Operation = {
@@ -53,7 +53,7 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
// sig test)
tx.signature = otherTx.signature;
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
const res = await bundleService.add(tx);
if ("hash" in res) {
@@ -62,11 +62,11 @@ Fixture.test("rejects bundle with invalid signature", async (fx) => {
assertEquals(res.failures.map((f) => f.type), ["invalid-signature"]);
// Bundle table remains empty
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
});
Fixture.test("rejects bundle with nonce from the past", async (fx) => {
const bundleService = await fx.createBundleService();
const bundleService = fx.createBundleService();
const [wallet] = await fx.setupWallets(1);
const tx = wallet.sign({
@@ -83,7 +83,7 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
],
});
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
const res = await bundleService.add(tx);
if ("hash" in res) {
@@ -92,13 +92,13 @@ Fixture.test("rejects bundle with nonce from the past", async (fx) => {
assertEquals(res.failures.map((f) => f.type), ["duplicate-nonce"]);
// Bundle table remains empty
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
});
Fixture.test(
"rejects bundle with invalid signature and nonce from the past",
async (fx) => {
const bundleService = await fx.createBundleService();
const bundleService = fx.createBundleService();
const [wallet, otherWallet] = await fx.setupWallets(2);
const operation: Operation = {
@@ -125,7 +125,7 @@ Fixture.test(
// https://github.com/thehubbleproject/hubble-bls/pull/20
tx.signature = otherTx.signature;
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
const res = await bundleService.add(tx);
if ("hash" in res) {
@@ -138,12 +138,12 @@ Fixture.test(
);
// Bundle table remains empty
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
},
);
Fixture.test("adds bundle with future nonce", async (fx) => {
const bundleService = await fx.createBundleService();
const bundleService = fx.createBundleService();
const [wallet] = await fx.setupWallets(1);
const tx = wallet.sign({
@@ -160,11 +160,11 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
],
});
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 0);
assertBundleSucceeds(await bundleService.add(tx));
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
});
// TODO (merge-ok): Add a mechanism for limiting the number of stored
@@ -172,7 +172,7 @@ Fixture.test("adds bundle with future nonce", async (fx) => {
// Fixture.test(
// "when future txs reach maxFutureTxs, the oldest ones are dropped",
// async (fx) => {
// const bundleService = await fx.createBundleService({
// const bundleService = fx.createBundleService({
// ...BundleService.defaultConfig,
// maxFutureTxs: 3,
// });

View File

@@ -1,7 +1,6 @@
import Range from "../src/helpers/Range.ts";
import {
assertEquals,
assertBundleSucceeds,
assertEquals,
BigNumber,
BlsWalletWrapper,
ethers,
@@ -14,23 +13,21 @@ import Fixture, {
const oneToken = ethers.utils.parseUnits("1.0", 18);
async function createBundleService(
function createBundleService(
fx: Fixture,
feesOverride?: Partial<typeof aggregationStrategyDefaultTestConfig["fees"]>,
feesOverride?: typeof aggregationStrategyDefaultTestConfig["fees"],
) {
return await fx.createBundleService(
{
...bundleServiceDefaultTestConfig,
maxAggregationSize: 24,
},
return fx.createBundleService(
bundleServiceDefaultTestConfig,
{
...aggregationStrategyDefaultTestConfig,
maxAggregationSize: 24,
fees: {
type: `token:${fx.testErc20.address}`,
perGas: BigNumber.from(10_000_000_000),
perByte: BigNumber.from(100_000_000_000_000),
...feesOverride,
maxGasPerBundle: 3000000,
fees: feesOverride ?? {
type: "token",
address: fx.testErc20.address,
allowLosses: true,
breakevenOperationCount: 4.5,
ethValueInTokens: 1300,
},
},
);
@@ -67,7 +64,7 @@ function approveAndSendTokensToOrigin(
}
Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
const bundleService = await createBundleService(fx);
const bundleService = createBundleService(fx);
const [wallet] = await fx.setupWallets(1);
@@ -91,7 +88,7 @@ Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
await fx.testErc20.balanceOf(wallet.address),
BigNumber.from(1000),
);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
@@ -101,11 +98,11 @@ Fixture.test("does not submit bundle with insufficient fee", async (fx) => {
await fx.testErc20.balanceOf(wallet.address),
BigNumber.from(1000),
);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
});
Fixture.test("submits bundle with sufficient token fee", async (fx) => {
const bundleService = await createBundleService(fx);
const bundleService = createBundleService(fx);
const [wallet] = await fx.setupWallets(1, {
tokenBalance: oneToken,
@@ -115,21 +112,28 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
approveAndSendTokensToOrigin(fx, await wallet.Nonce(), oneToken),
);
assertBundleSucceeds(await bundleService.add(bundle));
const bundleResponse = await bundleService.add(bundle);
assertBundleSucceeds(bundleResponse);
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
oneToken,
);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
await bundleService.waitForConfirmations();
assertEquals(await bundleService.bundleTable.count(), 0n);
if ("failures" in bundleResponse) {
throw new Error("Bundle failed to be created");
}
const bundleRow = await bundleService.bundleTable.findBundle(
bundleResponse.hash,
);
assertEquals(bundleRow?.status, "confirmed");
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
BigNumber.from(0),
@@ -137,22 +141,47 @@ Fixture.test("submits bundle with sufficient token fee", async (fx) => {
});
Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
const bundleService = await createBundleService(fx, {
const es = fx.ethereumService;
const bundleService = createBundleService(fx, {
type: "ether",
perByte: BigNumber.from(1),
perGas: BigNumber.from(1),
allowLosses: true,
breakevenOperationCount: 4.5,
});
const fee = BigNumber.from(2_000_000); // wei
const [wallet] = await fx.setupWallets(1, { tokenBalance: 0 });
const nonce = await wallet.Nonce();
await (await fx.adminWallet.sendTransaction({
to: wallet.address,
value: fee,
value: 1,
})).wait();
const es = fx.ethereumService;
const estimation = await bundleService.aggregationStrategy.estimateFee(
wallet.sign({
nonce,
actions: [
{
ethValue: 1,
contractAddress: es.utilities.address,
encodedFunction: es.utilities.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
],
}),
);
assertEquals(estimation.successes, [true]);
const fee = estimation.feeRequired
.add(estimation.feeRequired.div(5)); // +20% safety margin
await (await fx.adminWallet.sendTransaction({
to: wallet.address,
value: fee
.sub(1), // Already sent 1 wei before
})).wait();
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
@@ -167,21 +196,28 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
],
});
assertBundleSucceeds(await bundleService.add(bundle));
const bundleResponse = await bundleService.add(bundle);
assertBundleSucceeds(bundleResponse);
assertEquals(
await fx.adminWallet.provider.getBalance(wallet.address),
fee,
);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
await bundleService.waitForConfirmations();
assertEquals(await bundleService.bundleTable.count(), 0n);
if ("failures" in bundleResponse) {
throw new Error("Bundle failed to be created");
}
const bundleRow = await bundleService.bundleTable.findBundle(
bundleResponse.hash,
);
assertEquals(bundleRow?.status, "confirmed");
assertEquals(
await fx.adminWallet.provider.getBalance(wallet.address),
BigNumber.from(0),
@@ -189,18 +225,24 @@ Fixture.test("submits bundle with sufficient eth fee", async (fx) => {
});
Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) => {
const bundleService = await createBundleService(fx);
const breakevenOperationCount = 4.5;
const [wallet1, wallet2] = await fx.setupWallets(2, {
const bundleService = createBundleService(fx, {
type: "token",
address: fx.testErc20.address,
allowLosses: true,
breakevenOperationCount,
ethValueInTokens: 1,
});
const wallets = await fx.setupWallets(10, {
tokenBalance: oneToken.mul(10),
});
const nonce1 = await wallet1.Nonce();
const nonce2 = await wallet2.Nonce();
const nonce = await wallets[0].Nonce();
async function addBundle(
wallet: BlsWalletWrapper,
nonce: BigNumber,
fee: BigNumber,
) {
const bundle = wallet.sign(
@@ -210,152 +252,49 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient fee", async (fx) =>
assertBundleSucceeds(await bundleService.add(bundle));
}
// 6 good bundles from wallet 1 (each pays one token)
await addBundle(wallet1, nonce1.add(0), oneToken);
await addBundle(wallet1, nonce1.add(1), oneToken);
await addBundle(wallet1, nonce1.add(2), oneToken);
await addBundle(wallet1, nonce1.add(3), oneToken);
await addBundle(wallet1, nonce1.add(4), oneToken);
await addBundle(wallet1, nonce1.add(5), oneToken);
// For the purposes of this test, we don't want the bundleService prematurely
// running a submission on fewer bundles than we're trying to process
bundleService.config.breakevenOperationCount = Infinity;
// 6 good bundles
await addBundle(wallets[0], oneToken);
await addBundle(wallets[1], oneToken);
await addBundle(wallets[2], oneToken);
await addBundle(wallets[3], oneToken);
await addBundle(wallets[4], oneToken);
await addBundle(wallets[5], oneToken);
// 7th bundle should fail because 1 wei is an insufficient fee
await addBundle(wallet1, nonce1.add(6), BigNumber.from(1));
await addBundle(wallets[6], BigNumber.from(1));
// 3 more good bundles. These are from a different wallet so that the nonces
// can be correct independent of the success/failure of bundle #7 above.
await addBundle(wallet2, nonce2.add(0), oneToken);
await addBundle(wallet2, nonce2.add(1), oneToken);
await addBundle(wallet2, nonce2.add(2), oneToken);
// 3 more good bundles
await addBundle(wallets[7], oneToken);
await addBundle(wallets[8], oneToken);
await addBundle(wallets[9], oneToken);
assertEquals(await bundleService.bundleTable.count(), 10n);
// Restore this value now that all the bundles are added together
bundleService.config.breakevenOperationCount = breakevenOperationCount;
assertEquals(await bundleService.bundleTable.count(), 10);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
await bundleService.waitForConfirmations();
assertEquals(await bundleService.bundleTable.count(), 1n);
const remainingBundles = fx.allBundles(bundleService);
const remainingPendingBundles = remainingBundles
.filter((bundle) => bundle.status === "pending");
assertEquals(
await fx.testErc20.balanceOf(wallet1.address),
oneToken.mul(4), // 6 tokens spent from wallet 1
);
assertEquals(remainingBundles.length, 10);
assertEquals(remainingPendingBundles.length, 1);
assertEquals(
await fx.testErc20.balanceOf(wallet2.address),
oneToken.mul(7), // 3 tokens spent from wallet 2
);
});
Fixture.test("submits 9/10 bundles when 7th has insufficient gas-based fee", async (fx) => {
const bundleService = await createBundleService(fx, {
// This test is targeting the logic which needs to run when the
// calldata-based gas shortcut doesn't work. We just set the per byte fee to
// zero to make that clear.
perByte: BigNumber.from(0),
});
const baseFee = BigNumber.from(1_000_000).mul(1e9); // Note 1
const fee = BigNumber.from(1_950_000).mul(1e9);
const [wallet1, wallet2] = await fx.setupWallets(2, {
tokenBalance: fee.mul(10),
});
const nonce1 = await wallet1.Nonce();
const nonce2 = await wallet2.Nonce();
async function addBundle(
wallet: BlsWalletWrapper,
nonce: BigNumber,
fee: BigNumber,
) {
const bundle = wallet.sign(
approveAndSendTokensToOrigin(fx, nonce, fee),
);
assertBundleSucceeds(await bundleService.add(bundle));
}
// 6 good bundles from wallet 1 (each pays one token)
await addBundle(wallet1, nonce1.add(0), fee.add(baseFee)); // Note 1
await addBundle(wallet1, nonce1.add(1), fee);
await addBundle(wallet1, nonce1.add(2), fee);
await addBundle(wallet1, nonce1.add(3), fee);
await addBundle(wallet1, nonce1.add(4), fee);
await addBundle(wallet1, nonce1.add(5), fee);
// Note 1: The first bundle has a base fee added because there's an overhead
// of doing a bundle. This is a bit unrealistic but it makes the test less
// brittle.
// 7th bundle should fail because 1 wei is an insufficient fee
await addBundle(wallet1, nonce1.add(6), BigNumber.from(1));
// 3 more good bundles. These are from a different wallet so that the nonces
// can be correct independent of the success/failure of bundle #7 above.
await addBundle(wallet2, nonce2.add(0), fee);
await addBundle(wallet2, nonce2.add(1), fee);
await addBundle(wallet2, nonce2.add(2), fee);
assertEquals(await bundleService.bundleTable.count(), 10n);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
await bundleService.waitForConfirmations();
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(
await fx.testErc20.balanceOf(wallet1.address),
fee.mul(4).sub(baseFee), // 6 fees spent from wallet 1
);
assertEquals(
await fx.testErc20.balanceOf(wallet2.address),
fee.mul(7), // 3 fees spent from wallet 2
);
});
Fixture.test("submits 1/3 bundles when bundle#3 fails the shortcut fee test but bundle#2 also fails the full fee test", async (fx) => {
const bundleService = await createBundleService(fx, {
perGas: BigNumber.from(100_000_000_000),
});
const [wallet] = await fx.setupWallets(2, {
tokenBalance: oneToken.mul(10),
});
const nonce = await wallet.Nonce();
const bundleFees = [
// Passes
BigNumber.from(140_000_000).mul(1e9),
// Passes shortcut test but fails full test
BigNumber.from(80_000_000).mul(1e9),
// Fails shortcut test
BigNumber.from(1),
];
for (const i of Range(bundleFees.length)) {
const bundle = wallet.sign(
approveAndSendTokensToOrigin(fx, nonce.add(i), bundleFees[i]),
);
assertBundleSucceeds(await bundleService.add(bundle));
}
assertEquals(await bundleService.bundleTable.count(), 3n);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
await bundleService.waitForConfirmations();
assertEquals(await bundleService.bundleTable.count(), 2n);
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
oneToken.mul(10).sub(bundleFees[0]),
);
await Promise.all(wallets.map((wallet, i) =>
(async () => {
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
// Every wallet should have successfully spent one token, except the 7th
i === 6 ? oneToken.mul(10) : oneToken.mul(9),
);
})()
));
});

View File

@@ -1,23 +1,22 @@
import { assertEquals, assertBundleSucceeds, BigNumber } from "./deps.ts";
import Fixture, {
aggregationStrategyDefaultTestConfig,
bundleServiceDefaultTestConfig,
} from "./helpers/Fixture.ts";
import { assertBundleSucceeds, assertEquals, BigNumber } from "./deps.ts";
import Fixture, { bundleServiceDefaultTestConfig } from "./helpers/Fixture.ts";
import Range from "../src/helpers/Range.ts";
import { AggregationStrategyConfig } from "../src/app/AggregationStrategy.ts";
import nil from "../src/helpers/nil.ts";
const bundleServiceConfig = {
...bundleServiceDefaultTestConfig,
maxAggregationSize: 5,
maxAggregationDelayMillis: 5000,
};
const aggregationStrategyConfig = {
...aggregationStrategyDefaultTestConfig,
maxAggregationSize: 5,
const aggregationStrategyConfig: AggregationStrategyConfig = {
maxGasPerBundle: 900000,
fees: nil,
bundleCheckingConcurrency: 8,
};
Fixture.test("submits a single action in a timed submission", async (fx) => {
const bundleService = await fx.createBundleService(
const bundleService = fx.createBundleService(
bundleServiceConfig,
aggregationStrategyConfig,
);
@@ -38,13 +37,14 @@ Fixture.test("submits a single action in a timed submission", async (fx) => {
],
});
assertBundleSucceeds(await bundleService.add(bundle));
const bundleResponse = await bundleService.add(bundle);
assertBundleSucceeds(bundleResponse);
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
BigNumber.from(1000),
);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
@@ -54,28 +54,40 @@ Fixture.test("submits a single action in a timed submission", async (fx) => {
await fx.testErc20.balanceOf(wallet.address),
BigNumber.from(1001),
);
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 1);
if ("failures" in bundleResponse) {
throw new Error("Bundle failed to be created");
}
const bundleRow = await bundleService.bundleTable.findBundle(
bundleResponse.hash,
);
assertEquals(bundleRow?.status, "confirmed");
const bundleReceipt = bundleService.receiptFromBundle(bundleRow!);
assertEquals(bundleReceipt?.bundleHash, bundleResponse.hash);
});
Fixture.test("submits a full submission without delay", async (fx) => {
const bundleService = await fx.createBundleService(
const bundleService = fx.createBundleService(
bundleServiceConfig,
aggregationStrategyConfig,
);
const [wallet] = await fx.setupWallets(1);
const walletNonce = await wallet.Nonce();
const wallets = await fx.setupWallets(5);
const firstWallet = wallets[0];
const nonce = await firstWallet.Nonce();
const bundles = Range(5).map((i) =>
const bundles = wallets.map((wallet) =>
wallet.sign({
nonce: walletNonce.add(i),
nonce,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, 1],
[firstWallet.address, 1],
),
},
],
@@ -92,35 +104,33 @@ Fixture.test("submits a full submission without delay", async (fx) => {
// Check mints have occurred, ensuring a submission has occurred even though
// the clock has not advanced
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
await fx.testErc20.balanceOf(firstWallet.address),
BigNumber.from(1005), // 1000 (initial) + 5 * 1 (mint txs)
);
});
Fixture.test(
[
"submits submission from over-full bundle table without delay and submits",
"leftover bundles after delay",
].join(" "),
"submits multiple aggregations when provided with too many user bundles",
async (fx) => {
const bundleService = await fx.createBundleService(
const bundleService = fx.createBundleService(
bundleServiceConfig,
aggregationStrategyConfig,
);
const [wallet] = await fx.setupWallets(1);
const walletNonce = await wallet.Nonce();
const wallets = await fx.setupWallets(7);
const firstWallet = wallets[0];
const nonce = await firstWallet.Nonce();
const bundles = Range(7).map((i) =>
const bundles = wallets.map((wallet) =>
wallet.sign({
nonce: walletNonce.add(i),
nonce,
actions: [
{
ethValue: 0,
contractAddress: fx.testErc20.address,
encodedFunction: fx.testErc20.interface.encodeFunctionData(
"mint",
[wallet.address, 1],
[firstWallet.address, 1],
),
},
],
@@ -128,7 +138,7 @@ Fixture.test(
);
// Prevent submission from triggering on max aggregation size.
bundleService.config.maxAggregationSize = Infinity;
bundleService.config.breakevenOperationCount = Infinity;
for (const b of bundles) {
assertBundleSucceeds(await bundleService.add(b));
@@ -137,36 +147,34 @@ Fixture.test(
// Restore max aggregation size for testing. (This way we hit the edge case
// that the aggregator has access to more actions than it can fit into a
// single submission, which happens but is race-dependent.)
bundleService.config.maxAggregationSize = 5;
bundleService.config.breakevenOperationCount = 4.5;
await bundleService.submissionTimer.trigger();
await bundleService.waitForConfirmations();
// Check mints have occurred, ensuring a submission has occurred even though the
// clock has not advanced
if ((fx.allBundles(bundleService)).length > 0) {
await bundleService.submissionTimer.trigger();
await bundleService.waitForConfirmations();
}
// Check mints have occurred
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
BigNumber.from(1005), // 1000 (initial) + 5 * 1 (mint txs)
);
// Leftover txs
const remainingBundles = await fx.allBundles(bundleService);
assertEquals(remainingBundles.length, 2);
await bundleService.submissionTimer.trigger();
await bundleService.waitForConfirmations();
assertEquals(
await fx.testErc20.balanceOf(wallet.address),
await fx.testErc20.balanceOf(firstWallet.address),
BigNumber.from(1007), // 1000 (initial) + 7 * 1 (mint txs)
);
const confirmationEvents = fx.appEvents.filter((ev) =>
ev.type === "submission-confirmed"
);
assertEquals(confirmationEvents.length, 2);
},
);
Fixture.test(
"submits 3 bundles in reverse (incorrect) nonce order",
async (fx) => {
const bundleService = await fx.createBundleService(
const bundleService = fx.createBundleService(
bundleServiceConfig,
aggregationStrategyConfig,
);
@@ -205,8 +213,11 @@ Fixture.test(
);
assertEquals(await wallet.Nonce(), BigNumber.from(2));
// 2 mints should be left as both failed submission pre-check
let remainingBundles = await fx.allBundles(bundleService);
assertEquals(remainingBundles.length, 2);
let remainingBundles = fx.allBundles(bundleService);
let remainingPendingBundles = remainingBundles.filter((bundle) =>
bundle.status === "pending"
);
assertEquals(remainingPendingBundles.length, 2);
// Re-run submissions
await bundleService.submissionTimer.trigger();
@@ -220,8 +231,11 @@ Fixture.test(
);
assertEquals(await wallet.Nonce(), BigNumber.from(3));
// 1 mints (nonce 3) should be left as it failed submission pre-check
remainingBundles = await fx.allBundles(bundleService);
assertEquals(remainingBundles.length, 1);
remainingBundles = fx.allBundles(bundleService);
remainingPendingBundles = remainingBundles.filter((bundle) =>
bundle.status === "pending"
);
assertEquals(remainingPendingBundles.length, 1);
// Simulate 1 block being mined
await fx.mine(1);
@@ -237,13 +251,16 @@ Fixture.test(
BigNumber.from(1003), // 1000 (initial) + 3 * 1 (mint txs)
);
assertEquals(await wallet.Nonce(), BigNumber.from(4));
remainingBundles = await fx.allBundles(bundleService);
assertEquals(remainingBundles.length, 0);
remainingBundles = fx.allBundles(bundleService);
remainingPendingBundles = remainingBundles.filter((bundle) =>
bundle.status === "pending"
);
assertEquals(remainingPendingBundles.length, 0);
},
);
Fixture.test("retains failing bundle when its eligibility delay is smaller than MAX_ELIGIBILITY_DELAY", async (fx) => {
const bundleService = await fx.createBundleService(
const bundleService = fx.createBundleService(
{
...bundleServiceConfig,
maxEligibilityDelay: 300,
@@ -274,16 +291,16 @@ Fixture.test("retains failing bundle when its eligibility delay is smaller than
await bundleService.runPendingTasks();
assertBundleSucceeds(res);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
});
Fixture.test("removes failing bundle when its eligibility delay is larger than MAX_ELIGIBILITY_DELAY", async (fx) => {
const bundleService = await fx.createBundleService(
Fixture.test("updates status of failing bundle when its eligibility delay is larger than MAX_ELIGIBILITY_DELAY", async (fx) => {
const bundleService = fx.createBundleService(
{
...bundleServiceConfig,
maxEligibilityDelay: 300,
@@ -314,7 +331,7 @@ Fixture.test("removes failing bundle when its eligibility delay is larger than M
await bundleService.runPendingTasks();
assertBundleSucceeds(res);
assertEquals(await bundleService.bundleTable.count(), 1n);
assertEquals(await bundleService.bundleTable.count(), 1);
const [bundleRow] = await bundleService.bundleTable.all();
@@ -326,5 +343,11 @@ Fixture.test("removes failing bundle when its eligibility delay is larger than M
fx.clock.advance(5000);
await bundleService.submissionTimer.waitForCompletedSubmissions(1);
assertEquals(await bundleService.bundleTable.count(), 0n);
assertEquals(await bundleService.bundleTable.count(), 1);
if ("failures" in res) {
throw new Error("Bundle failed to be created");
}
const failedBundleRow = await bundleService.bundleTable.findBundle(res.hash);
assertEquals(failedBundleRow?.status, "failed");
});

View File

@@ -1,38 +1,13 @@
import { assertEquals, BigNumber } from "./deps.ts";
import { assertEquals, BigNumber, sqlite } from "./deps.ts";
import BundleTable, { BundleRow } from "../src/app/BundleTable.ts";
import createQueryClient from "../src/app/createQueryClient.ts";
let counter = 0;
function test(name: string, fn: (bundleTable: BundleTable) => Promise<void>) {
Deno.test({
name,
sanitizeResources: false,
fn: async () => {
const tableName = `bundles_test_${counter++}_${Date.now()}`;
const queryClient = createQueryClient(() => {});
const table = await BundleTable.create(queryClient, tableName);
try {
await fn(table);
} finally {
try {
await table.drop();
await queryClient.disconnect();
} catch (error) {
console.error("cleanup error:", error);
}
}
},
});
}
import nil from "../src/helpers/nil.ts";
const sampleRows: BundleRow[] = [
{
id: 0,
id: 1,
hash: "0x0",
status: "pending",
bundle: {
senderPublicKeys: [["0x01", "0x02", "0x03", "0x04"]],
operations: [
@@ -51,21 +26,24 @@ const sampleRows: BundleRow[] = [
},
eligibleAfter: BigNumber.from(0),
nextEligibilityDelay: BigNumber.from(1),
submitError: nil,
receipt: nil,
},
];
test("Starts with zero transactions", async (table) => {
assertEquals(await table.count(), 0n);
Deno.test("Starts with zero transactions", () => {
const table = new BundleTable(new sqlite.DB());
assertEquals(table.count(), 0);
});
test("Has one transaction after adding transaction", async (table) => {
await table.add(sampleRows[0]);
assertEquals(await table.count(), 1n);
Deno.test("Has one transaction after adding transaction", () => {
const table = new BundleTable(new sqlite.DB());
table.add(sampleRows[0]);
assertEquals(table.count(), 1);
});
test("Can retrieve transaction", async (table) => {
await table.add(sampleRows[0]);
assertEquals(await table.all(), [{ ...sampleRows[0] }]);
Deno.test("Can retrieve transaction", () => {
const table = new BundleTable(new sqlite.DB());
table.add(sampleRows[0]);
assertEquals(table.all(), [{ ...sampleRows[0] }]);
});

View File

@@ -15,7 +15,7 @@ export function assertEquals<L, R extends L>(left: L, right: R) {
export function assertBundleSucceeds(res: AddBundleResponse) {
if ("failures" in res) {
throw new AssertionError("expected bundle to succeed");
throw new AssertionError(`expected bundle to succeed. failures: ${JSON.stringify(res.failures)}`);
}
}

View File

@@ -7,12 +7,11 @@ import {
MockERC20,
MockERC20__factory,
NetworkConfig,
QueryClient,
sqlite,
} from "../../deps.ts";
import testRng from "./testRng.ts";
import EthereumService from "../../src/app/EthereumService.ts";
import createQueryClient from "../../src/app/createQueryClient.ts";
import Range from "../../src/helpers/Range.ts";
import Mutex from "../../src/helpers/Mutex.ts";
import TestClock from "./TestClock.ts";
@@ -23,31 +22,31 @@ import nil, { isNotNil } from "../../src/helpers/nil.ts";
import getNetworkConfig from "../../src/helpers/getNetworkConfig.ts";
import BundleService from "../../src/app/BundleService.ts";
import BundleTable, { BundleRow } from "../../src/app/BundleTable.ts";
import AggregationStrategy from "../../src/app/AggregationStrategy.ts";
import AggregationStrategy, {
AggregationStrategyConfig,
} from "../../src/app/AggregationStrategy.ts";
// deno-lint-ignore no-explicit-any
type ExplicitAny = any;
let existingClient: QueryClient | nil = nil;
export const bundleServiceDefaultTestConfig:
typeof BundleService.defaultConfig = {
bundleQueryLimit: 100,
maxAggregationSize: 12,
breakevenOperationCount: 4.5,
maxAggregationDelayMillis: 5000,
maxUnconfirmedAggregations: 3,
maxEligibilityDelay: 300,
};
export const aggregationStrategyDefaultTestConfig:
typeof AggregationStrategy.defaultConfig = {
maxAggregationSize: 12,
fees: {
type: "ether",
perGas: BigNumber.from(0),
perByte: BigNumber.from(0),
},
};
export const aggregationStrategyDefaultTestConfig: AggregationStrategyConfig = {
maxGasPerBundle: 1500000,
fees: {
type: "ether",
allowLosses: true,
breakevenOperationCount: 4.5,
},
bundleCheckingConcurrency: 8,
};
export default class Fixture {
static test(
@@ -147,33 +146,36 @@ export default class Fixture {
return this.rng.seed("blsPrivateKey", ...extraSeeds).address();
}
async createBundleService(
createBundleService(
config = bundleServiceDefaultTestConfig,
aggregationStrategyConfig = aggregationStrategyDefaultTestConfig,
) {
const suffix = this.rng.seed("table-name-suffix").address().slice(2, 12);
existingClient = createQueryClient(this.emit, existingClient);
const queryClient = existingClient;
const tablesMutex = new Mutex();
const tableName = `bundles_test_${suffix}`;
const table = await BundleTable.createFresh(queryClient, tableName);
const table = new BundleTable(
new sqlite.DB(),
(sql, params) => {
if (env.LOG_QUERIES) {
this.emit({
type: "db-query",
data: { sql, params },
});
}
},
);
const aggregationStrategy = (
const aggregationStrategy =
aggregationStrategyConfig === aggregationStrategyDefaultTestConfig
? this.aggregationStrategy
: new AggregationStrategy(
this.blsWalletSigner,
this.ethereumService,
aggregationStrategyConfig,
)
);
);
const bundleService = new BundleService(
this.emit,
this.clock,
queryClient,
tablesMutex,
table,
this.blsWalletSigner,
@@ -191,16 +193,18 @@ export default class Fixture {
}
async mine(numBlocks: number): Promise<void> {
const provider = this.ethereumService.wallet
.provider as ethers.providers.JsonRpcProvider;
for (let i = 0; i < numBlocks; i++) {
await provider.send("evm_mine", []);
// Sending 0 eth instead of using evm_mine since geth doesn't support it.
await (await this.adminWallet.sendTransaction({
to: this.adminWallet.address,
value: 0,
})).wait();
}
}
allBundles(
bundleService: BundleService,
): Promise<BundleRow[]> {
): BundleRow[] {
return bundleService.bundleTable.all();
}

View File

@@ -1,12 +1,12 @@
ETHERSCAN_API_KEY=
ROPSTEN_URL=fill_me_in
RINKEBY_URL=fill_me_in
ARBITRUM_TESTNET_URL=https://rinkeby.arbitrum.io/rpc
ARBITRUM_TESTNET_URL=TODO_REMOVE
ARBITRUM_GOERLI_URL=https://goerli-rollup.arbitrum.io/rpc
ARBITRUM_URL=https://arb1.arbitrum.io/rpc
OPTIMISM_LOCAL_URL=http://localhost:8545
OPTIMISM_TESETNET_URL=https://kovan.optimism.io
OPTIMISM_URL=https://mainnet.optimism.io
OPTIMISM_GOERLI_URL=https://goerli.optimism.io
# Only used for deploying the deployer contract at the same address on each evm network
DEPLOYER_MNEMONIC="sock poet alone around radar forum quiz session observe rebel another choice"
@@ -18,8 +18,6 @@ DEPLOYER_CONTRACT_ADDRESS=0x036d996D6855B83cd80142f2933d8C2617dA5617
MAIN_MNEMONIC="test test test test test test test test test test test junk"
PRIVATE_KEY_AGG=0000000000000000000000000000000000000000000000000000000000000a99
PRIVATE_KEY_AGG_OKOV=0000000000000000000000000000000000000000000000000000000000000001
PRIVATE_KEY_AGG_RINKARBY=0000000000000000000000000000000000000000000000000000000000000001
PRIVATE_KEY_AGG_ARB1=0000000000000000000000000000000000000000000000000000000000000001
PRIVATE_KEY_002=0000000000000000000000000000000000000000000000000000000000000002
PRIVATE_KEY_003=0000000000000000000000000000000000000000000000000000000000000003

View File

@@ -35,5 +35,18 @@ module.exports = {
ignores: [],
},
],
// TODO (merge-ok) Remove and fix lint error
"node/no-unpublished-import": ["warn"],
// https://github.com/typescript-eslint/typescript-eslint/blob/main/docs/linting/TROUBLESHOOTING.md#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
"no-undef": "off",
},
overrides: [
{
// chai expect statements
files: ["*.test.ts"],
rules: {
"no-unused-expressions": "off",
},
},
],
};

View File

@@ -4,7 +4,7 @@
node_modules
coverage
coverage.json
/typechain
/typechain-types
networks/local.json
#Hardhat files
@@ -17,3 +17,6 @@ cache-ovm
#editor files
.vscode
#yarn
yarn-error.log

View File

@@ -25,4 +25,4 @@ will be made between that one BLS transaction and 31 normal token transfers.
| Commit | Tx Type | Number Txs | L1 Calldata Units Used | L1 Transaction Units | L2 Computation Units | L2 Storage Units | L1 Calldata Cost | L2 Tx Cost | L2 Storage Cost | L2 Computation Cost | Total Cost (ETH) | Tx Hash |
| ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- | ----------- |---------------------| ----------- |------------------|
| 116c920b2469d279773c2546b0f00575828c11c2 | BLS | 31 | 23388 | 1 | 312116 | 1 | 0.0015691 | 0.0001342 | 0.0000292 | 0.0001821 | 0.0019145 | 0xae4c5f62536743630eab5056671296e130bcd9d64650013a86c268fd59c6bc81 |
| 116c920b2469d279773c2546b0f00575828c11c2 | Normal | 31 | 60388 | 31 | 25730 | 0 | 0.0040514 | 0.0041596 | 0.0000000 | 0.0000150 | 0.0082260 | 0x78cfceea76233ed83a49d67919ec4e6ce30d71a15cbcb64821514a1eabed257c |
| 116c920b2469d279773c2546b0f00575828c11c2 | Normal | 31 | 60388 | 31 | 25730 | 0 | 0.0040514 | 0.0041596 | 0.0000000 | 0.0000150 | 0.0082260 | 0x78cfceea76233ed83a49d67919ec4e6ce30d71a15cbcb64821514a1eabed257c |

View File

@@ -1,7 +1,11 @@
# BLS Contract Wallet
Lower-cost layer 2 transactions via a smart contract wallet.
**Note:** _These contracts are in the process of being formally audited and are currently NOT recommended for production use._
## Background
Smart contract wallets give users additional safety mechanisms independent of any wallet UI they may use, but are expensive to deploy (and use on) on Ethereum's layer 1.
Layer 2 solutions like Optimism and Arbitrum greatly lower this cost-barrier, and allow more users to benefit from smart contract wallets. This is primarily due to these being general purpose computation solutions.
@@ -9,9 +13,11 @@ Layer 2 solutions like Optimism and Arbitrum greatly lower this cost-barrier, an
DApps bridged to layer 2 will be more usable than those only on layer 1 thanks to faster transactions at lower-cost, but there are further gas savings to be had by dapps and users.
## Savings
Parameters of external layer 2 transactions are stored on layer 1 when "co-ordinators" record state changes. Reducing the size/number of parameters sent to layer 2 calls greatly reduces the layer 1 cost co-ordinators would need to recoup from users.
So as well as the benefits of smart contract wallets and layer 2 usage, gas savings from reduced call data is achieved in 3 ways:
1. single aggregated signature (BLS)
2. de-duplicate of parameters across aggregated txs
3. compressed parameters
@@ -19,38 +25,50 @@ So as well as the benefits of smart contract wallets and layer 2 usage, gas savi
Note: each of these savings is proportional to the number of transactions submitted in a batch. So when using all three methods, additional savings are roughly O(3n).
# Usage
1. Create bls keypair from signer/wallet
2. Sign creation message and either: send it via an agreggator, or directly pass in a call to the Verification Gateway contract
- receive contract wallet address
- receive contract wallet address
3. Create contract wallet with existing ECDSA keypair
## See it in action
See `extension`
# Components
## Layer 2 contract: Verification Gateway
Creates contract wallets deterministically (create2) with the hash of respective bls public keys. It verifies a set of actions (`Operation`) that have been signed with a known bls keypair, then calls the corresponding wallet passing parameters for it to action. Generally this will be an aggregated signature for many different wallets' Operations (`Bundle`).
## Layer 2 contract: BLS Wallet
A smart contract wallet for users to interact with layer 2 dapps. Created via the aforementioned verification gateway.
Wallets use the proxy upgrade method, and can call upon their proxy admin to change their implementation. Wallets can also choose to set a different contract as their trusted verification gateway.
### Upgradability
The verification gateway (VG1) is the `owner` of a single proxy admin (PA1), and is responsible for all VG1 wallets. A wallet can call `walletAdminCall` on VG1 to then call `upgrade` to change it's implementation.
If in the future a new verification gateway is created (say VG2/PA2), a wallet can choose to set it's trusted gateway to this instead. That means VG1 will no longer be permitted to make arbitrary calls to the wallet, only VG2. Note: PA1 will remain as the proxy admin of the wallet. The wallet can change this to PA2 via an admin call on VG1 to `changeProxyAdmin`.
## Client tool: BLS Wallet/Signer
Wallets (eg Metamask, Argent, ...) to implement BLS keypair generation and signing.
## Relayer node: Aggregators
Network to take bls-signed messages, aggregate signatures, then action them via a call to the Verification Gateway.
## Layer 2 node: Coordinators
Network that takes layer 2 transactions and creates blocks. general purpose computation solutions (Optimism, Arbitrum, zkSync)
## Message format
For a smart contract wallet to perform an action, the signed message must contain:
- the hash of the bls public key that signed the message (the full public key is mapped in the Verification Gateway)
- nonce of the smart contract wallet
- address of the smart contract for the wallet to call
@@ -59,23 +77,26 @@ For a smart contract wallet to perform an action, the signed message must contai
- amount to transfer
## Layer 2 contract: Further optimisations
While the Verification Gateway requires only one aggregated signature (rather than each signature of a set of messages and data), the other optimisations can be gained incrementally via preceding smart contracts.
| Compressed data | No duplicates | Aggregated signature | Contract to call |
|----|----|----|----|
| ✓ | ✓ | ✓ | Decompressor |
| | ✓ | ✓ | Expander |
| | | ✓ | Verification Gateway |
| Compressed data | No duplicates | Aggregated signature | Contract to call |
| --------------- | ------------- | -------------------- | -------------------- |
| ✓ | ✓ | ✓ | Decompressor |
| | ✓ | ✓ | Expander |
| | | ✓ | Verification Gateway |
# Diagrams
## Optimistic Rollups
"Currently every tx on OR puts an ECDSA signature on chain." - BWH
Simplification of Optimism's L2 solution:
![Optimistic Rollups](images/optimisticRollups.svg)
## Transactions via BLS signature aggregator
"We want to replace this with a BLS signature." - BWH
Proposed solution to make use of [BLS](https://github.com/thehubbleproject/hubble-contracts/blob/master/contracts/libs/BLS.sol) lib:
@@ -91,19 +112,26 @@ Proposed solution to make use of [BLS](https://github.com/thehubbleproject/hubbl
For each network, the deployer contract can be deployed with the following script (only needed once)
`DEPLOY_DEPLOYER=true yarn hardhat run scripts/deploy-deployer.ts --network <network-name>`
## Integration tests
## Arbitrum
To run integration tests:
1. cd into `./contracts` and run `yarn start-hardhat`
2. cd into `./aggregator` and run `./programs/aggregator.ts`
3. from `./contracts`, run `yarn test-integration`.
## Optimism's L2 (paused)
- clone https://github.com/ethereum-optimism/optimism
- follow instructions (using latest version of docker)
- in `opt/`, run script - `docker-compose up`
- L1 - http://localhost:9545 (chainId 31337)
- L2 - http://localhost:8545 (chainId 420)
- L1 - http://localhost:9545 (chainId 31337)
- L2 - http://localhost:8545 (chainId 420)
## Deploy scripts
Specify network - `yarn hardhat run scripts/<#_script.ts> --network arbitrum-testnet`
Specify network - `yarn hardhat run scripts/<#_script.ts> --network arbitrum-goerli`
# License
MIT

View File

@@ -1,3 +1,4 @@
/node_modules
/dist
yarn-error.log
/.nyc_output

View File

@@ -1,6 +1,6 @@
*
!/dist/src/**/*
!/dist/typechain/**/*
!/dist/typechain-types/**/*
!/src/**/*
!/package.json
!/README.md

5
contracts/clients/.nycrc Normal file
View File

@@ -0,0 +1,5 @@
{
"extends": "@istanbuljs/nyc-config-typescript",
"all": true,
"include": ["src/**/*.ts"]
}

View File

@@ -1,6 +1,8 @@
# BLS Wallet Clients
*Client libraries for interacting with BLS Wallet components*
[![npm version](https://img.shields.io/npm/v/bls-wallet-clients)](https://www.npmjs.com/package/bls-wallet-clients)
_Client libraries for interacting with BLS Wallet components_
## Network Config
@@ -18,23 +20,56 @@ const netCfg: NetworkConfig = await getConfig(
## Aggregator
Exposes typed functions for interacting with the Aggregator's HTTP api.
Exposes typed functions for interacting with the Aggregator's HTTP API.
### Add a bundle to an aggregator
```ts
import { Aggregator } from 'bls-wallet-clients';
import { Aggregator } from "bls-wallet-clients";
const aggregator = new Aggregator('https://rinkarby.blswallet.org');
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
const resp = await aggregator.add(bundle); // See BlsWalletWrapper section below
// Aggregator did not accept bundle
if ("failures" in resp) {
throw new Error(resp.failures.join(", "));
}
```
await aggregator.addTransaction(...);
### Get the bundle receipt that contains the transaction hash you can lookup on a block explorer
You will have to poll for the bundle receipt once you have added a bundle to an aggregator. The transaction hash is located on the bundle receipt. The property you need is `bundleReceipt.transactionHash`. This represents the transaction hash for the bundle submitted to the Verification Gatewaty, and can be used in a block explorer.
Note this transaction is reprentative of the entire bundle submitted by the aggregator, and does not represent individual operations. To retrieve information about individual operations, use the get `getOperationResults` helper method which is explained under the [VerificationGateway](#verificationgateway) section below.
```ts
import { Aggregator } from "bls-wallet-clients";
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
const resp = await aggregator.add(bundle); // See BlsWalletWrapper section below
// Aggregator did not accept bundle
if ("failures" in resp) {
throw new Error(resp.failures.join(", "));
}
let receipt;
while (!receipt) {
receipt = await aggregator.lookupReceipt(resp.hash);
// There was an issue submitting the bundle on chain
if (receipt && "submitError" in receipt) {
throw new Error(receipt.submitError);
}
// Some function which waits i.e. setTimeout
await sleep(5000);
}
```
## BlsWalletWrapper
Wraps a BLS wallet, storing the private key and providing `.sign(...)` to
produce a `Bundle`, that can be used with `aggregator.addTransaction(...)`.
produce a `Bundle`, that can be used with `aggregator.add(...)`. Make sure the bls wallet you're trying to use has enough ETH to send transactions. You can either fund a wallet before it's created, or after the wallet is lazily created from its first transaction (bundle).
```ts
import { BlsWalletWrapper } from 'bls-wallet-clients';
import { BlsWalletWrapper } from "bls-wallet-clients";
const wallet = await BlsWalletWrapper.connect(
privateKey,
@@ -46,16 +81,196 @@ const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
{
contract: someToken, // An ethers.Contract
method: 'transfer',
args: [recipientAddress, ethers.utils.parseUnits('1', 18)],
ethValue: 0,
contractAddress: someToken.address, // An ethers.Contract
encodedFunction: someToken.interface.encodeFunctionData("transfer", [
"0x...some address...",
ethers.BigNumber.from(1).pow(18),
]),
},
// Additional actions can go here. When using multiple actions, they'll
// either all succeed or all fail.
],
});
await aggregator.addTransaction(bundle);
await aggregator.add(bundle);
```
### Sending a regular ETH transaction
```ts
// Follow the same steps as the first BlsWalletWrapper example, but construct the bundle actions like so:
const amountToTransfer = ethers.utils.parseUnits("1");
const reciever = "0x1234...";
const bundle = wallet.sign({
nonce,
actions: [
{
ethValue: amountToTransfer, // amount of ETH you want to transfer
contractAddress: reciever, // receiver address. Can be a contract address or an EOA
encodedFunction: "0x", // leave this as "0x" when just sending ETH
},
],
});
```
### Constructing actions to be agnostic to both ETH transfers and contract interactions
```ts
// Follow the same steps as the first BlsWalletWrapper example, but construct the bundle actions like so:
const transactions = [
{
value: ethers.utils.parseUnits("1"), // amount of ETH you want to transfer
to: "0x1234...", // to address. Can be a contract address or an EOA
},
];
const actions: ActionData[] = transactions.map((tx) => ({
ethValue: tx.value ?? "0",
contractAddress: tx.to,
encodedFunction: tx.data ?? "0x", // in this example, there is no data property on the tx object, so "0x" will be used
}));
const bundle = wallet.sign({
nonce,
actions,
});
```
## Estimating and paying fees
User bundles must pay fees to compensate the aggregator. Fees can be paid by adding an additional action to the users bundle that pays tx.origin. For more info on how fees work, see [aggregator fees](../../aggregator/README.md#fees).
Practically, this means you have to first estimate the fee using `aggregator.estimateFee`, and then add an additional action to a user bundle that pays the aggregator with the amount returned from `estimateFee`. When estimating a payment, you should include this additional action with a payment of zero wei, otherwise the additional action will increase the fee that needs to be paid. Additionally, the `feeRequired` value returned from `estimateFee` is the absolute minimum fee required at the time of estimation, therefore, you should pay slightly extra to ensure the bundle has a good chance of being submitted successfully.
### Paying aggregator fees with native currency (ETH)
```ts
import { BlsWalletWrapper, Aggregator } from "bls-wallet-clients";
const wallet = await BlsWalletWrapper.connect(
privateKey,
verificationGatewayAddress,
provider,
);
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
// Create a fee estimate bundle
const estimateFeeBundle = wallet.sign({
nonce,
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: 1,
// Provide 1 wei with this action so that the fee transfer to
// tx.origin can be included in the gas estimate.
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
],
});
const feeEstimate = await aggregator.estimateFee(estimateFeeBundle);
// Add a safety premium to the fee to account for fluctuations in gas estimation
const safetyDivisor = 5;
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
const safetyPremium = feeRequired.div(safetyDivisor);
const safeFee = feeRequired.add(safetyPremium);
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: safeFee, // fee amount
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
],
});
```
### Paying aggregator fees with custom currency (ERC20)
The aggregator must be set up to accept ERC20 tokens in order for this to work.
```ts
import { BlsWalletWrapper, Aggregator } from "bls-wallet-clients";
const wallet = await BlsWalletWrapper.connect(
privateKey,
verificationGatewayAddress,
provider,
);
const aggregator = new Aggregator("https://arbitrum-goerli.blswallet.org");
// Create a fee estimate bundle
const estimateFeeBundle = wallet.sign({
nonce,
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData("approve", [
aggregatorUtilitiesContract.address,
1,
]),
},
{
ethValue: 0,
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendTokenToTxOrigin",
[tokenContract.address, 1],
),
},
],
});
const feeEstimate = await aggregator.estimateFee(estimateFeeBundle);
// Add a safety premium to the fee to account for fluctuations in gas estimation
const safetyDivisor = 5;
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
const safetyPremium = feeRequired.div(safetyDivisor);
const safeFee = feeRequired.add(safetyPremium);
const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
...actions, // ... add your user actions here (approve, transfer, etc.)
// Note the additional approve action when transfering ERC20 tokens
{
ethValue: 0,
contractAddress: tokenContract.address,
encodedFunction: tokenContract.interface.encodeFunctionData("approve", [
aggregatorUtilitiesContract.address,
safeFee, // fee amount
]),
},
{
ethValue: 0,
contractAddress: aggregatorUtilitiesContract.address,
encodedFunction: aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendTokenToTxOrigin",
[
tokenContract.address,
safeFee, // fee amount
],
),
},
],
});
```
## VerificationGateway
@@ -65,7 +280,7 @@ Exposes `VerificationGateway` and `VerificationGateway__factory` generated by
interactions with the `VerificationGateway`.
```ts
import { VerificationGateway__factory } from 'bls-wallet-clients';
import { VerificationGateway__factory } from "bls-wallet-clients";
const verificationGateway = VerificationGateway__factory.connect(
verificationGatewayAddress,
@@ -75,6 +290,31 @@ const verificationGateway = VerificationGateway__factory.connect(
await verificationGateway.processBundle(bundle);
```
You can get the results of the operations in a bundle using `getOperationResults`.
```ts
import { getOperationResults, decodeError } from 'bls-wallet-clients';
...
const txn = await verificationGateway.processBundle(bundle);
const txnReceipt = txn.wait();
const opResults = getOperationResults(txnReceipt);
// Includes data from WalletOperationProcessed event,
// as well as parsed errors with action index
const { error } = opResults[0];
console.log(error?.actionIndex); // ex. 0 (as BigNumber)
console.log(error?.message); // ex. "some require failure message"
// If you want more granular ability to decode an error message
// you can use the decodeError function.
const errorData = '0x5c66760100000000.............000000000000';
const opResultError = decodeError(errorData);
console.log(opResultError.actionIndex); // ex. 0 (as BigNumber)
console.log(opResultError.message); // ex. "ERC20: insufficient allowance"
```
## Signer
Utilities for signing, aggregating and verifying transaction bundles using the
@@ -91,11 +331,17 @@ import ethers from "ethers";
import { initBlsWalletSigner } from "bls-wallet-clients";
(async () => {
const signer = await initBlsWalletSigner({ chainId: 10 });
const privateKey = "0x...256 bits of private hex data here";
const verificationGatewayAddress = "0x123...456";
const signer = await initBlsWalletSigner({
chainId: 10,
privateKey,
verificationGatewayAddress
});
const someToken = new ethers.Contract(
...
// See https://docs.ethers.io/v5/getting-started/
);
@@ -107,14 +353,47 @@ import { initBlsWalletSigner } from "bls-wallet-clients";
// If you don't want to call a function and just send `ethValue` above,
// use '0x' to signify an empty byte array here
encodedFunction: someToken.interface.encodeFunctionData(
"transfer",
["0x...some address...", ethers.BigNumber.from(10).pow(18)],
),
encodedFunction: someToken.interface.encodeFunctionData("transfer", [
"0x...some address...",
ethers.BigNumber.from(10).pow(18),
]),
},
privateKey,
);
// Send bundle to an aggregator or use it with VerificationGateway directly.
})();
```
## Local Development
### Setup
```sh
yarn install
```
### Build
```sh
yarn build
```
### Tests
```sh
yarn test
```
### Use in Extension or another project
```sh
yarn build
yarn link
cd other/project/dir
yarn "link bls-wallet-clients"
```
## Troubleshooting tips
- Make sure your bls-wallet-clients package is up-to-date and check out our [releases page](https://github.com/web3well/bls-wallet/releases) for info on breaking changes.
- Check network values such as the verification gateway address or the aggregator url are up-to-date. The most up-to-date values are located in the relevant [network config](./../contracts/networks) file. If you're deploying to a custom network, you'll have to check these against your own records as these won't be in the network directory.

View File

@@ -1,6 +1,6 @@
{
"name": "bls-wallet-clients",
"version": "0.6.0",
"version": "0.8.2-1452ef5",
"description": "Client libraries for interacting with BLS Wallet components",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@@ -8,24 +8,34 @@
"author": "Andrew Morris",
"license": "MIT",
"private": false,
"engines": {
"node": ">=16.0.0",
"yarn": ">=1.0.0"
},
"scripts": {
"build": "rm -rf dist && mkdir dist && cp -rH typechain dist/typechain && find ./dist/typechain -type f \\! -name '*.d.ts' -name '*.ts' -delete && tsc",
"build": "rm -rf dist && mkdir dist && cp -rH typechain-types dist/typechain-types && find ./dist/typechain-types -type f \\! -name '*.d.ts' -name '*.ts' -delete && tsc",
"watch": "tsc -w",
"test": "mocha dist/**/*.test.js",
"premerge": "yarn build && yarn test",
"test": "nyc --reporter=text --reporter=html mocha --require ts-node/register --require source-map-support/register --require ./test/init.ts --recursive **/*.test.ts",
"premerge": "yarn test",
"publish-experimental": "node scripts/showVersion.js >.version && npm version $(node scripts/showBaseVersion.js)-$(git rev-parse HEAD | head -c7) --allow-same-version && npm publish --tag experimental && npm version $(cat .version) && rm .version",
"publish-experimental-dry-run": "node scripts/showVersion.js >.version && npm version $(node scripts/showBaseVersion.js)-$(git rev-parse HEAD | head -c7) --allow-same-version && npm publish --tag experimental --dry-run && npm version $(cat .version) && rm .version"
},
"dependencies": {
"@thehubbleproject/bls": "^0.5.1",
"ethers": "5.5.4"
"ethers": "^5.7.2",
"node-fetch": "2.6.7"
},
"devDependencies": {
"@types/chai": "^4.3.0",
"@types/mocha": "^9.1.0",
"chai": "^4.3.6",
"mocha": "^9.2.2",
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^10.0.1",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"mocha": "^10.2.0",
"nyc": "^15.1.0",
"source-map-support": "^0.5.21",
"typescript": "^4.6.2"
"ts-node": "^10.9.1",
"typescript": "^4.9.4"
}
}

View File

@@ -1,3 +1,5 @@
import nodeFetch from "node-fetch";
import { ContractReceipt } from "ethers";
import { Bundle, bundleToDto } from "./signer";
// TODO: Rename to BundleFailure?
@@ -33,15 +35,35 @@ export type EstimateFeeResponse = {
successes: boolean[];
};
export type BundleReceipt = {
transactionIndex: string;
blockHash: string;
blockNumber: string;
export type BundleReceiptError = {
submitError: string | undefined;
};
/**
* The BLS Wallet specific values in a {@link BundleReceipt}.
*/
export type BlsBundleReceipt = {
bundleHash: string;
};
/**
* The bundle receipt returned from a BLS Wallet Aggregator instance. It is a combination of an ethers {@link ContractReceipt} and a {@link BlsBundleReceipt} type.
*/
export type BundleReceipt = ContractReceipt & BlsBundleReceipt;
/**
* Client used to interact with a BLS Wallet Aggregator instance
*/
export default class Aggregator {
// Fetch implementation to use
private readonly fetchImpl;
origin: string;
/**
* Constructs an Aggregator object
*
* @param url URL of the aggregator instance
*/
constructor(url: string) {
const parsedUrl = new URL(url);
@@ -50,8 +72,16 @@ export default class Aggregator {
}
this.origin = new URL(url).origin;
// Prefer runtime's imeplmentation of fetch over node-fetch
this.fetchImpl = "fetch" in globalThis ? fetch.bind(globalThis) : nodeFetch;
}
/**
* Sends a bundle to the aggregator
*
* @param bundle Bundle to send
* @returns The hash of the bundle or an array of failures if the aggregator did not accept the bundle
*/
async add(
bundle: Bundle,
): Promise<{ hash: string } | { failures: TransactionFailure[] }> {
@@ -68,24 +98,36 @@ export default class Aggregator {
return json;
}
/**
* Estimates the fee required for a bundle by the aggreagtor to submit it.
*
* @param bundle Bundle to estimates the fee for
* @returns Estimate of the fee needed to submit the bundle
*/
async estimateFee(bundle: Bundle): Promise<EstimateFeeResponse> {
const result = await this.jsonPost("/estimateFee", bundleToDto(bundle));
return result as EstimateFeeResponse;
}
async lookupReceipt(hash: string): Promise<BundleReceipt | undefined> {
const response = await fetch(`${this.origin}/bundleReceipt/${hash}`);
if (response.status === 404) {
return undefined;
}
return await response.json();
/**
* Looks for a transaction receipt for a Bundle sent to the aggregator.
* This will return undefined if the bundle has not yet been submitted by the aggregator.
*
* @param hash Hash of the bundle to find a transaction receipt for.
* @returns The bundle receipt, a submission error if the aggregator was unable to submit the bundle on chain, or undefined if the receipt was not found.
*/
async lookupReceipt(
hash: string,
): Promise<BundleReceipt | BundleReceiptError | undefined> {
return this.jsonGet<BundleReceipt | BundleReceiptError>(
`${this.origin}/bundleReceipt/${hash}`,
);
}
// Note: This should be private instead of exposed. Leaving as is for compatibility.
async jsonPost(path: string, body: unknown): Promise<unknown> {
const resp = await fetch(`${this.origin}${path}`, {
const resp = await this.fetchImpl(`${this.origin}${path}`, {
method: "POST",
body: JSON.stringify(body),
headers: {
@@ -105,4 +147,20 @@ export default class Aggregator {
return json;
}
private async jsonGet<T>(path: string): Promise<T | undefined> {
const resp = await this.fetchImpl(path);
const respText = await resp.text();
if (!respText) {
return undefined;
}
const json = JSON.parse(respText);
const isValidNonEmptyJson = json && Object.keys(json).length;
if (isValidNonEmptyJson) {
return json as T;
}
return undefined;
}
}

View File

@@ -0,0 +1,453 @@
/* eslint-disable camelcase */
import { ethers, BigNumber } from "ethers";
import { Deferrable } from "ethers/lib/utils";
import { ActionData, Bundle, PublicKey } from "./signer/types";
import Aggregator, { BundleReceipt } from "./Aggregator";
import BlsSigner, {
TransactionBatchResponse,
// Used for sendTransactionBatch TSdoc comment
// eslint-disable-next-line no-unused-vars
TransactionBatch,
UncheckedBlsSigner,
_constructorGuard,
} from "./BlsSigner";
import poll from "./helpers/poll";
import BlsWalletWrapper from "./BlsWalletWrapper";
import {
AggregatorUtilities__factory,
BLSWallet__factory,
VerificationGateway__factory,
} from "../typechain-types";
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
/** Public key linked to actions parsed from a bundle */
export type PublicKeyLinkedToActions = {
publicKey: PublicKey;
actions: Array<ActionData>;
};
export default class BlsProvider extends ethers.providers.JsonRpcProvider {
readonly aggregator: Aggregator;
readonly verificationGatewayAddress: string;
readonly aggregatorUtilitiesAddress: string;
/**
* @param aggregatorUrl The url for an aggregator instance
* @param verificationGatewayAddress Verification gateway contract address
* @param aggregatorUtilitiesAddress Aggregator utilities contract address
* @param url Rpc url
* @param network The network the provider should connect to
*/
constructor(
aggregatorUrl: string,
verificationGatewayAddress: string,
aggregatorUtilitiesAddress: string,
url?: string,
network?: ethers.providers.Networkish,
) {
super(url, network);
this.aggregator = new Aggregator(aggregatorUrl);
this.verificationGatewayAddress = verificationGatewayAddress;
this.aggregatorUtilitiesAddress = aggregatorUtilitiesAddress;
}
/**
* @param transaction Transaction request object
* @returns An estimate of the amount of gas that would be required to submit the transaction to the network
*/
override async estimateGas(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<BigNumber> {
const resolvedTransaction = await ethers.utils.resolveProperties(
transaction,
);
if (!resolvedTransaction.to) {
throw new TypeError("Transaction.to should be defined");
}
if (!resolvedTransaction.from) {
throw new TypeError("Transaction.from should be defined");
}
const action: ActionData = {
ethValue: resolvedTransaction.value?.toString() ?? "0",
contractAddress: resolvedTransaction.to.toString(),
encodedFunction: resolvedTransaction.data?.toString() ?? "0x",
};
const nonce = await this.getTransactionCount(
resolvedTransaction.from.toString(),
);
const actionWithFeePaymentAction =
this._addFeePaymentActionForFeeEstimation([action]);
// TODO: (merge-ok) bls-wallet #560 Estimate fee without requiring a signed bundle
// There is no way to estimate the cost of a bundle without signing a bundle. The
// alternative would be to use a signer instance in this method which is undesirable,
// as this would result in tight coupling between a provider and a signer.
const throwawayPrivateKey = await BlsWalletWrapper.getRandomBlsPrivateKey();
const throwawayBlsWalletWrapper = await BlsWalletWrapper.connect(
throwawayPrivateKey,
this.verificationGatewayAddress,
this,
);
const feeEstimate = await this.aggregator.estimateFee(
throwawayBlsWalletWrapper.sign({
nonce,
actions: [...actionWithFeePaymentAction],
}),
);
const feeRequired = BigNumber.from(feeEstimate.feeRequired);
return addSafetyPremiumToFee(feeRequired);
}
/**
* Sends transaction to be executed. Adds the signed bundle to the aggregator
*
* @param signedTransaction A signed bundle
* @returns A transaction response object that can be awaited to get the transaction receipt
*/
override async sendTransaction(
signedTransaction: string | Promise<string>,
): Promise<ethers.providers.TransactionResponse> {
const resolvedTransaction = await signedTransaction;
const bundle: Bundle = JSON.parse(resolvedTransaction);
if (bundle.operations.length > 1) {
throw new Error(
"Can only operate on single operations. Call provider.sendTransactionBatch instead",
);
}
const result = await this.aggregator.add(bundle);
if ("failures" in result) {
throw new Error(JSON.stringify(result.failures));
}
const actionData: ActionData = {
ethValue: bundle.operations[0].actions[0].ethValue,
contractAddress: bundle.operations[0].actions[0].contractAddress,
encodedFunction: bundle.operations[0].actions[0].encodedFunction,
};
return await this._constructTransactionResponse(
actionData,
bundle.senderPublicKeys[0],
result.hash,
);
}
/**
* @param signedTransactionBatch A signed {@link TransactionBatch}
* @returns A transaction batch response object that can be awaited to get the transaction receipt
*/
async sendTransactionBatch(
signedTransactionBatch: string,
): Promise<TransactionBatchResponse> {
const bundle: Bundle = JSON.parse(signedTransactionBatch);
const result = await this.aggregator.add(bundle);
if ("failures" in result) {
throw new Error(JSON.stringify(result.failures));
}
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
bundle.senderPublicKeys.map((publicKey, i) => {
const operation = bundle.operations[i];
const actions = operation.actions;
return {
publicKey,
actions,
};
});
return await this._constructTransactionBatchResponse(
publicKeysLinkedToActions,
result.hash,
);
}
/**
* @param privateKey Private key for the account the signer represents
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
* @returns A new BlsSigner instance
*/
override getSigner(
privateKey: string,
addressOrIndex?: string | number,
): BlsSigner {
return new BlsSigner(_constructorGuard, this, privateKey, addressOrIndex);
}
/**
* @param privateKey Private key for the account the signer represents
* @param addressOrIndex (Not Used) address or index of the account, managed by the connected Ethereum node
* @returns A new UncheckedBlsSigner instance
*/
override getUncheckedSigner(
privateKey: string,
addressOrIndex?: string,
): UncheckedBlsSigner {
return this.getSigner(privateKey, addressOrIndex).connectUnchecked();
}
/**
* Gets the transaction receipt associated with the transaction (bundle) hash
*
* @remarks The transaction hash argument corresponds to a bundle hash and cannot be used on a block explorer.
* Instead, the transaction hash returned in the transaction receipt from this method can be used in a block explorer.
*
* @param transactionHash The transaction hash returned from the BlsProvider and BlsSigner sendTransaction methods. This is technically a bundle hash
* @returns The transaction receipt that corressponds to the transaction hash (bundle hash)
*/
override async getTransactionReceipt(
transactionHash: string | Promise<string>,
): Promise<ethers.providers.TransactionReceipt> {
const resolvedTransactionHash = await transactionHash;
return this._getTransactionReceipt(resolvedTransactionHash, 1, 20);
}
/**
* Gets the transaction receipt associated with the transaction (bundle) hash
*
* @remarks The transaction hash argument cannot be used on a block explorer. It instead corresponds to a bundle hash.
* The transaction hash returned in the transaction receipt from this method can be used in a block explorer.
*
* @param transactionHash The transaction hash returned from sending a transaction. This is technically a bundle hash
* @param confirmations (Not used) the number of confirmations to wait for before returning the transaction receipt
* @param retries The number of retries to poll the receipt for
* @returns
*/
override async waitForTransaction(
transactionHash: string,
confirmations?: number,
retries?: number,
): Promise<ethers.providers.TransactionReceipt> {
return this._getTransactionReceipt(
transactionHash,
confirmations ?? 1,
retries ?? 20,
);
}
/**
* @param address The address that the method gets the transaction count from
* @param blockTag The specific block tag to get the transaction count from
* @returns The number of transactions an account has sent
*/
override async getTransactionCount(
address: string | Promise<string>,
blockTag?:
| ethers.providers.BlockTag
| Promise<ethers.providers.BlockTag>
| undefined,
): Promise<number> {
const walletContract = BLSWallet__factory.connect(await address, this);
const code = await walletContract.provider.getCode(address, blockTag);
if (code === "0x") {
// The wallet doesn't exist yet. Wallets are lazily created, so the nonce
// is effectively zero, since that will be accepted as valid for a first
// operation that also creates the wallet.
return 0;
}
return Number(await walletContract.nonce());
}
async _getTransactionReceipt(
transactionHash: string,
confirmations: number,
retries: number,
): Promise<ethers.providers.TransactionReceipt> {
const getBundleReceipt = async () =>
await this.aggregator.lookupReceipt(transactionHash);
const bundleExists = (result: BundleReceipt) => !result;
const bundleReceipt = await poll(
getBundleReceipt,
bundleExists,
retries,
2000,
);
if (!bundleReceipt) {
throw new Error(
`Could not find bundle receipt for transaction hash: ${transactionHash}`,
);
}
return {
to: bundleReceipt.to,
from: bundleReceipt.from,
contractAddress: bundleReceipt.contractAddress,
transactionIndex: bundleReceipt.transactionIndex,
root: bundleReceipt.root,
gasUsed: bundleReceipt.gasUsed,
logsBloom: bundleReceipt.logsBloom,
blockHash: bundleReceipt.blockHash,
transactionHash: bundleReceipt.transactionHash,
logs: bundleReceipt.logs,
blockNumber: bundleReceipt.blockNumber,
confirmations: bundleReceipt.confirmations ?? confirmations,
cumulativeGasUsed: bundleReceipt.effectiveGasPrice,
effectiveGasPrice: bundleReceipt.effectiveGasPrice,
byzantium: bundleReceipt.byzantium,
type: bundleReceipt.type,
status: bundleReceipt.status,
};
}
_addFeePaymentActionForFeeEstimation(
actions: Array<ActionData>,
): Array<ActionData> {
const aggregatorUtilitiesContract = AggregatorUtilities__factory.connect(
this.aggregatorUtilitiesAddress,
this,
);
return [
...actions,
{
// Provide 1 wei with this action so that the fee transfer to
// tx.origin can be included in the gas estimate.
ethValue: 1,
contractAddress: this.aggregatorUtilitiesAddress,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
];
}
_addFeePaymentActionWithSafeFee(
actions: Array<ActionData>,
fee: BigNumber,
): Array<ActionData> {
const aggregatorUtilitiesContract = AggregatorUtilities__factory.connect(
this.aggregatorUtilitiesAddress,
this,
);
return [
...actions,
{
ethValue: fee,
contractAddress: this.aggregatorUtilitiesAddress,
encodedFunction:
aggregatorUtilitiesContract.interface.encodeFunctionData(
"sendEthToTxOrigin",
),
},
];
}
async _constructTransactionResponse(
action: ActionData,
publicKey: PublicKey,
hash: string,
nonce?: BigNumber,
): Promise<ethers.providers.TransactionResponse> {
const chainId = await this.send("eth_chainId", []);
if (!nonce) {
nonce = await BlsWalletWrapper.Nonce(
publicKey,
this.verificationGatewayAddress,
this,
);
}
const verificationGateway = VerificationGateway__factory.connect(
this.verificationGatewayAddress,
this,
);
const from = await BlsWalletWrapper.AddressFromPublicKey(
publicKey,
verificationGateway,
);
return {
hash,
to: action.contractAddress,
from,
nonce: nonce.toNumber(),
gasLimit: BigNumber.from("0x0"),
data: action.encodedFunction.toString(),
value: BigNumber.from(action.ethValue),
chainId: parseInt(chainId, 16),
type: 2,
confirmations: 1,
wait: (confirmations?: number) => {
return this.waitForTransaction(hash, confirmations);
},
};
}
async _constructTransactionBatchResponse(
publicKeysLinkedToActions: Array<PublicKeyLinkedToActions>,
hash: string,
nonce?: BigNumber,
): Promise<TransactionBatchResponse> {
const chainId = await this.send("eth_chainId", []);
const verificationGateway = VerificationGateway__factory.connect(
this.verificationGatewayAddress,
this,
);
const transactions: Array<ethers.providers.TransactionResponse> = [];
for (const publicKeyLinkedToActions of publicKeysLinkedToActions) {
const from = await BlsWalletWrapper.AddressFromPublicKey(
publicKeyLinkedToActions.publicKey,
verificationGateway,
);
if (!nonce) {
nonce = await BlsWalletWrapper.Nonce(
publicKeyLinkedToActions.publicKey,
this.verificationGatewayAddress,
this,
);
}
for (const action of publicKeyLinkedToActions.actions) {
if (action.contractAddress === this.aggregatorUtilitiesAddress) {
break;
}
transactions.push({
hash,
to: action.contractAddress,
from,
nonce: nonce!.toNumber(),
gasLimit: BigNumber.from("0x0"),
data: action.encodedFunction.toString(),
value: BigNumber.from(action.ethValue),
chainId: parseInt(chainId, 16),
type: 2,
confirmations: 1,
wait: (confirmations?: number) => {
return this.waitForTransaction(hash, confirmations);
},
});
}
}
return {
transactions,
awaitBatchReceipt: (confirmations?: number) => {
return this.waitForTransaction(hash, confirmations);
},
};
}
}

View File

@@ -0,0 +1,539 @@
/* eslint-disable camelcase */
import { ethers, BigNumber, Signer, Bytes, BigNumberish } from "ethers";
import {
AccessListish,
Deferrable,
hexlify,
isBytes,
RLP,
} from "ethers/lib/utils";
import BlsProvider, { PublicKeyLinkedToActions } from "./BlsProvider";
import BlsWalletWrapper from "./BlsWalletWrapper";
import addSafetyPremiumToFee from "./helpers/addSafetyDivisorToFee";
import { ActionData, bundleToDto } from "./signer";
export const _constructorGuard = {};
/**
* Based on draft wallet_batchTransactions rpc proposal https://hackmd.io/HFHohGDbRSGgUFI2rk22bA?view
*
* @property gas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Transaction gas limit
* @property maxPriorityFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) Miner tip aka priority fee
* @property maxFeePerGas - (THIS PROPERTY IS NOT USED BY BLS WALLET) The maximum total fee per gas the sender is willing to pay (includes the network/base fee and miner/priority fee) in wei
* @property nonce - Integer of a nonce. This allows overwriting your own pending transactions that use the same nonce
* @property chainId - Chain ID that this transaction is valid on
* @property accessList - (THIS PROPERTY IS NOT USED BY BLS WALLET) EIP-2930 access list
*/
export type BatchOptions = {
gas?: BigNumberish;
maxPriorityFeePerGas: BigNumberish;
maxFeePerGas: BigNumberish;
nonce: BigNumberish;
chainId: number;
accessList?: AccessListish;
};
/**
* @property transactions - An array of Ethers transaction objects
* @property batchOptions - Optional batch options taken into account by smart contract wallets. See {@link BatchOptions}
*/
export type TransactionBatch = {
transactions: Array<ethers.providers.TransactionRequest>;
batchOptions?: BatchOptions;
};
/**
* @property transactions - An array of Ethers transaction response objects
* @property awaitBatchReceipt - A function that returns a promise that resolves to a transaction receipt
*/
export interface TransactionBatchResponse {
transactions: Array<ethers.providers.TransactionResponse>;
awaitBatchReceipt: (
confirmations?: number,
) => Promise<ethers.providers.TransactionReceipt>;
}
export default class BlsSigner extends Signer {
override readonly provider: BlsProvider;
readonly verificationGatewayAddress!: string;
readonly aggregatorUtilitiesAddress!: string;
wallet!: BlsWalletWrapper;
_index: number;
_address: string;
readonly initPromise: Promise<void>;
/**
* @param constructorGuard Prevents BlsSigner constructor being called directly
* @param provider BlsProvider accociated with this signer
* @param privateKey Private key for the account this signer represents
* @param addressOrIndex (Not used) Address or index of this account, managed by the connected Ethereum node
*/
constructor(
constructorGuard: Record<string, unknown>,
provider: BlsProvider,
privateKey: string | Promise<string>,
readonly addressOrIndex?: string | number,
) {
super();
this.provider = provider;
this.verificationGatewayAddress = this.provider.verificationGatewayAddress;
this.aggregatorUtilitiesAddress = this.provider.aggregatorUtilitiesAddress;
this.initPromise = this.initializeWallet(privateKey);
if (constructorGuard !== _constructorGuard) {
throw new Error(
"do not call the BlsSigner constructor directly; use provider.getSigner",
);
}
if (addressOrIndex === null || addressOrIndex === undefined) {
addressOrIndex = 0;
}
if (typeof addressOrIndex === "string") {
this._address = this.provider.formatter.address(addressOrIndex);
this._index = null as any;
} else if (typeof addressOrIndex === "number") {
this._address = null as any;
this._index = addressOrIndex;
} else {
throw new Error(`
invalid address or index. addressOrIndex: ${addressOrIndex}`);
}
}
/** Instantiates a BLS Wallet and then connects the signer to it */
private async initializeWallet(privateKey: string | Promise<string>) {
const resolvedPrivateKey = await privateKey;
this.wallet = await BlsWalletWrapper.connect(
resolvedPrivateKey,
this.verificationGatewayAddress,
this.provider,
);
}
/**
* Sends transactions to be executed. Converts the TransactionRequest
* to a bundle and adds it to the aggregator
*
* @remarks The transaction hash returned in the transaction response does
* NOT correspond to a transaction hash that can be viewed on a block
* explorer. It instead represents the bundle hash, which can be used to
* get a transaction receipt that has a hash that can be used on a block explorer
*
* @param transaction Transaction request object
* @returns A transaction response object that can be awaited to get the transaction receipt
*/
override async sendTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionResponse> {
await this.initPromise;
if (!transaction.to) {
throw new TypeError("Transaction.to should be defined");
}
const validatedTransaction = await this._validateTransaction(transaction);
const nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
const action: ActionData = {
ethValue: validatedTransaction.value?.toString() ?? "0",
contractAddress: validatedTransaction.to!.toString(),
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
};
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
[action],
feeEstimate,
);
const bundle = this.wallet.sign({
nonce,
actions: [...actionsWithSafeFee],
});
const result = await this.provider.aggregator.add(bundle);
if ("failures" in result) {
throw new Error(JSON.stringify(result.failures));
}
return await this.provider._constructTransactionResponse(
action,
bundle.senderPublicKeys[0],
result.hash,
nonce,
);
}
/**
* @param transactionBatch A transaction batch object
* @returns A transaction batch response object that can be awaited to get the transaction receipt
*/
async sendTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<TransactionBatchResponse> {
await this.initPromise;
const validatedTransactionBatch = await this._validateTransactionBatch(
transactionBatch,
);
let nonce: BigNumber;
if (transactionBatch.batchOptions) {
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
} else {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
}
const actions: Array<ActionData> = transactionBatch.transactions.map(
(transaction) => {
return {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to!.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
};
},
);
const actionsWithFeePaymentAction =
this.provider._addFeePaymentActionForFeeEstimation(actions);
const feeEstimate = await this.provider.aggregator.estimateFee(
this.wallet.sign({
nonce,
actions: [...actionsWithFeePaymentAction],
}),
);
const safeFee = addSafetyPremiumToFee(
BigNumber.from(feeEstimate.feeRequired),
);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
actions,
safeFee,
);
const bundle = this.wallet.sign({
nonce,
actions: [...actionsWithSafeFee],
});
const result = await this.provider.aggregator.add(bundle);
if ("failures" in result) {
throw new Error(JSON.stringify(result.failures));
}
const publicKeysLinkedToActions: Array<PublicKeyLinkedToActions> =
bundle.senderPublicKeys.map((publicKey, i) => {
const operation = bundle.operations[i];
const actions = operation.actions;
return {
publicKey,
actions,
};
});
return await this.provider._constructTransactionBatchResponse(
publicKeysLinkedToActions,
result.hash,
nonce,
);
}
/**
* @returns The address associated with the BlsSigner
*/
async getAddress(): Promise<string> {
await this.initPromise;
if (this._address) {
return this._address;
}
this._address = this.wallet.address;
return this._address;
}
/**
* This method passes calls through to the underlying node and allows users to unlock EOA accounts through this provider.
* The personal namespace is used to manage keys for ECDSA signing. BLS keys are not supported natively by execution clients.
*/
async unlock(password: string): Promise<boolean> {
const provider = this.provider;
const address = await this.getAddress();
return provider.send("personal_unlockAccount", [
address.toLowerCase(),
password,
null,
]);
}
/**
* @remarks Signs a transaction that can be executed by the BlsProvider
*
* @param transaction Transaction request object
* @returns A signed bundle as a string
*/
override async signTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<string> {
await this.initPromise;
const validatedTransaction = await this._validateTransaction(transaction);
const action: ActionData = {
ethValue: validatedTransaction.value?.toString() ?? "0",
contractAddress: validatedTransaction.to!.toString(),
encodedFunction: validatedTransaction.data?.toString() ?? "0x",
};
const nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
const feeEstimate = await this.provider.estimateGas(validatedTransaction);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
[action],
feeEstimate,
);
const bundle = this.wallet.sign({
nonce,
actions: [...actionsWithSafeFee],
});
return JSON.stringify(bundleToDto(bundle));
}
/**
* Signs a transaction batch that can be executed by the BlsProvider
*
* @param transactionBatch A transaction batch object
* @returns A signed bundle containing all transactions from the transaction batch as a string
*/
async signTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<string> {
await this.initPromise;
const validatedTransactionBatch = await this._validateTransactionBatch(
transactionBatch,
);
let nonce: BigNumber;
if (transactionBatch.batchOptions) {
nonce = validatedTransactionBatch.batchOptions!.nonce as BigNumber;
} else {
nonce = await BlsWalletWrapper.Nonce(
this.wallet.PublicKey(),
this.verificationGatewayAddress,
this.provider,
);
}
const actions: Array<ActionData> = transactionBatch.transactions.map(
(transaction) => {
return {
ethValue: transaction.value?.toString() ?? "0",
contractAddress: transaction.to!.toString(),
encodedFunction: transaction.data?.toString() ?? "0x",
};
},
);
const actionsWithFeePaymentAction =
this.provider._addFeePaymentActionForFeeEstimation(actions);
const feeEstimate = await this.provider.aggregator.estimateFee(
this.wallet.sign({
nonce,
actions: [...actionsWithFeePaymentAction],
}),
);
const safeFee = addSafetyPremiumToFee(
BigNumber.from(feeEstimate.feeRequired),
);
const actionsWithSafeFee = this.provider._addFeePaymentActionWithSafeFee(
actions,
safeFee,
);
const bundle = this.wallet.sign({
nonce,
actions: [...actionsWithSafeFee],
});
return JSON.stringify(bundleToDto(bundle));
}
/** Signs a message */
// TODO: bls-wallet #201 Come back to this once we support EIP-1271
override async signMessage(message: Bytes | string): Promise<string> {
await this.initPromise;
if (isBytes(message)) {
message = hexlify(message);
}
const signedMessage = this.wallet.signMessage(message);
return RLP.encode(signedMessage);
}
override connect(provider: ethers.providers.Provider): BlsSigner {
throw new Error("cannot alter JSON-RPC Signer connection");
}
async _signTypedData(
domain: any,
types: Record<string, Array<any>>,
value: Record<string, any>,
): Promise<string> {
throw new Error("_signTypedData() is not implemented");
}
/**
* @returns A new Signer object which does not perform additional checks when sending a transaction
*/
connectUnchecked(): BlsSigner {
return new UncheckedBlsSigner(
_constructorGuard,
this.provider,
this.wallet?.blsWalletSigner.privateKey ??
(async (): Promise<string> => {
await this.initPromise;
return this.wallet.blsWalletSigner.privateKey;
})(),
this._address || this._index,
);
}
/**
* @param transaction Transaction request object
* @returns Transaction hash for the transaction, corresponds to a bundle hash
*/
async sendUncheckedTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<string> {
const transactionResponse = await this.sendTransaction(transaction);
return transactionResponse.hash;
}
async _legacySignMessage(message: Bytes | string): Promise<string> {
throw new Error("_legacySignMessage() is not implemented");
}
async _validateTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionRequest> {
const resolvedTransaction = await ethers.utils.resolveProperties(
transaction,
);
if (!resolvedTransaction.to) {
throw new TypeError("Transaction.to should be defined");
}
if (!resolvedTransaction.from) {
resolvedTransaction.from = await this.getAddress();
}
return resolvedTransaction;
}
async _validateTransactionBatch(
transactionBatch: TransactionBatch,
): Promise<TransactionBatch> {
const signerAddress = await this.getAddress();
const validatedTransactions: Array<ethers.providers.TransactionRequest> =
transactionBatch.transactions.map((transaction, i) => {
if (!transaction.to) {
throw new TypeError(`Transaction.to is missing on transaction ${i}`);
}
if (!transaction.from) {
transaction.from = signerAddress;
}
return {
...transaction,
};
});
const validatedBatchOptions = transactionBatch.batchOptions
? await this._validateBatchOptions(transactionBatch.batchOptions)
: transactionBatch.batchOptions;
return {
transactions: validatedTransactions,
batchOptions: validatedBatchOptions,
};
}
async _validateBatchOptions(
batchOptions: BatchOptions,
): Promise<BatchOptions> {
const expectedChainId = await this.getChainId();
if (batchOptions.chainId !== expectedChainId) {
throw new Error(
`Supplied chain ID ${batchOptions.chainId} does not match the expected chain ID ${expectedChainId}`,
);
}
batchOptions.nonce = BigNumber.from(batchOptions.nonce);
return batchOptions;
}
}
export class UncheckedBlsSigner extends BlsSigner {
/**
* As with other transaction methods, the transaction hash returned represents the bundle hash, NOT a transaction hash you can use on a block explorer
*
* @param transaction Transaction request object
* @returns The transaction response object with only the transaction hash property populated with a valid value
*/
override async sendTransaction(
transaction: Deferrable<ethers.providers.TransactionRequest>,
): Promise<ethers.providers.TransactionResponse> {
await this.initPromise;
const transactionResponse = await super.sendTransaction(transaction);
return {
hash: transactionResponse.hash,
nonce: NaN,
gasLimit: BigNumber.from(0),
gasPrice: BigNumber.from(0),
data: "",
value: BigNumber.from(0),
chainId: 0,
confirmations: 0,
from: "",
wait: (confirmations?: number) => {
return this.provider.waitForTransaction(
transactionResponse.hash,
confirmations,
);
},
};
}
}

View File

@@ -0,0 +1,74 @@
import { providers } from "ethers";
import {
BNPairingPrecompileCostEstimator,
BNPairingPrecompileCostEstimator__factory as BNPairingPrecompileCostEstimatorFactory,
Create2Deployer,
Create2Deployer__factory as Create2DeployerFactory,
VerificationGateway,
VerificationGateway__factory as VerificationGatewayFactory,
BLSOpen,
BLSOpen__factory as BLSOpenFactory,
BLSExpander,
BLSExpander__factory as BLSExpanderFactory,
AggregatorUtilities,
AggregatorUtilities__factory as AggregatorUtilitiesFactory,
MockERC20,
MockERC20__factory as MockERC20Factory,
} from "../typechain-types";
import { NetworkConfig } from "./NetworkConfig";
/**
* BLS Wallet Contracts
*/
export type BlsWalletContracts = Readonly<{
create2Deployer: Create2Deployer;
precompileCostEstimator: BNPairingPrecompileCostEstimator;
verificationGateway: VerificationGateway;
blsLibrary: BLSOpen;
blsExpander: BLSExpander;
aggregatorUtilities: AggregatorUtilities;
testToken: MockERC20;
}>;
/**
* Connects to all deployed BLS Wallet contracts using a Network Config
*
* @param provider ether.js provider
* @param networkConfig NetworkConfig containing contract dpeloyment information
* @returns BLS Wallet contracts connected to provider
*/
export const connectToContracts = async (
provider: providers.Provider,
{ addresses }: NetworkConfig,
): Promise<BlsWalletContracts> => {
const [
create2Deployer,
precompileCostEstimator,
verificationGateway,
blsLibrary,
blsExpander,
aggregatorUtilities,
testToken,
] = await Promise.all([
Create2DeployerFactory.connect(addresses.create2Deployer, provider),
BNPairingPrecompileCostEstimatorFactory.connect(
addresses.create2Deployer,
provider,
),
VerificationGatewayFactory.connect(addresses.verificationGateway, provider),
BLSOpenFactory.connect(addresses.blsLibrary, provider),
BLSExpanderFactory.connect(addresses.blsExpander, provider),
AggregatorUtilitiesFactory.connect(addresses.utilities, provider),
MockERC20Factory.connect(addresses.testToken, provider),
]);
return {
create2Deployer,
precompileCostEstimator,
verificationGateway,
blsLibrary,
blsExpander,
aggregatorUtilities,
testToken,
};
};

View File

@@ -1,6 +1,7 @@
import { ethers, BigNumber } from "ethers";
import { solidityKeccak256 } from "ethers/lib/utils";
/* eslint-disable camelcase */
import { ethers, BigNumber } from "ethers";
import { keccak256, solidityKeccak256, solidityPack } from "ethers/lib/utils";
import {
BlsWalletSigner,
initBlsWalletSigner,
@@ -12,100 +13,162 @@ import {
import {
BLSWallet,
// eslint-disable-next-line camelcase
BLSWallet__factory,
// eslint-disable-next-line camelcase
TransparentUpgradeableProxy__factory,
// eslint-disable-next-line camelcase
VerificationGateway,
VerificationGateway__factory,
} from "../typechain";
} from "../typechain-types";
import getRandomBlsPrivateKey from "./signer/getRandomBlsPrivateKey";
type SignerOrProvider = ethers.Signer | ethers.providers.Provider;
/**
* Class representing a BLS Wallet
*/
export default class BlsWalletWrapper {
public address: string;
private constructor(
public blsWalletSigner: BlsWalletSigner,
public privateKey: string,
public address: string,
public walletContract: BLSWallet,
) {}
) {
this.address = walletContract.address;
}
/** Get the wallet contract address for the given key, if it exists. */
static async BLSWallet(
privateKey: string,
verificationGateway: VerificationGateway,
): Promise<BLSWallet> {
const contractAddress = await BlsWalletWrapper.Address(
privateKey,
verificationGateway.address,
verificationGateway.provider,
);
return BLSWallet__factory.connect(
contractAddress,
verificationGateway.provider,
);
}
/**
* Gets the address for this wallet.
*
* This could be:
* - The address the wallet is registered to on the VerificationGateway.
* - The expected address if it has not already be created/registered.
* - The original wallet address before it was recovered to another key pair.
*
* Throws an exception if wallet was recovered to a different private key.
*
* @param privateKey private key associated with the wallet
* @param verificationGatewayAddress address of the VerficationGateway contract
* @param signerOrProvider ethers.js Signer or Provider
* @returns The wallet's address
*/
static async Address(
privateKey: string,
verificationGatewayAddress: string,
signerOrProvider: SignerOrProvider,
/**
* Internal value associated with the bls-wallet-signer library that can be
* provided as an optimization, otherwise it will be created
* automatically.
*/
blsWalletSigner?: BlsWalletSigner,
): Promise<string> {
blsWalletSigner ??= await this.#BlsWalletSigner(signerOrProvider);
const blsWalletSigner = await this.#BlsWalletSigner(
signerOrProvider,
privateKey,
verificationGatewayAddress,
);
const verificationGateway = VerificationGateway__factory.connect(
verificationGatewayAddress,
signerOrProvider,
);
const pubKeyHash = blsWalletSigner.getPublicKeyHash();
const [proxyAdminAddress, blsWalletLogicAddress] = await Promise.all([
verificationGateway.walletProxyAdmin(),
verificationGateway.blsWalletLogic(),
]);
const initFunctionParams =
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
verificationGatewayAddress,
]);
return ethers.utils.getCreate2Address(
verificationGatewayAddress,
blsWalletSigner.getPublicKeyHash(privateKey),
ethers.utils.solidityKeccak256(
["bytes", "bytes"],
[
TransparentUpgradeableProxy__factory.bytecode,
ethers.utils.defaultAbiCoder.encode(
["address", "address", "bytes"],
[blsWalletLogicAddress, proxyAdminAddress, initFunctionParams],
),
],
),
const existingAddress = await verificationGateway.walletFromHash(
pubKeyHash,
);
const hasExistingAddress = !BigNumber.from(existingAddress).isZero();
if (hasExistingAddress) {
return existingAddress;
}
const expectedAddress = await this.ExpectedAddress(
verificationGateway,
pubKeyHash,
);
this.validateWalletNotRecovered(
blsWalletSigner,
verificationGateway,
expectedAddress,
);
return expectedAddress;
}
/** Get the wallet contract address for the given public key */
static async AddressFromPublicKey(
publicKey: PublicKey,
verificationGateway: VerificationGateway,
): Promise<string> {
const pubKeyHash = keccak256(solidityPack(["uint256[4]"], [publicKey]));
const existingAddress = await verificationGateway.walletFromHash(
pubKeyHash,
);
if (!BigNumber.from(existingAddress).isZero()) {
return existingAddress;
}
return this.ExpectedAddress(verificationGateway, pubKeyHash);
}
static async getRandomBlsPrivateKey(): Promise<string> {
return getRandomBlsPrivateKey();
}
/**
* Instantiate a `BLSWallet` associated with the provided key if the
* Instantiate a `BLSWallet` associated with the provided private key.
* associated wallet contract already exists.
*
* Throws an exception if wallet was recovered to a different private key.
*
* @param privateKey private key associated with the wallet
* @param verificationGatewayAddress address of the VerficationGateway contract
* @param provider ethers.js Provider
* @returns a BLS Wallet
*/
static async connect(
privateKey: string,
verificationGatewayAddress: string,
provider: ethers.providers.Provider,
): Promise<BlsWalletWrapper> {
const network = await provider.getNetwork();
const blsWalletSigner = await initBlsWalletSigner({
chainId: network.chainId,
});
const contractAddress = await BlsWalletWrapper.Address(
privateKey,
const verificationGateway = VerificationGateway__factory.connect(
verificationGatewayAddress,
provider,
);
const blsWalletSigner = await initBlsWalletSigner({
chainId: (await verificationGateway.provider.getNetwork()).chainId,
privateKey,
verificationGatewayAddress,
});
const walletContract = BLSWallet__factory.connect(
contractAddress,
provider,
const blsWalletWrapper = new BlsWalletWrapper(
blsWalletSigner,
await BlsWalletWrapper.BLSWallet(privateKey, verificationGateway),
);
return new BlsWalletWrapper(
blsWalletSigner,
privateKey,
contractAddress,
walletContract,
return blsWalletWrapper;
}
async syncWallet(verificationGateway: VerificationGateway) {
this.address = await BlsWalletWrapper.Address(
this.blsWalletSigner.privateKey,
verificationGateway.address,
verificationGateway.provider,
);
this.walletContract = BLSWallet__factory.connect(
this.address,
verificationGateway.provider,
);
}
@@ -114,7 +177,9 @@ export default class BlsWalletWrapper {
* block.
*/
async Nonce(): Promise<BigNumber> {
const code = await this.walletContract.provider.getCode(this.address);
const code = await this.walletContract.provider.getCode(
this.walletContract.address,
);
if (code === "0x") {
// The wallet doesn't exist yet. Wallets are lazily created, so the nonce
@@ -164,12 +229,12 @@ export default class BlsWalletWrapper {
/** Sign an operation, producing a `Bundle` object suitable for use with an aggregator. */
sign(operation: Operation): Bundle {
return this.blsWalletSigner.sign(operation, this.privateKey);
return this.blsWalletSigner.sign(operation, this.walletContract.address);
}
/** Sign a message */
signMessage(message: string): Signature {
return this.blsWalletSigner.signMessage(message, this.privateKey);
return this.blsWalletSigner.signMessage(message);
}
/**
@@ -178,7 +243,7 @@ export default class BlsWalletWrapper {
* @returns Wallet's BLS public key.
*/
PublicKey(): PublicKey {
return this.blsWalletSigner.getPublicKey(this.privateKey);
return this.blsWalletSigner.getPublicKey();
}
/**
@@ -187,17 +252,164 @@ export default class BlsWalletWrapper {
* @returns Wallet's BLS public key as a string.
*/
PublicKeyStr(): string {
return this.blsWalletSigner.getPublicKeyStr(this.privateKey);
return this.blsWalletSigner.getPublicKeyStr();
}
async getSetRecoveryHashBundle(
salt: string,
recoverWalletAddress: string,
): Promise<Bundle> {
const saltHash = ethers.utils.formatBytes32String(salt);
const walletHash = this.blsWalletSigner.getPublicKeyHash();
const recoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[recoverWalletAddress, walletHash, saltHash],
);
return this.sign({
nonce: await this.Nonce(),
actions: [
{
ethValue: 0,
contractAddress: this.walletContract.address,
encodedFunction: this.walletContract.interface.encodeFunctionData(
"setRecoveryHash",
[recoveryHash],
),
},
],
});
}
async getRecoverWalletBundle(
recoveryAddress: string,
newPrivateKey: string,
recoverySalt: string,
verificationGateway: VerificationGateway,
): Promise<Bundle> {
const updatedWallet = await BlsWalletWrapper.connect(
newPrivateKey,
verificationGateway.address,
verificationGateway.provider,
);
const addressMessage = solidityPack(["address"], [recoveryAddress]);
const addressSignature = updatedWallet.signMessage(addressMessage);
const recoveryWalletHash = await verificationGateway.hashFromWallet(
recoveryAddress,
);
const saltHash = ethers.utils.formatBytes32String(recoverySalt);
return this.sign({
nonce: await this.Nonce(),
actions: [
{
ethValue: 0,
contractAddress: verificationGateway.address,
encodedFunction: verificationGateway.interface.encodeFunctionData(
"recoverWallet",
[
addressSignature,
recoveryWalletHash,
saltHash,
updatedWallet.PublicKey(),
],
),
},
],
});
}
static async #BlsWalletSigner(
signerOrProvider: SignerOrProvider,
privateKey: string,
verificationGatewayAddress: string,
): Promise<BlsWalletSigner> {
const chainId =
"getChainId" in signerOrProvider
? await signerOrProvider.getChainId()
: (await signerOrProvider.getNetwork()).chainId;
return await initBlsWalletSigner({ chainId });
return await initBlsWalletSigner({
chainId,
privateKey,
verificationGatewayAddress,
});
}
/**
* Binds the BlsWalletSigner instance to a new private key and chainId
*
* @returns The updated BlsWalletSigner object
*/
async setBlsWalletSigner(
signerOrProvider: SignerOrProvider,
privateKey: string,
): Promise<BlsWalletSigner> {
const chainId =
"getChainId" in signerOrProvider
? await signerOrProvider.getChainId()
: (await signerOrProvider.getNetwork()).chainId;
const newBlsWalletSigner = await initBlsWalletSigner({
chainId,
privateKey,
verificationGatewayAddress: this.walletContract.address,
});
this.blsWalletSigner = newBlsWalletSigner;
return newBlsWalletSigner;
}
// Calculates the expected address the wallet will be created at
private static async ExpectedAddress(
verificationGateway: VerificationGateway,
pubKeyHash: string,
): Promise<string> {
const [proxyAdminAddress, blsWalletLogicAddress] = await Promise.all([
verificationGateway.walletProxyAdmin(),
verificationGateway.blsWalletLogic(),
]);
const initFunctionParams =
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
verificationGateway.address,
]);
return ethers.utils.getCreate2Address(
verificationGateway.address,
pubKeyHash,
ethers.utils.solidityKeccak256(
["bytes", "bytes"],
[
TransparentUpgradeableProxy__factory.bytecode,
ethers.utils.defaultAbiCoder.encode(
["address", "address", "bytes"],
[blsWalletLogicAddress, proxyAdminAddress, initFunctionParams],
),
],
),
);
}
private static async validateWalletNotRecovered(
blsWalletSigner: BlsWalletSigner,
verificationGateway: VerificationGateway,
walletAddress: string,
): Promise<void> {
const pubKeyHash = blsWalletSigner.getPublicKeyHash();
const existingPubKeyHash = await verificationGateway.hashFromWallet(
walletAddress,
);
const walletIsAlreadyRegistered =
!BigNumber.from(existingPubKeyHash).isZero();
const pubKeyHashesDoNotMatch = pubKeyHash !== existingPubKeyHash;
if (walletIsAlreadyRegistered && pubKeyHashesDoNotMatch) {
throw new Error(
`wallet at ${walletAddress} has been recovered from public key hash ${pubKeyHash} to ${existingPubKeyHash}`,
);
}
}
}

View File

@@ -0,0 +1,61 @@
import { NetworkConfig, validateConfig } from "./NetworkConfig";
/**
* Config representing the deployed state of bls-wallet contracts
* across multiple networks.
*/
export type MultiNetworkConfig = {
[networkKey: string]: NetworkConfig;
};
/**
* Unvalidated MultiNetworkConfig
*/
export type UnvalidatedMultiNetworkConfig = Record<
string,
Record<string, Record<string, unknown>>
>;
type ReadFileFunc = (filePath: string) => Promise<string>;
/**
* Validates and returns a multi-network config.
*
* @param cfg The config object to validate.
*/
export function validateMultiConfig(
cfg: MultiNetworkConfig,
): MultiNetworkConfig {
const isEmpty = !Object.keys(cfg).length;
if (isEmpty) {
throw new Error("config is empty");
}
const multiConfig: MultiNetworkConfig = {};
for (const [networkKey, networkConfig] of Object.entries(cfg)) {
try {
multiConfig[networkKey] = validateConfig(networkConfig);
} catch (err) {
const castErr = err as Error;
const newErr = new Error(`${networkKey}: ${castErr.message}`);
newErr.stack = castErr.stack;
throw newErr;
}
}
return multiConfig;
}
/**
* Retrieves, validates, and returns a multi-network config.
*
* @param networkConfigPath Path to config JSON file.
* @param readFileFunc Callback to retrieve the config. This could be via fetch, fs.readFile, etc.
*/
export async function getMultiConfig(
configPath: string,
readFileFunc: ReadFileFunc,
): Promise<NetworkConfig> {
const cfg = JSON.parse(await readFileFunc(configPath));
validateMultiConfig(cfg);
return cfg;
}

View File

@@ -43,13 +43,14 @@ export type NetworkConfig = {
};
type ReadFileFunc = (filePath: string) => Promise<string>;
type UnvalidatedConfig = Record<string, Record<string, unknown>>;
/**
* Validates and returns a network config.
*
* @param cfg The config object to validate.
*/
export function validateConfig(cfg: any): NetworkConfig {
export function validateConfig(cfg: UnvalidatedConfig): NetworkConfig {
return {
parameters: assertUnknownRecord(cfg.parameters),
addresses: {
@@ -75,6 +76,7 @@ export function validateConfig(cfg: any): NetworkConfig {
/**
* Retrieves, validates, and returns a network config.
* @deprecated Use getMultiConfig instead.
*
* @param networkConfigPath Path to config JSON file.
* @param readFileFunc Callback to retrieve the config. This could be via fetch, fs.readFile, etc.

Some files were not shown because too many files have changed in this diff Show More