mirror of
https://github.com/getwax/bls-wallet.git
synced 2026-01-10 08:07:54 -05:00
Compare commits
277 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b40d29c82d | ||
|
|
5c6d96f7e5 | ||
|
|
ccc8a59f2f | ||
|
|
57e7ec3d24 | ||
|
|
0a00d8606f | ||
|
|
940dd11de9 | ||
|
|
02298b8029 | ||
|
|
08c059acb4 | ||
|
|
54e752f503 | ||
|
|
bc3d1463f1 | ||
|
|
bc37324932 | ||
|
|
a4a0e951a8 | ||
|
|
f3da0977b8 | ||
|
|
b8cefff40e | ||
|
|
59e1a65242 | ||
|
|
771de1fbab | ||
|
|
2b5ad82d5c | ||
|
|
c508211f18 | ||
|
|
6e00912981 | ||
|
|
d1adf2572a | ||
|
|
4888426a4e | ||
|
|
cd103faf84 | ||
|
|
20fc590cd7 | ||
|
|
99dbaadda9 | ||
|
|
02ffad7f85 | ||
|
|
84dd0a09d8 | ||
|
|
e4ab6b53db | ||
|
|
097174b243 | ||
|
|
93c7935dc6 | ||
|
|
b06435e3d7 | ||
|
|
bb8bd2458e | ||
|
|
6379006133 | ||
|
|
4bc4701faf | ||
|
|
5b7231b980 | ||
|
|
3cfabaa4b1 | ||
|
|
b8ae335449 | ||
|
|
221b7b94e0 | ||
|
|
8f17aa983d | ||
|
|
6a3df55a38 | ||
|
|
200718814d | ||
|
|
0904d8169d | ||
|
|
d476a53d0d | ||
|
|
5b771c0e8c | ||
|
|
2b16c7367d | ||
|
|
0c956188a3 | ||
|
|
6ec971ecac | ||
|
|
80e56c1bb5 | ||
|
|
05660dd468 | ||
|
|
aa0ba1ad47 | ||
|
|
feaf497c22 | ||
|
|
06a3dd025a | ||
|
|
cac08ec5a6 | ||
|
|
7c93ccea89 | ||
|
|
647c1bbf4d | ||
|
|
c2bf4bbc16 | ||
|
|
2d28b7b17a | ||
|
|
92b9b136a7 | ||
|
|
c3e79a7c8e | ||
|
|
163af9d49c | ||
|
|
f2a490aaf2 | ||
|
|
1481c69dc0 | ||
|
|
17052cdaa1 | ||
|
|
def1eaf208 | ||
|
|
5a33e43945 | ||
|
|
998ee6ba30 | ||
|
|
f0a3b56a44 | ||
|
|
4147726d2a | ||
|
|
192db97b01 | ||
|
|
e2ee017824 | ||
|
|
a485d84364 | ||
|
|
ae5e1adcc2 | ||
|
|
1ee24813ed | ||
|
|
4dfa0bdd4a | ||
|
|
de5c288d07 | ||
|
|
db0cf3acc8 | ||
|
|
035f1872fe | ||
|
|
60ed167b2d | ||
|
|
1860ecbd18 | ||
|
|
e830f49714 | ||
|
|
8753978719 | ||
|
|
fec0f12e45 | ||
|
|
dcfd13d8b3 | ||
|
|
665ea530fc | ||
|
|
b547361d86 | ||
|
|
b513d9feab | ||
|
|
8d4257aac2 | ||
|
|
e4ad6d317c | ||
|
|
080513ec12 | ||
|
|
3c9490b5a6 | ||
|
|
e7decded4c | ||
|
|
427d05ce82 | ||
|
|
a6e5d6d3d8 | ||
|
|
bcbab0d08a | ||
|
|
1303ffea9b | ||
|
|
356b0674e7 | ||
|
|
90499370b5 | ||
|
|
0007a8491d | ||
|
|
eeb7f506ab | ||
|
|
8a7187c041 | ||
|
|
68547ca88f | ||
|
|
576802a79a | ||
|
|
ce8620c978 | ||
|
|
ce9e654815 | ||
|
|
19bdc19e8d | ||
|
|
b05525a1b8 | ||
|
|
53e2f518d5 | ||
|
|
9d22092b2c | ||
|
|
3936b59e22 | ||
|
|
a7d8209664 | ||
|
|
6ed993d951 | ||
|
|
23613b42b8 | ||
|
|
92195d1f3e | ||
|
|
5b2168e262 | ||
|
|
8d5b0867fe | ||
|
|
03ced09fc2 | ||
|
|
b267fcd3f3 | ||
|
|
297fba186b | ||
|
|
ebb90e2f9a | ||
|
|
6608add701 | ||
|
|
6d07b754dc | ||
|
|
3ac37a0e43 | ||
|
|
b1bac6fa79 | ||
|
|
87eb0b53fb | ||
|
|
17b7293057 | ||
|
|
11414de511 | ||
|
|
6c6972ce5c | ||
|
|
a8b5e4c9d7 | ||
|
|
07c59cd0bd | ||
|
|
3512982eff | ||
|
|
7a37dc89d0 | ||
|
|
17654c5042 | ||
|
|
2f62a6da8f | ||
|
|
ee2a533c72 | ||
|
|
6f45f7942f | ||
|
|
df6daffcce | ||
|
|
287c1c64ee | ||
|
|
c13e9800a4 | ||
|
|
4548bf0a77 | ||
|
|
e64b637440 | ||
|
|
fd6f60d39c | ||
|
|
3a519cd6e4 | ||
|
|
56bdf8a1df | ||
|
|
600d898fc0 | ||
|
|
8f5eff131a | ||
|
|
eebff6d205 | ||
|
|
bcf6aef290 | ||
|
|
ed7bbf5397 | ||
|
|
0aacf46234 | ||
|
|
723740348f | ||
|
|
63d9470cad | ||
|
|
c285e8dfaf | ||
|
|
cf46dbf3ff | ||
|
|
427ce33267 | ||
|
|
7a7cc0a9d4 | ||
|
|
6d143fcbc8 | ||
|
|
8c8e840dc6 | ||
|
|
d30c692263 | ||
|
|
014871ee8d | ||
|
|
20d46b4900 | ||
|
|
268ffe41f8 | ||
|
|
a0a2ee878b | ||
|
|
3a9c6ce4cd | ||
|
|
3106e1dd0a | ||
|
|
8745c185b9 | ||
|
|
668362304b | ||
|
|
e89a9151e6 | ||
|
|
c435fe0471 | ||
|
|
956607a479 | ||
|
|
04af3ae83a | ||
|
|
27b0864c79 | ||
|
|
76a7daa0bd | ||
|
|
b38e216585 | ||
|
|
5da15cb16f | ||
|
|
44b89fd8f3 | ||
|
|
b8122ced5c | ||
|
|
7613237f96 | ||
|
|
368aa43085 | ||
|
|
68b5446a6f | ||
|
|
a0f858dbbb | ||
|
|
83e30afea1 | ||
|
|
d9616333a4 | ||
|
|
0570216fb2 | ||
|
|
0515caacfc | ||
|
|
767eafe338 | ||
|
|
d76fb1c227 | ||
|
|
ec2ae3eb2d | ||
|
|
964b4c7098 | ||
|
|
4259ee5bce | ||
|
|
06a1b7318e | ||
|
|
3c9cbe6c3e | ||
|
|
6b86b95f3c | ||
|
|
911ffa4865 | ||
|
|
c5f460969f | ||
|
|
a2fb8f7039 | ||
|
|
87696c3251 | ||
|
|
a4dbeb00e9 | ||
|
|
2a24a15460 | ||
|
|
9040e90457 | ||
|
|
3d483d6b76 | ||
|
|
7d6aa698ec | ||
|
|
7d240daaa5 | ||
|
|
5f866991dc | ||
|
|
0959db5c4c | ||
|
|
c6c37ffc47 | ||
|
|
1a80eb4a4a | ||
|
|
a7c5de25aa | ||
|
|
a2bd4165b3 | ||
|
|
d4bf0b38c4 | ||
|
|
64b312e646 | ||
|
|
1abdcd46df | ||
|
|
64db0b7eba | ||
|
|
b809a22cee | ||
|
|
94ae095acf | ||
|
|
d52d598573 | ||
|
|
8a4c6e52f1 | ||
|
|
f6473e72af | ||
|
|
0f82acf931 | ||
|
|
bfada65260 | ||
|
|
79423ec79f | ||
|
|
5225cb1883 | ||
|
|
f4f5703eb6 | ||
|
|
36825dbbe6 | ||
|
|
54e90045fb | ||
|
|
89c8b28d20 | ||
|
|
aa5c2fea6b | ||
|
|
66c257cacd | ||
|
|
501afd7d64 | ||
|
|
676471b3fe | ||
|
|
26686cdfe3 | ||
|
|
7cbd6a7617 | ||
|
|
1c3b1c76b3 | ||
|
|
ed9ab2a103 | ||
|
|
b38547126e | ||
|
|
e1aa4de7fb | ||
|
|
c0fa1cdb91 | ||
|
|
7f5e5e2f27 | ||
|
|
8761d5e7f6 | ||
|
|
c62dceb6a9 | ||
|
|
d8dd145ae8 | ||
|
|
e43ddbd0fe | ||
|
|
ce73b22a50 | ||
|
|
449f5b439a | ||
|
|
3fcddfe2f7 | ||
|
|
0eee5e34b4 | ||
|
|
7fb3a62655 | ||
|
|
7c15195aa6 | ||
|
|
b15c81ed2e | ||
|
|
a2756603bc | ||
|
|
7fc2b45419 | ||
|
|
ba83438cf0 | ||
|
|
e0d47566db | ||
|
|
39d02cbea0 | ||
|
|
b8ae6e9370 | ||
|
|
1b403b9300 | ||
|
|
6ce54b404d | ||
|
|
dbdd56795d | ||
|
|
1358430dbb | ||
|
|
b200502594 | ||
|
|
5f506f5f14 | ||
|
|
c982fe7a69 | ||
|
|
718be15cac | ||
|
|
ae6db5b3f7 | ||
|
|
d4cfe0e4f6 | ||
|
|
aad71f738f | ||
|
|
1e88e2711f | ||
|
|
b6e057bed8 | ||
|
|
5c1388e95c | ||
|
|
c3169e93b4 | ||
|
|
847b02d31e | ||
|
|
b5c30bedb2 | ||
|
|
350a31b66f | ||
|
|
36396a74d9 | ||
|
|
78465e133e | ||
|
|
363a518acd | ||
|
|
565e767d12 | ||
|
|
49a3661e52 | ||
|
|
9cc3db2fce |
@@ -27,21 +27,21 @@ runs:
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: |
|
||||
cp .env.release .env
|
||||
envsubst < config.release.json > config.json
|
||||
yarn install --frozen-lockfile
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: |
|
||||
CRYPTO_COMPARE_API_KEY=${CRYPTO_COMPARE_API_KEY} \
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-testnet.json \
|
||||
NETWORK_CONFIGS_DIR=../contracts/networks \
|
||||
yarn build:${{ inputs.browser }}
|
||||
- working-directory: ./extension
|
||||
shell: bash
|
||||
run: mv ./extension/${{ inputs.file-name }} ./extension/quill-${{ inputs.file-name }}
|
||||
|
||||
- uses: softprops/action-gh-release@v1
|
||||
- uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
tag_name: ${{ inputs.tag-name }}
|
||||
tag: ${{ inputs.tag-name }}
|
||||
# Note: This path is from repo root
|
||||
# working-directory is not applied
|
||||
files: ./extension/extension/quill-${{ inputs.file-name }}
|
||||
# working-directory is not applied
|
||||
file: ./extension/extension/quill-${{ inputs.file-name }}
|
||||
overwrite: true
|
||||
|
||||
2
.github/workflows/aggregator-proxy.yml
vendored
2
.github/workflows/aggregator-proxy.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
paths:
|
||||
- 'aggregator-proxy/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator-proxy/**'
|
||||
|
||||
|
||||
14
.github/workflows/aggregator.yml
vendored
14
.github/workflows/aggregator.yml
vendored
@@ -6,11 +6,19 @@ on:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
# Check for breaking changes from contracts
|
||||
- 'contracts/**'
|
||||
- '.github/workflows/aggregator.yml'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'aggregator/**'
|
||||
# Check for breaking changes from contracts
|
||||
- 'contracts/**'
|
||||
- '.github/workflows/aggregator.yml'
|
||||
branches-ignore:
|
||||
# Changes targeting this branch should be tested+fixed when being merged
|
||||
# into main
|
||||
- contract-updates
|
||||
|
||||
defaults:
|
||||
run:
|
||||
@@ -70,7 +78,7 @@ jobs:
|
||||
|
||||
- working-directory: ./
|
||||
run: docker-compose up -d postgres
|
||||
- run: cp .env.example .env
|
||||
- run: cp .env.local.example .env
|
||||
- run: deno test --allow-net --allow-env --allow-read --unstable
|
||||
|
||||
# Cleanup
|
||||
|
||||
10
.github/workflows/clients.yml
vendored
10
.github/workflows/clients.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
paths:
|
||||
- 'contracts/clients/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'contracts/clients/**'
|
||||
|
||||
@@ -17,6 +15,14 @@ defaults:
|
||||
working-directory: ./contracts/clients
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/setup-contracts-clients
|
||||
- run: yarn build
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
|
||||
2
.github/workflows/contracts.yml
vendored
2
.github/workflows/contracts.yml
vendored
@@ -8,8 +8,6 @@ on:
|
||||
- 'contracts/**'
|
||||
- '!contracts/clients/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'contracts/**'
|
||||
- '!contracts/clients/**'
|
||||
|
||||
7
.github/workflows/extension.yml
vendored
7
.github/workflows/extension.yml
vendored
@@ -7,8 +7,6 @@ on:
|
||||
paths:
|
||||
- 'extension/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
paths:
|
||||
- 'extension/**'
|
||||
|
||||
@@ -43,10 +41,7 @@ jobs:
|
||||
node-version: ${{ env.NODEJS_VERSION }}
|
||||
cache: yarn
|
||||
cache-dependency-path: extension/yarn.lock
|
||||
# Valid network config not needed to test build.
|
||||
- working-directory: ./contracts/networks
|
||||
run: echo "{}" > "local.json"
|
||||
- run: cp .env.example .env
|
||||
- run: cp config.example.json config.json
|
||||
- run: yarn install --frozen-lockfile
|
||||
# For now, just check that chrome builds
|
||||
- run: yarn build:chrome
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,2 +1,3 @@
|
||||
.data
|
||||
.DS_Store
|
||||
.idea
|
||||
@@ -8,6 +8,7 @@ You can watch a full end-to-end demo of the project [here](https://www.youtube.c
|
||||
|
||||
- [See an overview of BLS Wallet & how the components work together](./docs/system_overview.md)
|
||||
- [Use BLS Wallet in a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md)
|
||||
- [Use BLS Wallet in your L2 dApp for cheaper, multi action transactions](./docs/use_bls_wallet_dapp.md)
|
||||
- Setup the BLS Wallet components for:
|
||||
- [Local develeopment](./docs/local_development.md)
|
||||
- [Remote development](./docs/remote_development.md)
|
||||
@@ -34,6 +35,14 @@ npm package which provides easy to use constructs to interact with the contracts
|
||||
|
||||
Prototype browser extension used to manage BLS Wallets and sign transactions.
|
||||
|
||||
|
||||
## Contract Deployments
|
||||
|
||||
See [./contracts/networks](./contracts/networks/) for a list of all contract deployment (network) manifests. Have an L2/rollup testnet you'd like BLS Wallet deployed on? [Open an issue](https://github.com/web3well/bls-wallet/issues/new) or [Deploy it yourself](./docs/remote_development.md)
|
||||
|
||||
- [Arbitrum Goerli](./contracts/networks/arbitrum-goerli.json)
|
||||
- [Arbitrum Rinkby](./contracts/networks/arbitrum-testnet.json) (deprecated, outdated)
|
||||
|
||||
## Ways to Contribute
|
||||
|
||||
- [Work on an open issue](https://github.com/web3well/bls-wallet/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22)
|
||||
|
||||
@@ -37,3 +37,13 @@ runAggregatorProxy(
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
## Instant wallet without dapp-sponsored transaction
|
||||

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

|
||||
|
||||
## Example dApp using a proxy aggregator
|
||||
|
||||
- https://github.com/JohnGuilding/single-pool-dex
|
||||
@@ -21,7 +21,7 @@
|
||||
"@types/koa__cors": "^3.3.0",
|
||||
"@types/koa__router": "^8.0.11",
|
||||
"@types/node-fetch": "^2.6.1",
|
||||
"bls-wallet-clients": "^0.6.0",
|
||||
"bls-wallet-clients": "0.8.0",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"io-ts-reporters": "^2.0.1",
|
||||
|
||||
@@ -452,9 +452,6 @@
|
||||
version "5.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@ethersproject/rlp/-/rlp-5.5.0.tgz#530f4f608f9ca9d4f89c24ab95db58ab56ab99a0"
|
||||
integrity sha512-hLv8XaQ8PTI9g2RHoQGf/WSxBfTB/NudRacbzdxmst5VHAqd1sMibWG7SENzT5Dj3yZ3kJYx+WiRYEcQTAkcYA==
|
||||
dependencies:
|
||||
"@ethersproject/bytes" "^5.5.0"
|
||||
"@ethersproject/logger" "^5.5.0"
|
||||
|
||||
"@ethersproject/rlp@5.6.0", "@ethersproject/rlp@^5.5.0", "@ethersproject/rlp@^5.6.0":
|
||||
version "5.6.0"
|
||||
@@ -885,10 +882,10 @@ bech32@1.1.4:
|
||||
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
|
||||
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==
|
||||
|
||||
bls-wallet-clients@^0.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.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.8.0.tgz#7b6510dede672fcbcfd743caa85a038fa26aad11"
|
||||
integrity sha512-cutglRs+1vRiFPmo5uwMRZRjkeNxr/X3NiyUqsB4VgIRQ+EixCMXvuY7BDok5yby6GWzXox2ZAqJkGTFuHyr2w==
|
||||
dependencies:
|
||||
"@thehubbleproject/bls" "^0.5.1"
|
||||
ethers "5.5.4"
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
RPC_URL=http://localhost:8545
|
||||
RPC_URL=https://goerli-rollup.arbitrum.io/rpc
|
||||
|
||||
USE_TEST_NET=false
|
||||
|
||||
ORIGIN=http://localhost:3000
|
||||
PORT=3000
|
||||
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/local.json
|
||||
PRIVATE_KEY_AGG=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-goerli.json
|
||||
PRIVATE_KEY_AGG=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
|
||||
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
|
||||
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
|
||||
|
||||
PG_HOST=localhost
|
||||
PG_HOST=127.0.0.1
|
||||
PG_PORT=5432
|
||||
PG_USER=bls
|
||||
PG_PASSWORD=generate-a-strong-password
|
||||
|
||||
32
aggregator/.env.local.example
Normal file
32
aggregator/.env.local.example
Normal file
@@ -0,0 +1,32 @@
|
||||
RPC_URL=http://localhost:8545
|
||||
|
||||
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
|
||||
|
||||
PG_HOST=localhost
|
||||
PG_PORT=5432
|
||||
PG_USER=bls
|
||||
PG_PASSWORD=generate-a-strong-password
|
||||
PG_DB_NAME=bls_aggregator
|
||||
|
||||
BUNDLE_TABLE_NAME=bundles
|
||||
BUNDLE_QUERY_LIMIT=100
|
||||
MAX_ELIGIBILITY_DELAY=300
|
||||
|
||||
MAX_AGGREGATION_SIZE=12
|
||||
MAX_AGGREGATION_DELAY_MILLIS=5000
|
||||
MAX_UNCONFIRMED_AGGREGATIONS=3
|
||||
|
||||
LOG_QUERIES=false
|
||||
TEST_LOGGING=false
|
||||
|
||||
FEE_TYPE=ether
|
||||
FEE_PER_GAS=0
|
||||
FEE_PER_BYTE=0
|
||||
2
aggregator/.gitignore
vendored
2
aggregator/.gitignore
vendored
@@ -1,4 +1,4 @@
|
||||
.env*
|
||||
!.env.example
|
||||
!.env*.example
|
||||
cov_profile*
|
||||
/build
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM denoland/deno:1.20.6
|
||||
FROM denoland/deno:1.23.4
|
||||
|
||||
ADD build /app
|
||||
WORKDIR /app
|
||||
|
||||
@@ -29,6 +29,34 @@ you might have:
|
||||
If you don't have a `.env`, you will need to append `--env <name>` to all
|
||||
commands.
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
| Name | Example Value | Description |
|
||||
| ---------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| RPC_URL | https://localhost:8545 | The RPC endpoint for an EVM node that the BLS Wallet contracts are deployed on |
|
||||
| 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 |
|
||||
| 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 |
|
||||
| PG_HOST | 127.0.0.1 | Postgres database host |
|
||||
| PG_PORT | 5432 | Postgres database port |
|
||||
| PG_USER | bls | Postgres database user |
|
||||
| PG_PASSWORD | generate-a-strong-password | Postgres database password |
|
||||
| PG_DB_NAME | bls_aggregator | Postgres database name |
|
||||
| BUNDLE_TABLE_NAME | bundles | Postgres table name for bundles |
|
||||
| BUNDLE_QUERY_LIMIT | 100 | Maximum number of bundles returned from Postgres |
|
||||
| MAX_AGGREGATION_SIZE | 12 | Maximum number of actions from bundles which will be aggregated together for submission on chain |
|
||||
| MAX_AGGREGATION_DELAY_MILLIS | 5000 | Maximum amount of time in milliseconds aggregator will wait before submitting bundles on chain |
|
||||
| MAX_UNCONFIRMED_AGGREGATIONS | 3 | Maximum unconfirmed bundle aggregations that will be submitted on chain. Multiplied with `MAX_AGGREGATION_SIZE` to determine maximum of unconfirmed on chain actions |
|
||||
| LOG_QUERIES | false | Whether to print Postgres queries in event log.`TEST_LOGGING` must be enabled |
|
||||
| TEST_LOGGING | false | Whether to print aggregator server events to stdout. Useful for debugging & logging. |
|
||||
| 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 |
|
||||
| FEE_PER_GAS | 0 | Minimum amount per gas (gasPrice) the aggregator will accept in ETH/chain native currency/ERC20 tokens |
|
||||
| FEE_PER_BYTE | 0 | Minimum amount per calldata byte the aggregator will accept in ETH/chain native currency/ERC20 tokens (rollup L1 cost) |
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
#### With docker-compose
|
||||
@@ -170,10 +198,17 @@ deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregato
|
||||
|
||||
#### Transaction reverted: function call to a non-contract account
|
||||
|
||||
- Is `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s `COST_ESTIMATOR_ADDRESS` set to the right precompile cost estimator's contract address?
|
||||
- Is `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s
|
||||
`COST_ESTIMATOR_ADDRESS` set to the right precompile cost estimator's contract
|
||||
address?
|
||||
- Are the BLS Wallet contracts deployed on the correct network?
|
||||
- Is `NETWORK_CONFIG_PATH` in `.env` set to the right config?
|
||||
|
||||
#### Deno version
|
||||
|
||||
Make sure your Deno version is
|
||||
[up to date.](https://deno.land/manual/getting_started/installation#updating)
|
||||
|
||||
### Notable Components
|
||||
|
||||
- **src/chain**: Should contain all of the contract interactions, exposing more
|
||||
|
||||
@@ -49,7 +49,7 @@ export type {
|
||||
PublicKey,
|
||||
Signature,
|
||||
VerificationGateway,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.6.0";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.0";
|
||||
|
||||
export {
|
||||
Aggregator as AggregatorClient,
|
||||
@@ -59,10 +59,10 @@ export {
|
||||
getConfig,
|
||||
MockERC20__factory,
|
||||
VerificationGateway__factory,
|
||||
} from "https://esm.sh/bls-wallet-clients@0.6.0";
|
||||
} from "https://esm.sh/bls-wallet-clients@0.8.0";
|
||||
|
||||
// Workaround for esbuild's export-star bug
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.6.0";
|
||||
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.8.0";
|
||||
const {
|
||||
bundleFromDto,
|
||||
bundleToDto,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write
|
||||
#!/usr/bin/env -S deno run --unstable --allow-run --allow-read --allow-write --allow-env
|
||||
|
||||
import { dirname, parseArgs } from "../deps.ts";
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import {
|
||||
BigNumber,
|
||||
BlsWalletSigner,
|
||||
BlsWalletWrapper,
|
||||
Bundle,
|
||||
delay,
|
||||
ethers,
|
||||
@@ -151,15 +152,22 @@ 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({
|
||||
for (const walletAddr of walletAddresses) {
|
||||
const signedCorrectly = this.blsWalletSigner.verify(bundle, walletAddr);
|
||||
if (!signedCorrectly) {
|
||||
failures.push({
|
||||
type: "invalid-signature",
|
||||
description: "invalid signature",
|
||||
});
|
||||
description: `invalid signature for wallet address ${walletAddr}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
failures.push(...await this.ethereumService.checkNonces(bundle));
|
||||
@@ -205,6 +213,8 @@ export default class BundleService {
|
||||
|
||||
return {
|
||||
transactionIndex: receipt.transactionIndex,
|
||||
transactionHash: receipt.transactionHash,
|
||||
bundleHash: hash,
|
||||
blockHash: receipt.blockHash,
|
||||
blockNumber: receipt.blockNumber,
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import Range from "../src/helpers/Range.ts";
|
||||
import {
|
||||
assertEquals,
|
||||
assertBundleSucceeds,
|
||||
assertEquals,
|
||||
BigNumber,
|
||||
BlsWalletWrapper,
|
||||
ethers,
|
||||
@@ -255,7 +255,7 @@ Fixture.test("submits 9/10 bundles when 7th has insufficient gas-based fee", asy
|
||||
});
|
||||
|
||||
const baseFee = BigNumber.from(1_000_000).mul(1e9); // Note 1
|
||||
const fee = BigNumber.from(1_950_000).mul(1e9);
|
||||
const fee = BigNumber.from(1_900_000).mul(1e9);
|
||||
|
||||
const [wallet1, wallet2] = await fx.setupWallets(2, {
|
||||
tokenBalance: fee.mul(10),
|
||||
|
||||
@@ -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)}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ ETHERSCAN_API_KEY=
|
||||
ROPSTEN_URL=fill_me_in
|
||||
RINKEBY_URL=fill_me_in
|
||||
ARBITRUM_TESTNET_URL=https://rinkeby.arbitrum.io/rpc
|
||||
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
|
||||
|
||||
2
contracts/.gitignore
vendored
2
contracts/.gitignore
vendored
@@ -4,7 +4,7 @@
|
||||
node_modules
|
||||
coverage
|
||||
coverage.json
|
||||
/typechain
|
||||
/typechain-types
|
||||
networks/local.json
|
||||
|
||||
#Hardhat files
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
*
|
||||
!/dist/src/**/*
|
||||
!/dist/typechain/**/*
|
||||
!/dist/typechain-types/**/*
|
||||
!/src/**/*
|
||||
!/package.json
|
||||
!/README.md
|
||||
|
||||
@@ -80,6 +80,24 @@ 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 } 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"
|
||||
```
|
||||
|
||||
## Signer
|
||||
|
||||
Utilities for signing, aggregating and verifying transaction bundles using the
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bls-wallet-clients",
|
||||
"version": "0.6.0",
|
||||
"version": "0.8.0",
|
||||
"description": "Client libraries for interacting with BLS Wallet components",
|
||||
"main": "dist/src/index.js",
|
||||
"types": "dist/src/index.d.ts",
|
||||
@@ -13,10 +13,9 @@
|
||||
"yarn": ">=1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist && mkdir dist && cp -rH typechain dist/typechain && find ./dist/typechain -type f \\! -name '*.d.ts' -name '*.ts' -delete && tsc",
|
||||
"build": "rm -rf dist && mkdir dist && cp -rH typechain-types dist/typechain-types && find ./dist/typechain-types -type f \\! -name '*.d.ts' -name '*.ts' -delete && tsc",
|
||||
"watch": "tsc -w",
|
||||
"pretest": "yarn build",
|
||||
"test": "mocha dist/**/*.test.js",
|
||||
"test": "mocha --require ts-node/register --require source-map-support/register --require ./test/init.ts **/*.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"
|
||||
@@ -26,11 +25,14 @@
|
||||
"ethers": "5.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/chai": "^4.3.0",
|
||||
"@types/mocha": "^9.1.0",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"chai": "^4.3.6",
|
||||
"mocha": "^9.2.2",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"mocha": "^10.1.0",
|
||||
"source-map-support": "^0.5.21",
|
||||
"typescript": "^4.6.2"
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.8.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import { ethers, BigNumber } from "ethers";
|
||||
import { solidityKeccak256 } from "ethers/lib/utils";
|
||||
import { keccak256, solidityKeccak256, solidityPack } from "ethers/lib/utils";
|
||||
|
||||
import {
|
||||
BlsWalletSigner,
|
||||
@@ -12,34 +14,63 @@ 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";
|
||||
|
||||
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
|
||||
* @param blsWalletSigner (optional) a BLS Wallet signer
|
||||
* @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);
|
||||
@@ -48,64 +79,90 @@ export default class BlsWalletWrapper {
|
||||
verificationGatewayAddress,
|
||||
signerOrProvider,
|
||||
);
|
||||
const pubKeyHash = blsWalletSigner.getPublicKeyHash(privateKey);
|
||||
|
||||
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,
|
||||
privateKey,
|
||||
);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
|
||||
const walletContract = BLSWallet__factory.connect(
|
||||
contractAddress,
|
||||
provider,
|
||||
);
|
||||
|
||||
return new BlsWalletWrapper(
|
||||
const blsWalletWrapper = new BlsWalletWrapper(
|
||||
blsWalletSigner,
|
||||
privateKey,
|
||||
contractAddress,
|
||||
walletContract,
|
||||
await BlsWalletWrapper.BLSWallet(privateKey, verificationGateway),
|
||||
);
|
||||
|
||||
return blsWalletWrapper;
|
||||
}
|
||||
|
||||
async syncWallet(verificationGateway: VerificationGateway) {
|
||||
this.address = await BlsWalletWrapper.Address(
|
||||
this.privateKey,
|
||||
verificationGateway.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
|
||||
this.walletContract = BLSWallet__factory.connect(
|
||||
this.address,
|
||||
verificationGateway.provider,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -114,7 +171,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,7 +223,11 @@ 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.privateKey,
|
||||
this.walletContract.address,
|
||||
);
|
||||
}
|
||||
|
||||
/** Sign a message */
|
||||
@@ -200,4 +263,57 @@ export default class BlsWalletWrapper {
|
||||
|
||||
return await initBlsWalletSigner({ chainId });
|
||||
}
|
||||
|
||||
// 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,
|
||||
privateKey: string,
|
||||
): Promise<void> {
|
||||
const pubKeyHash = blsWalletSigner.getPublicKeyHash(privateKey);
|
||||
const existingPubKeyHash = await verificationGateway.hashFromWallet(
|
||||
walletAddress,
|
||||
);
|
||||
|
||||
const walletIsAlreadyRegistered =
|
||||
!BigNumber.from(existingPubKeyHash).isZero();
|
||||
const pubKeyHashesDoNotMatch = pubKeyHash !== existingPubKeyHash;
|
||||
|
||||
if (walletIsAlreadyRegistered && pubKeyHashesDoNotMatch) {
|
||||
throw new Error(
|
||||
`wallet at ${walletAddress} has been recovered from public key hash ${pubKeyHash} to ${existingPubKeyHash}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
61
contracts/clients/src/MultiNetworkConfig.ts
Normal file
61
contracts/clients/src/MultiNetworkConfig.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { NetworkConfig, validateConfig } from "./NetworkConfig";
|
||||
|
||||
/**
|
||||
* Config representing the deployed state of bls-wallet contracts
|
||||
* across multiple networks.
|
||||
*/
|
||||
export type MultiNetworkConfig = {
|
||||
[networkKey: string]: NetworkConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unvalidated MultiNetworkConfig
|
||||
*/
|
||||
export type UnvalidatedMultiNetworkConfig = Record<
|
||||
string,
|
||||
Record<string, Record<string, unknown>>
|
||||
>;
|
||||
|
||||
type ReadFileFunc = (filePath: string) => Promise<string>;
|
||||
|
||||
/**
|
||||
* Validates and returns a multi-network config.
|
||||
*
|
||||
* @param cfg The config object to validate.
|
||||
*/
|
||||
export function validateMultiConfig(
|
||||
cfg: MultiNetworkConfig,
|
||||
): MultiNetworkConfig {
|
||||
const isEmpty = !Object.keys(cfg).length;
|
||||
if (isEmpty) {
|
||||
throw new Error("config is empty");
|
||||
}
|
||||
|
||||
const multiConfig: MultiNetworkConfig = {};
|
||||
for (const [networkKey, networkConfig] of Object.entries(cfg)) {
|
||||
try {
|
||||
multiConfig[networkKey] = validateConfig(networkConfig);
|
||||
} catch (err) {
|
||||
const castErr = err as Error;
|
||||
const newErr = new Error(`${networkKey}: ${castErr.message}`);
|
||||
newErr.stack = castErr.stack;
|
||||
throw newErr;
|
||||
}
|
||||
}
|
||||
return multiConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves, validates, and returns a multi-network config.
|
||||
*
|
||||
* @param networkConfigPath Path to config JSON file.
|
||||
* @param readFileFunc Callback to retrieve the config. This could be via fetch, fs.readFile, etc.
|
||||
*/
|
||||
export async function getMultiConfig(
|
||||
configPath: string,
|
||||
readFileFunc: ReadFileFunc,
|
||||
): Promise<NetworkConfig> {
|
||||
const cfg = JSON.parse(await readFileFunc(configPath));
|
||||
validateMultiConfig(cfg);
|
||||
return cfg;
|
||||
}
|
||||
@@ -43,13 +43,14 @@ export type NetworkConfig = {
|
||||
};
|
||||
|
||||
type ReadFileFunc = (filePath: string) => Promise<string>;
|
||||
type UnvalidatedConfig = Record<string, Record<string, unknown>>;
|
||||
|
||||
/**
|
||||
* Validates and returns a network config.
|
||||
*
|
||||
* @param cfg The config object to validate.
|
||||
*/
|
||||
export function validateConfig(cfg: any): NetworkConfig {
|
||||
export function validateConfig(cfg: UnvalidatedConfig): NetworkConfig {
|
||||
return {
|
||||
parameters: assertUnknownRecord(cfg.parameters),
|
||||
addresses: {
|
||||
@@ -75,6 +76,7 @@ export function validateConfig(cfg: any): NetworkConfig {
|
||||
|
||||
/**
|
||||
* Retrieves, validates, and returns a network config.
|
||||
* @deprecated Use getMultiConfig instead.
|
||||
*
|
||||
* @param networkConfigPath Path to config JSON file.
|
||||
* @param readFileFunc Callback to retrieve the config. This could be via fetch, fs.readFile, etc.
|
||||
|
||||
171
contracts/clients/src/OperationResults.ts
Normal file
171
contracts/clients/src/OperationResults.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { BigNumber, ContractReceipt, utils } from "ethers";
|
||||
import assert from "./helpers/assert";
|
||||
import { ActionData } from "./signer";
|
||||
|
||||
const errorSelectors = {
|
||||
Error: calculateAndCheckSelector("Error(string)", "0x08c379a0"),
|
||||
|
||||
Panic: calculateAndCheckSelector("Panic(uint256)", "0x4e487b71"),
|
||||
|
||||
ActionError: calculateAndCheckSelector(
|
||||
"ActionError(uint256,bytes)",
|
||||
"0x5c667601",
|
||||
),
|
||||
};
|
||||
|
||||
const actionErrorId = utils
|
||||
.keccak256(new TextEncoder().encode("ActionError(uint256,bytes)"))
|
||||
.slice(0, 10);
|
||||
|
||||
assert(actionErrorId === "0x5c667601");
|
||||
|
||||
type OperationResultError = {
|
||||
actionIndex?: BigNumber;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export type OperationResult = {
|
||||
walletAddress: string;
|
||||
nonce: BigNumber;
|
||||
actions: ActionData[];
|
||||
success: Boolean;
|
||||
results: string[];
|
||||
error?: OperationResultError;
|
||||
};
|
||||
|
||||
const getError = (
|
||||
success: boolean,
|
||||
results: string[],
|
||||
): OperationResultError | undefined => {
|
||||
if (success) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Single event "WalletOperationProcessed(address indexed wallet, uint256 nonce, bool success, bytes[] results)"
|
||||
// Get the first (only) result from "results" argument.
|
||||
const [errorData] = results;
|
||||
|
||||
if (!errorData.startsWith(errorSelectors.ActionError)) {
|
||||
throw new Error(
|
||||
[
|
||||
`errorResult does not begin with ActionError selector`,
|
||||
`(${errorSelectors.ActionError}): ${errorData}`,
|
||||
].join(" "),
|
||||
);
|
||||
}
|
||||
|
||||
// remove methodId (4bytes after 0x)
|
||||
const actionErrorArgBytes = `0x${errorData.slice(10)}`;
|
||||
|
||||
let actionIndex: BigNumber | undefined;
|
||||
let message: string;
|
||||
|
||||
try {
|
||||
const [actionIndexDecoded, actionErrorData] = utils.defaultAbiCoder.decode(
|
||||
["uint256", "bytes"],
|
||||
actionErrorArgBytes,
|
||||
) as [BigNumber, string];
|
||||
|
||||
actionIndex = actionIndexDecoded;
|
||||
|
||||
const actionErrorDataBody = `0x${actionErrorData.slice(10)}`;
|
||||
|
||||
if (actionErrorData.startsWith(errorSelectors.Error)) {
|
||||
[message] = utils.defaultAbiCoder.decode(["string"], actionErrorDataBody);
|
||||
} else if (actionErrorData.startsWith(errorSelectors.Panic)) {
|
||||
const [panicCode] = utils.defaultAbiCoder.decode(
|
||||
["uint256"],
|
||||
actionErrorDataBody,
|
||||
) as [BigNumber];
|
||||
|
||||
message = [
|
||||
`Panic: ${panicCode.toHexString()}`,
|
||||
"(See Panic(uint256) in the solidity docs:",
|
||||
"https://docs.soliditylang.org/_/downloads/en/latest/pdf/)",
|
||||
].join(" ");
|
||||
} else {
|
||||
message = `Unexpected action error data: ${actionErrorData}`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
message = `Unexpected error data: ${errorData}`;
|
||||
}
|
||||
|
||||
return {
|
||||
actionIndex,
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
export const getOperationResults = (
|
||||
txnReceipt: ContractReceipt,
|
||||
): OperationResult[] => {
|
||||
if (!txnReceipt.events || !txnReceipt.events.length) {
|
||||
throw new Error(
|
||||
`no events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
const walletOpProcessedEvents = txnReceipt.events.filter(
|
||||
(e) => e.event === "WalletOperationProcessed",
|
||||
);
|
||||
if (!walletOpProcessedEvents.length) {
|
||||
throw new Error(
|
||||
`no WalletOperationProcessed events found in transaction ${txnReceipt.transactionHash}`,
|
||||
);
|
||||
}
|
||||
|
||||
return walletOpProcessedEvents.reduce<OperationResult[]>(
|
||||
(opResults, { args }) => {
|
||||
if (!args) {
|
||||
throw new Error("WalletOperationProcessed event missing args");
|
||||
}
|
||||
const { wallet, nonce, actions: rawActions, success, results } = args;
|
||||
|
||||
const actions = rawActions.map(
|
||||
({
|
||||
ethValue,
|
||||
contractAddress,
|
||||
encodedFunction,
|
||||
}: {
|
||||
ethValue: BigNumber;
|
||||
contractAddress: string;
|
||||
encodedFunction: string;
|
||||
}) => ({
|
||||
ethValue,
|
||||
contractAddress,
|
||||
encodedFunction,
|
||||
}),
|
||||
);
|
||||
const error = getError(success, results);
|
||||
|
||||
return [
|
||||
...opResults,
|
||||
{
|
||||
walletAddress: wallet,
|
||||
nonce,
|
||||
actions,
|
||||
success,
|
||||
results,
|
||||
error,
|
||||
},
|
||||
];
|
||||
},
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
function calculateSelector(signature: string) {
|
||||
return utils.keccak256(new TextEncoder().encode(signature)).slice(0, 10);
|
||||
}
|
||||
|
||||
function calculateAndCheckSelector(signature: string, expected: string) {
|
||||
const selector = calculateSelector(signature);
|
||||
|
||||
assert(
|
||||
selector === expected,
|
||||
`Selector for ${signature} was not ${expected}`,
|
||||
);
|
||||
|
||||
return selector;
|
||||
}
|
||||
@@ -2,22 +2,29 @@ import Aggregator from "./Aggregator";
|
||||
import BlsWalletWrapper from "./BlsWalletWrapper";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { VerificationGateway__factory } from "../typechain/factories/VerificationGateway__factory";
|
||||
import type { VerificationGateway } from "../typechain/VerificationGateway";
|
||||
import { VerificationGateway__factory } from "../typechain-types/factories/contracts/VerificationGateway__factory";
|
||||
import type { VerificationGateway } from "../typechain-types/contracts/VerificationGateway";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { AggregatorUtilities__factory } from "../typechain/factories/AggregatorUtilities__factory";
|
||||
import type { AggregatorUtilities } from "../typechain/AggregatorUtilities";
|
||||
import { AggregatorUtilities__factory } from "../typechain-types/factories/contracts/AggregatorUtilities__factory";
|
||||
import type { AggregatorUtilities } from "../typechain-types/contracts/AggregatorUtilities";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { ERC20__factory } from "../typechain/factories/ERC20__factory";
|
||||
import type { ERC20 } from "../typechain/ERC20";
|
||||
import { ERC20__factory } from "../typechain-types/factories/@openzeppelin/contracts/token/ERC20/ERC20__factory";
|
||||
import type { ERC20 } from "../typechain-types/@openzeppelin/contracts/token/ERC20/ERC20";
|
||||
|
||||
// eslint-disable-next-line camelcase
|
||||
import { MockERC20__factory } from "../typechain/factories/MockERC20__factory";
|
||||
import type { MockERC20 } from "../typechain/MockERC20";
|
||||
import { MockERC20__factory } from "../typechain-types/factories/contracts/mock/MockERC20__factory";
|
||||
import type { MockERC20 } from "../typechain-types/contracts/mock/MockERC20";
|
||||
|
||||
import { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
|
||||
import {
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
} from "./MultiNetworkConfig";
|
||||
|
||||
import { OperationResult, getOperationResults } from "./OperationResults";
|
||||
|
||||
export * from "./signer";
|
||||
|
||||
@@ -27,6 +34,11 @@ export {
|
||||
NetworkConfig,
|
||||
getConfig,
|
||||
validateConfig,
|
||||
MultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
validateMultiConfig,
|
||||
OperationResult,
|
||||
getOperationResults,
|
||||
// eslint-disable-next-line camelcase
|
||||
VerificationGateway__factory,
|
||||
VerificationGateway,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { keccak256, solidityPack } from "ethers/lib/utils";
|
||||
import { Operation } from "./types";
|
||||
|
||||
export default (chainId: number) =>
|
||||
(operation: Operation): string => {
|
||||
(operation: Operation, walletAddress: string): string => {
|
||||
let encodedActionData = "0x";
|
||||
|
||||
for (const action of operation.actions) {
|
||||
@@ -18,7 +18,7 @@ export default (chainId: number) =>
|
||||
}
|
||||
|
||||
return solidityPack(
|
||||
["uint256", "uint256", "bytes32"],
|
||||
[chainId, operation.nonce, keccak256(encodedActionData)],
|
||||
["uint256", "address", "uint256", "bytes32"],
|
||||
[chainId, walletAddress, operation.nonce, keccak256(encodedActionData)],
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,10 +8,9 @@ export default (
|
||||
domain: Uint8Array,
|
||||
chainId: number,
|
||||
) =>
|
||||
(operation: Operation, privateKey: string): Bundle => {
|
||||
const message = encodeMessageForSigning(chainId)(operation);
|
||||
(operation: Operation, privateKey: string, walletAddress: string): Bundle => {
|
||||
const signer = signerFactory.getSigner(domain, privateKey);
|
||||
|
||||
const message = encodeMessageForSigning(chainId)(operation, walletAddress);
|
||||
const signature = signer.sign(message);
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,18 +1,25 @@
|
||||
import { BigNumber } from "ethers";
|
||||
import { VerificationGateway } from "../../typechain";
|
||||
import { BigNumberish, BytesLike } from "ethers";
|
||||
|
||||
export type Bundle = Parameters<VerificationGateway["processBundle"]>[0];
|
||||
export type Operation = Bundle["operations"][number];
|
||||
export type ActionData = {
|
||||
ethValue: BigNumberish;
|
||||
contractAddress: string;
|
||||
encodedFunction: BytesLike;
|
||||
};
|
||||
|
||||
export type Operation = {
|
||||
nonce: BigNumberish;
|
||||
actions: ActionData[];
|
||||
};
|
||||
|
||||
export type Bundle = {
|
||||
signature: [BigNumberish, BigNumberish];
|
||||
senderPublicKeys: [BigNumberish, BigNumberish, BigNumberish, BigNumberish][];
|
||||
operations: Operation[];
|
||||
};
|
||||
|
||||
export type PublicKey = Bundle["senderPublicKeys"][number];
|
||||
export type Signature = Bundle["signature"];
|
||||
|
||||
export type ActionData = {
|
||||
ethValue: BigNumber;
|
||||
contractAddress: string;
|
||||
encodedFunction: string;
|
||||
};
|
||||
|
||||
export type ActionDataDto = {
|
||||
ethValue: string;
|
||||
contractAddress: string;
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { Bundle } from "./types";
|
||||
import isValidEmptyBundle from "./isValidEmptyBundle";
|
||||
|
||||
export default (domain: Uint8Array, chainId: number) =>
|
||||
(bundle: Bundle): boolean => {
|
||||
(bundle: Bundle, walletAddress: string): boolean => {
|
||||
// hubbleBls verifier incorrectly rejects empty bundles
|
||||
if (isValidEmptyBundle(bundle)) {
|
||||
return true;
|
||||
@@ -25,6 +25,8 @@ export default (domain: Uint8Array, chainId: number) =>
|
||||
BigNumber.from(n2).toHexString(),
|
||||
BigNumber.from(n3).toHexString(),
|
||||
]),
|
||||
bundle.operations.map(encodeMessageForSigning(chainId)),
|
||||
bundle.operations.map((op) =>
|
||||
encodeMessageForSigning(chainId)(op, walletAddress),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
88
contracts/clients/test/config.test.ts
Normal file
88
contracts/clients/test/config.test.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { expect } from "chai";
|
||||
import {
|
||||
UnvalidatedMultiNetworkConfig,
|
||||
getMultiConfig,
|
||||
} from "../src/MultiNetworkConfig";
|
||||
|
||||
const getValue = (networkKey: string, propName: string) =>
|
||||
`${networkKey}-${propName}`;
|
||||
|
||||
const getSingleConfig = (networkKey: string) => ({
|
||||
parameters: {},
|
||||
addresses: {
|
||||
create2Deployer: getValue(networkKey, "create2Deployer"),
|
||||
precompileCostEstimator: getValue(networkKey, "precompileCostEstimator"),
|
||||
verificationGateway: getValue(networkKey, "verificationGateway"),
|
||||
blsLibrary: getValue(networkKey, "blsLibrary"),
|
||||
blsExpander: getValue(networkKey, "blsExpander"),
|
||||
utilities: getValue(networkKey, "utilities"),
|
||||
testToken: getValue(networkKey, "testToken"),
|
||||
},
|
||||
auxiliary: {
|
||||
chainid: 123,
|
||||
domain: getValue(networkKey, "domain"),
|
||||
genesisBlock: 456,
|
||||
deployedBy: getValue(networkKey, "deployedBy"),
|
||||
version: getValue(networkKey, "version"),
|
||||
},
|
||||
});
|
||||
|
||||
const network1 = "network1";
|
||||
const network2 = "network2";
|
||||
|
||||
describe("MultiNetworkConfig", () => {
|
||||
let validConfig: UnvalidatedMultiNetworkConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
validConfig = {
|
||||
[network1]: getSingleConfig(network1),
|
||||
[network2]: getSingleConfig(network2),
|
||||
};
|
||||
});
|
||||
|
||||
describe("getMultiConfig", () => {
|
||||
it("suceeds with valid config", async () => {
|
||||
await expect(
|
||||
getMultiConfig("", async () => JSON.stringify(validConfig)),
|
||||
).to.eventually.deep.equal(validConfig);
|
||||
});
|
||||
|
||||
it("fails if config is not json", async () => {
|
||||
await expect(getMultiConfig("", async () => "")).to.eventually.be
|
||||
.rejected;
|
||||
});
|
||||
|
||||
it("fails if config is empty", async () => {
|
||||
await expect(getMultiConfig("", async () => "{}")).to.eventually.be
|
||||
.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network1}.addresses.verificationGateway is removed`, async () => {
|
||||
delete validConfig[network1].addresses.verificationGateway;
|
||||
|
||||
await expect(getMultiConfig("", async () => JSON.stringify(validConfig)))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network2}.auxiliary is removed`, async () => {
|
||||
delete validConfig[network1].auxiliary;
|
||||
|
||||
await expect(getMultiConfig("", async () => JSON.stringify(validConfig)))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network1}.addresses.blsLibrary is set to a number`, async () => {
|
||||
validConfig[network1].addresses.blsLibrary = 1337;
|
||||
|
||||
await expect(getMultiConfig("", async () => JSON.stringify(validConfig)))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
|
||||
it(`fails if ${network2}.auxiliary.chainid is set to a string`, async () => {
|
||||
validConfig[network1].auxiliary.chainid = "off-the-chain";
|
||||
|
||||
await expect(getMultiConfig("", async () => JSON.stringify(validConfig)))
|
||||
.to.eventually.be.rejected;
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,3 @@
|
||||
import "source-map-support/register";
|
||||
|
||||
import { BigNumber } from "ethers";
|
||||
import { keccak256, arrayify } from "ethers/lib/utils";
|
||||
import { expect } from "chai";
|
||||
@@ -14,6 +12,9 @@ const weiPerToken = BigNumber.from(10).pow(18);
|
||||
const samples = (() => {
|
||||
const dummy256HexString = "0x" + "0123456789".repeat(10).slice(0, 64);
|
||||
const contractAddress = dummy256HexString;
|
||||
// Random addresses
|
||||
const walletAddress = "0x1337AF0f4b693fd1c36d7059a0798Ff05a60DFFE";
|
||||
const otherWalletAddress = "0x42C8157D539825daFD6586B119db53761a2a91CD";
|
||||
|
||||
const bundleTemplate: Operation = {
|
||||
nonce: BigNumber.from(123),
|
||||
@@ -39,6 +40,8 @@ const samples = (() => {
|
||||
bundleTemplate,
|
||||
privateKey,
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -49,23 +52,24 @@ describe("index", () => {
|
||||
domain,
|
||||
});
|
||||
|
||||
const { bundleTemplate, privateKey, otherPrivateKey } = samples;
|
||||
const { bundleTemplate, privateKey, otherPrivateKey, walletAddress } =
|
||||
samples;
|
||||
|
||||
const bundle = sign(bundleTemplate, privateKey);
|
||||
const bundle = sign(bundleTemplate, privateKey, walletAddress);
|
||||
|
||||
expect(bundle.signature).to.deep.equal([
|
||||
"0x0f8af80a400b731f4f2ddcd29816f296cca75e34816d466512a703631de3bb69",
|
||||
"0x023d76b485531a8dbc087b2d6f25563ad7f6d81d25f5f123186d0ec26da5e2d0",
|
||||
"0x2c1b0dc6643375e05a6f2ba3d23b1ce941253010b13a127e22f5db647dc37952",
|
||||
"0x0338f96fc67ce194a74a459791865ac2eb304fc214fd0962775078d12aea5b7e",
|
||||
]);
|
||||
|
||||
expect(verify(bundle)).to.equal(true);
|
||||
expect(verify(bundle, walletAddress)).to.equal(true);
|
||||
|
||||
const bundleBadSig = {
|
||||
...bundle,
|
||||
signature: sign(bundleTemplate, otherPrivateKey).signature,
|
||||
signature: sign(bundleTemplate, otherPrivateKey, walletAddress).signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadSig)).to.equal(false);
|
||||
expect(verify(bundleBadSig, walletAddress)).to.equal(false);
|
||||
|
||||
const bundleBadMessage: Bundle = {
|
||||
senderPublicKeys: bundle.senderPublicKeys,
|
||||
@@ -85,7 +89,7 @@ describe("index", () => {
|
||||
signature: bundle.signature,
|
||||
};
|
||||
|
||||
expect(verify(bundleBadMessage)).to.equal(false);
|
||||
expect(verify(bundleBadMessage, walletAddress)).to.equal(false);
|
||||
});
|
||||
|
||||
it("aggregates transactions", async () => {
|
||||
@@ -94,38 +98,50 @@ describe("index", () => {
|
||||
domain,
|
||||
});
|
||||
|
||||
const { bundleTemplate, privateKey } = samples;
|
||||
const {
|
||||
bundleTemplate,
|
||||
privateKey,
|
||||
otherPrivateKey,
|
||||
walletAddress,
|
||||
otherWalletAddress,
|
||||
} = samples;
|
||||
|
||||
const bundle1 = sign(bundleTemplate, privateKey);
|
||||
const bundle2 = aggregate([bundle1, bundle1]);
|
||||
const bundle1 = sign(bundleTemplate, privateKey, walletAddress);
|
||||
const bundle2 = sign(bundleTemplate, otherPrivateKey, otherWalletAddress);
|
||||
const aggBundle = aggregate([bundle1, bundle2]);
|
||||
|
||||
expect(bundle2.signature).to.deep.equal([
|
||||
"0x0008678ea56953fdca1b007b2685d3ed164b11de015f0a87ee844860c8e6cf30",
|
||||
"0x2bc51003125b2da84a01e639c3c2be270a9b93ed82498bffbead65c6f07df708",
|
||||
expect(aggBundle.signature).to.deep.equal([
|
||||
"0x2319fc81d339dce4678c73429dfd2f11766742ed1e41df5a2ba2bf4863d877b5",
|
||||
"0x1bb25c15ad1f2f967a80a7a65c7593fcd66b59bf092669707baf2db726e8e714",
|
||||
]);
|
||||
|
||||
expect(verify(bundle2)).to.equal(true);
|
||||
expect(verify(bundle1, walletAddress)).to.equal(true);
|
||||
expect(verify(bundle2, otherWalletAddress)).to.equal(true);
|
||||
|
||||
const bundle2BadMessage: Bundle = {
|
||||
...bundle2,
|
||||
expect(verify(bundle1, otherWalletAddress)).to.equal(false);
|
||||
expect(verify(bundle2, walletAddress)).to.equal(false);
|
||||
|
||||
const aggBundleBadMessage: Bundle = {
|
||||
...aggBundle,
|
||||
operations: [
|
||||
bundle2.operations[0],
|
||||
aggBundle.operations[0],
|
||||
{
|
||||
...bundle2.operations[1],
|
||||
...aggBundle.operations[1],
|
||||
|
||||
// Pretend this client signed to pay a million tokens
|
||||
actions: [
|
||||
{
|
||||
...bundle2.operations[1].actions[0],
|
||||
...aggBundle.operations[1].actions[0],
|
||||
ethValue: weiPerToken.mul(1000000),
|
||||
},
|
||||
...bundle2.operations[1].actions.slice(1),
|
||||
...aggBundle.operations[1].actions.slice(1),
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(verify(bundle2BadMessage)).to.equal(false);
|
||||
expect(verify(aggBundleBadMessage, walletAddress)).to.equal(false);
|
||||
expect(verify(aggBundleBadMessage, otherWalletAddress)).to.equal(false);
|
||||
});
|
||||
|
||||
it("can aggregate transactions which already have multiple subTransactions", async () => {
|
||||
@@ -134,7 +150,7 @@ describe("index", () => {
|
||||
domain,
|
||||
});
|
||||
|
||||
const { bundleTemplate, privateKey } = samples;
|
||||
const { bundleTemplate, privateKey, walletAddress } = samples;
|
||||
|
||||
const bundles = Range(4).map((i) =>
|
||||
sign(
|
||||
@@ -148,6 +164,7 @@ describe("index", () => {
|
||||
],
|
||||
},
|
||||
privateKey,
|
||||
walletAddress,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -156,7 +173,7 @@ describe("index", () => {
|
||||
|
||||
const aggAggBundle = aggregate([aggBundle1, aggBundle2]);
|
||||
|
||||
expect(verify(aggAggBundle)).to.equal(true);
|
||||
expect(verify(aggAggBundle, walletAddress)).to.equal(true);
|
||||
});
|
||||
|
||||
it("generates expected publicKeyStr", async () => {
|
||||
@@ -196,6 +213,6 @@ describe("index", () => {
|
||||
|
||||
const emptyBundle = aggregate([]);
|
||||
|
||||
expect(verify(emptyBundle)).to.equal(true);
|
||||
expect(verify(emptyBundle, samples.walletAddress)).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
||||
4
contracts/clients/test/init.ts
Normal file
4
contracts/clients/test/init.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import chai from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
|
||||
chai.use(chaiAsPromised);
|
||||
@@ -1 +0,0 @@
|
||||
../typechain
|
||||
1
contracts/clients/typechain-types
Symbolic link
1
contracts/clients/typechain-types
Symbolic link
@@ -0,0 +1 @@
|
||||
../typechain-types
|
||||
@@ -2,6 +2,13 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@cspotcode/source-map-support@^0.8.0":
|
||||
version "0.8.1"
|
||||
resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"
|
||||
integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping" "0.3.9"
|
||||
|
||||
"@ethersproject/abi@5.5.0", "@ethersproject/abi@^5.5.0":
|
||||
version "5.5.0"
|
||||
resolved "https://registry.npmjs.org/@ethersproject/abi/-/abi-5.5.0.tgz"
|
||||
@@ -708,6 +715,24 @@
|
||||
"@ethersproject/properties" "^5.6.0"
|
||||
"@ethersproject/strings" "^5.6.0"
|
||||
|
||||
"@jridgewell/resolve-uri@^3.0.3":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78"
|
||||
integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==
|
||||
|
||||
"@jridgewell/sourcemap-codec@^1.4.10":
|
||||
version "1.4.14"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24"
|
||||
integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==
|
||||
|
||||
"@jridgewell/trace-mapping@0.3.9":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9"
|
||||
integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri" "^3.0.3"
|
||||
"@jridgewell/sourcemap-codec" "^1.4.10"
|
||||
|
||||
"@thehubbleproject/bls@^0.5.1":
|
||||
version "0.5.1"
|
||||
resolved "https://registry.yarnpkg.com/@thehubbleproject/bls/-/bls-0.5.1.tgz#6b0565f56fc9c8896dcf3c8f0e2214b69a06167f"
|
||||
@@ -716,20 +741,52 @@
|
||||
ethers "^5.5.3"
|
||||
mcl-wasm "^1.0.0"
|
||||
|
||||
"@types/chai@^4.3.0":
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.0.tgz#23509ebc1fa32f1b4d50d6a66c4032d5b8eaabdc"
|
||||
integrity sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==
|
||||
"@tsconfig/node10@^1.0.7":
|
||||
version "1.0.9"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.9.tgz#df4907fc07a886922637b15e02d4cebc4c0021b2"
|
||||
integrity sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==
|
||||
|
||||
"@types/mocha@^9.1.0":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.0.tgz#baf17ab2cca3fcce2d322ebc30454bff487efad5"
|
||||
integrity sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==
|
||||
"@tsconfig/node12@^1.0.7":
|
||||
version "1.0.11"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d"
|
||||
integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==
|
||||
|
||||
"@ungap/promise-all-settled@1.1.2":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@ungap/promise-all-settled/-/promise-all-settled-1.1.2.tgz#aa58042711d6e3275dd37dc597e5d31e8c290a44"
|
||||
integrity sha512-sL/cEvJWAnClXw0wHk85/2L0G6Sj8UB0Ctc1TEMbKSsmpRosqhwj9gWgFRZSrBr2f9tiXISwNhCPmlfqUqyb9Q==
|
||||
"@tsconfig/node14@^1.0.0":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1"
|
||||
integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==
|
||||
|
||||
"@tsconfig/node16@^1.0.2":
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.3.tgz#472eaab5f15c1ffdd7f8628bd4c4f753995ec79e"
|
||||
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
|
||||
|
||||
"@types/chai-as-promised@^7.1.5":
|
||||
version "7.1.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai-as-promised/-/chai-as-promised-7.1.5.tgz#6e016811f6c7a64f2eed823191c3a6955094e255"
|
||||
integrity sha512-jStwss93SITGBwt/niYrkf2C+/1KTeZCZl1LaeezTlqppAKeoQC7jxyqYuP72sxBGKCIbw7oHgbYssIRzT5FCQ==
|
||||
dependencies:
|
||||
"@types/chai" "*"
|
||||
|
||||
"@types/chai@*", "@types/chai@^4.3.3":
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.3.tgz#3c90752792660c4b562ad73b3fbd68bf3bc7ae07"
|
||||
integrity sha512-hC7OMnszpxhZPduX+m+nrx+uFoLkWOMiR4oa/AZF3MuSETYTZmFfJAHqZEM8MVlvfG7BEUcgvtwoCTxBp6hm3g==
|
||||
|
||||
"@types/mocha@^10.0.0":
|
||||
version "10.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-10.0.0.tgz#3d9018c575f0e3f7386c1de80ee66cc21fbb7a52"
|
||||
integrity sha512-rADY+HtTOA52l9VZWtgQfn4p+UDVM2eDVkMZT1I6syp0YKxW2F9v+0pbRZLsvskhQv/vMb6ZfCay81GHbz5SHg==
|
||||
|
||||
acorn-walk@^8.1.1:
|
||||
version "8.2.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1"
|
||||
integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==
|
||||
|
||||
acorn@^8.4.1:
|
||||
version "8.8.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
|
||||
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
|
||||
|
||||
aes-js@3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -761,6 +818,11 @@ anymatch@~3.1.2:
|
||||
normalize-path "^3.0.0"
|
||||
picomatch "^2.0.4"
|
||||
|
||||
arg@^4.1.0:
|
||||
version "4.1.3"
|
||||
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089"
|
||||
integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==
|
||||
|
||||
argparse@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||
@@ -799,6 +861,13 @@ brace-expansion@^1.1.7:
|
||||
balanced-match "^1.0.0"
|
||||
concat-map "0.0.1"
|
||||
|
||||
brace-expansion@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
|
||||
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
braces@~3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107"
|
||||
@@ -826,6 +895,13 @@ camelcase@^6.0.0:
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e"
|
||||
integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA==
|
||||
|
||||
chai-as-promised@^7.1.1:
|
||||
version "7.1.1"
|
||||
resolved "https://registry.yarnpkg.com/chai-as-promised/-/chai-as-promised-7.1.1.tgz#08645d825deb8696ee61725dbf590c012eb00ca0"
|
||||
integrity sha512-azL6xMoi+uxu6z4rhWQ1jbdUhOMhis2PvscD/xjLqNMkv3BPPp2JyyuTHOrf9BOosGpNQ11v6BKv/g57RXbiaA==
|
||||
dependencies:
|
||||
check-error "^1.0.2"
|
||||
|
||||
chai@^4.3.6:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.6.tgz#ffe4ba2d9fa9d6680cc0b370adae709ec9011e9c"
|
||||
@@ -893,10 +969,15 @@ concat-map@0.0.1:
|
||||
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
|
||||
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
|
||||
|
||||
debug@4.3.3:
|
||||
version "4.3.3"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664"
|
||||
integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==
|
||||
create-require@^1.1.0:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333"
|
||||
integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==
|
||||
|
||||
debug@4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
|
||||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
@@ -917,6 +998,11 @@ diff@5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b"
|
||||
integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==
|
||||
|
||||
diff@^4.0.1:
|
||||
version "4.0.2"
|
||||
resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d"
|
||||
integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==
|
||||
|
||||
elliptic@6.5.4:
|
||||
version "6.5.4"
|
||||
resolved "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz"
|
||||
@@ -1076,11 +1162,6 @@ glob@7.2.0:
|
||||
once "^1.3.0"
|
||||
path-is-absolute "^1.0.0"
|
||||
|
||||
growl@1.10.5:
|
||||
version "1.10.5"
|
||||
resolved "https://registry.yarnpkg.com/growl/-/growl-1.10.5.tgz#f2735dc2283674fa67478b10181059355c369e5e"
|
||||
integrity sha512-qBr4OuELkhPenW6goKVXiv47US3clb3/IbuWF9KNKEijAy9oeHxU9IgzjvJhHkUzhaj7rOUD7+YGWqUjLp5oSA==
|
||||
|
||||
has-flag@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b"
|
||||
@@ -1160,11 +1241,6 @@ is-unicode-supported@^0.1.0:
|
||||
resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7"
|
||||
integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==
|
||||
|
||||
isexe@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10"
|
||||
integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=
|
||||
|
||||
js-sha3@0.8.0:
|
||||
version "0.8.0"
|
||||
resolved "https://registry.npmjs.org/js-sha3/-/js-sha3-0.8.0.tgz"
|
||||
@@ -1199,6 +1275,11 @@ loupe@^2.3.1:
|
||||
dependencies:
|
||||
get-func-name "^2.0.0"
|
||||
|
||||
make-error@^1.1.1:
|
||||
version "1.3.6"
|
||||
resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2"
|
||||
integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==
|
||||
|
||||
mcl-wasm@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/mcl-wasm/-/mcl-wasm-1.0.1.tgz#b1f76785be72d33b7ccb2ae6c3f4760949a8ebb7"
|
||||
@@ -1214,12 +1295,12 @@ minimalistic-crypto-utils@^1.0.1:
|
||||
resolved "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz"
|
||||
integrity sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=
|
||||
|
||||
minimatch@4.2.1:
|
||||
version "4.2.1"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-4.2.1.tgz#40d9d511a46bdc4e563c22c3080cde9c0d8299b4"
|
||||
integrity sha512-9Uq1ChtSZO+Mxa/CL1eGizn2vRn3MlLgzhT0Iz8zaY8NdvxvB0d5QdPFmCKf7JKA9Lerx5vRrnwO03jsSfGG9g==
|
||||
minimatch@5.0.1:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.0.1.tgz#fb9022f7528125187c92bd9e9b6366be1cf3415b"
|
||||
integrity sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
brace-expansion "^2.0.1"
|
||||
|
||||
minimatch@^3.0.4:
|
||||
version "3.0.4"
|
||||
@@ -1228,32 +1309,29 @@ minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
mocha@^9.2.2:
|
||||
version "9.2.2"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-9.2.2.tgz#d70db46bdb93ca57402c809333e5a84977a88fb9"
|
||||
integrity sha512-L6XC3EdwT6YrIk0yXpavvLkn8h+EU+Y5UcCHKECyMbdUIxyMuZj4bX4U9e1nvnvUUvQVsV2VHQr5zLdcUkhW/g==
|
||||
mocha@^10.1.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.1.0.tgz#dbf1114b7c3f9d0ca5de3133906aea3dfc89ef7a"
|
||||
integrity sha512-vUF7IYxEoN7XhQpFLxQAEMtE4W91acW4B6En9l97MwE9stL1A9gusXfoHZCLVHDUJ/7V5+lbCM6yMqzo5vNymg==
|
||||
dependencies:
|
||||
"@ungap/promise-all-settled" "1.1.2"
|
||||
ansi-colors "4.1.1"
|
||||
browser-stdout "1.3.1"
|
||||
chokidar "3.5.3"
|
||||
debug "4.3.3"
|
||||
debug "4.3.4"
|
||||
diff "5.0.0"
|
||||
escape-string-regexp "4.0.0"
|
||||
find-up "5.0.0"
|
||||
glob "7.2.0"
|
||||
growl "1.10.5"
|
||||
he "1.2.0"
|
||||
js-yaml "4.1.0"
|
||||
log-symbols "4.1.0"
|
||||
minimatch "4.2.1"
|
||||
minimatch "5.0.1"
|
||||
ms "2.1.3"
|
||||
nanoid "3.3.1"
|
||||
nanoid "3.3.3"
|
||||
serialize-javascript "6.0.0"
|
||||
strip-json-comments "3.1.1"
|
||||
supports-color "8.1.1"
|
||||
which "2.0.2"
|
||||
workerpool "6.2.0"
|
||||
workerpool "6.2.1"
|
||||
yargs "16.2.0"
|
||||
yargs-parser "20.2.4"
|
||||
yargs-unparser "2.0.0"
|
||||
@@ -1268,10 +1346,10 @@ ms@2.1.3:
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
||||
nanoid@3.3.1:
|
||||
version "3.3.1"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
|
||||
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
|
||||
nanoid@3.3.3:
|
||||
version "3.3.3"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
|
||||
integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
|
||||
|
||||
normalize-path@^3.0.0, normalize-path@~3.0.0:
|
||||
version "3.0.0"
|
||||
@@ -1410,27 +1488,44 @@ to-regex-range@^5.0.1:
|
||||
dependencies:
|
||||
is-number "^7.0.0"
|
||||
|
||||
ts-node@^10.9.1:
|
||||
version "10.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.1.tgz#e73de9102958af9e1f0b168a6ff320e25adcff4b"
|
||||
integrity sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==
|
||||
dependencies:
|
||||
"@cspotcode/source-map-support" "^0.8.0"
|
||||
"@tsconfig/node10" "^1.0.7"
|
||||
"@tsconfig/node12" "^1.0.7"
|
||||
"@tsconfig/node14" "^1.0.0"
|
||||
"@tsconfig/node16" "^1.0.2"
|
||||
acorn "^8.4.1"
|
||||
acorn-walk "^8.1.1"
|
||||
arg "^4.1.0"
|
||||
create-require "^1.1.0"
|
||||
diff "^4.0.1"
|
||||
make-error "^1.1.1"
|
||||
v8-compile-cache-lib "^3.0.1"
|
||||
yn "3.1.1"
|
||||
|
||||
type-detect@^4.0.0, type-detect@^4.0.5:
|
||||
version "4.0.8"
|
||||
resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c"
|
||||
integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==
|
||||
|
||||
typescript@^4.6.2:
|
||||
version "4.6.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.6.2.tgz#fe12d2727b708f4eef40f51598b3398baa9611d4"
|
||||
integrity sha512-HM/hFigTBHZhLXshn9sN37H085+hQGeJHJ/X7LpBWLID/fbc2acUMfU+lGD98X81sKP+pFa9f0DZmCwB9GnbAg==
|
||||
typescript@^4.8.4:
|
||||
version "4.8.4"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6"
|
||||
integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==
|
||||
|
||||
which@2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
|
||||
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
|
||||
dependencies:
|
||||
isexe "^2.0.0"
|
||||
v8-compile-cache-lib@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
|
||||
integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==
|
||||
|
||||
workerpool@6.2.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"
|
||||
integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==
|
||||
workerpool@6.2.1:
|
||||
version "6.2.1"
|
||||
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.1.tgz#46fc150c17d826b86a008e5a4508656777e9c343"
|
||||
integrity sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==
|
||||
|
||||
wrap-ansi@^7.0.0:
|
||||
version "7.0.0"
|
||||
@@ -1489,6 +1584,11 @@ yargs@16.2.0:
|
||||
y18n "^5.0.5"
|
||||
yargs-parser "^20.2.2"
|
||||
|
||||
yn@3.1.1:
|
||||
version "3.1.1"
|
||||
resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50"
|
||||
integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==
|
||||
|
||||
yocto-queue@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||
|
||||
@@ -148,16 +148,18 @@ contract BLSWallet is Initializable, IWallet
|
||||
bool success,
|
||||
bytes[] memory results
|
||||
) {
|
||||
incrementNonce(); // before operation to prevent reentrancy
|
||||
try this._performOperation(op) returns (
|
||||
bytes[] memory _results
|
||||
) {
|
||||
success = true;
|
||||
results = _results;
|
||||
}
|
||||
catch {
|
||||
catch (bytes memory returnData) {
|
||||
success = false;
|
||||
results = new bytes[](1);
|
||||
results[0] = returnData;
|
||||
}
|
||||
incrementNonce(); // regardless of outcome of operation
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,11 +185,23 @@ contract BLSWallet is Initializable, IWallet
|
||||
else {
|
||||
(success, result) = address(a.contractAddress).call(a.encodedFunction);
|
||||
}
|
||||
require(success);
|
||||
|
||||
if (success == false) {
|
||||
revert IWallet.ActionError(i, result);
|
||||
}
|
||||
|
||||
results[i] = result;
|
||||
}
|
||||
}
|
||||
|
||||
function stripMethodId(bytes memory encodedFunction) pure private returns(bytes memory) {
|
||||
bytes memory params = new bytes(encodedFunction.length - 4);
|
||||
for (uint256 i=0; i<params.length; i++) {
|
||||
params[i] = encodedFunction[i+4];
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
function clearApprovedProxyAdminFunctionHash() public onlyTrustedGateway {
|
||||
approvedProxyAdminFunctionHash = 0;
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ contract VerificationGateway
|
||||
event WalletOperationProcessed(
|
||||
address indexed wallet,
|
||||
uint256 nonce,
|
||||
bool result
|
||||
IWallet.ActionData[] actions,
|
||||
bool success,
|
||||
bytes[] results
|
||||
);
|
||||
|
||||
event PendingBLSKeySet(
|
||||
@@ -86,7 +88,27 @@ contract VerificationGateway
|
||||
|
||||
for (uint256 i = 0; i<opLength; i++) {
|
||||
// construct params for signature verification
|
||||
messages[i] = messagePoint(bundle.operations[i]);
|
||||
bytes32 keyHash = keccak256(abi.encodePacked(bundle.senderPublicKeys[i]));
|
||||
address walletAddress = address(walletFromHash[keyHash]);
|
||||
if (walletAddress == address(0)) {
|
||||
walletAddress = address(uint160(uint(keccak256(abi.encodePacked(
|
||||
bytes1(0xff),
|
||||
address(this),
|
||||
keyHash,
|
||||
keccak256(abi.encodePacked(
|
||||
type(TransparentUpgradeableProxy).creationCode,
|
||||
abi.encode(
|
||||
address(blsWalletLogic),
|
||||
address(walletProxyAdmin),
|
||||
getInitializeData()
|
||||
)
|
||||
))
|
||||
)))));
|
||||
}
|
||||
messages[i] = messagePoint(
|
||||
walletAddress,
|
||||
bundle.operations[i]
|
||||
);
|
||||
}
|
||||
|
||||
bool verified = blsLib.verifyMultiple(
|
||||
@@ -160,11 +182,23 @@ contract VerificationGateway
|
||||
// ensure first parameter is the calling wallet address
|
||||
bytes memory encodedAddress = abi.encode(address(wallet));
|
||||
uint8 selectorOffset = 4;
|
||||
for (uint256 i=0; i<32; i++) {
|
||||
require(
|
||||
(encodedFunction[selectorOffset+i] == encodedAddress[i]),
|
||||
"VG: first param to proxy admin is not calling wallet"
|
||||
);
|
||||
|
||||
bytes4 selectorId = bytes4(encodedFunction);
|
||||
|
||||
// ensure not calling Ownable functions of ProxyAdmin
|
||||
require((selectorId != Ownable.transferOwnership.selector)
|
||||
&& (selectorId != Ownable.renounceOwnership.selector),
|
||||
"VG: cannot change ownership"
|
||||
);
|
||||
|
||||
if (selectorId != Ownable.owner.selector) {
|
||||
require(encodedFunction.length >= 32, "VG: Expected admin params");
|
||||
for (uint256 i=0; i<32; i++) {
|
||||
require(
|
||||
(encodedFunction[selectorOffset+i] == encodedAddress[i]),
|
||||
"VG: first param to proxy admin is not calling wallet"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
wallet.setAnyPending();
|
||||
@@ -273,7 +307,9 @@ contract VerificationGateway
|
||||
emit WalletOperationProcessed(
|
||||
address(wallet),
|
||||
bundle.operations[i].nonce,
|
||||
successes[i]
|
||||
bundle.operations[i].actions,
|
||||
successes[i],
|
||||
results[i]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -358,6 +394,7 @@ contract VerificationGateway
|
||||
}
|
||||
|
||||
function messagePoint(
|
||||
address walletAddress,
|
||||
IWallet.Operation memory op
|
||||
) internal view returns (
|
||||
uint256[2] memory
|
||||
@@ -377,6 +414,7 @@ contract VerificationGateway
|
||||
BLS_DOMAIN,
|
||||
abi.encodePacked(
|
||||
block.chainid,
|
||||
walletAddress,
|
||||
op.nonce,
|
||||
keccak256(encodedActionData)
|
||||
)
|
||||
|
||||
@@ -17,6 +17,8 @@ interface IWallet {
|
||||
bytes encodedFunction;
|
||||
}
|
||||
|
||||
error ActionError(uint256 actionIndex, bytes errorData);
|
||||
|
||||
function initialize(address gateway) external;
|
||||
function nonce() external returns (uint256);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
import { HardhatUserConfig, task } from "hardhat/config";
|
||||
import { HardhatUserConfig, task, types } from "hardhat/config";
|
||||
import "@nomiclabs/hardhat-etherscan";
|
||||
import "@nomiclabs/hardhat-waffle";
|
||||
import "@typechain/hardhat";
|
||||
@@ -22,6 +22,28 @@ task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Don't run this unless you really need to...
|
||||
task("privateKeys", "Prints the private keys for accounts")
|
||||
.addParam("force", "Whether the command should be run", false, types.boolean)
|
||||
.setAction(async ({ force }: { force: boolean }, hre) => {
|
||||
if (!force) {
|
||||
throw new Error("are you sure you want to run this task? (--force true)");
|
||||
}
|
||||
|
||||
const separator = "-".repeat(3);
|
||||
console.log(separator);
|
||||
|
||||
for (let i = 0; i < accounts.count; i++) {
|
||||
const wallet = hre.ethers.Wallet.fromMnemonic(
|
||||
accounts.mnemonic,
|
||||
`m/44'/60'/0'/0/${i}`,
|
||||
);
|
||||
console.log(`${i}: ${wallet.address}`);
|
||||
console.log(wallet.privateKey);
|
||||
console.log(separator);
|
||||
}
|
||||
});
|
||||
|
||||
task("fundDeployer", "Sends ETH to create2Deployer contract from first signer")
|
||||
.addOptionalParam("amount", "Amount of ETH to send", "1.0")
|
||||
.setAction(async ({ amount }: { amount: string }, hre) => {
|
||||
@@ -37,6 +59,23 @@ task("fundDeployer", "Sends ETH to create2Deployer contract from first signer")
|
||||
await txnRes.wait();
|
||||
});
|
||||
|
||||
task("sendEth", "Sends ETH to an address")
|
||||
.addParam("address", "Address to send ETH to", undefined, types.string)
|
||||
.addOptionalParam("amount", "Amount of ETH to send", "1.0")
|
||||
.setAction(
|
||||
async ({ address, amount }: { address: string; amount: string }, hre) => {
|
||||
const [account0] = await hre.ethers.getSigners();
|
||||
|
||||
console.log(`${account0.address} -> ${address} ${amount} ETH`);
|
||||
|
||||
const txnRes = await account0.sendTransaction({
|
||||
to: address,
|
||||
value: hre.ethers.utils.parseEther(amount),
|
||||
});
|
||||
await txnRes.wait();
|
||||
},
|
||||
);
|
||||
|
||||
// Do any needed pre-test setup here.
|
||||
task("test").setAction(async (_taskArgs, _hre, runSuper) => {
|
||||
chai.use(chaiAsPromised);
|
||||
@@ -102,6 +141,11 @@ const config: HardhatUserConfig = {
|
||||
accounts,
|
||||
gasPrice: 1408857682, // 287938372,
|
||||
},
|
||||
arbitrum_goerli: {
|
||||
// chainId: 421613
|
||||
url: process.env.ARBITRUM_GOERLI_URL,
|
||||
accounts,
|
||||
},
|
||||
arbitrum: {
|
||||
// chainId: 42161
|
||||
url: process.env.ARBITRUM_URL,
|
||||
|
||||
19
contracts/networks/arbitrum-goerli.json
Normal file
19
contracts/networks/arbitrum-goerli.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"parameters": {},
|
||||
"addresses": {
|
||||
"create2Deployer": "0x036d996D6855B83cd80142f2933d8C2617dA5617",
|
||||
"precompileCostEstimator": "0x22E4a5251C1F02de8369Dd6f192033F6CB7531A4",
|
||||
"blsLibrary": "0xF8a11BA6eceC43e23c9896b857128a4269290e39",
|
||||
"verificationGateway": "0xae7DF242c589D479A5cF8fEA681736e0E0Bb1FB9",
|
||||
"blsExpander": "0x4473e39a5F33A83B81387bb5F816354F04E724a3",
|
||||
"utilities": "0x76cE3c1F2E6d87c355560fCbd28ccAcAe03f95F6",
|
||||
"testToken": "0x5081a39b8A5f0E35a8D959395a630b68B74Dd30f"
|
||||
},
|
||||
"auxiliary": {
|
||||
"chainid": 421613,
|
||||
"domain": "0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
|
||||
"genesisBlock": 1206441,
|
||||
"deployedBy": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
|
||||
"version": "bc3d1463f163b742026f951a2574016966b5c857"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,19 @@
|
||||
{
|
||||
"parameters": {},
|
||||
"addresses": {
|
||||
"create2Deployer": "0xc1326d37b446bC7df7b36348C963BFcc8eF98Ce3",
|
||||
"precompileCostEstimator": "0x7a89f10F307Bd51b81eF8D1B2c5fa74c7E2d006D",
|
||||
"blsLibrary": "0x6F6a92362EA4299B5668dC4A75282bBFd42D4804",
|
||||
"verificationGateway": "0x697B3E6258B08201d316b31D69805B5F666b62C8",
|
||||
"blsExpander": "0xaf6E02eAf7855D587ffDE5c424a0991570b56944",
|
||||
"utilities": "0x5C176B9F019Bfe90cEc3b2492cC5e20f11c97855",
|
||||
"testToken": "0x09f2C81263B8C079CcE299B4B5b4C32cba0aA0F9"
|
||||
},
|
||||
"auxiliary": {
|
||||
"chainid": 421611,
|
||||
"domain": "0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
|
||||
"genesisBlock": 11355502,
|
||||
"deployedBy": "0xcc12Dd5DefC8BCccAbfBA4bFBFECe09B4EDBF263",
|
||||
"version": "bea30d9171772faf855cdab909f321ce0834a495"
|
||||
}
|
||||
}
|
||||
"parameters": {},
|
||||
"addresses": {
|
||||
"create2Deployer": "0x036d996D6855B83cd80142f2933d8C2617dA5617",
|
||||
"precompileCostEstimator": "0x22E4a5251C1F02de8369Dd6f192033F6CB7531A4",
|
||||
"blsLibrary": "0x52ED3BAF9F4b60c67D2796e8ED5f35AfA3c4938a",
|
||||
"verificationGateway": "0xa15954659EFce154a3B45cE88D8158A02bE2049A",
|
||||
"blsExpander": "0x1a1C1285a5DB87264Ca8a67075e3a27b304d3fBD",
|
||||
"utilities": "0xeC098e366368fC2269140053cE899F307A2c9209",
|
||||
"testToken": "0xAfFFb6c8e061904C2cd69F372D6a8293Db7eC0A8"
|
||||
},
|
||||
"auxiliary": {
|
||||
"chainid": 421611,
|
||||
"domain": "0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
|
||||
"genesisBlock": 14402717,
|
||||
"deployedBy": "0x6435e511f8908D5C733898C81831a4A3aFE31D07",
|
||||
"version": "45def922036c441aa1559419470a131de3ce8ae4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,48 +12,49 @@
|
||||
"check-ts": "tsc --noEmit",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"test": "hardhat test",
|
||||
"premerge": "rm -rf artifacts cache typechain && hardhat compile && lint && check-ts && yarn --cwd clients premerge && test"
|
||||
"premerge": "rm -rf artifacts cache typechain-types && hardhat compile && lint && check-ts && yarn --cwd clients premerge && test"
|
||||
},
|
||||
"author": "James Zaki",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@openzeppelin/contracts": "^4.3.2"
|
||||
"@openzeppelin/contracts": "^4.7.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nomiclabs/hardhat-ethers": "^2.0.0",
|
||||
"@nomiclabs/hardhat-etherscan": "^2.1.3",
|
||||
"@nomiclabs/hardhat-waffle": "^2.0.0",
|
||||
"@typechain/ethers-v5": "^7.0.1",
|
||||
"@typechain/hardhat": "^2.3.0",
|
||||
"@types/chai": "^4.2.21",
|
||||
"@nomiclabs/hardhat-ethers": "^2.2.1",
|
||||
"@nomiclabs/hardhat-etherscan": "^3.1.2",
|
||||
"@nomiclabs/hardhat-waffle": "^2.0.3",
|
||||
"@typechain/ethers-v5": "^10.1.0",
|
||||
"@typechain/hardhat": "^6.1.3",
|
||||
"@types/chai": "^4.3.3",
|
||||
"@types/chai-as-promised": "^7.1.5",
|
||||
"@types/mocha": "^9.0.0",
|
||||
"@types/node": "^16.4.13",
|
||||
"@typescript-eslint/eslint-plugin": "^4.29.1",
|
||||
"@typescript-eslint/parser": "^4.29.1",
|
||||
"chai": "^4.2.0",
|
||||
"@types/mocha": "^10.0.0",
|
||||
"@types/node": "^18.11.8",
|
||||
"@typescript-eslint/eslint-plugin": "^5.42.0",
|
||||
"@typescript-eslint/parser": "^5.42.0",
|
||||
"chai": "^4.3.6",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"dotenv": "^10.0.0",
|
||||
"eslint": "^7.29.0",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
"eslint-config-standard": "^16.0.3",
|
||||
"eslint-plugin-import": "^2.23.4",
|
||||
"dotenv": "^16.0.3",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-config-standard": "^17.0.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-n": "^15.4.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-promise": "^5.1.0",
|
||||
"ethereum-waffle": "^3.0.0",
|
||||
"ethers": "5.5.4",
|
||||
"hardhat": "^2.6.7",
|
||||
"hardhat-gas-reporter": "^1.0.4",
|
||||
"mcl-wasm": "^0.8.0",
|
||||
"prettier": "^2.3.2",
|
||||
"prettier-plugin-solidity": "^1.0.0-beta.13",
|
||||
"solhint": "^3.3.6",
|
||||
"solidity-coverage": "^0.7.16",
|
||||
"ts-node": "^10.1.0",
|
||||
"typechain": "^5.1.2",
|
||||
"typescript": "^4.3.5",
|
||||
"web3": "^1.7.0",
|
||||
"web3-utils": "^1.7.0"
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-promise": "^6.1.1",
|
||||
"ethereum-waffle": "^3.4.4",
|
||||
"ethers": "^5.7.2",
|
||||
"hardhat": "^2.12.1",
|
||||
"hardhat-gas-reporter": "^1.0.9",
|
||||
"mcl-wasm": "^1.0.3",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-plugin-solidity": "^1.0.0-beta.24",
|
||||
"solhint": "^3.3.7",
|
||||
"solidity-coverage": "^0.8.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"typechain": "^8.1.0",
|
||||
"typescript": "^4.8.4",
|
||||
"web3": "^1.8.0",
|
||||
"web3-utils": "^1.8.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
/**
|
||||
* Example usage:
|
||||
*
|
||||
* yarn hardhat run scripts/test/send_eth.ts --network gethDev
|
||||
*/
|
||||
|
||||
/* eslint-disable no-process-exit */
|
||||
import { ethers } from "hardhat";
|
||||
|
||||
// Change this to the address you want to send eth to.
|
||||
// We should bring in an npm package at some point to parse cli args.
|
||||
const receivingAddress = "0x6266142188e26AfA67Caf6FDD581edc1958d7172";
|
||||
// Change this to the amount of eth you want to send.
|
||||
const amountEth = "1.0";
|
||||
|
||||
async function main() {
|
||||
const [account0] = await ethers.getSigners();
|
||||
console.log(`${account0.address} -> ${receivingAddress} ${amountEth} ETH`);
|
||||
await account0.sendTransaction({
|
||||
to: receivingAddress,
|
||||
value: ethers.utils.parseEther(amountEth),
|
||||
});
|
||||
console.log("done");
|
||||
}
|
||||
|
||||
// We recommend this pattern to be able to use async/await everywhere
|
||||
// and properly handle errors.
|
||||
main()
|
||||
.then(() => {
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -4,7 +4,7 @@ import "@nomiclabs/hardhat-ethers";
|
||||
import { ethers } from "hardhat";
|
||||
import { Wallet, BigNumber, Contract, ContractFactory } from "ethers";
|
||||
import deployerContract from "./deployDeployer";
|
||||
import { Create2Deployer } from "../../typechain";
|
||||
import { Create2Deployer } from "../../typechain-types";
|
||||
|
||||
export default class Create2Fixture {
|
||||
private constructor(public deployerWallet?: Wallet) {}
|
||||
|
||||
@@ -19,7 +19,11 @@ import {
|
||||
import Range from "./Range";
|
||||
import assert from "./assert";
|
||||
import Create2Fixture from "./Create2Fixture";
|
||||
import { VerificationGateway, BLSOpen, ProxyAdmin } from "../../typechain";
|
||||
import {
|
||||
VerificationGateway,
|
||||
BLSOpen,
|
||||
ProxyAdmin,
|
||||
} from "../../typechain-types";
|
||||
|
||||
export default class Fixture {
|
||||
static readonly ECDSA_ACCOUNTS_LENGTH = 5;
|
||||
@@ -119,7 +123,10 @@ export default class Fixture {
|
||||
// Perform an empty transaction to trigger wallet creation
|
||||
await (
|
||||
await verificationGateway.processBundle(
|
||||
wallet.sign({ nonce: BigNumber.from(0), actions: [] }),
|
||||
wallet.sign({
|
||||
nonce: BigNumber.from(0),
|
||||
actions: [],
|
||||
}),
|
||||
)
|
||||
).wait();
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { utils, BigNumber, Signer, Contract } from "ethers";
|
||||
import { BlsWalletWrapper } from "../../clients/src";
|
||||
|
||||
import Fixture from "./Fixture";
|
||||
import { IERC20 } from "../../typechain";
|
||||
import { IERC20 } from "../../typechain-types";
|
||||
|
||||
export default class TokenHelper {
|
||||
static readonly initialSupply = utils.parseUnits("1000000");
|
||||
|
||||
@@ -3,7 +3,7 @@ import "@nomiclabs/hardhat-ethers";
|
||||
|
||||
import { ethers } from "hardhat";
|
||||
import { Wallet } from "ethers";
|
||||
import { Create2Deployer } from "../../typechain";
|
||||
import { Create2Deployer } from "../../typechain-types";
|
||||
import defaultDeployerWalletHardhat from "./defaultDeployerWallet";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
@@ -6,7 +6,7 @@ import deployerContract, {
|
||||
defaultDeployerAddress,
|
||||
defaultDeployerWallet,
|
||||
} from "../shared/helpers/deployDeployer";
|
||||
import { Create2Deployer } from "../typechain";
|
||||
import { Create2Deployer } from "../typechain-types";
|
||||
|
||||
describe("Deployer", async function () {
|
||||
let Create2Deployer: ContractFactory;
|
||||
|
||||
@@ -7,7 +7,7 @@ import { BlsWalletWrapper, Signature } from "../clients/src";
|
||||
import Fixture from "../shared/helpers/Fixture";
|
||||
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
|
||||
import { defaultDeployerAddress } from "../shared/helpers/deployDeployer";
|
||||
import { BLSWallet, VerificationGateway } from "../typechain";
|
||||
import { BLSWallet, VerificationGateway } from "../typechain-types";
|
||||
|
||||
const signWalletAddress = async (
|
||||
fx: Fixture,
|
||||
@@ -18,7 +18,7 @@ const signWalletAddress = async (
|
||||
const wallet = await BlsWalletWrapper.connect(
|
||||
signerPrivKey,
|
||||
fx.verificationGateway.address,
|
||||
fx.provider,
|
||||
fx.verificationGateway.provider,
|
||||
);
|
||||
return wallet.signMessage(addressMessage);
|
||||
};
|
||||
@@ -134,7 +134,14 @@ describe("Recovery", async function () {
|
||||
});
|
||||
|
||||
it("should recover before bls key update", async function () {
|
||||
await fx.call(wallet1, blsWallet, "setRecoveryHash", [recoveryHash], 1);
|
||||
let recoveredWalletNonce = 1;
|
||||
await fx.call(
|
||||
wallet1,
|
||||
blsWallet,
|
||||
"setRecoveryHash",
|
||||
[recoveryHash],
|
||||
recoveredWalletNonce++,
|
||||
);
|
||||
|
||||
const attackSignature = await signWalletAddress(
|
||||
fx,
|
||||
@@ -149,11 +156,27 @@ describe("Recovery", async function () {
|
||||
vg,
|
||||
"setBLSKeyForWallet",
|
||||
[attackSignature, walletAttacker.PublicKey()],
|
||||
1,
|
||||
recoveredWalletNonce++,
|
||||
);
|
||||
const pendingKey = await Promise.all(
|
||||
[0, 1, 2, 3].map(async (i) =>
|
||||
(await vg.pendingBLSPublicKeyFromHash(hash1, i)).toHexString(),
|
||||
),
|
||||
);
|
||||
expect(pendingKey).to.deep.equal(walletAttacker.PublicKey());
|
||||
|
||||
await fx.advanceTimeBy(safetyDelaySeconds / 2); // wait half the time
|
||||
await fx.call(wallet1, vg, "setPendingBLSKeyForWallet", [], 2);
|
||||
// NB: advancing the time makes an empty tx with lazywallet[1]
|
||||
// Here this seems to be wallet2, not wallet (wallet being recovered)
|
||||
// recoveredWalletNonce++
|
||||
|
||||
await fx.call(
|
||||
wallet1,
|
||||
vg,
|
||||
"setPendingBLSKeyForWallet",
|
||||
[],
|
||||
recoveredWalletNonce++,
|
||||
);
|
||||
|
||||
const addressSignature = await signWalletAddress(
|
||||
fx,
|
||||
@@ -173,6 +196,9 @@ describe("Recovery", async function () {
|
||||
expect(await vg.walletFromHash(hash2)).to.eql(wallet1.address);
|
||||
|
||||
await fx.advanceTimeBy(safetyDelaySeconds / 2 + 1); // wait remainder the time
|
||||
// NB: advancing the time makes an empty tx with lazywallet[1]
|
||||
// Here this seems to be wallet1, the wallet being recovered
|
||||
recoveredWalletNonce++;
|
||||
|
||||
// check attacker's key not set after waiting full safety delay
|
||||
await fx.call(
|
||||
@@ -182,12 +208,23 @@ describe("Recovery", async function () {
|
||||
[],
|
||||
await walletAttacker.Nonce(),
|
||||
);
|
||||
/**
|
||||
* TODO (merge-ok)
|
||||
*
|
||||
* Event thought typechain types are symlinked between contracts
|
||||
* and clients, there appears to be a mismatch here passing in
|
||||
* VerificationGateway. This may be due to differing typescript
|
||||
* versions between contracts and clients, or something else.
|
||||
*
|
||||
* For now cast to 'any'.
|
||||
*/
|
||||
await wallet2.syncWallet(vg as any);
|
||||
await fx.call(
|
||||
wallet2,
|
||||
vg,
|
||||
"setPendingBLSKeyForWallet",
|
||||
[],
|
||||
await wallet2.Nonce(),
|
||||
recoveredWalletNonce++, // await wallet2.Nonce(),
|
||||
);
|
||||
|
||||
expect(await vg.walletFromHash(hash1)).to.not.equal(blsWallet.address);
|
||||
@@ -199,10 +236,10 @@ describe("Recovery", async function () {
|
||||
// // verify recovered bls key can successfully call wallet-only function (eg setTrustedGateway)
|
||||
const res = await fx.callStatic(
|
||||
wallet2,
|
||||
fx.verificationGateway,
|
||||
vg,
|
||||
"setTrustedBLSGateway",
|
||||
[hash2, fx.verificationGateway.address],
|
||||
3,
|
||||
[hash2, vg.address],
|
||||
recoveredWalletNonce, // await wallet2.Nonce(),
|
||||
);
|
||||
expect(res.successes[0]).to.equal(true);
|
||||
});
|
||||
@@ -248,4 +285,52 @@ describe("Recovery", async function () {
|
||||
.recoverWallet(addressSignature, hashAttacker, salt, wallet1Key),
|
||||
).to.be.rejectedWith("VG: Signature not verified for wallet address");
|
||||
});
|
||||
|
||||
it("should NOT allow a bundle to be executed on a wallet with the same BLS pubkey but different address (replay attack)", async function () {
|
||||
// Run empty operation on wallet 2 to align nonces after recovery.
|
||||
const emptyBundle = wallet2.sign({
|
||||
nonce: await wallet2.Nonce(),
|
||||
actions: [],
|
||||
});
|
||||
const emptyBundleTxn = await fx.verificationGateway.processBundle(
|
||||
emptyBundle,
|
||||
);
|
||||
await emptyBundleTxn.wait();
|
||||
|
||||
// Set wallet 1's pubkey to wallet 2's through recovery
|
||||
// This will also unregister wallet 2 from VG
|
||||
await fx.call(wallet1, blsWallet, "setRecoveryHash", [recoveryHash], 1);
|
||||
|
||||
const addressSignature = await signWalletAddress(
|
||||
fx,
|
||||
wallet1.address,
|
||||
wallet2.privateKey,
|
||||
);
|
||||
|
||||
const recoveryTxn = await fx.verificationGateway
|
||||
.connect(recoverySigner)
|
||||
.recoverWallet(addressSignature, hash1, salt, wallet2.PublicKey());
|
||||
await recoveryTxn.wait();
|
||||
|
||||
const [wallet1PubkeyHash, wallet2PubkeyHash, wallet1Nonce, wallet2Nonce] =
|
||||
await Promise.all([
|
||||
vg.hashFromWallet(wallet1.address),
|
||||
vg.hashFromWallet(wallet2.address),
|
||||
wallet1.Nonce(),
|
||||
wallet2.Nonce(),
|
||||
]);
|
||||
expect(wallet1PubkeyHash).to.eql(hash2);
|
||||
expect(wallet2PubkeyHash).to.eql(hash2);
|
||||
expect(wallet1Nonce.toNumber()).to.eql(wallet2Nonce.toNumber());
|
||||
|
||||
// Attempt to run a bundle from wallet 2 through wallet 1
|
||||
// Signiuture verification should fail as addresses differ
|
||||
const invalidBundle = wallet2.sign({
|
||||
nonce: await wallet2.Nonce(),
|
||||
actions: [],
|
||||
});
|
||||
await expect(
|
||||
fx.verificationGateway.processBundle(invalidBundle),
|
||||
).to.be.rejectedWith("VG: Sig not verified");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { expect } from "chai";
|
||||
import { BigNumber } from "ethers";
|
||||
import { BigNumber, ContractReceipt } from "ethers";
|
||||
import { solidityPack } from "ethers/lib/utils";
|
||||
import { ethers, network } from "hardhat";
|
||||
|
||||
import { BLSOpen, ProxyAdmin } from "../typechain";
|
||||
import { ActionData, BlsWalletWrapper } from "../clients/src";
|
||||
import { BLSOpen, ProxyAdmin } from "../typechain-types";
|
||||
import {
|
||||
ActionData,
|
||||
BlsWalletWrapper,
|
||||
getOperationResults,
|
||||
} from "../clients/src";
|
||||
import Fixture from "../shared/helpers/Fixture";
|
||||
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
|
||||
import { defaultDeployerAddress } from "../shared/helpers/deployDeployer";
|
||||
@@ -14,6 +18,25 @@ import {
|
||||
} from "../shared/helpers/callProxyAdmin";
|
||||
import Create2Fixture from "../shared/helpers/Create2Fixture";
|
||||
|
||||
const expectOperationsToSucceed = (txnReceipt: ContractReceipt) => {
|
||||
const opResults = getOperationResults(txnReceipt);
|
||||
for (const opRes of opResults) {
|
||||
expect(opRes.success).to.eql(true);
|
||||
expect(opRes.error).to.eql(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
const expectOperationFailure = (
|
||||
txnReceipt: ContractReceipt,
|
||||
errorMessage: string,
|
||||
) => {
|
||||
const opResults = getOperationResults(txnReceipt);
|
||||
expect(opResults).to.have.lengthOf(1);
|
||||
expect(opResults[0].success).to.equal(false);
|
||||
expect(opResults[0].error.actionIndex.toNumber()).to.eql(0);
|
||||
expect(opResults[0].error.message).to.eql(errorMessage);
|
||||
};
|
||||
|
||||
describe("Upgrade", async function () {
|
||||
this.beforeAll(async function () {
|
||||
// deploy the deployer contract for the transient hardhat network
|
||||
@@ -47,10 +70,11 @@ describe("Upgrade", async function () {
|
||||
const wallet = await fx.lazyBlsWallets[0]();
|
||||
|
||||
// prepare call
|
||||
await proxyAdminCall(fx, wallet, "upgrade", [
|
||||
const txnReceipt1 = await proxyAdminCall(fx, wallet, "upgrade", [
|
||||
wallet.address,
|
||||
mockWalletUpgraded.address,
|
||||
]);
|
||||
expectOperationsToSucceed(txnReceipt1);
|
||||
|
||||
// Advance time one week
|
||||
const latestTimestamp = (await ethers.provider.getBlock("latest"))
|
||||
@@ -62,10 +86,11 @@ describe("Upgrade", async function () {
|
||||
]);
|
||||
|
||||
// make call
|
||||
await proxyAdminCall(fx, wallet, "upgrade", [
|
||||
const txnReceipt2 = await proxyAdminCall(fx, wallet, "upgrade", [
|
||||
wallet.address,
|
||||
mockWalletUpgraded.address,
|
||||
]);
|
||||
expectOperationsToSucceed(txnReceipt2);
|
||||
|
||||
const newBLSWallet = MockWalletUpgraded.attach(wallet.address);
|
||||
await (await newBLSWallet.setNewData(wallet.address)).wait();
|
||||
@@ -99,7 +124,7 @@ describe("Upgrade", async function () {
|
||||
const wallet = await BlsWalletWrapper.connect(
|
||||
blsSecret,
|
||||
fx.verificationGateway.address,
|
||||
fx.provider,
|
||||
fx.verificationGateway.provider,
|
||||
);
|
||||
// Sign simple address message
|
||||
const addressMessage = solidityPack(["address"], [walletAddress]);
|
||||
@@ -114,10 +139,13 @@ describe("Upgrade", async function () {
|
||||
const changeProxyAction = bundle.operations[0].actions[0];
|
||||
|
||||
// prepare call
|
||||
await proxyAdminCall(fx, walletOldVg, "changeProxyAdmin", [
|
||||
walletAddress,
|
||||
proxyAdmin2Address,
|
||||
]);
|
||||
const txnReceipt = await proxyAdminCall(
|
||||
fx,
|
||||
walletOldVg,
|
||||
"changeProxyAdmin",
|
||||
[walletAddress, proxyAdmin2Address],
|
||||
);
|
||||
expectOperationsToSucceed(txnReceipt);
|
||||
|
||||
// Advance time one week
|
||||
await fx.advanceTimeBy(safetyDelaySeconds + 1);
|
||||
@@ -284,13 +312,13 @@ describe("Upgrade", async function () {
|
||||
const wallet1 = await BlsWalletWrapper.connect(
|
||||
lazyWallet1.privateKey,
|
||||
vg1.address,
|
||||
fx.provider,
|
||||
vg1.provider,
|
||||
);
|
||||
|
||||
const wallet2 = await BlsWalletWrapper.connect(
|
||||
lazyWallet2.privateKey,
|
||||
vg1.address,
|
||||
fx.provider,
|
||||
vg1.provider,
|
||||
);
|
||||
|
||||
const hash1 = wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey);
|
||||
@@ -347,4 +375,46 @@ describe("Upgrade", async function () {
|
||||
expect(await vg1.walletFromHash(hash2)).to.equal(wallet1.address);
|
||||
expect(await vg1.hashFromWallet(wallet1.address)).to.equal(hash2);
|
||||
});
|
||||
|
||||
it("should NOT allow walletAdminCall where first param is not calling wallet", async function () {
|
||||
const wallet1 = await fx.lazyBlsWallets[0]();
|
||||
const wallet2 = await fx.lazyBlsWallets[1]();
|
||||
|
||||
const txnReceipt = await proxyAdminCall(fx, wallet1, "upgrade", [
|
||||
wallet2.address,
|
||||
wallet2.address,
|
||||
]);
|
||||
expectOperationFailure(
|
||||
txnReceipt,
|
||||
"VG: first param to proxy admin is not calling wallet",
|
||||
);
|
||||
});
|
||||
|
||||
it("should NOT allow walletAdminCall to ProxyAdmin.transferOwnership", async function () {
|
||||
const wallet = await fx.lazyBlsWallets[0]();
|
||||
|
||||
const txnReceipt = await proxyAdminCall(fx, wallet, "transferOwnership", [
|
||||
wallet.address,
|
||||
]);
|
||||
expectOperationFailure(txnReceipt, "VG: cannot change ownership");
|
||||
});
|
||||
|
||||
it("should NOT allow walletAdminCall to ProxyAdmin.renounceOwnership", async function () {
|
||||
const wallet = await fx.lazyBlsWallets[0]();
|
||||
|
||||
const txnReceipt = await proxyAdminCall(
|
||||
fx,
|
||||
wallet,
|
||||
"renounceOwnership",
|
||||
[],
|
||||
);
|
||||
expectOperationFailure(txnReceipt, "VG: cannot change ownership");
|
||||
});
|
||||
|
||||
it("call function with no params", async function () {
|
||||
const wallet = await fx.lazyBlsWallets[0]();
|
||||
|
||||
const txnReceipt = await proxyAdminCall(fx, wallet, "owner", []);
|
||||
expectOperationsToSucceed(txnReceipt);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,11 +5,12 @@ import { ethers, network } from "hardhat";
|
||||
import Fixture from "../shared/helpers/Fixture";
|
||||
import TokenHelper from "../shared/helpers/TokenHelper";
|
||||
|
||||
import { BigNumber } from "ethers";
|
||||
import { BigNumber, ContractReceipt } from "ethers";
|
||||
import { parseEther, solidityPack } from "ethers/lib/utils";
|
||||
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
|
||||
// import splitHex256 from "../shared/helpers/splitHex256";
|
||||
import { defaultDeployerAddress } from "../shared/helpers/deployDeployer";
|
||||
import { getOperationResults } from "../clients/src";
|
||||
|
||||
describe("WalletActions", async function () {
|
||||
if (`${process.env.DEPLOYER_DEPLOYMENT}` === "true") {
|
||||
@@ -288,11 +289,21 @@ describe("WalletActions", async function () {
|
||||
const th = new TokenHelper(fx);
|
||||
const [sender, recipient] = await th.walletTokenSetup();
|
||||
|
||||
await (
|
||||
const r: ContractReceipt = await (
|
||||
await fx.verificationGateway.processBundle(
|
||||
sender.sign({
|
||||
nonce: await sender.Nonce(),
|
||||
actions: [
|
||||
// Send tokens to recipient.
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: th.testToken.address,
|
||||
encodedFunction: th.testToken.interface.encodeFunctionData(
|
||||
"transfer",
|
||||
[recipient.address, th.userStartAmount],
|
||||
),
|
||||
},
|
||||
|
||||
// Try to send ourselves a lot of tokens from address zero, which
|
||||
// obviously shouldn't work.
|
||||
{
|
||||
@@ -307,21 +318,16 @@ describe("WalletActions", async function () {
|
||||
],
|
||||
),
|
||||
},
|
||||
|
||||
// Send tokens to recipient.
|
||||
{
|
||||
ethValue: 0,
|
||||
contractAddress: th.testToken.address,
|
||||
encodedFunction: th.testToken.interface.encodeFunctionData(
|
||||
"transfer",
|
||||
[recipient.address, th.userStartAmount],
|
||||
),
|
||||
},
|
||||
],
|
||||
}),
|
||||
)
|
||||
).wait();
|
||||
|
||||
const opResults = getOperationResults(r);
|
||||
expect(opResults).to.have.lengthOf(1);
|
||||
expect(opResults[0].error.actionIndex.toNumber()).to.eql(1);
|
||||
expect(opResults[0].error.message).to.eql("ERC20: insufficient allowance");
|
||||
|
||||
const recipientBalance = await th.testToken.balanceOf(recipient.address);
|
||||
|
||||
// Should be unchanged because the operation that would have added tokens
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
"include": ["./scripts", "./test", "./typechain"],
|
||||
"include": ["./scripts", "./test", "./typechain-types"],
|
||||
"files": ["./hardhat.config.ts"]
|
||||
}
|
||||
|
||||
7760
contracts/yarn.lock
7760
contracts/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ services:
|
||||
command: >
|
||||
--datadir dev-chain/
|
||||
--http
|
||||
--http.api eth,web3,personal,net
|
||||
--http.addr=0.0.0.0
|
||||
--http.vhosts='*'
|
||||
--dev
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
## Docs
|
||||
|
||||
- [See an overview of BLS Wallet & how the components work together](./docs/system_overview.md)
|
||||
- [Use BLS Wallet in a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md)
|
||||
- [See an overview of BLS Wallet & how the components work together](./system_overview.md)
|
||||
- [Use BLS Wallet in a browser/NodeJS/Deno app](./use_bls_wallet_clients.md)
|
||||
- [Use BLS Wallet in your L2 dApp for cheaper, multi action transactions](./use_bls_wallet_dapp.md)
|
||||
- Setup the BLS Wallet components for:
|
||||
- [Local develeopment](./docs/local_development.md)
|
||||
- [Remote development](./docs/remote_development.md)
|
||||
- [Local develeopment](./local_development.md)
|
||||
- [Remote development](./remote_development.md)
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 231 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 153 KiB |
@@ -1,6 +1,7 @@
|
||||
# Local Development
|
||||
|
||||
These steps will setup this repo on your machine for local development for the majority of the components in this repo.
|
||||
By default the extension will connect to contracts already deployed on Arbitrum Nitro testnet and a public Aggregator running on https://arbitrum-goerli.blswallet.org/
|
||||
If you would like to target a remote network instead, add the addtional steps in [Remote Development](./remote_development.md) as well.
|
||||
|
||||
## Dependencies
|
||||
@@ -18,12 +19,17 @@ If you would like to target a remote network instead, add the addtional steps in
|
||||
|
||||
## Setup
|
||||
|
||||
Install the latest Node 16. If using nvm to manage node versions, run this in the root directory:
|
||||
```sh
|
||||
nvm install
|
||||
```
|
||||
|
||||
Run the repo setup script
|
||||
```sh
|
||||
./setup.ts
|
||||
```
|
||||
|
||||
Then choose to target either a local Hardhat node or the Arbitrum Testnet.
|
||||
Then choose to target either a local Hardhat node or the Arbitrum Testnet. If you choose to run on Arbitrum Goerli skip ahead until tests.
|
||||
|
||||
### Chain (RPC Node)
|
||||
|
||||
@@ -45,7 +51,14 @@ Deploy all `bls-wallet` contracts.
|
||||
yarn hardhat run scripts/deploy_all.ts --network gethDev
|
||||
```
|
||||
|
||||
## Run
|
||||
## Aggregator
|
||||
|
||||
make these changes in aggregator > .env
|
||||
|
||||
RPC_URL=http://localhost:8545
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/local.json
|
||||
|
||||
In a seperate terminal/shell instance
|
||||
|
||||
```sh
|
||||
docker-compose up -d postgres # Or see local postgres instructions in ./aggregator/README.md#PostgreSQL
|
||||
@@ -59,6 +72,16 @@ cd ./extension
|
||||
yarn run dev:chrome # or dev:firefox, dev:opera
|
||||
```
|
||||
|
||||
## Extension
|
||||
|
||||
make these changes in extension > .env
|
||||
|
||||
```
|
||||
AGGREGATOR_URL=http://localhost:3000/
|
||||
DEFAULT_CHAIN_ID=31337
|
||||
NETWORK_CONFIG=./contracts/networks/local.json
|
||||
```
|
||||
|
||||
### Chrome
|
||||
|
||||
1. Go to Chrome's [extension page](chrome://extensions).
|
||||
@@ -85,6 +108,20 @@ cd ../extension
|
||||
yarn link bls-wallet-clients
|
||||
```
|
||||
|
||||
If you would like live updates to from the clients package to trigger reloads of the extension, be sure to comment out this section of `./extension/weback.config.js`:
|
||||
```javascript
|
||||
...
|
||||
module.exports = {
|
||||
...
|
||||
watchOptions: {
|
||||
// Remove this if you want to watch for changes
|
||||
// from a linked package, such as bls-wallet-clients.
|
||||
ignored: /node_modules/,
|
||||
},
|
||||
...
|
||||
};
|
||||
```
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -8,7 +8,7 @@ Follow the instructions for [Local Development](./local_development.md), replaci
|
||||
|
||||
### Deployer account
|
||||
|
||||
BLS Wallet contract deploys use `CREATE2` to maintain consistent addresses across networks. As such, a create2 deployer contract is used and listed in `./contracts/.env` under the environment variables `DEPLOYER_MNEMONIC` & `DEPLOYER_SET_INDEX`. The HD address will need to be funded in order to deploy the contracts.
|
||||
BLS Wallet contract deploys use `CREATE2` to maintain consistent addresses across networks. As such, a create2 deployer contract is used and listed in `./contracts/.env` under the environment variables `DEPLOYER_MNEMONIC` & `DEPLOYER_SET_INDEX`. The hierarchical deterministic (HD) wallet address will need to be funded in order to deploy the contracts.
|
||||
|
||||
If you do not need consistent addresses, for example on a local or testnet network, you can replace the `DEPLOYER_MNEMONIC` with another seed phrase which already has a funded account.
|
||||
|
||||
@@ -66,40 +66,27 @@ PRIVATE_KEY_ADMIN=PK1
|
||||
|
||||
### Extension
|
||||
|
||||
Check the [controller constants file](../extension/source/Controllers/constants.ts) to see if your network is already added. If not, you will need to add chainid & supported networks entries for your network/chain. These changes can be committed.
|
||||
Check the [`config.json` file](../extension//config.json) to see if your network is already added. If not, you will need to add the relevant properties for your network/chain. These changes can be committed.
|
||||
|
||||
Then, update this value in `./extension/.env`.
|
||||
```
|
||||
...
|
||||
## Example: Arbitrum Testnet (Arbitrum Goerli Testnet)
|
||||
|
||||
DEFAULT_CHAIN_ID=YOUR_CHAIN_ID
|
||||
...
|
||||
```
|
||||
You will need two ETH addresses with Abitrum Goerli ETH and their private keys (PRIVATE_KEY_AGG & PRIVATE_KEY_ADMIN) for running the aggregator. It is **NOT** recommended that you use any primary wallets with ETH Mainnet assets.
|
||||
|
||||
## Run
|
||||
|
||||
Follow the remaing instruction in [Local Development](./local_development.md) starting with the `Run` section.
|
||||
|
||||
## Example: 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 your web3 wallet extension, such as MetaMask.
|
||||
You can get Goerli ETH at https://goerlifaucet.com/ or 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 Goerli in your web3 wallet extension, such as MetaMask.
|
||||
|
||||
Update these values in `./aggregator/.env`.
|
||||
```
|
||||
RPC_URL=https://rinkeby.arbitrum.io/rpc
|
||||
RPC_URL=https://goerli-rollup.arbitrum.io/rpc
|
||||
...
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-testnet.json
|
||||
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-goerli.json
|
||||
PRIVATE_KEY_AGG=PK0
|
||||
PRIVATE_KEY_ADMIN=PK1
|
||||
...
|
||||
```
|
||||
|
||||
And then update this value in `./extension/.env`.
|
||||
```
|
||||
And then ensure the `defaultNetwork` value in `./extension/config.json` is set to `arbitrum-goerli`.
|
||||
```json
|
||||
...
|
||||
|
||||
DEFAULT_CHAIN_ID=421611
|
||||
"defaultNetwork": "arbitrum-goerli",
|
||||
...
|
||||
```
|
||||
```
|
||||
@@ -1,8 +1,10 @@
|
||||
# System Overview
|
||||
|
||||
## Layer 2 Amsterdam April 2022 Presentation
|
||||
## Presentations
|
||||
|
||||
https://youtu.be/Ke4L_PXIi8M?t=22380
|
||||
- [Layer 2 Amsterdam April 2022](https://youtu.be/Ke4L_PXIi8M?t=22380)
|
||||
- [EthPrague June 2022](https://www.youtube.com/watch?v=F4gNVq07CHc)
|
||||
- [PSE Learn & Share July 20220](https://www.youtube.com/watch?v=FRw_B8bd4VI)
|
||||
|
||||
## Overview Diagram
|
||||
|
||||
|
||||
@@ -8,17 +8,15 @@ This walkthrough will show you how to submit an ERC20 transfer to the BLS Wallet
|
||||
# npm
|
||||
npm install bls-wallet-clients
|
||||
# yarn
|
||||
yarn install bls-wallet-clients
|
||||
yarn add bls-wallet-clients
|
||||
# deno in example further below
|
||||
```
|
||||
|
||||
You will also need to have [ethers](https://docs.ethers.io) installed.
|
||||
|
||||
## Import
|
||||
|
||||
```typescript
|
||||
import { providers } from "ethers";
|
||||
import { Aggregator, BLSWalletWrapper, getConfig } from "bls-wallet-clients";
|
||||
import { Aggregator, BlsWalletWrapper, getConfig } from "bls-wallet-clients";
|
||||
```
|
||||
|
||||
### Deno
|
||||
@@ -27,7 +25,7 @@ You can use [esm.sh](https://esm.sh/) or a similar service to get Deno compatibl
|
||||
|
||||
```typescript
|
||||
import { providers } from "https://esm.sh/ethers@latest";
|
||||
import { Aggregator, BLSWalletWrapper, getConfig } from "https://esm.sh/bls-wallet-clients@latest";
|
||||
import { Aggregator, BlsWalletWrapper, getConfig } from "https://esm.sh/bls-wallet-clients@latest";
|
||||
```
|
||||
|
||||
## Get Deployed Contract Addresses
|
||||
@@ -39,38 +37,49 @@ If you would like to deploy to a remote network, see [Remote development](./remo
|
||||
## Send a transaction
|
||||
|
||||
```typescript
|
||||
import { readFile } from "fs/promises";
|
||||
import { readFile } from 'fs/promises';
|
||||
|
||||
// import fetch from 'node-fetch'; // Add this if using nodejs<18
|
||||
import { ethers, providers } from 'ethers';
|
||||
import { Aggregator, BlsWalletWrapper, getConfig } from 'bls-wallet-clients';
|
||||
|
||||
// globalThis.fetch = fetch; // Add this if using nodejs<18
|
||||
|
||||
// Instantiate a provider via browser extension, such as Metamask
|
||||
const provider = providers.Web3Provider(window.ethereum);
|
||||
// const provider = new providers.Web3Provider(window.ethereum);
|
||||
// Or via RPC
|
||||
const provider = providers.JsonRpcProvider();
|
||||
const provider = new providers.JsonRpcProvider('https://goerli-rollup.arbitrum.io/rpc');
|
||||
// See https://docs.ethers.io/v5/getting-started/ for more options
|
||||
|
||||
// Get the deployed contract addresses for the network.
|
||||
// Here, we will get the Arbitrum testnet.
|
||||
// See local_development.md for deploying locally and
|
||||
// remote_development.md for deploying to a remote network.
|
||||
const netCfg = await getConfig("../contracts/networks/arbitrum-testnet.json", async (path) => readFile(path));
|
||||
const netCfg = await getConfig(
|
||||
'../contracts/networks/arbitrum-goerli.json',
|
||||
async (path) => readFile(path),
|
||||
);
|
||||
|
||||
const privateKey = "0x...";
|
||||
// 32 random bytes
|
||||
const privateKey = '0x0001020304050607080910111213141516171819202122232425262728293031';
|
||||
|
||||
// Note that if a wallet doesn't yet exist, it will be
|
||||
// lazily created on the first transaction.
|
||||
const wallet = await BlsWallerWrapper.connect(
|
||||
const wallet = await BlsWalletWrapper.connect(
|
||||
privateKey,
|
||||
netCfg.contracts.verificationGateway,
|
||||
netCfg.addresses.verificationGateway,
|
||||
provider
|
||||
);
|
||||
|
||||
const erc20Address = "0x...";
|
||||
const erc20Address = netCfg.addresses.testToken; // Or some other ERC20 token
|
||||
const erc20Abi = [
|
||||
"function transfer(address to, uint amount) returns (bool)",
|
||||
'function mint(address to, uint amount) returns (bool)',
|
||||
];
|
||||
const erc20 = new ethers.Contract(erc20Address, erc20Abi, provider);
|
||||
|
||||
const recipientAddress = "0x...";
|
||||
console.log('Contract wallet:', wallet.address);
|
||||
console.log('Test token:', erc20.address);
|
||||
|
||||
const nonce = await wallet.Nonce();
|
||||
// All of the actions in a bundle are atomic, if one
|
||||
// action fails they will all fail.
|
||||
@@ -78,16 +87,41 @@ const bundle = wallet.sign({
|
||||
nonce,
|
||||
actions: [
|
||||
{
|
||||
contract: erc20,
|
||||
method: "transfer",
|
||||
args: [recipientAddress, ethers.utils.parseUnits("1", 18)],
|
||||
// Mint ourselves one test token
|
||||
ethValue: 0,
|
||||
contractAddress: erc20.address,
|
||||
encodedFunction: erc20.interface.encodeFunctionData(
|
||||
'mint',
|
||||
[wallet.address, ethers.utils.parseUnits('1', 18)],
|
||||
),
|
||||
},
|
||||
|
||||
],
|
||||
});
|
||||
|
||||
const aggregator = new Aggregator("https://rinkarby.blswallet.org");
|
||||
await aggregator.add(bundle);
|
||||
const aggregator = new Aggregator('https://arbitrum-goerli.blswallet.org');
|
||||
|
||||
console.log('Sending bundle to the aggregator');
|
||||
const addResult = await aggregator.add(bundle);
|
||||
|
||||
if ('failures' in addResult) {
|
||||
throw new Error(addResult.failures.join('\n'));
|
||||
}
|
||||
|
||||
console.log('Bundle hash:', addResult.hash);
|
||||
|
||||
const checkConfirmation = async () => {
|
||||
console.log('Checking for confirmation')
|
||||
const maybeReceipt = await aggregator.lookupReceipt(addResult.hash);
|
||||
|
||||
if (maybeReceipt === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Confirmed in block', maybeReceipt.blockNumber);
|
||||
provider.off('block', checkConfirmation);
|
||||
};
|
||||
|
||||
provider.on('block', checkConfirmation);
|
||||
```
|
||||
|
||||
## More
|
||||
|
||||
120
docs/use_bls_wallet_dapp.md
Normal file
120
docs/use_bls_wallet_dapp.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Use BLS Wallet In Your L2 dApp
|
||||
|
||||
This guide will show you how to use BLS Wallet in your L2 dApp (Layer 2 decentralized application) so you can utilize multi-action transactions.
|
||||
|
||||
## Download, Install, & Setup Quill
|
||||
|
||||
[Quill](../extension/) is a protoype browser extension wallet which intergrates [bls-wallet-clients](../contracts/clients/) to communicate with the BLS Wallet smart contracts & transaction aggregator. It supports most of the functionality in [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193).
|
||||
|
||||
Currently, we have the contracts deployed to the networks/chains listed [here](https://github.com/web3well/bls-wallet/tree/main/contracts/networks). If your desired network isn't there, you can use the [Remote Development](./remote_development.md) contract deployment intrsuctions or request a network deploy by [opening an issue](https://github.com/web3well/bls-wallet/issues/new) or [starting a discussion](https://github.com/web3well/bls-wallet/discussions/new).
|
||||
|
||||
Below are the instructions for 2 ways you can add Quill to your browser.
|
||||
|
||||
### Prebuilt
|
||||
|
||||
Go to the [releases page](https://github.com/web3well/bls-wallet/releases) and scroll down to the latest release. In the `Assets` section, download the extension for either Chrome, Firefox, or Opera. To install, simply drag and drop the file into your browser on the extensions page or follow instructions for installing extensions from a file for your browser.
|
||||
|
||||
### From Repo
|
||||
|
||||
Follow the instructions in either [Local Development](./local_development.md) or [Remote Development](./remote_development.md) to setup this repo and install Quill.
|
||||
|
||||
### Setup Quill
|
||||
|
||||
After installing the extension, Quill will auto-open and guide you through the setup process.
|
||||
|
||||
## Connect Your dApp to Quill
|
||||
|
||||
Next, connect your dApp to Quill just like you would any other extension wallet.
|
||||
|
||||
`ethers.js`
|
||||
```typescript
|
||||
import { providers } from 'ethers';
|
||||
|
||||
const provider = new providers.Web3Provider(window.ethereum);
|
||||
|
||||
await window.ethereum.request({ method: "eth_accounts" });
|
||||
```
|
||||
|
||||
Or similarly with [web3modal](https://github.com/WalletConnect/web3modal#usage) or [rainbow-connect](https://rainbowkit.vercel.app/docs/installation#configure)
|
||||
|
||||
## Send Multi-Action Transaction
|
||||
|
||||
Finally, you can populate & send your multi-action transaction. In the following example, we will do an approve & swap with a DEX (decentralized exchange) in one transaction.
|
||||
|
||||
### Check that window.ethereum Supports Multi-Action Transactions
|
||||
|
||||
Since any browser extension wallet could be used, make sure that it is Quill before allowing a multi-action transaction.
|
||||
|
||||
```typescript
|
||||
const areMultiActionTransactionSupported = !!window.ethereum.isQuill;
|
||||
// Branch here depending on the result
|
||||
if (areMultiActionTransactionSupported) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Populate the Transactions (Actions)
|
||||
|
||||
First, we will populate the transactions (actions) we want to send.
|
||||
|
||||
```typescript
|
||||
// Get the signer and connect to the contracts.
|
||||
//
|
||||
// If you want the contracts to always have write access
|
||||
// for a specific account, pass the signer in as the
|
||||
// provider instead and skip calling connect on them.
|
||||
const signer = provider.getSigner();
|
||||
|
||||
const erc20Contract = new ethers.Contract(erc20Address, erc20Abi, provider);
|
||||
const dexContract = new ethers.Contract(dexAddress, dex20Abi, provider);
|
||||
|
||||
// Populate the token approval transaction.
|
||||
const approveTransaction = await erc20Contract
|
||||
.connect(signer)
|
||||
.populateTransaction.approve(dexAddress, amount);
|
||||
|
||||
// Populate the token swap transaction.
|
||||
const swapTransaction = await dexContract
|
||||
.connect(signer)
|
||||
.populateTransaction.swap(erc20Address, amount, otherERC20Address);
|
||||
```
|
||||
|
||||
### Send the Transaction
|
||||
|
||||
Then, send the populated transactions.
|
||||
|
||||
Quill's [eth_sendTransaction](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_sendtransaction) accepts a modified `params` property into which more than one transaction object can be passed in. [Make sure window.ethereum can accept multiple transactions](#check-that-windowethereum-supports-multi-action-transactions) before passing more than one in.
|
||||
|
||||
```typescript
|
||||
const transactionHash = await window.ethereum.request({
|
||||
method: "eth_sendTransaction",
|
||||
params: [approveTransaction, swapTransaction],
|
||||
});
|
||||
|
||||
const transactionReceipt = await provider.getTransactionReceipt(transactionHash);
|
||||
// Do anything else you need to with the transaction receipt.
|
||||
```
|
||||
|
||||
You also can still send normal one-off transactions as you normally would, and still get the gas saving benefits of having your transaction aggregated with other transactions.
|
||||
|
||||
```typescript
|
||||
const transferTransaction = await erc20Contract
|
||||
.connect(signer)
|
||||
.approve(otherAddress, amount);
|
||||
|
||||
await transferTransaction.wait();
|
||||
```
|
||||
|
||||
## How Does This All Work?
|
||||
|
||||
See the [System Overview](./system_overview.md) for more details on what's happening behind the scenes.
|
||||
|
||||
## Example dApps Which Use BLS Wallet
|
||||
|
||||
- https://github.com/kautukkundan/BLSWallet-ERC20-demo
|
||||
- https://github.com/voltrevo/bls-wallet-billboard
|
||||
- https://github.com/JohnGuilding/single-pool-dex
|
||||
|
||||
## Coming soon
|
||||
|
||||
- Gasless transaction example.
|
||||
@@ -8,10 +8,8 @@
|
||||
// Do not transform modules to CJS
|
||||
"modules": false,
|
||||
"targets": {
|
||||
"chrome": "49",
|
||||
"firefox": "52",
|
||||
"opera": "36",
|
||||
"edge": "79"
|
||||
"chrome": "92",
|
||||
"firefox": "90"
|
||||
}
|
||||
}
|
||||
],
|
||||
@@ -19,26 +17,7 @@
|
||||
["@babel/react", { "runtime": "automatic" }]
|
||||
],
|
||||
"plugins": [
|
||||
["@babel/plugin-proposal-class-properties"],
|
||||
[
|
||||
"@babel/plugin-transform-destructuring",
|
||||
{
|
||||
"useBuiltIns": true
|
||||
}
|
||||
],
|
||||
[
|
||||
"@babel/plugin-proposal-object-rest-spread",
|
||||
{
|
||||
"useBuiltIns": true
|
||||
}
|
||||
],
|
||||
[
|
||||
// Polyfills the runtime needed for async/await and generators
|
||||
"@babel/plugin-transform-runtime",
|
||||
{
|
||||
"helpers": false,
|
||||
"regenerator": true
|
||||
}
|
||||
]
|
||||
"@babel/plugin-proposal-class-properties",
|
||||
"@babel/plugin-proposal-private-methods"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
PRIVATE_KEY_STORAGE_KEY=default-private-key
|
||||
AGGREGATOR_URL=http://localhost:3000
|
||||
DEFAULT_CHAIN_ID=31337
|
||||
CRYPTO_COMPARE_API_KEY=
|
||||
@@ -1,4 +0,0 @@
|
||||
PRIVATE_KEY_STORAGE_KEY=quill-private-key
|
||||
AGGREGATOR_URL=https://arbitrum-testnet.blswallet.org
|
||||
DEFAULT_CHAIN_ID=421611
|
||||
CRYPTO_COMPARE_API_KEY=injected-from-github-actions-environment
|
||||
1814
extension/.eslintrc.js
Normal file
1814
extension/.eslintrc.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,65 +0,0 @@
|
||||
{
|
||||
"extends": [
|
||||
"@abhijithvijayan/eslint-config/typescript",
|
||||
"@abhijithvijayan/eslint-config/node",
|
||||
"@abhijithvijayan/eslint-config/react"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": "./tsconfig.json",
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 11
|
||||
},
|
||||
"rules": {
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"consistent-return": "off",
|
||||
"no-console": "off",
|
||||
"no-continue": "off",
|
||||
"no-extend-native": "off",
|
||||
"no-constant-condition": "off",
|
||||
"no-return-await": "off",
|
||||
"react/jsx-props-no-spreading": "off",
|
||||
"react/no-set-state": "off",
|
||||
"react/destructuring-assignment": "off",
|
||||
"jsx-a11y/label-has-associated-control": "off",
|
||||
"class-methods-use-this": "off",
|
||||
"max-classes-per-file": "off",
|
||||
"no-param-reassign": "off",
|
||||
"node/no-missing-import": "off",
|
||||
"node/no-unpublished-import": "off",
|
||||
"node/no-unsupported-features/es-syntax": [
|
||||
"error",
|
||||
{
|
||||
"ignores": ["modules"]
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error",
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
],
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-use-before-define": "off",
|
||||
"@typescript-eslint/lines-between-class-members": "off"
|
||||
},
|
||||
"env": {
|
||||
"webextensions": true,
|
||||
"es2020": true,
|
||||
"browser": true,
|
||||
"node": true
|
||||
},
|
||||
"settings": {
|
||||
"node": {
|
||||
"tryExtensions": [".tsx"] // append tsx to the list as well
|
||||
}
|
||||
}
|
||||
}
|
||||
2
extension/.gitignore
vendored
2
extension/.gitignore
vendored
@@ -206,3 +206,5 @@ dist/
|
||||
# Non-template additions
|
||||
.env*
|
||||
!.env.example
|
||||
/build
|
||||
/config.json
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"bracketSpacing": true,
|
||||
"jsxBracketSameLine": false,
|
||||
"printWidth": 80,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"proseWrap": "always"
|
||||
}
|
||||
27
extension/buildMultiNetworkConfig.js
Normal file
27
extension/buildMultiNetworkConfig.js
Normal file
@@ -0,0 +1,27 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const networksConfigDir =
|
||||
process.env.NETWORKS_CONFIG_DIR ??
|
||||
path.join(__dirname, '..', 'contracts', 'networks');
|
||||
|
||||
fs.mkdirSync(path.join(__dirname, 'build'), { recursive: true });
|
||||
|
||||
const networkFilenames = fs.readdirSync(networksConfigDir);
|
||||
|
||||
const multiNetworkConfig = {};
|
||||
|
||||
for (const filename of networkFilenames) {
|
||||
if (!filename.endsWith('.json')) {
|
||||
throw new Error('Unexpected non-json file');
|
||||
}
|
||||
|
||||
multiNetworkConfig[filename.slice(0, -'.json'.length)] = JSON.parse(
|
||||
fs.readFileSync(path.join(networksConfigDir, filename), 'utf-8'),
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, 'build', 'multiNetworkConfig.json'),
|
||||
JSON.stringify(multiNetworkConfig, null, 2),
|
||||
);
|
||||
91
extension/config.example.json
Normal file
91
extension/config.example.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"defaultNetwork": "arbitrum-goerli",
|
||||
"builtinNetworks": {
|
||||
"mainnet": {
|
||||
"blockExplorerUrl": "http://etherscan.io/",
|
||||
"chainId": "0x1",
|
||||
"displayName": "Mainnet",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://mainnet.infura.io/v3/d0f317090d6645b6b494ddc6f1cce5ad",
|
||||
"chainCurrencyName": "Ethereum",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://mainnet.blswallet.org",
|
||||
"networkKey": "mainnet",
|
||||
"hidden": true
|
||||
},
|
||||
"local": {
|
||||
"blockExplorerUrl": "N/A",
|
||||
"chainId": "0x7a69",
|
||||
"displayName": "Local Network",
|
||||
"logo": "",
|
||||
"rpcTarget": "http://127.0.0.1:8545",
|
||||
"chainCurrencyName": "Ethereum",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "http://localhost:3000",
|
||||
"networkKey": "local"
|
||||
},
|
||||
"arbitrum-testnet": {
|
||||
"blockExplorerUrl": "https://rinkeby-explorer.arbitrum.io",
|
||||
"chainId": "0x66eeb",
|
||||
"displayName": "Arbitrum Test Network",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://rinkeby.arbitrum.io/rpc",
|
||||
"chainCurrencyName": "Arbitrum Ether",
|
||||
"chainCurrency": "ARETH",
|
||||
"aggregatorUrl": "https://arbitrum-testnet.blswallet.org",
|
||||
"networkKey": "arbitrum-testnet"
|
||||
},
|
||||
"arbitrum-goerli": {
|
||||
"blockExplorerUrl": "https://goerli-rollup-explorer.arbitrum.io",
|
||||
"chainId": "0x66EED",
|
||||
"displayName": "Arbitrum Goerli",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://goerli-rollup.arbitrum.io/rpc",
|
||||
"chainCurrencyName": "Ethereum",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://arbitrum-goerli.blswallet.org",
|
||||
"networkKey": "arbitrum-goerli"
|
||||
},
|
||||
"arbitrum": {
|
||||
"blockExplorerUrl": "https://explorer.arbitrum.io",
|
||||
"chainId": "0xa4b1",
|
||||
"displayName": "Arbitrum One",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://arb1.arbitrum.io/rpc",
|
||||
"chainCurrencyName": "Ether",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://arbitrum.blswallet.org",
|
||||
"networkKey": "arbitrum",
|
||||
"hidden": true
|
||||
},
|
||||
"optimism-kovan": {
|
||||
"blockExplorerUrl": "https://kovan-optimistic.etherscan.io",
|
||||
"chainId": "0x45",
|
||||
"displayName": "Optimism Test Network",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://kovan.optimism.io",
|
||||
"chainCurrencyName": "Optimistic Kovan Ether",
|
||||
"chainCurrency": "KOR",
|
||||
"aggregatorUrl": "https://optimism-kovan.blswallet.org",
|
||||
"networkKey": "optimism-kovan",
|
||||
"hidden": true
|
||||
},
|
||||
"optimism": {
|
||||
"blockExplorerUrl": "https://optimistic.etherscan.io",
|
||||
"chainId": "0xa",
|
||||
"displayName": "Optimism",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://mainnet.optimism.io",
|
||||
"chainCurrencyName": "Ether",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://optimism.blswallet.org",
|
||||
"networkKey": "optimism",
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
"currencyConversion": {
|
||||
"api": "https://min-api.cryptocompare.com/data/price",
|
||||
"apiKey": "<Add your api key>",
|
||||
"pollInterval": 30000
|
||||
}
|
||||
}
|
||||
91
extension/config.release.json
Normal file
91
extension/config.release.json
Normal file
@@ -0,0 +1,91 @@
|
||||
{
|
||||
"defaultNetwork": "arbitrum-goerli",
|
||||
"builtinNetworks": {
|
||||
"mainnet": {
|
||||
"blockExplorerUrl": "http://etherscan.io/",
|
||||
"chainId": "0x1",
|
||||
"displayName": "Mainnet",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://mainnet.infura.io/v3/d0f317090d6645b6b494ddc6f1cce5ad",
|
||||
"chainCurrencyName": "Ethereum",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://mainnet.blswallet.org",
|
||||
"networkKey": "mainnet",
|
||||
"hidden": true
|
||||
},
|
||||
"local": {
|
||||
"blockExplorerUrl": "N/A",
|
||||
"chainId": "0x7a69",
|
||||
"displayName": "Local Network",
|
||||
"logo": "",
|
||||
"rpcTarget": "http://127.0.0.1:8545",
|
||||
"chainCurrencyName": "Ethereum",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "http://localhost:3000",
|
||||
"networkKey": "local"
|
||||
},
|
||||
"arbitrum-testnet": {
|
||||
"blockExplorerUrl": "https://rinkeby-explorer.arbitrum.io",
|
||||
"chainId": "0x66eeb",
|
||||
"displayName": "Arbitrum Test Network",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://rinkeby.arbitrum.io/rpc",
|
||||
"chainCurrencyName": "Arbitrum Ether",
|
||||
"chainCurrency": "ARETH",
|
||||
"aggregatorUrl": "https://arbitrum-testnet.blswallet.org",
|
||||
"networkKey": "arbitrum-testnet"
|
||||
},
|
||||
"arbitrum-goerli": {
|
||||
"blockExplorerUrl": "https://goerli-rollup-explorer.arbitrum.io",
|
||||
"chainId": "0x66EED",
|
||||
"displayName": "Arbitrum Goerli",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://goerli-rollup.arbitrum.io/rpc",
|
||||
"chainCurrencyName": "Ethereum",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://arbitrum-goerli.blswallet.org",
|
||||
"networkKey": "arbitrum-goerli"
|
||||
},
|
||||
"arbitrum": {
|
||||
"blockExplorerUrl": "https://explorer.arbitrum.io",
|
||||
"chainId": "0xa4b1",
|
||||
"displayName": "Arbitrum One",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://arb1.arbitrum.io/rpc",
|
||||
"chainCurrencyName": "Ether",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://arbitrum.blswallet.org",
|
||||
"networkKey": "arbitrum",
|
||||
"hidden": true
|
||||
},
|
||||
"optimism-kovan": {
|
||||
"blockExplorerUrl": "https://kovan-optimistic.etherscan.io",
|
||||
"chainId": "0x45",
|
||||
"displayName": "Optimism Test Network",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://kovan.optimism.io",
|
||||
"chainCurrencyName": "Optimistic Kovan Ether",
|
||||
"chainCurrency": "KOR",
|
||||
"aggregatorUrl": "https://optimism-kovan.blswallet.org",
|
||||
"networkKey": "optimism-kovan",
|
||||
"hidden": true
|
||||
},
|
||||
"optimism": {
|
||||
"blockExplorerUrl": "https://optimistic.etherscan.io",
|
||||
"chainId": "0xa",
|
||||
"displayName": "Optimism",
|
||||
"logo": "",
|
||||
"rpcTarget": "https://mainnet.optimism.io",
|
||||
"chainCurrencyName": "Ether",
|
||||
"chainCurrency": "ETH",
|
||||
"aggregatorUrl": "https://optimism.blswallet.org",
|
||||
"networkKey": "optimism",
|
||||
"hidden": true
|
||||
}
|
||||
},
|
||||
"currencyConversion": {
|
||||
"api": "https://min-api.cryptocompare.com/data/price",
|
||||
"apiKey": "${CRYPTO_COMPARE_API_KEY}",
|
||||
"pollInterval": 30000
|
||||
}
|
||||
}
|
||||
@@ -11,7 +11,6 @@
|
||||
"url": "https://blswallet.org"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0 <18.0.0",
|
||||
"yarn": ">=1.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -22,8 +21,8 @@
|
||||
"build:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack",
|
||||
"build:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack",
|
||||
"build": "yarn run build:chrome && yarn run build:firefox && yarn run build:opera",
|
||||
"check-ts": "tsc --noEmit",
|
||||
"lint": "eslint . --ext .ts,.tsx",
|
||||
"check-ts": "node buildMultiNetworkConfig.js && tsc --noEmit",
|
||||
"lint": "./scripts/addFileStubsIfNeeded.sh && eslint . --ext .ts,.tsx,.js --max-warnings 0",
|
||||
"lint:fix": "eslint . --ext .ts,.tsx --fix"
|
||||
},
|
||||
"resolutions": {
|
||||
@@ -32,95 +31,89 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@ethereumjs/tx": "^3.5.1",
|
||||
"@toruslabs/openlogin-jrpc": "^1.7.2",
|
||||
"@tanstack/react-table": "^8.2.3",
|
||||
"@types/bs58check": "^2.1.0",
|
||||
"advanced-css-reset": "^1.2.2",
|
||||
"async-mutex": "^0.3.2",
|
||||
"axios": "^0.25.0",
|
||||
"bls-wallet-clients": "0.6.0",
|
||||
"dotenv-webpack": "^7.0.3",
|
||||
"axios": "^0.27.2",
|
||||
"bls-wallet-clients": "0.8.0",
|
||||
"browser-passworder": "^2.0.3",
|
||||
"bs58check": "^2.1.2",
|
||||
"crypto-browserify": "^3.12.0",
|
||||
"emoji-log": "^1.0.2",
|
||||
"end-of-stream": "^1.4.4",
|
||||
"eth-query": "^2.1.2",
|
||||
"eth-rpc-errors": "^4.0.3",
|
||||
"ethers": "5.5.4",
|
||||
"ethers": "5.6.9",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fp-ts": "^2.12.1",
|
||||
"io-ts": "^2.2.16",
|
||||
"is-stream": "^3.0.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"phosphor-react": "^1.4.0",
|
||||
"pump": "^3.0.0",
|
||||
"qrcode.react": "^3.1.0",
|
||||
"react": "^17.0.2",
|
||||
"react-blockies": "^1.4.1",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-modal": "^3.15.1",
|
||||
"react-router-dom": "^6.2.1",
|
||||
"react-table": "^7.7.0",
|
||||
"readable-stream": "^3.6.0",
|
||||
"sass": "^1.44.0",
|
||||
"typed-emitter": "^1.4.0",
|
||||
"typescript": "^4.5.3",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"typed-emitter": "^2.1.0",
|
||||
"typescript": "^4.7.4",
|
||||
"webext-base-css": "^1.3.2",
|
||||
"webextension-polyfill": "^0.8.0",
|
||||
"webextension-polyfill": "^0.9.0",
|
||||
"zxcvbn": "^4.4.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@abhijithvijayan/eslint-config": "^2.6.3",
|
||||
"@abhijithvijayan/eslint-config-airbnb": "^1.0.2",
|
||||
"@abhijithvijayan/tsconfig": "^1.3.0",
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/eslint-parser": "^7.16.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.0",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.16.0",
|
||||
"@babel/plugin-transform-destructuring": "^7.16.0",
|
||||
"@babel/plugin-transform-runtime": "^7.16.4",
|
||||
"@babel/preset-env": "^7.16.4",
|
||||
"@babel/preset-react": "^7.16.0",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@types/axios": "^0.14.0",
|
||||
"@types/end-of-stream": "^1.4.1",
|
||||
"@types/lodash-es": "^4.17.5",
|
||||
"@types/pump": "^1.1.1",
|
||||
"@types/react": "^17.0.37",
|
||||
"@types/react-blockies": "^1.4.1",
|
||||
"@types/react-dom": "^17.0.11",
|
||||
"@types/react-modal": "^3.13.1",
|
||||
"@types/react-router-dom": "^5.3.3",
|
||||
"@types/react-table": "^7.7.9",
|
||||
"@types/readable-stream": "^2.3.12",
|
||||
"@types/webextension-polyfill": "^0.8.2",
|
||||
"@types/webpack": "^4.41.29",
|
||||
"@types/webextension-polyfill": "^0.9.0",
|
||||
"@types/webpack": "^5.28.0",
|
||||
"@types/zxcvbn": "^4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"@typescript-eslint/eslint-plugin": "^5.30.4",
|
||||
"@typescript-eslint/parser": "^5.30.4",
|
||||
"autoprefixer": "^10.4.2",
|
||||
"babel-loader": "^8.2.3",
|
||||
"clean-webpack-plugin": "^3.0.0",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"babel-loader": "^8.2.5",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "^5.2.5",
|
||||
"eslint": "^7.27.0",
|
||||
"eslint-config-prettier": "^6.15.0",
|
||||
"eslint-plugin-import": "^2.23.3",
|
||||
"eslint-plugin-jsx-a11y": "^6.4.1",
|
||||
"css-loader": "^6.7.1",
|
||||
"dotenv-webpack": "^8.0.0",
|
||||
"eslint": "^8.20.0",
|
||||
"eslint-config-airbnb": "^19.0.4",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
"eslint-plugin-jsx-a11y": "^6.6.0",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^3.4.0",
|
||||
"eslint-plugin-react": "^7.23.2",
|
||||
"eslint-plugin-react-hooks": "^4.2.0",
|
||||
"filemanager-webpack-plugin": "^3.1.1",
|
||||
"fork-ts-checker-webpack-plugin": "^6.5.0",
|
||||
"html-webpack-plugin": "^4.5.2",
|
||||
"mini-css-extract-plugin": "^1.6.0",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.6",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"eslint-plugin-react": "^7.30.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"filemanager-webpack-plugin": "^7.0.0",
|
||||
"fork-ts-checker-webpack-plugin": "^7.2.11",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"mini-css-extract-plugin": "^2.6.1",
|
||||
"optimize-css-assets-webpack-plugin": "^6.0.1",
|
||||
"postcss": "^8.4.5",
|
||||
"postcss-loader": "^4.3.0",
|
||||
"postcss-loader": "^7.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"resolve-url-loader": "^3.1.3",
|
||||
"sass-loader": "^10.2.0",
|
||||
"resolve-url-loader": "^5.0.0",
|
||||
"sass-loader": "^13.0.1",
|
||||
"tailwindcss": "^3.0.13",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"webpack": "^4.46.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-extension-reloader": "^1.1.4",
|
||||
"wext-manifest-loader": "^2.3.0",
|
||||
"terser-webpack-plugin": "^5.3.3",
|
||||
"webpack": "~5.73.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-ext-reloader": "^1.1.9",
|
||||
"wext-manifest-loader": "^2.4.1",
|
||||
"wext-manifest-webpack-plugin": "^1.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
15
extension/scripts/addFileStubsIfNeeded.sh
Executable file
15
extension/scripts/addFileStubsIfNeeded.sh
Executable file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
PROJECT_DIR="$SCRIPT_DIR/.."
|
||||
|
||||
if [ ! -f "$PROJECT_DIR/config.json" ]; then
|
||||
echo {} >"$PROJECT_DIR/config.json"
|
||||
fi
|
||||
|
||||
if [ ! -f "$PROJECT_DIR/build/multiNetworkConfig.json" ]; then
|
||||
mkdir -p "$PROJECT_DIR/build"
|
||||
echo {} >"$PROJECT_DIR/build/multiNetworkConfig.json"
|
||||
fi
|
||||
35
extension/source/Config.ts
Normal file
35
extension/source/Config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import * as io from 'io-ts';
|
||||
|
||||
import configJson from '../config.json';
|
||||
import { ProviderConfig } from './background/ProviderConfig';
|
||||
import assertType from './cells/assertType';
|
||||
import optional from './types/optional';
|
||||
|
||||
const Config = io.type({
|
||||
defaultNetwork: io.string,
|
||||
currencyConversion: io.type({
|
||||
api: io.string,
|
||||
apiKey: io.string,
|
||||
|
||||
// Note: We can afford to poll relatively frequently because we only fetch
|
||||
// currency information when we actually need it, via the magic of cells.
|
||||
// TODO: Enable even more aggressive polling intervals by tying
|
||||
// `LongPollingCell`s to user activity (mouse movement, etc). This would
|
||||
// require some visible indication that the value is not being updated
|
||||
// though (like a grey filter) so that if you keep the window open on the
|
||||
// side of your screen you can get an indication that the value isn't
|
||||
// being kept up to date.
|
||||
pollInterval: io.number,
|
||||
}),
|
||||
builtinNetworks: io.record(io.string, optional(ProviderConfig)),
|
||||
});
|
||||
|
||||
type Config = io.TypeOf<typeof Config>;
|
||||
|
||||
export function loadConfig(): Config {
|
||||
assertType(configJson, Config);
|
||||
|
||||
return configJson;
|
||||
}
|
||||
|
||||
export default Config;
|
||||
@@ -1,67 +1,209 @@
|
||||
import { ethers } from 'ethers';
|
||||
import { FunctionComponent, useEffect, useMemo, useState } from 'react';
|
||||
import { FunctionComponent, useState } from 'react';
|
||||
import { runtime } from 'webextension-polyfill';
|
||||
import TaskQueue from '../common/TaskQueue';
|
||||
|
||||
// components, styles and UI
|
||||
import { Check, X, CaretLeft, CaretRight } from 'phosphor-react';
|
||||
import { ethers } from 'ethers';
|
||||
import Button from '../components/Button';
|
||||
import CompactQuillHeading from '../components/CompactQuillHeading';
|
||||
import { DEFAULT_CHAIN_ID_HEX } from '../env';
|
||||
import { useInputDecode } from '../hooks/useInputDecode';
|
||||
import formatCompactAddress from '../Popup/helpers/formatCompactAddress';
|
||||
import {
|
||||
PromptMessage,
|
||||
SendTransactionParams,
|
||||
TransactionStatus,
|
||||
} from '../types/Rpc';
|
||||
import TransactionCard from './TransactionCard';
|
||||
import onAction from '../helpers/onAction';
|
||||
import { useQuill } from '../QuillContext';
|
||||
import useCell from '../cells/useCell';
|
||||
import Loading from '../components/Loading';
|
||||
import CurrencyDisplay from '../components/CurrencyDisplay';
|
||||
import ChainCurrency from '../components/ChainCurrency';
|
||||
import PreferredCurrency from '../components/PreferredCurrency';
|
||||
|
||||
const Confirm: FunctionComponent = () => {
|
||||
const [id, setId] = useState<string>();
|
||||
const [to, setTo] = useState<string>('0x');
|
||||
const [value, setValue] = useState<string>('0');
|
||||
const [data, setData] = useState<string>('0x');
|
||||
const quill = useQuill();
|
||||
|
||||
// TODO (merge-ok) update component to work across multiple chains/networks
|
||||
const chainId = DEFAULT_CHAIN_ID_HEX;
|
||||
const { loading, method } = useInputDecode(data, to, chainId);
|
||||
const id = new URL(window.location.href).searchParams.get('id');
|
||||
const transactions = useCell(quill.cells.transactions);
|
||||
|
||||
const cleanupTasks = useMemo(() => new TaskQueue(), []);
|
||||
const [current, setCurrent] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
const params = new URL(window.location.href).searchParams;
|
||||
setId(params.get('id') || '0');
|
||||
setTo(params.get('to') || '0x');
|
||||
setValue(params.get('value') || '0');
|
||||
setData(params.get('data') || '0x');
|
||||
if (transactions === undefined) {
|
||||
return <Loading />;
|
||||
}
|
||||
|
||||
return cleanupTasks.run();
|
||||
}, [cleanupTasks]);
|
||||
const tx = transactions.outgoing.find((t) => t.id === id);
|
||||
|
||||
const respondTx = (result: string) => {
|
||||
if (tx === undefined) {
|
||||
return <>Error: Tx not found</>;
|
||||
}
|
||||
|
||||
const respondTx = (result: PromptMessage['result']) => {
|
||||
runtime.sendMessage(undefined, { id, result });
|
||||
};
|
||||
|
||||
const nextTx = () => {
|
||||
setCurrent((current + 1) % tx.actions.length);
|
||||
};
|
||||
const prevTx = () => {
|
||||
setCurrent((current - 1) % tx.actions.length);
|
||||
};
|
||||
|
||||
const calculateTotal = (allActions: SendTransactionParams[]) => {
|
||||
const total = allActions.reduce(
|
||||
(acc, cur) => acc.add(ethers.BigNumber.from(cur.value)),
|
||||
ethers.BigNumber.from(0),
|
||||
);
|
||||
return ethers.utils.formatEther(total);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="confirm">
|
||||
<div className="section">
|
||||
<CompactQuillHeading />
|
||||
<div className="flex flex-col justify-between h-screen bg-grey-200">
|
||||
<div className="p-4 flex justify-between text-white bg-blue-700">
|
||||
Transaction request
|
||||
</div>
|
||||
<div className="section prompt">
|
||||
{loading ? (
|
||||
'loading...'
|
||||
) : (
|
||||
<>
|
||||
<div>{method}</div>
|
||||
<div>to: {formatCompactAddress(to)}</div>
|
||||
<div>value: {ethers.utils.formatEther(value)} ETH</div>
|
||||
<div>
|
||||
data:
|
||||
<div className="data">{data}</div>
|
||||
<div className="flex-grow p-4">
|
||||
<div className="">
|
||||
{/* site info */}
|
||||
<div className="flex gap-4">
|
||||
<div className="h-10 w-10 bg-grey-400 rounded-full" />
|
||||
<div className="leading-5">
|
||||
<div className="">AppName</div>
|
||||
<div className="text-blue-400">https://app-url.com/</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">AppName is making requests to your wallet</div>
|
||||
|
||||
{tx.actions.length > 1 && (
|
||||
<div className="mt-4 flex justify-end text-body self-center gap-3">
|
||||
{current + 1} of {tx.actions?.length}
|
||||
<div
|
||||
className={[
|
||||
'bg-grey-400',
|
||||
'rounded-md',
|
||||
'p-1',
|
||||
'hover:bg-grey-500',
|
||||
'cursor-pointer',
|
||||
].join(' ')}
|
||||
{...onAction(prevTx)}
|
||||
>
|
||||
<CaretLeft size={20} className="self-center" />
|
||||
</div>
|
||||
<div
|
||||
className={[
|
||||
'bg-grey-400',
|
||||
'rounded-md',
|
||||
'p-1',
|
||||
'hover:bg-grey-500',
|
||||
'cursor-pointer',
|
||||
].join(' ')}
|
||||
{...onAction(nextTx)}
|
||||
>
|
||||
<CaretRight size={20} className="self-center" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col">
|
||||
<div className="mt-4">
|
||||
{tx.actions[current] && (
|
||||
<TransactionCard {...tx.actions[current]} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={[
|
||||
'mt-4',
|
||||
'p-4',
|
||||
'bg-grey-300',
|
||||
'rounded-md',
|
||||
'grid',
|
||||
'gap-3',
|
||||
].join(' ')}
|
||||
style={{
|
||||
gridTemplateColumns: '1fr auto auto 0.75rem auto auto',
|
||||
gridTemplateAreas: `
|
||||
"a1 b1 c1 . d1 e1"
|
||||
"a2 b2 c2 . d2 e2"
|
||||
"a3 a3 a3 a3 a3 a3"
|
||||
"a4 b4 c4 . d4 e4"
|
||||
`,
|
||||
}}
|
||||
>
|
||||
<div style={{ gridArea: 'a1' }}>Value</div>
|
||||
<div style={{ gridArea: 'b1' }} className="font-bold text-right">
|
||||
{calculateTotal(tx.actions)}
|
||||
</div>
|
||||
<div style={{ gridArea: 'c1' }}>
|
||||
<ChainCurrency />
|
||||
</div>
|
||||
<div style={{ gridArea: 'd1' }} className="font-bold text-right">
|
||||
<CurrencyDisplay
|
||||
chainValue={Number(calculateTotal(tx.actions))}
|
||||
includeLabel={false}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ gridArea: 'e1' }}>
|
||||
<PreferredCurrency />
|
||||
</div>
|
||||
|
||||
<Button className="btn-primary" onPress={() => respondTx('Yes')}>
|
||||
Confirm
|
||||
</Button>
|
||||
<Button className="btn-secondary" onPress={() => respondTx('No')}>
|
||||
Reject
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
<div style={{ gridArea: 'a2' }}>Fee</div>
|
||||
<div style={{ gridArea: 'b2' }} className="font-bold text-right">
|
||||
0
|
||||
</div>
|
||||
<div style={{ gridArea: 'c2' }}>
|
||||
<ChainCurrency />
|
||||
</div>
|
||||
<div style={{ gridArea: 'd2' }} className="font-bold text-right">
|
||||
<CurrencyDisplay chainValue={0} includeLabel={false} />
|
||||
</div>
|
||||
<div style={{ gridArea: 'e2' }}>
|
||||
<PreferredCurrency />
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{ gridArea: 'a3' }}
|
||||
className="border-b border-grey-500"
|
||||
/>
|
||||
|
||||
<div style={{ gridArea: 'a4' }}>Total</div>
|
||||
<div style={{ gridArea: 'b4' }} className="font-bold text-right">
|
||||
{calculateTotal(tx.actions)}
|
||||
</div>
|
||||
<div style={{ gridArea: 'c4' }}>
|
||||
<ChainCurrency />
|
||||
</div>
|
||||
<div style={{ gridArea: 'd4' }} className="font-bold text-right">
|
||||
<CurrencyDisplay
|
||||
chainValue={Number(calculateTotal(tx.actions))}
|
||||
includeLabel={false}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ gridArea: 'e4' }}>
|
||||
<PreferredCurrency />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex bg-white p-4 justify-between">
|
||||
<Button
|
||||
className="btn-secondary"
|
||||
onPress={() => respondTx(TransactionStatus.REJECTED)}
|
||||
>
|
||||
<div className="flex justify-between gap-3">
|
||||
Reject All <X size={20} className="self-center" />
|
||||
</div>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
className="btn-primary"
|
||||
onPress={() => respondTx(TransactionStatus.APPROVED)}
|
||||
>
|
||||
<div className="flex justify-between gap-3">
|
||||
Confirm All <Check size={20} className="self-center" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
79
extension/source/Confirm/TransactionCard.tsx
Normal file
79
extension/source/Confirm/TransactionCard.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React from 'react';
|
||||
import Blockies from 'react-blockies';
|
||||
import { ArrowRight } from 'phosphor-react';
|
||||
import { ethers } from 'ethers';
|
||||
import { SendTransactionParams } from '../types/Rpc';
|
||||
import formatCompactAddress from '../helpers/formatCompactAddress';
|
||||
import { useInputDecode } from '../hooks/useInputDecode';
|
||||
|
||||
const TransactionCard: React.FC<SendTransactionParams> = ({
|
||||
data,
|
||||
from,
|
||||
to,
|
||||
value,
|
||||
gas,
|
||||
gasPrice,
|
||||
}) => {
|
||||
const { loading, method, args } = useInputDecode(data || '0x');
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-md p-4 border border-blue-400">
|
||||
<div className="flex gap-4 w-full justify-between">
|
||||
<Blockies seed={from} className="rounded-md" size={5} scale={8} />
|
||||
<div className="flex justify-between flex-grow align-middle">
|
||||
<div className="leading-snug">
|
||||
<div>from</div>
|
||||
<div className="font-bold">{formatCompactAddress(from)}</div>
|
||||
</div>
|
||||
<ArrowRight size={20} alignmentBaseline="central" />
|
||||
<div className="leading-snug">
|
||||
<div>to</div>
|
||||
<div className="font-bold">{formatCompactAddress(to)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4">
|
||||
<div className="break-all">
|
||||
details:{' '}
|
||||
<span className="font-bold">{loading ? 'loading...' : method}</span>
|
||||
<div className="text-[9pt] mt-2 font-mono">
|
||||
{loading
|
||||
? 'loading params...'
|
||||
: // prettier-ignore
|
||||
args.map((arg, i) => (
|
||||
<div key={arg}>
|
||||
{i + 1}. {arg.toString()}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-6 gap-3">
|
||||
<div className="w-60 border-r border-grey-400">
|
||||
<div>Value</div>
|
||||
<div className="break-all text-[9.5pt] font-bold">
|
||||
{ethers.utils.formatEther(value || '0x0')} ETH
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-60 border-r border-grey-400">
|
||||
<div>Gas Price</div>
|
||||
<div className="break-all text-[9.5pt] font-bold">
|
||||
{ethers.utils.formatUnits(gasPrice || '0x0', 'gwei')} gwei
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-60">
|
||||
<div>Gas usage</div>
|
||||
<div className="break-all text-[9.5pt] font-bold">
|
||||
{ethers.utils.formatUnits(gas || '0x0', 'wei')} wei
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TransactionCard;
|
||||
@@ -1,7 +1,22 @@
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import Confirm from './Confirm';
|
||||
|
||||
import '../contentScript';
|
||||
import '../styles/index.scss';
|
||||
import './styles.scss';
|
||||
|
||||
ReactDOM.render(<Confirm />, document.getElementById('confirm-root'));
|
||||
import ReactDOM from 'react-dom';
|
||||
import Browser from 'webextension-polyfill';
|
||||
|
||||
import QuillEthereumProvider from '../QuillEthereumProvider';
|
||||
import Confirm from './Confirm';
|
||||
import { QuillContextProvider } from '../QuillContext';
|
||||
|
||||
window.ethereum = new QuillEthereumProvider(true);
|
||||
|
||||
window.debug ??= {};
|
||||
window.debug.Browser = Browser;
|
||||
|
||||
ReactDOM.render(
|
||||
<QuillContextProvider>
|
||||
<Confirm />
|
||||
</QuillContextProvider>,
|
||||
document.getElementById('confirm-root'),
|
||||
);
|
||||
|
||||
@@ -1,116 +0,0 @@
|
||||
import {
|
||||
BasePostMessageStream,
|
||||
ObjectMultiplex,
|
||||
Stream,
|
||||
} from '@toruslabs/openlogin-jrpc';
|
||||
import pump from 'pump';
|
||||
import { runtime } from 'webextension-polyfill';
|
||||
import { CONTENT_SCRIPT, INPAGE, PROVIDER } from '../common/constants';
|
||||
import PortDuplexStream from '../common/PortStream';
|
||||
|
||||
function canInjectScript() {
|
||||
if (window.document.doctype?.name !== 'html') return false;
|
||||
if (window.location.pathname.endsWith('.pdf')) return false;
|
||||
if (document.documentElement.nodeName.toLowerCase() !== 'html') return false;
|
||||
|
||||
// Can add other checks later
|
||||
return true;
|
||||
}
|
||||
|
||||
function injectScript() {
|
||||
try {
|
||||
const container = document.head || document.documentElement;
|
||||
const pageContentScriptTag = document.createElement('script');
|
||||
pageContentScriptTag.src = runtime.getURL('js/pageContentScript.bundle.js');
|
||||
container.insertBefore(pageContentScriptTag, container.children[0]);
|
||||
// Can remove after script injection
|
||||
container.removeChild(pageContentScriptTag);
|
||||
} catch (error) {
|
||||
console.error(error, 'Quill script injection failed');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up two-way communication streams between the
|
||||
* browser extension and local per-page browser context.
|
||||
*
|
||||
*/
|
||||
async function setupStreams() {
|
||||
// the transport-specific streams for communication between inpage and background
|
||||
const pageStream = new BasePostMessageStream({
|
||||
name: CONTENT_SCRIPT,
|
||||
target: INPAGE,
|
||||
});
|
||||
const extensionPort = runtime.connect(undefined, {
|
||||
name: CONTENT_SCRIPT,
|
||||
});
|
||||
const extensionStream = new PortDuplexStream(extensionPort);
|
||||
|
||||
// create and connect channel muxers
|
||||
// so we can handle the channels individually
|
||||
const pageMux = new ObjectMultiplex();
|
||||
const extensionMux = new ObjectMultiplex();
|
||||
|
||||
pump(
|
||||
pageMux as unknown as Stream,
|
||||
pageStream as unknown as Stream,
|
||||
pageMux as unknown as Stream,
|
||||
(err) => logStreamDisconnectWarning('Quill Inpage Multiplex', err),
|
||||
);
|
||||
pump(
|
||||
extensionMux as unknown as Stream,
|
||||
extensionStream as unknown as Stream,
|
||||
extensionMux as unknown as Stream,
|
||||
(err) => {
|
||||
logStreamDisconnectWarning('Quill Background Multiplex', err);
|
||||
window.postMessage(
|
||||
{
|
||||
target: INPAGE, // the post-message-stream "target"
|
||||
data: {
|
||||
// this object gets passed to obj-multiplex
|
||||
name: PROVIDER, // the obj-multiplex channel name
|
||||
data: {
|
||||
jsonrpc: '2.0',
|
||||
method: 'QUILL_STREAM_FAILURE',
|
||||
},
|
||||
},
|
||||
},
|
||||
window.location.origin,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// forward communication across inpage-background for these channels only
|
||||
forwardTrafficBetweenMuxes(PROVIDER, pageMux, extensionMux);
|
||||
}
|
||||
|
||||
function forwardTrafficBetweenMuxes(
|
||||
channelName: string,
|
||||
muxA: ObjectMultiplex,
|
||||
muxB: ObjectMultiplex,
|
||||
) {
|
||||
const channelA = muxA.createStream(channelName);
|
||||
const channelB = muxB.createStream(channelName);
|
||||
pump(
|
||||
channelA as unknown as Stream,
|
||||
channelB as unknown as Stream,
|
||||
channelA as unknown as Stream,
|
||||
(error) =>
|
||||
console.debug(
|
||||
`Quill: Muxed traffic for channel "${channelName}" failed.`,
|
||||
error,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function logStreamDisconnectWarning(remoteLabel: string, error: unknown) {
|
||||
console.debug(
|
||||
`Quill: Content script lost connection to "${remoteLabel}".`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
if (canInjectScript()) {
|
||||
injectScript();
|
||||
setupStreams();
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
import { Mutex } from 'async-mutex';
|
||||
import EthQuery from '../rpcHelpers/EthQuery';
|
||||
import BaseController from '../BaseController';
|
||||
|
||||
import PollingBlockTracker from '../Block/PollingBlockTracker';
|
||||
import { SafeEventEmitterProvider } from '../Network/INetworkController';
|
||||
import NetworkController from '../Network/NetworkController';
|
||||
import { PreferencesState } from '../Preferences/IPreferencesController';
|
||||
import {
|
||||
AccountInformation,
|
||||
AccountTrackerConfig,
|
||||
AccountTrackerState,
|
||||
IAccountTrackerController,
|
||||
} from './IAccountTrackerController';
|
||||
|
||||
/**
|
||||
* Tracks accounts based on blocks.
|
||||
* If block tracker provides latest block, we query accounts from it.
|
||||
* Preferences state changes also retrigger accounts update.
|
||||
* Network state changes also retrigger accounts update.
|
||||
*/
|
||||
class AccountTrackerController
|
||||
extends BaseController<AccountTrackerConfig, AccountTrackerState>
|
||||
implements
|
||||
IAccountTrackerController<AccountTrackerConfig, AccountTrackerState>
|
||||
{
|
||||
private provider: SafeEventEmitterProvider;
|
||||
|
||||
private blockTracker: PollingBlockTracker;
|
||||
|
||||
private mutex = new Mutex();
|
||||
|
||||
private ethQuery: EthQuery;
|
||||
|
||||
private getIdentities: () => PreferencesState['identities'];
|
||||
|
||||
private getCurrentChainId: NetworkController['getNetworkIdentifier'];
|
||||
|
||||
constructor({
|
||||
config,
|
||||
state,
|
||||
provider,
|
||||
blockTracker,
|
||||
getCurrentChainId,
|
||||
getIdentities,
|
||||
onPreferencesStateChange,
|
||||
}: {
|
||||
config: AccountTrackerConfig;
|
||||
state: Partial<AccountTrackerState>;
|
||||
provider: SafeEventEmitterProvider;
|
||||
blockTracker: PollingBlockTracker;
|
||||
getCurrentChainId: NetworkController['getNetworkIdentifier'];
|
||||
getIdentities: () => PreferencesState['identities'];
|
||||
onPreferencesStateChange: (
|
||||
listener: (preferencesState: PreferencesState) => void,
|
||||
) => void;
|
||||
}) {
|
||||
super({ config, state });
|
||||
this.defaultState = {
|
||||
accounts: {},
|
||||
};
|
||||
this.defaultConfig = {
|
||||
_currentBlock: '',
|
||||
};
|
||||
this.initialize();
|
||||
this.provider = provider;
|
||||
this.blockTracker = blockTracker;
|
||||
this.ethQuery = new EthQuery(provider);
|
||||
|
||||
// This starts the blockTracker internal tracking
|
||||
this.blockTracker.on('latest', (block: string) => {
|
||||
this.configure({ _currentBlock: block });
|
||||
this.refresh();
|
||||
});
|
||||
|
||||
this.getIdentities = getIdentities;
|
||||
this.getCurrentChainId = getCurrentChainId;
|
||||
onPreferencesStateChange(() => {
|
||||
this.syncAccounts();
|
||||
this.refresh();
|
||||
});
|
||||
console.log(this.provider, 'eth provider in account tracker');
|
||||
}
|
||||
|
||||
syncAccounts(): void {
|
||||
const { accounts } = this.state;
|
||||
const addresses = Object.keys(this.getIdentities());
|
||||
const existing = Object.keys(accounts);
|
||||
const newAddresses = addresses.filter(
|
||||
(address) => existing.indexOf(address) === -1,
|
||||
);
|
||||
const oldAddresses = existing.filter(
|
||||
(address) => addresses.indexOf(address) === -1,
|
||||
);
|
||||
newAddresses.forEach((address) => {
|
||||
accounts[address] = { balance: '0x0' };
|
||||
});
|
||||
oldAddresses.forEach((address) => {
|
||||
delete accounts[address];
|
||||
});
|
||||
this.update({ accounts: { ...accounts } });
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
const releaseLock = await this.mutex.acquire();
|
||||
try {
|
||||
const { accounts } = this.state;
|
||||
const currentBlock = this.config._currentBlock;
|
||||
if (!currentBlock) return;
|
||||
const addresses = Object.keys(accounts);
|
||||
await Promise.all(
|
||||
addresses.map((x) => this._updateAccount(x, currentBlock)),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateAccount(
|
||||
address: string,
|
||||
currentBlock: string,
|
||||
): Promise<void> {
|
||||
const currentChainId = this.getCurrentChainId();
|
||||
if (currentChainId === 'loading') {
|
||||
return;
|
||||
}
|
||||
const balance = await this.ethQuery.request({
|
||||
method: 'eth_getBalance',
|
||||
params: [address, currentBlock],
|
||||
});
|
||||
const result: AccountInformation = {
|
||||
balance: balance as string,
|
||||
};
|
||||
// update accounts state
|
||||
const { accounts: newAccounts } = this.state;
|
||||
// only populate if the entry is still present
|
||||
if (!newAccounts[address]) return;
|
||||
newAccounts[address] = result;
|
||||
this.update({ accounts: newAccounts });
|
||||
}
|
||||
}
|
||||
|
||||
export default AccountTrackerController;
|
||||
@@ -1,37 +0,0 @@
|
||||
import { BaseConfig, BaseState, IController } from '../interfaces';
|
||||
|
||||
export interface IAccountTrackerController<C, S> extends IController<C, S> {
|
||||
/**
|
||||
* Syncs accounts from preferences controller
|
||||
*/
|
||||
syncAccounts(): void;
|
||||
|
||||
/**
|
||||
* Refreshes the balances of all accounts
|
||||
*/
|
||||
refresh(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface AccountTrackerConfig extends BaseConfig {
|
||||
_currentBlock?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account information object
|
||||
*/
|
||||
export interface AccountInformation {
|
||||
/**
|
||||
* Hex string of an account balance in wei (base unit)
|
||||
*/
|
||||
balance: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Account tracker controller state
|
||||
*/
|
||||
export interface AccountTrackerState extends BaseState {
|
||||
/**
|
||||
* Map of addresses to account information
|
||||
*/
|
||||
accounts: { [address: string]: AccountInformation }; // address here is public address
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import { SafeEventEmitter } from '@toruslabs/openlogin-jrpc';
|
||||
|
||||
import { BaseConfig, BaseState, IController } from './interfaces';
|
||||
|
||||
/**
|
||||
* Controller class that provides configuration, state management, and subscriptions
|
||||
*/
|
||||
class BaseController<C extends BaseConfig, S extends BaseState>
|
||||
extends SafeEventEmitter
|
||||
implements IController<C, S>
|
||||
{
|
||||
/**
|
||||
* Default options used to configure this controller
|
||||
*/
|
||||
defaultConfig: C = {} as C;
|
||||
|
||||
/**
|
||||
* Default state set on this controller
|
||||
*/
|
||||
defaultState: S = {} as S;
|
||||
|
||||
/**
|
||||
* Determines if listeners are notified of state changes
|
||||
*/
|
||||
disabled = false;
|
||||
|
||||
/**
|
||||
* Name of this controller used during composition
|
||||
*/
|
||||
name = 'BaseController';
|
||||
|
||||
private readonly initialConfig: C;
|
||||
|
||||
private readonly initialState: S;
|
||||
|
||||
private internalConfig: C = this.defaultConfig;
|
||||
|
||||
private internalState: S = this.defaultState;
|
||||
|
||||
/**
|
||||
* Creates a BaseController instance. Both initial state and initial
|
||||
* configuration options are merged with defaults upon initialization.
|
||||
*
|
||||
* @param config - Initial options used to configure this controller
|
||||
* @param state - Initial state to set on this controller
|
||||
*/
|
||||
constructor({
|
||||
config = {} as C,
|
||||
state = {} as S,
|
||||
}: {
|
||||
config?: Partial<C>;
|
||||
state?: Partial<S>;
|
||||
}) {
|
||||
super();
|
||||
// Use assign since generics can't be spread: https://git.io/vpRhY
|
||||
this.initialState = state as S;
|
||||
this.initialConfig = config as C;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current controller configuration options
|
||||
*
|
||||
* @returns - Current configuration
|
||||
*/
|
||||
get config(): C {
|
||||
return this.internalConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves current controller state
|
||||
*
|
||||
* @returns - Current state
|
||||
*/
|
||||
get state(): S {
|
||||
return this.internalState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates controller configuration
|
||||
*
|
||||
* @param config - New configuration options
|
||||
* @param overwrite - Overwrite config instead of merging
|
||||
* @param fullUpdate - Boolean that defines if the update is partial or not
|
||||
*/
|
||||
configure(config: Partial<C>, overwrite = false, fullUpdate = true): void {
|
||||
if (fullUpdate) {
|
||||
this.internalConfig = overwrite
|
||||
? (config as C)
|
||||
: Object.assign(this.internalConfig, config);
|
||||
|
||||
Object.keys(this.internalConfig).forEach((key) => {
|
||||
if (typeof (this.internalConfig as any)[key] !== 'undefined') {
|
||||
(this as any)[key as string] = (this.internalConfig as any)[key];
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Object.keys(config).forEach((key) => {
|
||||
if (typeof (this.internalConfig as any)[key] !== 'undefined') {
|
||||
(this.internalConfig as any)[key] = (config as any)[key];
|
||||
(this as any)[key as string] = (config as any)[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates controller state
|
||||
*
|
||||
* @param state - New state
|
||||
* @param overwrite - Overwrite state instead of merging
|
||||
*/
|
||||
update(state: Partial<S>, overwrite = false): void {
|
||||
this.internalState = overwrite
|
||||
? { ...(state as S) }
|
||||
: { ...this.internalState, ...state };
|
||||
this.emit('store', this.internalState);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enables the controller. This sets each config option as a member
|
||||
* variable on this instance and triggers any defined setters. This
|
||||
* also sets initial state and triggers any listeners.
|
||||
*
|
||||
* @returns - This controller instance
|
||||
*/
|
||||
protected initialize(): this {
|
||||
this.internalState = this.defaultState;
|
||||
this.internalConfig = this.defaultConfig;
|
||||
this.configure(this.initialConfig);
|
||||
this.update(this.initialState);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
export default BaseController;
|
||||
@@ -1,191 +0,0 @@
|
||||
import BaseController from '../BaseController';
|
||||
import {
|
||||
BaseBlockTrackerConfig,
|
||||
BaseBlockTrackerState,
|
||||
} from './IBlockTrackerController';
|
||||
|
||||
const sec = 1000;
|
||||
|
||||
const calculateSum = (accumulator: number, currentValue: number) =>
|
||||
accumulator + currentValue;
|
||||
const blockTrackerEvents: string[] = ['sync', 'latest'];
|
||||
|
||||
export class BaseBlockTracker<
|
||||
C extends BaseBlockTrackerConfig,
|
||||
S extends BaseBlockTrackerState,
|
||||
> extends BaseController<C, S> {
|
||||
name = 'BaseBlockTracker';
|
||||
|
||||
private _blockResetTimeout?: number;
|
||||
|
||||
constructor({
|
||||
config = {},
|
||||
state = {},
|
||||
}: {
|
||||
config: Partial<C>;
|
||||
state: Partial<S>;
|
||||
}) {
|
||||
super({ config, state });
|
||||
|
||||
// config
|
||||
|
||||
this.defaultState = {
|
||||
_currentBlock: '',
|
||||
_isRunning: false,
|
||||
} as S;
|
||||
|
||||
this.defaultConfig = {
|
||||
blockResetDuration: 20 * sec,
|
||||
} as C;
|
||||
|
||||
this.initialize();
|
||||
|
||||
// bind functions for internal use
|
||||
this._onNewListener = this._onNewListener.bind(this);
|
||||
this._onRemoveListener = this._onRemoveListener.bind(this);
|
||||
this._resetCurrentBlock = this._resetCurrentBlock.bind(this);
|
||||
|
||||
// listen for handler changes
|
||||
this._setupInternalEvents();
|
||||
}
|
||||
|
||||
isRunning(): boolean | undefined {
|
||||
return this.state._isRunning;
|
||||
}
|
||||
|
||||
getCurrentBlock(): string | undefined {
|
||||
return this.state._currentBlock;
|
||||
}
|
||||
|
||||
async getLatestBlock(): Promise<string> {
|
||||
// return if available
|
||||
if (this.state._currentBlock) {
|
||||
return this.state._currentBlock;
|
||||
}
|
||||
// wait for a new latest block
|
||||
const latestBlock = await new Promise((resolve: (state: string) => void) =>
|
||||
this.once('latest', (newState: BaseBlockTrackerState) => {
|
||||
if (newState._currentBlock) {
|
||||
resolve(newState._currentBlock);
|
||||
}
|
||||
}),
|
||||
);
|
||||
// return newly set current block
|
||||
return latestBlock;
|
||||
}
|
||||
|
||||
// dont allow module consumer to remove our internal event listeners
|
||||
removeAllListeners(eventName?: string): this {
|
||||
if (eventName) {
|
||||
super.removeAllListeners(eventName);
|
||||
} else {
|
||||
super.removeAllListeners();
|
||||
}
|
||||
// re-add internal events
|
||||
this._setupInternalEvents();
|
||||
// trigger stop check just in case
|
||||
this._onRemoveListener();
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* To be implemented in subclass.
|
||||
*/
|
||||
protected _start(): void {
|
||||
// default behavior is noop
|
||||
}
|
||||
|
||||
/**
|
||||
* To be implemented in subclass.
|
||||
*/
|
||||
protected _end(): void {
|
||||
// default behavior is noop
|
||||
}
|
||||
|
||||
protected _newPotentialLatest(newBlock: string): void {
|
||||
const currentBlock = this.state._currentBlock;
|
||||
// only update if blok number is higher
|
||||
if (
|
||||
currentBlock &&
|
||||
Number.parseInt(newBlock, 16) <= Number.parseInt(currentBlock, 16)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._setCurrentBlock(newBlock);
|
||||
}
|
||||
|
||||
private _setupInternalEvents(): void {
|
||||
// first remove listeners for idempotency
|
||||
this.removeListener('newListener', this._onNewListener);
|
||||
this.removeListener('removeListener', this._onRemoveListener);
|
||||
// then add them
|
||||
this.on('removeListener', this._onRemoveListener);
|
||||
this.on('newListener', this._onNewListener);
|
||||
}
|
||||
|
||||
private _onNewListener(): void {
|
||||
this._maybeStart();
|
||||
}
|
||||
|
||||
private _onRemoveListener(): void {
|
||||
// `removeListener` is called *after* the listener is removed
|
||||
if (this._getBlockTrackerEventCount() > 0) {
|
||||
return;
|
||||
}
|
||||
this._maybeEnd();
|
||||
}
|
||||
|
||||
private _maybeStart(): void {
|
||||
if (this.state._isRunning) {
|
||||
return;
|
||||
}
|
||||
this.state._isRunning = true;
|
||||
// cancel setting latest block to stale
|
||||
this._cancelBlockResetTimeout();
|
||||
this._start();
|
||||
}
|
||||
|
||||
private _maybeEnd(): void {
|
||||
if (!this.state._isRunning) {
|
||||
return;
|
||||
}
|
||||
this.state._isRunning = false;
|
||||
this._setupBlockResetTimeout();
|
||||
this._end();
|
||||
}
|
||||
|
||||
private _getBlockTrackerEventCount(): number {
|
||||
return blockTrackerEvents
|
||||
.map((eventName) => this.listenerCount(eventName))
|
||||
.reduce(calculateSum);
|
||||
}
|
||||
|
||||
private _setCurrentBlock(newBlock: string): void {
|
||||
const oldBlock = this.state._currentBlock;
|
||||
this.update({
|
||||
_currentBlock: newBlock,
|
||||
} as S);
|
||||
this.emit('latest', newBlock);
|
||||
this.emit('sync', { oldBlock, newBlock });
|
||||
}
|
||||
|
||||
private _setupBlockResetTimeout(): void {
|
||||
// clear any existing timeout
|
||||
this._cancelBlockResetTimeout();
|
||||
// clear latest block when stale
|
||||
this._blockResetTimeout = window.setTimeout(
|
||||
this._resetCurrentBlock,
|
||||
this.config.blockResetDuration,
|
||||
);
|
||||
}
|
||||
|
||||
private _cancelBlockResetTimeout(): void {
|
||||
if (this._blockResetTimeout) {
|
||||
clearTimeout(this._blockResetTimeout);
|
||||
}
|
||||
}
|
||||
|
||||
private _resetCurrentBlock(): void {
|
||||
this.update({ _currentBlock: '' } as Partial<S>);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { BaseConfig, BaseState } from '../interfaces';
|
||||
import { SafeEventEmitterProvider } from '../Network/INetworkController';
|
||||
|
||||
export interface BaseBlockTrackerConfig extends BaseConfig {
|
||||
blockResetDuration?: number;
|
||||
}
|
||||
|
||||
export interface PollingBlockTrackerConfig extends BaseBlockTrackerConfig {
|
||||
provider: SafeEventEmitterProvider;
|
||||
pollingInterval: number;
|
||||
retryTimeout: number;
|
||||
setSkipCacheFlag: boolean;
|
||||
}
|
||||
|
||||
export interface BaseBlockTrackerState extends BaseState {
|
||||
/**
|
||||
* block number in hex string
|
||||
*/
|
||||
_currentBlock?: string;
|
||||
_isRunning?: boolean;
|
||||
}
|
||||
|
||||
export type PollingBlockTrackerState = BaseBlockTrackerState;
|
||||
@@ -1,98 +0,0 @@
|
||||
import { BaseBlockTracker } from './BaseBlockTracker';
|
||||
import { createRandomId, timeout } from '../utils';
|
||||
import {
|
||||
PollingBlockTrackerConfig,
|
||||
PollingBlockTrackerState,
|
||||
} from './IBlockTrackerController';
|
||||
import { ExtendedJsonRpcRequest } from '../Network/INetworkController';
|
||||
|
||||
const sec = 1000;
|
||||
|
||||
class PollingBlockTracker extends BaseBlockTracker<
|
||||
PollingBlockTrackerConfig,
|
||||
PollingBlockTrackerState
|
||||
> {
|
||||
constructor({
|
||||
config,
|
||||
state = {},
|
||||
}: {
|
||||
config: Partial<PollingBlockTrackerConfig> &
|
||||
Pick<PollingBlockTrackerConfig, 'provider'>;
|
||||
state: Partial<PollingBlockTrackerState>;
|
||||
}) {
|
||||
// parse + validate args
|
||||
if (!config.provider) {
|
||||
throw new Error('PollingBlockTracker - no provider specified.');
|
||||
}
|
||||
super({ config, state });
|
||||
|
||||
// config
|
||||
this.defaultConfig = {
|
||||
provider: config.provider,
|
||||
pollingInterval: 20 * sec,
|
||||
retryTimeout: 2 * sec,
|
||||
setSkipCacheFlag: false,
|
||||
};
|
||||
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
// trigger block polling
|
||||
async checkForLatestBlock(): Promise<string> {
|
||||
await this._updateLatestBlock();
|
||||
return this.getLatestBlock();
|
||||
}
|
||||
|
||||
protected _start(): void {
|
||||
this._synchronize().catch((err) => this.emit('error', err));
|
||||
}
|
||||
|
||||
private async _synchronize(): Promise<void> {
|
||||
while (this.state._isRunning) {
|
||||
try {
|
||||
await this._updateLatestBlock();
|
||||
await timeout(this.config.pollingInterval);
|
||||
} catch (err: unknown) {
|
||||
const newErr = new Error(
|
||||
`PollingBlockTracker - encountered an error while attempting to update latest block:\n${
|
||||
(err as Error).stack
|
||||
}`,
|
||||
);
|
||||
try {
|
||||
this.emit('error', newErr);
|
||||
} catch (emitErr) {
|
||||
console.error(newErr, emitErr);
|
||||
}
|
||||
await timeout(this.config.retryTimeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _updateLatestBlock(): Promise<void> {
|
||||
// fetch + set latest block
|
||||
const latestBlock = await this._fetchLatestBlock();
|
||||
this._newPotentialLatest(latestBlock);
|
||||
}
|
||||
|
||||
private async _fetchLatestBlock(): Promise<string> {
|
||||
try {
|
||||
const req: ExtendedJsonRpcRequest<[]> = {
|
||||
method: 'eth_blockNumber',
|
||||
jsonrpc: '2.0',
|
||||
id: createRandomId(),
|
||||
params: [],
|
||||
};
|
||||
|
||||
const res = await this.config.provider.sendAsync<[], string>(req);
|
||||
return res;
|
||||
} catch (error: unknown) {
|
||||
throw new Error(
|
||||
`PollingBlockTracker - encountered error fetching block:\n${
|
||||
(error as Error).message
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PollingBlockTracker;
|
||||
@@ -1,126 +0,0 @@
|
||||
import CellCollection from '../../cells/CellCollection';
|
||||
import ICell from '../../cells/ICell';
|
||||
import { CRYPTO_COMPARE_API_KEY } from '../../env';
|
||||
import {
|
||||
CurrencyControllerConfig,
|
||||
CurrencyControllerState,
|
||||
} from './ICurrencyController';
|
||||
|
||||
// every ten minutes
|
||||
const POLLING_INTERVAL = 600_000;
|
||||
|
||||
const defaultConfig: CurrencyControllerConfig = {
|
||||
pollInterval: POLLING_INTERVAL,
|
||||
};
|
||||
|
||||
export default class CurrencyController {
|
||||
private conversionInterval: number;
|
||||
public config: CurrencyControllerConfig;
|
||||
public state: ICell<CurrencyControllerState>;
|
||||
|
||||
constructor(
|
||||
config: CurrencyControllerConfig | undefined,
|
||||
storage: CellCollection,
|
||||
) {
|
||||
this.config = config ?? defaultConfig;
|
||||
|
||||
this.state = storage.Cell('CurrencyController', CurrencyControllerState, {
|
||||
currentCurrency: 'usd',
|
||||
conversionRate: 0,
|
||||
conversionDate: 'N/A',
|
||||
nativeCurrency: 'ETH',
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// PUBLIC METHODS
|
||||
//
|
||||
|
||||
public async update(stateUpdates: Partial<CurrencyControllerState>) {
|
||||
await this.state.write({
|
||||
...(await this.state.read()),
|
||||
...stateUpdates,
|
||||
});
|
||||
}
|
||||
|
||||
async updateConversionRate(): Promise<void> {
|
||||
let state: CurrencyControllerState | undefined;
|
||||
|
||||
try {
|
||||
state = await this.state.read();
|
||||
const apiUrl = `${
|
||||
this.config.api
|
||||
}?fsym=${state.nativeCurrency.toUpperCase()}&tsyms=${state.currentCurrency.toUpperCase()}&api_key=${CRYPTO_COMPARE_API_KEY}`;
|
||||
let response: Response;
|
||||
try {
|
||||
response = await fetch(apiUrl);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
error,
|
||||
'CurrencyController - Failed to request currency from cryptocompare',
|
||||
);
|
||||
return;
|
||||
}
|
||||
// parse response
|
||||
let parsedResponse: { [key: string]: number };
|
||||
try {
|
||||
parsedResponse = await response.json();
|
||||
} catch {
|
||||
console.error(
|
||||
new Error(
|
||||
`CurrencyController - Failed to parse response "${response.status}"`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
// set conversion rate
|
||||
// if (nativeCurrency === 'ETH') {
|
||||
// ETH
|
||||
// this.setConversionRate(Number(parsedResponse.bid))
|
||||
// this.setConversionDate(Number(parsedResponse.timestamp))
|
||||
// } else
|
||||
if (parsedResponse[state.currentCurrency.toUpperCase()]) {
|
||||
// ETC
|
||||
this.update({
|
||||
conversionRate: Number(
|
||||
parsedResponse[state.currentCurrency.toUpperCase()],
|
||||
),
|
||||
conversionDate: (Date.now() / 1000).toString(),
|
||||
});
|
||||
} else {
|
||||
this.update({
|
||||
conversionRate: 0,
|
||||
conversionDate: 'N/A',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// reset current conversion rate
|
||||
console.warn(
|
||||
'Quill - Failed to query currency conversion:',
|
||||
state?.nativeCurrency,
|
||||
state?.currentCurrency,
|
||||
error,
|
||||
);
|
||||
|
||||
this.update({
|
||||
conversionRate: 0,
|
||||
conversionDate: 'N/A',
|
||||
});
|
||||
|
||||
// throw error
|
||||
console.error(
|
||||
error,
|
||||
`CurrencyController - Failed to query rate for currency "${state?.currentCurrency}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public scheduleConversionInterval(): void {
|
||||
if (this.conversionInterval) {
|
||||
window.clearInterval(this.conversionInterval);
|
||||
}
|
||||
this.conversionInterval = window.setInterval(() => {
|
||||
this.updateConversionRate();
|
||||
}, this.config.pollInterval);
|
||||
}
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import * as io from 'io-ts';
|
||||
|
||||
import { BaseConfig } from '../interfaces';
|
||||
|
||||
export const CurrencyControllerState = io.type({
|
||||
currentCurrency: io.string,
|
||||
conversionRate: io.number,
|
||||
conversionDate: io.string,
|
||||
nativeCurrency: io.string,
|
||||
});
|
||||
|
||||
export type CurrencyControllerState = io.TypeOf<typeof CurrencyControllerState>;
|
||||
|
||||
export interface CurrencyControllerConfig extends BaseConfig {
|
||||
pollInterval: number;
|
||||
api?: string;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { Bundle, Operation } from 'bls-wallet-clients';
|
||||
import { BaseConfig, BaseState } from '../interfaces';
|
||||
import { SafeEventEmitterProvider } from '../Network/INetworkController';
|
||||
|
||||
export type KeyPair = {
|
||||
/**
|
||||
* Hex string without 0x prefix
|
||||
*/
|
||||
privateKey: string;
|
||||
/**
|
||||
* Address of the deployed contract wallet
|
||||
*/
|
||||
address: string;
|
||||
};
|
||||
|
||||
export interface KeyringControllerConfig extends BaseConfig {
|
||||
provider: SafeEventEmitterProvider;
|
||||
}
|
||||
|
||||
export interface KeyringControllerState extends BaseState {
|
||||
HDPhrase: string;
|
||||
wallets: KeyPair[];
|
||||
chainId: string;
|
||||
}
|
||||
|
||||
export interface IKeyringController {
|
||||
/**
|
||||
* Returns the addresses of all stored key pairs
|
||||
*/
|
||||
getAccounts(): string[];
|
||||
|
||||
/**
|
||||
* Creates a new key pair
|
||||
*/
|
||||
createAccount(): Promise<string>;
|
||||
|
||||
/**
|
||||
* Creates a Deterministic Account based on seed phrase
|
||||
*/
|
||||
createHDAccount(): Promise<string>;
|
||||
|
||||
/**
|
||||
* Imports a key pair
|
||||
* @param privateKey - Hex string without 0x prefix
|
||||
*/
|
||||
importAccount(privateKey: string): Promise<string>;
|
||||
|
||||
/**
|
||||
* Removes a key pair
|
||||
* @param address - Address of the key pair
|
||||
*/
|
||||
removeAccount(address: string): void;
|
||||
|
||||
/**
|
||||
* Signs a transaction of Type T
|
||||
* @param address - account to sign the tx with
|
||||
* @param tx - Transaction to sign
|
||||
*/
|
||||
signTransactions(address: string, tx: Operation): Promise<Bundle>;
|
||||
}
|
||||
@@ -1,146 +0,0 @@
|
||||
import { BlsWalletWrapper, Operation } from 'bls-wallet-clients';
|
||||
import { ethers } from 'ethers';
|
||||
import generateRandomHex from '../../helpers/generateRandomHex';
|
||||
import BaseController from '../BaseController';
|
||||
import {
|
||||
IKeyringController,
|
||||
KeyringControllerConfig,
|
||||
KeyringControllerState,
|
||||
} from './IKeyringController';
|
||||
import { NETWORK_CONFIG } from '../../env';
|
||||
import { getRPCURL } from '../utils';
|
||||
|
||||
export default class KeyringController
|
||||
extends BaseController<KeyringControllerConfig, KeyringControllerState>
|
||||
implements IKeyringController
|
||||
{
|
||||
name = 'KeyringController';
|
||||
|
||||
constructor({
|
||||
config,
|
||||
state,
|
||||
}: {
|
||||
config: Partial<KeyringControllerConfig>;
|
||||
state: Partial<KeyringControllerState>;
|
||||
}) {
|
||||
super({ config, state });
|
||||
this.defaultState = {
|
||||
wallets: state.wallets ?? [],
|
||||
HDPhrase: state.HDPhrase ?? '',
|
||||
} as KeyringControllerState;
|
||||
this.initialize();
|
||||
}
|
||||
|
||||
getAccounts(): string[] {
|
||||
return this.state.wallets.map((x) => x.address);
|
||||
}
|
||||
|
||||
setHDPhrase(phrase: string) {
|
||||
this.update({
|
||||
HDPhrase: phrase,
|
||||
});
|
||||
}
|
||||
|
||||
isOnboardingComplete = (): boolean => {
|
||||
return this.state.HDPhrase !== '';
|
||||
};
|
||||
|
||||
async createHDAccount(): Promise<string> {
|
||||
if (this.state.HDPhrase === '') {
|
||||
const { phrase } = ethers.Wallet.createRandom().mnemonic;
|
||||
this.setHDPhrase(phrase);
|
||||
}
|
||||
|
||||
const mnemonic = this.state.HDPhrase;
|
||||
const node = ethers.utils.HDNode.fromMnemonic(mnemonic);
|
||||
|
||||
const partialPath = "m/44'/60'/0'/0/";
|
||||
const path = partialPath + this.state.wallets.length;
|
||||
|
||||
const { privateKey } = node.derivePath(path);
|
||||
return this._createAccountAndUpdate(privateKey);
|
||||
}
|
||||
|
||||
async createAccount(): Promise<string> {
|
||||
const privateKey = generateRandomHex(256);
|
||||
return this._createAccountAndUpdate(privateKey);
|
||||
}
|
||||
|
||||
async importAccount(privateKey: string): Promise<string> {
|
||||
const existingWallet = this.state.wallets.find(
|
||||
(x) => x.privateKey.toLowerCase() === privateKey.toLowerCase(),
|
||||
);
|
||||
if (existingWallet) return existingWallet.address;
|
||||
|
||||
return this._createAccountAndUpdate(privateKey);
|
||||
}
|
||||
|
||||
removeAccount(address: string): void {
|
||||
const existingWallets = [...this.state.wallets];
|
||||
const index = this.state.wallets.findIndex((x) => x.address === address);
|
||||
if (index !== -1) {
|
||||
existingWallets.splice(index, 1);
|
||||
this.update({ wallets: existingWallets });
|
||||
}
|
||||
}
|
||||
|
||||
async signTransactions(address: string, tx: Operation) {
|
||||
const privKey = this._getPrivateKeyFor(address);
|
||||
const wallet = await this._getBLSWallet(privKey);
|
||||
|
||||
return wallet.sign(tx);
|
||||
}
|
||||
|
||||
async getNonce(address: string) {
|
||||
const privKey = this._getPrivateKeyFor(address);
|
||||
const wallet = await this._getBLSWallet(privKey);
|
||||
return wallet.Nonce();
|
||||
}
|
||||
|
||||
async _createAccountAndUpdate(privateKey: string): Promise<string> {
|
||||
const address = await this._getContractWalletAddress(privateKey);
|
||||
|
||||
if (address) {
|
||||
this.update({
|
||||
wallets: [
|
||||
...this.state.wallets,
|
||||
{
|
||||
privateKey,
|
||||
address,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
private _getPrivateKeyFor(address: string): string {
|
||||
const checksummedAddress = ethers.utils.getAddress(address);
|
||||
const keyPair = this.state.wallets.find(
|
||||
(x) => x.address === checksummedAddress,
|
||||
);
|
||||
if (!keyPair) throw new Error('key does not exist');
|
||||
return keyPair.privateKey;
|
||||
}
|
||||
|
||||
private _createProvider(): ethers.providers.Provider {
|
||||
return new ethers.providers.JsonRpcProvider(getRPCURL(this.state.chainId));
|
||||
}
|
||||
|
||||
private _getContractWalletAddress(privateKey: string): Promise<string> {
|
||||
return BlsWalletWrapper.Address(
|
||||
privateKey,
|
||||
NETWORK_CONFIG.addresses.verificationGateway,
|
||||
this._createProvider(),
|
||||
);
|
||||
}
|
||||
|
||||
private async _getBLSWallet(privateKey: string): Promise<BlsWalletWrapper> {
|
||||
return BlsWalletWrapper.connect(
|
||||
privateKey,
|
||||
NETWORK_CONFIG.addresses.verificationGateway,
|
||||
this._createProvider(),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
import {
|
||||
JRPCEngine,
|
||||
JRPCMiddleware,
|
||||
JRPCRequest,
|
||||
JRPCResponse,
|
||||
SafeEventEmitter,
|
||||
} from '@toruslabs/openlogin-jrpc';
|
||||
import { ProviderConfig } from '../constants';
|
||||
|
||||
import { BaseConfig, BaseState, IController } from '../interfaces';
|
||||
|
||||
/**
|
||||
* Custom network properties
|
||||
* @example isEIP1559Compatible: true etc.
|
||||
*/
|
||||
export interface NetworkProperties {
|
||||
[key: string]: number | string | boolean | unknown;
|
||||
|
||||
EIPS: {
|
||||
// undefined means we have not checked yet. (true or false means property is set)
|
||||
[key: string | number]: boolean | undefined;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
export interface NetworkState extends BaseState {
|
||||
/**
|
||||
* Chain Id for the current network
|
||||
*/
|
||||
chainId: string;
|
||||
providerConfig: ProviderConfig;
|
||||
properties: NetworkProperties;
|
||||
}
|
||||
|
||||
export interface NetworkConfig extends BaseConfig {
|
||||
providerConfig: ProviderConfig;
|
||||
}
|
||||
|
||||
export interface INetworkController<C, S> extends IController<C, S> {
|
||||
/**
|
||||
* Gets the chainId of the network
|
||||
*/
|
||||
getNetworkIdentifier(): string;
|
||||
|
||||
/**
|
||||
* Sets provider for the current network controller
|
||||
* @param providerConfig - Provider config object
|
||||
*/
|
||||
setProviderConfig(providerConfig: ProviderConfig): void;
|
||||
/**
|
||||
* Connects to the rpcUrl for the current selected provider
|
||||
*/
|
||||
lookupNetwork(): Promise<void>;
|
||||
}
|
||||
|
||||
export type BlockData = string | string[];
|
||||
|
||||
export type Block = Record<string, BlockData>;
|
||||
|
||||
export type SendAsyncCallBack = (
|
||||
err: Error,
|
||||
providerRes: JRPCResponse<Block>,
|
||||
) => void;
|
||||
|
||||
export type SendCallBack<U> = (err: any, providerRes: U | undefined) => void;
|
||||
|
||||
export type Payload = Partial<JRPCRequest<string[]>>;
|
||||
export interface SafeEventEmitterProvider extends SafeEventEmitter {
|
||||
request: <T, U>(req: JRPCRequest<T>) => Promise<U>;
|
||||
sendAsync: <T, U>(req: JRPCRequest<T>) => Promise<U>;
|
||||
send: <T, U>(req: JRPCRequest<T>, callback: SendCallBack<U>) => void;
|
||||
}
|
||||
|
||||
export interface ExtendedJsonRpcRequest<T> extends JRPCRequest<T> {
|
||||
skipCache?: boolean;
|
||||
}
|
||||
|
||||
export function providerFromEngine(
|
||||
engine: JRPCEngine,
|
||||
): SafeEventEmitterProvider {
|
||||
const provider: SafeEventEmitterProvider =
|
||||
new SafeEventEmitter() as SafeEventEmitterProvider;
|
||||
// handle both rpc send methods
|
||||
provider.sendAsync = async <T, U>(req: JRPCRequest<T>) => {
|
||||
const res = await engine.handle(req);
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return res.result as U;
|
||||
};
|
||||
|
||||
provider.request = async <T, U>(req: JRPCRequest<T>) => {
|
||||
const res = await engine.handle(req);
|
||||
if (res.error) {
|
||||
throw new Error(res.error);
|
||||
}
|
||||
return res.result as U;
|
||||
};
|
||||
|
||||
provider.send = <T, U>(
|
||||
req: JRPCRequest<T>,
|
||||
callback: (error: any, providerRes: U | undefined) => void,
|
||||
) => {
|
||||
if (typeof callback !== 'function') {
|
||||
throw new Error('Must provide callback to "send" method.');
|
||||
}
|
||||
engine.handle(req, (err, res) => {
|
||||
if (err) {
|
||||
callback(err, undefined);
|
||||
} else if (res.error) {
|
||||
callback(new Error(res.error), undefined);
|
||||
} else {
|
||||
callback(null, res.result as U);
|
||||
}
|
||||
});
|
||||
};
|
||||
// forward notifications
|
||||
if (engine.on) {
|
||||
engine.on('notification', (message: string) => {
|
||||
provider.emit('data', null, message);
|
||||
});
|
||||
}
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function providerFromMiddleware(
|
||||
middleware: JRPCMiddleware<string[], unknown>,
|
||||
): SafeEventEmitterProvider {
|
||||
const engine = new JRPCEngine();
|
||||
engine.push(middleware);
|
||||
const provider: SafeEventEmitterProvider = providerFromEngine(engine);
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function providerAsMiddleware(
|
||||
provider: SafeEventEmitterProvider,
|
||||
): JRPCMiddleware<unknown, unknown> {
|
||||
return async (req, res, _next, end) => {
|
||||
// send request to provider
|
||||
try {
|
||||
const providerRes: unknown = await provider.sendAsync<unknown, unknown>(
|
||||
req,
|
||||
);
|
||||
res.result = providerRes;
|
||||
return end();
|
||||
} catch (error: unknown) {
|
||||
return end(error as Error);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
import { providers } from 'ethers';
|
||||
import { JRPCEngine, JRPCMiddleware } from '@toruslabs/openlogin-jrpc';
|
||||
import { Mutex } from 'async-mutex';
|
||||
import EthQuery from '../rpcHelpers/EthQuery';
|
||||
|
||||
import BaseController from '../BaseController';
|
||||
import {
|
||||
PollingBlockTrackerConfig,
|
||||
PollingBlockTrackerState,
|
||||
} from '../Block/IBlockTrackerController';
|
||||
|
||||
import PollingBlockTracker from '../Block/PollingBlockTracker';
|
||||
import { ProviderConfig } from '../constants';
|
||||
import createEventEmitterProxy from '../createEventEmitterProxy';
|
||||
import createSwappableProxy from '../createSwappableProxy';
|
||||
import { getDefaultProviderConfig } from '../utils';
|
||||
import {
|
||||
createWalletMiddleware,
|
||||
IProviderHandlers,
|
||||
} from './createEthMiddleware';
|
||||
import { createJsonRpcClient } from './createJsonRpcClient';
|
||||
import {
|
||||
INetworkController,
|
||||
NetworkConfig,
|
||||
NetworkState,
|
||||
providerFromEngine,
|
||||
SafeEventEmitterProvider,
|
||||
} from './INetworkController';
|
||||
|
||||
// use state_get_balance for account balance
|
||||
|
||||
export default class NetworkController
|
||||
extends BaseController<NetworkConfig, NetworkState>
|
||||
implements INetworkController<NetworkConfig, NetworkState>
|
||||
{
|
||||
name = 'NetworkController';
|
||||
|
||||
_providerProxy: SafeEventEmitterProvider;
|
||||
|
||||
_blockTrackerProxy: PollingBlockTracker;
|
||||
|
||||
private mutex = new Mutex();
|
||||
|
||||
private _provider: SafeEventEmitterProvider | null = null;
|
||||
|
||||
private _blockTracker: PollingBlockTracker | null = null;
|
||||
|
||||
/**
|
||||
* Initialized before our provider is created.
|
||||
*/
|
||||
private ethQuery: EthQuery;
|
||||
|
||||
private _baseProviderHandlers: IProviderHandlers;
|
||||
|
||||
constructor({
|
||||
config,
|
||||
state,
|
||||
}: {
|
||||
config?: Partial<NetworkConfig>;
|
||||
state?: Partial<NetworkState>;
|
||||
}) {
|
||||
super({ config, state });
|
||||
this.defaultState = {
|
||||
chainId: 'loading',
|
||||
properties: {
|
||||
EIPS: { 1559: undefined },
|
||||
},
|
||||
providerConfig: getDefaultProviderConfig(),
|
||||
};
|
||||
this.initialize();
|
||||
// when a new network is set, we set to loading first and then when connection succeeds, we update the network
|
||||
}
|
||||
|
||||
getNetworkIdentifier(): string {
|
||||
return this.state.chainId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by orchestrator once while initializing the class
|
||||
* @param providerHandlers - JRPC handlers for provider
|
||||
* @returns - provider - Returns the providerProxy
|
||||
*/
|
||||
public initializeProvider(
|
||||
providerHandlers: IProviderHandlers,
|
||||
): SafeEventEmitterProvider {
|
||||
this._baseProviderHandlers = providerHandlers;
|
||||
this.configureProvider();
|
||||
this.lookupNetwork(); // Not awaiting this, because we don't want to block the initialization
|
||||
return this._providerProxy;
|
||||
}
|
||||
|
||||
getProvider(): SafeEventEmitterProvider {
|
||||
return this._providerProxy;
|
||||
}
|
||||
|
||||
setProviderConfig(config: ProviderConfig): void {
|
||||
this.update({
|
||||
providerConfig: { ...config },
|
||||
});
|
||||
this.refreshNetwork();
|
||||
}
|
||||
|
||||
getProviderConfig(): ProviderConfig {
|
||||
return this.state.providerConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the current network code
|
||||
*/
|
||||
async lookupNetwork(): Promise<void> {
|
||||
const { rpcTarget, chainId } = this.getProviderConfig();
|
||||
if (!chainId || !rpcTarget || !this._provider) {
|
||||
this.update({
|
||||
chainId: 'loading',
|
||||
properties: { EIPS: { 1559: undefined } },
|
||||
});
|
||||
return;
|
||||
}
|
||||
const query = this.ethQuery;
|
||||
if (!query) {
|
||||
return;
|
||||
}
|
||||
const releaseLock = await this.mutex.acquire();
|
||||
return new Promise((resolve, reject) => {
|
||||
// info_get_status
|
||||
query.sendAsync(
|
||||
{ method: 'net_version' },
|
||||
(error: Error, network: unknown) => {
|
||||
releaseLock();
|
||||
if (error) {
|
||||
this.update({
|
||||
chainId: 'loading',
|
||||
properties: {
|
||||
EIPS: { 1559: undefined },
|
||||
},
|
||||
});
|
||||
reject(error);
|
||||
}
|
||||
|
||||
this.update({
|
||||
// Network is returned as a string (base 10)
|
||||
chainId: `0x${Number.parseInt(network as string, 16).toString()}`,
|
||||
});
|
||||
// Don't need to wait for this
|
||||
this.getEIP1559Compatibility();
|
||||
this.emit('networkDidChange');
|
||||
resolve();
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async getEIP1559Compatibility(): Promise<boolean> {
|
||||
const { EIPS } = this.state.properties;
|
||||
// log.info('checking eip 1559 compatibility', EIPS[1559])
|
||||
if (EIPS[1559] !== undefined) {
|
||||
return EIPS[1559];
|
||||
}
|
||||
const latestBlock = await this.ethQuery.request<providers.Block>({
|
||||
method: 'eth_getBlockByNumber',
|
||||
params: ['latest', false],
|
||||
});
|
||||
const supportsEIP1559 =
|
||||
latestBlock && latestBlock.baseFeePerGas !== undefined;
|
||||
this.update({
|
||||
properties: {
|
||||
EIPS: { 1559: supportsEIP1559 },
|
||||
},
|
||||
});
|
||||
return supportsEIP1559;
|
||||
}
|
||||
|
||||
private configureProvider(): void {
|
||||
const { chainId, rpcTarget, ...rest } = this.getProviderConfig();
|
||||
if (!chainId || !rpcTarget) {
|
||||
throw new Error(
|
||||
'chainId and rpcTarget must be provider in providerConfig',
|
||||
);
|
||||
}
|
||||
this.configureStandardProvider({ chainId, rpcTarget, ...rest });
|
||||
}
|
||||
|
||||
private configureStandardProvider(providerConfig: ProviderConfig): void {
|
||||
const networkClient = createJsonRpcClient(providerConfig);
|
||||
this.setNetworkClient(networkClient);
|
||||
}
|
||||
|
||||
private setNetworkClient({
|
||||
networkMiddleware,
|
||||
blockTracker,
|
||||
}: {
|
||||
networkMiddleware: JRPCMiddleware<unknown, unknown>;
|
||||
blockTracker: PollingBlockTracker;
|
||||
}): void {
|
||||
const walletMiddleware = createWalletMiddleware(this._baseProviderHandlers);
|
||||
const engine = new JRPCEngine();
|
||||
engine.push(walletMiddleware);
|
||||
engine.push(networkMiddleware);
|
||||
const provider = providerFromEngine(engine);
|
||||
this.setProvider({ provider, blockTracker });
|
||||
}
|
||||
|
||||
private setProvider({
|
||||
provider,
|
||||
blockTracker,
|
||||
}: {
|
||||
provider: SafeEventEmitterProvider;
|
||||
blockTracker: PollingBlockTracker;
|
||||
}): void {
|
||||
if (this._providerProxy) {
|
||||
/* eslint @typescript-eslint/ban-ts-comment: "warn" -- TODO (merge-ok) Fix typing */
|
||||
// @ts-ignore
|
||||
this._providerProxy.setTarget(provider);
|
||||
} else {
|
||||
this._providerProxy =
|
||||
createSwappableProxy<SafeEventEmitterProvider>(provider);
|
||||
}
|
||||
|
||||
if (this._blockTrackerProxy) {
|
||||
/* eslint @typescript-eslint/ban-ts-comment: "warn" -- TODO (merge-ok) Fix typing */
|
||||
// @ts-ignore
|
||||
this._blockTrackerProxy.setTarget(blockTracker);
|
||||
} else {
|
||||
this._blockTrackerProxy = createEventEmitterProxy<
|
||||
PollingBlockTrackerConfig,
|
||||
PollingBlockTrackerState,
|
||||
PollingBlockTracker
|
||||
>(blockTracker, {
|
||||
eventFilter: 'skipInternal',
|
||||
});
|
||||
}
|
||||
|
||||
// set new provider and blockTracker
|
||||
this._provider = provider;
|
||||
provider.setMaxListeners(10);
|
||||
this._blockTracker = blockTracker;
|
||||
console.log(
|
||||
this._blockTracker,
|
||||
'set block tracker after switching network',
|
||||
);
|
||||
this.ethQuery = new EthQuery(provider);
|
||||
}
|
||||
|
||||
private refreshNetwork() {
|
||||
this.update({
|
||||
chainId: 'loading',
|
||||
properties: { EIPS: { 1559: undefined } },
|
||||
});
|
||||
this.configureProvider();
|
||||
this.lookupNetwork();
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import {
|
||||
createAsyncMiddleware,
|
||||
createScaffoldMiddleware,
|
||||
JRPCMiddleware,
|
||||
JRPCRequest,
|
||||
JRPCResponse,
|
||||
} from '@toruslabs/openlogin-jrpc';
|
||||
import { BigNumberish, BytesLike } from 'ethers';
|
||||
import web3_clientVersion from './web3_clientVersion';
|
||||
|
||||
type ProviderHandler<Params, Result> = (
|
||||
req: JRPCRequest<Params>,
|
||||
) => Promise<Result>;
|
||||
|
||||
function toAsyncMiddleware<Params, Result>(
|
||||
method: ProviderHandler<Params, Result>,
|
||||
) {
|
||||
return createAsyncMiddleware(
|
||||
async (req: JRPCRequest<Params>, res: JRPCResponse<Result>) => {
|
||||
res.result = await method(req);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export type SendTransactionParams = {
|
||||
from: string;
|
||||
to: string;
|
||||
gas?: BigNumberish;
|
||||
gasPrice?: BigNumberish;
|
||||
value?: BigNumberish;
|
||||
data: BytesLike;
|
||||
};
|
||||
|
||||
export type IProviderHandlers = Record<
|
||||
string,
|
||||
ProviderHandler<unknown, unknown>
|
||||
>;
|
||||
|
||||
export function createWalletMiddleware(
|
||||
handlers: IProviderHandlers,
|
||||
): JRPCMiddleware<string, unknown> {
|
||||
const asyncMiddlewares = Object.fromEntries(
|
||||
Object.keys(handlers).map((method) => [
|
||||
method,
|
||||
toAsyncMiddleware(handlers[method]),
|
||||
]),
|
||||
);
|
||||
|
||||
return createScaffoldMiddleware({ web3_clientVersion, ...asyncMiddlewares });
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
import {
|
||||
createAsyncMiddleware,
|
||||
JRPCEngineNextCallback,
|
||||
JRPCMiddleware,
|
||||
JRPCRequest,
|
||||
JRPCResponse,
|
||||
} from '@toruslabs/openlogin-jrpc';
|
||||
import { ethErrors } from 'eth-rpc-errors';
|
||||
|
||||
import { Block, Payload } from './INetworkController';
|
||||
|
||||
export interface FetchMiddlewareOptions {
|
||||
rpcTarget: string;
|
||||
originHttpHeaderKey?: string;
|
||||
}
|
||||
|
||||
export interface PayloadwithOrgin extends Payload {
|
||||
origin?: string;
|
||||
}
|
||||
export interface FetchMiddlewareFromReqOptions extends FetchMiddlewareOptions {
|
||||
req: PayloadwithOrgin;
|
||||
}
|
||||
|
||||
export interface FetchConfig {
|
||||
fetchUrl: string;
|
||||
fetchParams: Record<string, unknown>;
|
||||
}
|
||||
|
||||
const RETRIABLE_ERRORS: string[] = [
|
||||
// ignore server overload errors
|
||||
'Gateway timeout',
|
||||
'ETIMEDOUT',
|
||||
// ignore server sent html error pages
|
||||
// or truncated json responses
|
||||
'failed to parse response body',
|
||||
// ignore errors where http req failed to establish
|
||||
'Failed to fetch',
|
||||
];
|
||||
|
||||
function checkForHttpErrors(fetchRes: Response): void {
|
||||
// check for errors
|
||||
switch (fetchRes.status) {
|
||||
case 405:
|
||||
throw ethErrors.rpc.methodNotFound();
|
||||
|
||||
case 418:
|
||||
throw ethErrors.rpc.internal({
|
||||
message: `Request is being rate limited.`,
|
||||
});
|
||||
|
||||
case 503:
|
||||
case 504:
|
||||
throw ethErrors.rpc.internal({
|
||||
message:
|
||||
`Gateway timeout. The request took too long to process.` +
|
||||
`This can happen when querying over too wide a block range.`,
|
||||
});
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function timeout(duration: number): Promise<number> {
|
||||
return new Promise((resolve) => setTimeout(resolve, duration));
|
||||
}
|
||||
|
||||
function parseResponse(fetchRes: Response, body: Record<string, Block>): Block {
|
||||
// check for error code
|
||||
if (fetchRes.status !== 200) {
|
||||
throw ethErrors.rpc.internal({
|
||||
message: `Non-200 status code: '${fetchRes.status}'`,
|
||||
data: body,
|
||||
});
|
||||
}
|
||||
// check for rpc error
|
||||
if (body.error) {
|
||||
throw ethErrors.rpc.internal({
|
||||
data: body.error,
|
||||
});
|
||||
}
|
||||
// return successful result
|
||||
return body.result;
|
||||
}
|
||||
|
||||
export function createFetchConfigFromReq({
|
||||
req,
|
||||
rpcTarget,
|
||||
originHttpHeaderKey,
|
||||
}: FetchMiddlewareFromReqOptions): FetchConfig {
|
||||
const parsedUrl: URL = new URL(rpcTarget);
|
||||
|
||||
// prepare payload
|
||||
// copy only canonical json rpc properties
|
||||
const payload: Payload = {
|
||||
id: req.id,
|
||||
jsonrpc: req.jsonrpc,
|
||||
method: req.method,
|
||||
params: req.params,
|
||||
};
|
||||
|
||||
// extract 'origin' parameter from request
|
||||
const originDomain: string | undefined = req.origin;
|
||||
|
||||
// serialize request body
|
||||
const serializedPayload: string = JSON.stringify(payload);
|
||||
|
||||
// configure fetch params
|
||||
const fetchParams = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: serializedPayload,
|
||||
};
|
||||
|
||||
// optional: add request origin as header
|
||||
if (originHttpHeaderKey && originDomain) {
|
||||
(fetchParams.headers as Record<string, unknown>)[originHttpHeaderKey] =
|
||||
originDomain;
|
||||
}
|
||||
|
||||
return { fetchUrl: parsedUrl.href, fetchParams };
|
||||
}
|
||||
|
||||
export function createFetchMiddleware({
|
||||
rpcTarget,
|
||||
originHttpHeaderKey,
|
||||
}: FetchMiddlewareOptions): JRPCMiddleware<string[], Block> {
|
||||
return createAsyncMiddleware(
|
||||
async (
|
||||
req: JRPCRequest<string[]>,
|
||||
res: JRPCResponse<unknown>,
|
||||
_next: JRPCEngineNextCallback,
|
||||
) => {
|
||||
const { fetchUrl, fetchParams } = createFetchConfigFromReq({
|
||||
req,
|
||||
rpcTarget,
|
||||
originHttpHeaderKey,
|
||||
});
|
||||
|
||||
// attempt request multiple times
|
||||
const maxAttempts = 5;
|
||||
const retryInterval = 1000;
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
||||
try {
|
||||
const fetchRes: Response = await fetch(fetchUrl, fetchParams);
|
||||
// check for http errrors
|
||||
checkForHttpErrors(fetchRes);
|
||||
// parse response body
|
||||
const fetchBody: Record<string, Block> = await fetchRes.json();
|
||||
const result: Block = parseResponse(fetchRes, fetchBody);
|
||||
// set result and exit retry loop
|
||||
res.result = result;
|
||||
return;
|
||||
} catch (err: unknown) {
|
||||
const errMsg: string = (err as Error).toString();
|
||||
const isRetriable: boolean = RETRIABLE_ERRORS.some((phrase) =>
|
||||
errMsg.includes(phrase),
|
||||
);
|
||||
// re-throw error if not retriable
|
||||
if (!isRetriable) {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
// delay before retrying
|
||||
await timeout(retryInterval);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user