61 Commits

Author SHA1 Message Date
Andrew Morris
0b8fec818c Add cells readme 2022-06-07 14:41:28 +10:00
Andrew Morris
4fe1241ad2 DemoTable 2022-06-07 14:35:37 +10:00
Andrew Morris
23c037b97f Cleanup 2022-06-07 08:36:22 +10:00
Andrew Morris
bfbf059a13 Remove obsoleted rpc-based cell collection 2022-06-07 08:17:46 +10:00
Andrew Morris
d7cfb87e02 Add dark-theme 2022-06-06 20:50:26 +10:00
Andrew Morris
1e1820cc58 Improve label 2022-06-06 20:18:16 +10:00
Andrew Morris
d52c4884ae More event cleanup fixes 2022-06-06 20:10:50 +10:00
Andrew Morris
553a007804 Balance demo 2022-06-06 19:07:43 +10:00
Andrew Morris
e4d166e8a2 nitpicks 2022-06-06 17:43:09 +10:00
Andrew Morris
4718851812 Page selection, ethersProvider 2022-06-06 17:40:07 +10:00
Andrew Morris
39b7d1ebc4 Add blockNumber cell 2022-06-06 17:18:40 +10:00
Andrew Morris
bb167a419f Use elcc 2022-06-06 16:29:33 +10:00
Andrew Morris
7baed92053 Fix cleanup of end handler 2022-06-06 11:55:42 +10:00
Andrew Morris
6d2c98aaf4 Stop iteration more actively 2022-06-06 11:42:28 +10:00
Andrew Morris
8c9604d8e4 Make FormulaCell lazy 2022-06-06 10:12:06 +10:00
Andrew Morris
a63b0c9d68 Fix demo page 2022-06-06 08:07:54 +10:00
Andrew Morris
67bccb45a4 Use extensionLocalCellCollection in demo 2022-06-06 07:51:50 +10:00
Andrew Morris
69ac847bac localCellCollection 2022-06-06 07:51:25 +10:00
Andrew Morris
7d70686094 Allow cleaning up the change handler 2022-06-06 07:51:25 +10:00
Andrew Morris
82c0d3c58f Use change events in CellCollection 2022-06-06 07:50:56 +10:00
Andrew Morris
8281724ae2 Add events to IAsyncStorage, change extensionLocalStorage to singleton 2022-06-06 07:50:32 +10:00
Andrew Morris
ed6b7b77bd tmp 2022-06-06 07:49:58 +10:00
Andrew Morris
26fe1bfd6c useCellState 2022-06-06 07:48:58 +10:00
Andrew Morris
a76b5164f3 Remove useNewCell 2022-06-06 07:48:56 +10:00
Andrew Morris
f59860fbea Avoid useCellWithFallback 2022-06-06 07:48:40 +10:00
Andrew Morris
dcc45cc5fc useNewCell 2022-06-06 07:48:39 +10:00
Andrew Morris
0ab259f350 Fix CellIterator undefined bug 2022-06-06 07:47:15 +10:00
Andrew Morris
2719637f75 Merge branch 'bw-173-214-215-fix-rpc' into improve-storage 2022-06-03 15:12:06 +10:00
Andrew Morris
38d4051005 Merge branch 'bw-173-fix-rpc' into improve-storage 2022-06-03 10:52:47 +10:00
Andrew Morris
9cdce8b30e Fix .KeyringController in WalletPage 2022-06-03 10:31:38 +10:00
Andrew Morris
6512fd7fa6 Replace KeyringController().createHdAccount with rpc.private.quill_createHdAccount 2022-06-03 10:24:58 +10:00
Andrew Morris
674ef797fb Replace KeyringController().getAccounts with rpc.public.eth_accounts 2022-06-03 10:17:10 +10:00
Andrew Morris
d756102404 Move eth_accounts into typed rpc 2022-06-03 10:15:18 +10:00
Andrew Morris
7c5f95258e Replace internal rpc with public+private 2022-06-03 10:01:58 +10:00
Andrew Morris
5a78d77fcf Fix window.QuillController() in WalletWrapper 2022-06-03 09:27:27 +10:00
Andrew Morris
9e929d1b30 QuillContext 2022-06-03 09:17:44 +10:00
Andrew Morris
cae4e8ea5d Use versionedType when reading 2022-06-03 08:24:57 +10:00
Andrew Morris
94b3d966b7 Use a cell for CurrencyController.state 2022-06-03 08:15:28 +10:00
Andrew Morris
dc9e7b068c Allow async formulas 2022-06-02 17:30:27 +10:00
Andrew Morris
048edbb9cc Quill cells 2022-06-02 16:19:19 +10:00
Andrew Morris
a2bd81f033 Allow mixing unknowns in CellCollection 2022-06-02 15:15:40 +10:00
Andrew Morris
fa2f5fd968 Add counters 2022-06-02 13:58:00 +10:00
Andrew Morris
3c69d7438f CellDisplay 2022-06-02 13:42:44 +10:00
Andrew Morris
aa09b43a2a Add cells demo 2022-06-02 13:21:34 +10:00
Andrew Morris
8713cc1c7d useCell.ts, useReadableCell.ts 2022-06-02 12:44:59 +10:00
Andrew Morris
dba032f87b Enable removing cells 2022-06-02 12:28:57 +10:00
Andrew Morris
61a63dec41 MemoryCellCollection.ts 2022-06-02 12:24:23 +10:00
Andrew Morris
f1bf808638 Define IAsyncStorage abstraction and use it to generalize ExtensionLocalStorage 2022-06-02 12:19:30 +10:00
Andrew Morris
8f5f4ebbc8 ReadableCellIterator -> CellIterator 2022-06-02 11:56:10 +10:00
Andrew Morris
1c84872df8 Split out FormulaCell 2022-06-02 11:52:50 +10:00
Andrew Morris
54e644b24c MemoryCell.ts 2022-06-02 11:50:48 +10:00
Andrew Morris
375e0927d5 Split out CellIterator 2022-06-02 11:32:40 +10:00
Andrew Morris
aee5e56b76 Split out ICell 2022-06-02 11:29:51 +10:00
Andrew Morris
a894a5b1be StorageCell -> ExtensionLocalCell 2022-06-02 11:24:24 +10:00
Andrew Morris
ecd664fc0c Rename to cells/ExtensionLocalStorage.ts 2022-06-02 11:22:53 +10:00
Andrew Morris
02129fc5cf Remove unused functions 2022-06-02 11:18:52 +10:00
Andrew Morris
59f77ea95b FormulaCell 2022-06-02 11:03:11 +10:00
Andrew Morris
8d8d347fbe Define IReadableCell, split out ReadableCellIterator 2022-06-02 07:54:12 +10:00
Andrew Morris
fb077141a0 Use default to ensure there's always a value 2022-06-01 19:46:42 +10:00
Andrew Morris
f6d1e313fd Fixes, add iteration 2022-06-01 17:45:13 +10:00
Andrew Morris
016103a17e wip StorageManager 2022-06-01 16:47:33 +10:00
288 changed files with 13687 additions and 14029 deletions

View File

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

View File

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

22
.github/labeler.yml vendored
View File

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

View File

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

View File

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

View File

@@ -1,80 +0,0 @@
name: aggregator
on:
push:
branches:
- 'main'
paths:
- 'aggregator/**'
- '.github/workflows/aggregator.yml'
pull_request:
branches:
- 'main'
paths:
- 'aggregator/**'
- '.github/workflows/aggregator.yml'
defaults:
run:
working-directory: ./aggregator
env:
DENO_VERSION: 1.x
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- run: deno lint .
todos-fixmes:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- run: ./programs/lintTodos.ts
typescript:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- run: ./programs/checkTs.ts
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: denoland/setup-deno@v1
with:
deno-version: ${{ env.DENO_VERSION }}
- uses: ./.github/actions/setup-contracts-clients
# Setup contracts
- working-directory: ./contracts
run: yarn hardhat node &
- working-directory: ./contracts
run: yarn hardhat fundDeployer --network gethDev
- working-directory: ./contracts
run: yarn hardhat run scripts/deploy_all.ts --network gethDev
- working-directory: ./
run: docker-compose up -d postgres
- run: cp .env.local.example .env
- run: deno test --allow-net --allow-env --allow-read --unstable
# Cleanup
- working-directory: ./
run: docker-compose down

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

1
.gitignore vendored
View File

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

1
.nvmrc
View File

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

View File

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

21
LICENSE
View File

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

181
README.md
View File

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

View File

@@ -1,16 +1,5 @@
# Aggregator Proxy
[![npm version](https://img.shields.io/npm/v/bls-wallet-aggregator-proxy)](https://www.npmjs.com/package/bls-wallet-aggregator-proxy)
This package makes it easy to provide an aggregator by proxying another. The primary use-case is to expose a free aggregator based on one that requires payment by augmenting the bundles with transactions that pay `tx.origin`.
## Setup
```sh
npm install bls-wallet-aggregator-proxy
yarn install bls-wallet-aggregator-proxy
```
## Usage
```ts
@@ -20,7 +9,7 @@ import {
// AggregatorProxyCallback,
// ^ Alternatively, for manual control, import AggregatorProxyCallback to
// just generate the req,res callback for use with http.createServer
} from 'bls-wallet-aggregator-proxy';
} from 'aggregator-proxy';
runAggregatorProxy(
'https://arbitrum-testnet.blswallet.org',

View File

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

View File

@@ -885,10 +885,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.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/bls-wallet-clients/-/bls-wallet-clients-0.7.3.tgz#22fae2434c67b642d172023f259131233e396eb7"
integrity sha512-u2mwyf5+1KHYCyeutfl4jhEDCmsW/C+S54QKbicUkU26MvAGJ76/TxSOOIBiWTjzVDdDUBdJ/zQRoRd35GSGcQ==
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==
dependencies:
"@thehubbleproject/bls" "^0.5.1"
ethers "5.5.4"

View File

@@ -1,16 +1,16 @@
RPC_URL=https://goerli-rollup.arbitrum.io/rpc
RPC_URL=http://localhost:8545
USE_TEST_NET=false
ORIGIN=http://localhost:3000
PORT=3000
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-goerli.json
PRIVATE_KEY_AGG=0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
NETWORK_CONFIG_PATH=../contracts/networks/local.json
PRIVATE_KEY_AGG=0x0000000000000000000000000000000000000000000000000000000000000a99
PRIVATE_KEY_ADMIN=
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
PG_HOST=127.0.0.1
PG_HOST=localhost
PG_PORT=5432
PG_USER=bls
PG_PASSWORD=generate-a-strong-password

View File

@@ -1,32 +0,0 @@
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

View File

@@ -1,4 +1,4 @@
.env*
!.env*.example
!.env.example
cov_profile*
/build

View File

@@ -1,4 +1,4 @@
FROM denoland/deno:1.23.4
FROM denoland/deno:1.20.6
ADD build /app
WORKDIR /app

View File

@@ -8,7 +8,7 @@ Verification Gateway.
## Installation
Install [Deno](deno.land)
Install [Deno](deno.land).
### Configuration
@@ -168,16 +168,6 @@ You need to reload modules (`-r`):
deno run -r --allow-net --allow-env --allow-read --unstable ./programs/aggregator.ts
```
#### Transaction reverted: function call to a non-contract account
- Is `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s `COST_ESTIMATOR_ADDRESS` set to the right precompile cost estimator's contract address?
- Are the BLS Wallet contracts deployed on the correct network?
- Is `NETWORK_CONFIG_PATH` in `.env` set to the right config?
#### Deno version
Make sure your Deno version is [up to date.](https://deno.land/manual/getting_started/installation#updating)
### Notable Components
- **src/chain**: Should contain all of the contract interactions, exposing more

View File

@@ -49,7 +49,7 @@ export type {
PublicKey,
Signature,
VerificationGateway,
} from "https://esm.sh/bls-wallet-clients@0.7.3";
} from "https://esm.sh/bls-wallet-clients@0.6.0";
export {
Aggregator as AggregatorClient,
@@ -59,10 +59,10 @@ export {
getConfig,
MockERC20__factory,
VerificationGateway__factory,
} from "https://esm.sh/bls-wallet-clients@0.7.3";
} from "https://esm.sh/bls-wallet-clients@0.6.0";
// Workaround for esbuild's export-star bug
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.7.3";
import blsWalletClients from "https://esm.sh/bls-wallet-clients@0.6.0";
const {
bundleFromDto,
bundleToDto,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
import Range from "../src/helpers/Range.ts";
import {
assertBundleSucceeds,
assertEquals,
assertBundleSucceeds,
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_900_000).mul(1e9);
const fee = BigNumber.from(1_950_000).mul(1e9);
const [wallet1, wallet2] = await fx.setupWallets(2, {
tokenBalance: fee.mul(10),

View File

@@ -3,7 +3,6 @@ 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

View File

@@ -35,7 +35,5 @@ module.exports = {
ignores: [],
},
],
// TODO (merge-ok) Remove and fix lint error
"node/no-unpublished-import": ["warn"],
},
};

View File

@@ -91,6 +91,10 @@ Proposed solution to make use of [BLS](https://github.com/thehubbleproject/hubbl
For each network, the deployer contract can be deployed with the following script (only needed once)
`DEPLOY_DEPLOYER=true yarn hardhat run scripts/deploy-deployer.ts --network <network-name>`
## Arbitrum
## Optimism's L2 (paused)
- clone https://github.com/ethereum-optimism/optimism
- follow instructions (using latest version of docker)

View File

@@ -1,7 +1,5 @@
# BLS Wallet Clients
[![npm version](https://img.shields.io/npm/v/bls-wallet-clients)](https://www.npmjs.com/package/bls-wallet-clients)
*Client libraries for interacting with BLS Wallet components*
## Network Config
@@ -20,20 +18,20 @@ const netCfg: NetworkConfig = await getConfig(
## Aggregator
Exposes typed functions for interacting with the Aggregator's HTTP API.
Exposes typed functions for interacting with the Aggregator's HTTP api.
```ts
import { Aggregator } from 'bls-wallet-clients';
const aggregator = new Aggregator('https://rinkarby.blswallet.org');
await aggregator.add(...);
await aggregator.addTransaction(...);
```
## BlsWalletWrapper
Wraps a BLS wallet, storing the private key and providing `.sign(...)` to
produce a `Bundle`, that can be used with `aggregator.add(...)`.
produce a `Bundle`, that can be used with `aggregator.addTransaction(...)`.
```ts
import { BlsWalletWrapper } from 'bls-wallet-clients';
@@ -48,19 +46,16 @@ const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
{
ethValue: 0,
contractAddress: someToken.address, // An ethers.Contract
encodedFunction: someToken.interface.encodeFunctionData(
"transfer",
["0x...some address...", ethers.BigNumber.from(1).pow(18)],
),
contract: someToken, // An ethers.Contract
method: 'transfer',
args: [recipientAddress, ethers.utils.parseUnits('1', 18)],
},
// Additional actions can go here. When using multiple actions, they'll
// either all succeed or all fail.
],
});
await aggregator.add(bundle);
await aggregator.addTransaction(bundle);
```
## VerificationGateway
@@ -123,32 +118,3 @@ import { initBlsWalletSigner } from "bls-wallet-clients";
// Send bundle to an aggregator or use it with VerificationGateway directly.
})();
```
## Local Development
### Setup
```sh
yarn install
```
### Build
```sh
yarn build
```
### Tests
```sh
yarn test
```
### Use in Extension or another project
```sh
yarn build
yarn link
cd other/project/dir
yarn "link bls-wallet-clients"
```

View File

@@ -1,6 +1,6 @@
{
"name": "bls-wallet-clients",
"version": "0.7.3",
"version": "0.6.0",
"description": "Client libraries for interacting with BLS Wallet components",
"main": "dist/src/index.js",
"types": "dist/src/index.d.ts",
@@ -8,15 +8,11 @@
"author": "Andrew Morris",
"license": "MIT",
"private": false,
"engines": {
"node": ">=16.0.0",
"yarn": ">=1.0.0"
},
"scripts": {
"build": "rm -rf dist && mkdir dist && cp -rH typechain dist/typechain && find ./dist/typechain -type f \\! -name '*.d.ts' -name '*.ts' -delete && tsc",
"watch": "tsc -w",
"test": "mocha --require ts-node/register --require source-map-support/register --require ./test/init.ts **/*.test.ts",
"premerge": "yarn test",
"test": "mocha dist/**/*.test.js",
"premerge": "yarn build && 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"
},
@@ -25,14 +21,11 @@
"ethers": "5.5.4"
},
"devDependencies": {
"@types/chai": "^4.3.3",
"@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.1.1",
"@types/chai": "^4.3.0",
"@types/mocha": "^9.1.0",
"chai": "^4.3.6",
"chai-as-promised": "^7.1.1",
"mocha": "^10.0.0",
"mocha": "^9.2.2",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.1",
"typescript": "^4.8.2"
"typescript": "^4.6.2"
}
}

View File

@@ -57,7 +57,6 @@ export default class BlsWalletWrapper {
const initFunctionParams =
BLSWallet__factory.createInterface().encodeFunctionData("initialize", [
verificationGatewayAddress,
ethers.constants.AddressZero, // TODO: 4337 EntryPoint address
]);
return ethers.utils.getCreate2Address(

View File

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

View File

@@ -43,14 +43,13 @@ 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: UnvalidatedConfig): NetworkConfig {
export function validateConfig(cfg: any): NetworkConfig {
return {
parameters: assertUnknownRecord(cfg.parameters),
addresses: {
@@ -76,7 +75,6 @@ export function validateConfig(cfg: UnvalidatedConfig): 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.

View File

@@ -18,11 +18,6 @@ import { MockERC20__factory } from "../typechain/factories/MockERC20__factory";
import type { MockERC20 } from "../typechain/MockERC20";
import { NetworkConfig, getConfig, validateConfig } from "./NetworkConfig";
import {
MultiNetworkConfig,
getMultiConfig,
validateMultiConfig,
} from "./MultiNetworkConfig";
export * from "./signer";
@@ -32,9 +27,6 @@ export {
NetworkConfig,
getConfig,
validateConfig,
MultiNetworkConfig,
getMultiConfig,
validateMultiConfig,
// eslint-disable-next-line camelcase
VerificationGateway__factory,
VerificationGateway,

View File

@@ -1,88 +0,0 @@
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;
});
});
});

View File

@@ -1,3 +1,5 @@
import "source-map-support/register";
import { BigNumber } from "ethers";
import { keccak256, arrayify } from "ethers/lib/utils";
import { expect } from "chai";

View File

@@ -1,4 +0,0 @@
import chai from "chai";
import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);

View File

@@ -2,13 +2,6 @@
# 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"
@@ -715,24 +708,6 @@
"@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"
@@ -741,58 +716,21 @@
ethers "^5.5.3"
mcl-wasm "^1.0.0"
"@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/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/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==
"@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@^9.1.1":
version "9.1.1"
resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-9.1.1.tgz#e7c4f1001eefa4b8afbd1eee27a237fee3bf29c4"
integrity sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==
"@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==
"@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==
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"
resolved "https://registry.npmjs.org/aes-js/-/aes-js-3.0.0.tgz"
@@ -823,11 +761,6 @@ 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"
@@ -866,13 +799,6 @@ 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"
@@ -900,13 +826,6 @@ 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"
@@ -974,15 +893,10 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
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==
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==
dependencies:
ms "2.1.2"
@@ -1003,11 +917,6 @@ 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"
@@ -1167,6 +1076,11 @@ 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"
@@ -1246,6 +1160,11 @@ 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"
@@ -1280,11 +1199,6 @@ 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"
@@ -1300,12 +1214,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@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==
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==
dependencies:
brace-expansion "^2.0.1"
brace-expansion "^1.1.7"
minimatch@^3.0.4:
version "3.0.4"
@@ -1314,30 +1228,32 @@ minimatch@^3.0.4:
dependencies:
brace-expansion "^1.1.7"
mocha@^10.0.0:
version "10.0.0"
resolved "https://registry.yarnpkg.com/mocha/-/mocha-10.0.0.tgz#205447d8993ec755335c4b13deba3d3a13c4def9"
integrity sha512-0Wl+elVUD43Y0BqPZBzZt8Tnkw9CMUdNYnUsTfOM1vuhJVZL+kiesFYsqwBkEEuEixaiPe5ZQdqDgX2jddhmoA==
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==
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.4"
debug "4.3.3"
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 "5.0.1"
minimatch "4.2.1"
ms "2.1.3"
nanoid "3.3.3"
nanoid "3.3.1"
serialize-javascript "6.0.0"
strip-json-comments "3.1.1"
supports-color "8.1.1"
workerpool "6.2.1"
which "2.0.2"
workerpool "6.2.0"
yargs "16.2.0"
yargs-parser "20.2.4"
yargs-unparser "2.0.0"
@@ -1352,10 +1268,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.3:
version "3.3.3"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.3.tgz#fd8e8b7aa761fe807dba2d1b98fb7241bb724a25"
integrity sha512-p1sjXuopFs0xg+fPASzQ28agW1oHD7xDsd9Xkf3T15H3c/cifrFHVwrh74PdoklAPi+i7MdRsE47vm2r6JoB+w==
nanoid@3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35"
integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw==
normalize-path@^3.0.0, normalize-path@~3.0.0:
version "3.0.0"
@@ -1494,44 +1410,27 @@ 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.8.2:
version "4.8.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790"
integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw==
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==
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==
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"
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==
workerpool@6.2.0:
version "6.2.0"
resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.2.0.tgz#827d93c9ba23ee2019c3ffaff5c27fccea289e8b"
integrity sha512-Rsk5qQHJ9eowMH28Jwhe8HEbmdYDX4lwoMWshiCXugjtHqMD9ZbiqSDLxcsfdqsETPzVUtX5s1Z5kStiIM6l4A==
wrap-ansi@^7.0.0:
version "7.0.0"
@@ -1590,11 +1489,6 @@ 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"

View File

@@ -6,12 +6,12 @@ pragma abicoder v2;
//To avoid constructor params having forbidden evm bytecodes on Optimism
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "./interfaces/IWallet.sol";
import "./interfaces/UserOperation4337.sol";
/** Minimal upgradable smart contract wallet.
Generic calls can only be requested by its trusted gateway.
*/
contract BLSWallet is Initializable, IWallet
contract BLSWallet is Initializable, IBLSWallet
{
uint256 public nonce;
bytes32 public recoveryHash;
@@ -22,14 +22,19 @@ contract BLSWallet is Initializable, IWallet
uint256 pendingPAFunctionTime;
// BLS variables
uint256[4] public blsPublicKey;
uint256[4] pendingBLSPublicKey;
uint256 pendingBLSPublicKeyTime;
address public trustedBLSGateway;
address public trustedEntryPoint;
address pendingBLSGateway;
uint256 pendingGatewayTime;
event PendingRecoveryHashSet(
bytes32 pendingRecoveryHash
);
event PendingBLSKeySet(
uint256[4] pendingBLSKey
);
event PendingGatewaySet(
address pendingGateway
);
@@ -41,6 +46,10 @@ contract BLSWallet is Initializable, IWallet
bytes32 oldHash,
bytes32 newHash
);
event BLSKeySet(
uint256[4] oldBLSKey,
uint256[4] newBLSKey
);
event GatewayUpdated(
address oldGateway,
address newGateway
@@ -50,17 +59,42 @@ contract BLSWallet is Initializable, IWallet
);
function initialize(
address blsGateway,
address entryPoint
address blsGateway
) external initializer {
nonce = 0;
trustedBLSGateway = blsGateway;
trustedEntryPoint = entryPoint;
pendingGatewayTime = type(uint256).max;
pendingPAFunctionTime = type(uint256).max;
pendingRecoveryHashTime = type(uint256).max;
pendingBLSPublicKeyTime = type(uint256).max;
}
/** */
function latchBLSPublicKey(
uint256[4] memory blsKey
) public onlyTrustedGateway {
require(isZeroBLSKey(blsPublicKey), "BLSWallet: public key already set");
blsPublicKey = blsKey;
}
function isZeroBLSKey(uint256[4] memory blsKey) public pure returns (bool) {
bool isZero = true;
for (uint256 i=0; isZero && i<4; i++) {
isZero = (blsKey[i] == 0);
}
return isZero;
}
receive() external payable {}
fallback() external payable {}
/**
BLS public key format, contract can be upgraded for other types
*/
function getBLSPublicKey() external view returns (uint256[4] memory) {
return blsPublicKey;
}
/**
Wallet can update its recovery hash
*/
@@ -77,6 +111,16 @@ contract BLSWallet is Initializable, IWallet
}
}
/**
Wallet can update its BLS key
*/
function setBLSPublicKey(uint256[4] memory blsKey) public onlyThis {
require(isZeroBLSKey(blsKey) == false, "BLSWallet: blsKey must be non-zero");
pendingBLSPublicKey = blsKey;
pendingBLSPublicKeyTime = block.timestamp + 604800; // 1 week from now
emit PendingBLSKeySet(pendingBLSPublicKey);
}
/**
Wallet can migrate to a new gateway, eg additional signature support
*/
@@ -99,45 +143,51 @@ contract BLSWallet is Initializable, IWallet
Set results of any pending set operation if their respective timestamp has elapsed.
*/
function setAnyPending() public {
if (pendingRecoveryHashTime != 0 &&
block.timestamp > pendingRecoveryHashTime
) {
if (block.timestamp > pendingRecoveryHashTime) {
bytes32 previousRecoveryHash = recoveryHash;
recoveryHash = pendingRecoveryHash;
clearPendingRecoveryHash();
emit RecoveryHashUpdated(previousRecoveryHash, recoveryHash);
}
if (pendingGatewayTime != 0 &&
block.timestamp > pendingGatewayTime
) {
if (block.timestamp > pendingBLSPublicKeyTime) {
uint256[4] memory previousBLSPublicKey = blsPublicKey;
blsPublicKey = pendingBLSPublicKey;
pendingBLSPublicKeyTime = type(uint256).max;
pendingBLSPublicKey = [0,0,0,0];
emit BLSKeySet(previousBLSPublicKey, blsPublicKey);
}
if (block.timestamp > pendingGatewayTime) {
address previousGateway = trustedBLSGateway;
trustedBLSGateway = pendingBLSGateway;
pendingGatewayTime = 0;
pendingGatewayTime = type(uint256).max;
pendingBLSGateway = address(0);
emit GatewayUpdated(previousGateway, trustedBLSGateway);
}
if (
pendingPAFunctionTime != 0 &&
block.timestamp > pendingPAFunctionTime
) {
if (block.timestamp > pendingPAFunctionTime) {
approvedProxyAdminFunctionHash = pendingPAFunctionHash;
pendingPAFunctionTime = 0;
pendingPAFunctionTime = type(uint256).max;
pendingPAFunctionHash = 0;
emit ProxyAdminFunctionHashApproved(approvedProxyAdminFunctionHash);
}
}
function clearPendingRecoveryHash() internal {
pendingRecoveryHashTime = 0;
pendingRecoveryHashTime = type(uint256).max;
pendingRecoveryHash = bytes32(0);
}
function recover() public onlyTrustedGateway {
function recover(
uint256[4] calldata newBLSKey
) public onlyTrustedGateway {
// set new bls key
blsPublicKey = newBLSKey;
// clear any pending operations
clearPendingRecoveryHash();
pendingGatewayTime = 0;
pendingBLSPublicKeyTime = type(uint256).max;
pendingBLSPublicKey = [0,0,0,0];
pendingGatewayTime = type(uint256).max;
pendingBLSGateway = address(0);
pendingPAFunctionTime = 0;
pendingPAFunctionTime = type(uint256).max;
pendingPAFunctionHash = 0;
}
@@ -147,7 +197,7 @@ contract BLSWallet is Initializable, IWallet
*/
function performOperation(
IWallet.Operation calldata op
) public payable onlyTrusted thisNonce(op.nonce) returns (
) public payable onlyTrustedGateway thisNonce(op.nonce) returns (
bool success,
bytes[] memory results
) {
@@ -157,10 +207,8 @@ contract BLSWallet is Initializable, IWallet
success = true;
results = _results;
}
catch (bytes memory returnData) {
catch {
success = false;
results = new bytes[](1);
results[0] = returnData;
}
incrementNonce(); // regardless of outcome of operation
}
@@ -188,31 +236,11 @@ contract BLSWallet is Initializable, IWallet
else {
(success, result) = address(a.contractAddress).call(a.encodedFunction);
}
if (success == false) {
bytes memory indexByte = new bytes(1);
indexByte[0] = bytes1(uint8(78)); // "N";
if (i < 10) {
indexByte[0] = bytes1(uint8(48 + i)); // "0" - "9"
}
string memory message = string.concat(
string(indexByte),
" - ",
abi.decode(stripMethodId(result), (string)) // remove "Error" methodId, it gets added again on this throw
);
revert(message);
}
require(success);
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;
}
@@ -224,25 +252,6 @@ contract BLSWallet is Initializable, IWallet
nonce++;
}
// --- <4337> ---
function validateUserOp(
UserOperation4337 calldata userOp,
bytes32 requestId,
address aggregator,
uint256 missingWalletFunds
) external view {
require(aggregator == trustedBLSGateway);
require(userOp.nonce == nonce);
require(missingWalletFunds == 0);
}
function getAggregator() public view returns (address) {
return trustedBLSGateway;
}
// --- </4337> ---
modifier onlyThis() {
require(msg.sender == address(this), "BLSWallet: only callable from this");
_;
@@ -256,15 +265,6 @@ contract BLSWallet is Initializable, IWallet
_;
}
modifier onlyTrusted() {
require(
msg.sender == trustedBLSGateway ||
msg.sender == trustedEntryPoint,
"BLSWallet: only callable from trusted gateway or 4337 entry point"
);
_;
}
modifier thisNonce(uint256 opNonce) {
require(opNonce == nonce, "BLSWallet: only callable with current nonce");
_;

View File

@@ -8,7 +8,6 @@ import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "./interfaces/IWallet.sol";
import "./interfaces/UserOperation4337.sol";
/**
A non-upgradable gateway used to create BLSWallets and call them with
@@ -23,16 +22,11 @@ contract VerificationGateway
bytes32 BLS_DOMAIN = keccak256(abi.encodePacked(uint32(0xfeedbee5)));
uint8 constant BLS_KEY_LEN = 4;
IBLS public immutable blsLib;
IBLS public blsLib;
ProxyAdmin public immutable walletProxyAdmin;
address public immutable blsWalletLogic;
mapping(bytes32 => IWallet) public walletFromHash;
mapping(IWallet => uint256[BLS_KEY_LEN]) public blsKeyFromWallet;
address public blsWalletLogic;
mapping(bytes32 => IWallet) externalWalletsFromHash;
//mapping from an existing wallet's bls key hash to pending variables when setting a new BLS key
mapping(bytes32 => uint256[BLS_KEY_LEN]) public pendingBLSPublicKeyFromHash;
mapping(bytes32 => uint256[2]) public pendingMessageSenderSignatureFromHash;
mapping(bytes32 => uint256) public pendingBLSPublicKeyTimeFromHash;
/** Aggregated signature with corresponding senders + operations */
struct Bundle {
@@ -49,41 +43,30 @@ contract VerificationGateway
event WalletOperationProcessed(
address indexed wallet,
uint256 nonce,
IWallet.ActionData[] actions,
bool success,
bytes[] results
bool result
);
event PendingBLSKeySet(
bytes32 previousHash,
uint256[BLS_KEY_LEN] newBLSKey
);
event BLSKeySetForWallet(
uint256[BLS_KEY_LEN] newBLSKey,
IWallet wallet
);
/**
@param bls verified bls library contract address
*/
constructor(
IBLS bls,
address blsWalletImpl,
address proxyAdmin
address blsWalletImpl
) {
blsLib = bls;
blsWalletLogic = blsWalletImpl;
walletProxyAdmin = ProxyAdmin(proxyAdmin);
walletProxyAdmin = new ProxyAdmin();
}
/** Throw if bundle not valid or signature verification fails */
function verify(
Bundle memory bundle
Bundle calldata bundle
) public view {
uint256 opLength = bundle.operations.length;
require(
opLength == bundle.senderPublicKeys.length,
"VG: Sender/op length mismatch"
"VG: Sender and operation length mismatch"
);
uint256[2][] memory messages = new uint256[2][](opLength);
@@ -98,17 +81,37 @@ contract VerificationGateway
messages
);
require(verified, "VG: Sig not verified");
require(verified, "VG: All sigs not verified");
}
function hashFromWallet(IWallet wallet) public view returns (bytes32) {
uint256[BLS_KEY_LEN] memory blsKey = blsKeyFromWallet[wallet];
if (blsLib.isZeroBLSKey(blsKey)) {
return bytes32(0);
/**
Returns a BLSWallet if deployed from this contract, otherwise 0.
@param hash BLS public key hash used as salt for create2
@return BLSWallet at calculated address (if code exists), otherwise zero address
*/
function walletFromHash(bytes32 hash) public view returns (IWallet) {
//return wallet of hash registered explicitly
if (externalWalletsFromHash[hash] != IWallet(address(0))) {
return externalWalletsFromHash[hash];
}
return keccak256(abi.encodePacked(blsKey));
address walletAddress = address(uint160(uint(keccak256(abi.encodePacked(
bytes1(0xff),
address(this),
hash,
keccak256(abi.encodePacked(
type(TransparentUpgradeableProxy).creationCode,
abi.encode(
address(blsWalletLogic),
address(walletProxyAdmin),
getInitializeData()
)
))
)))));
if (!hasCode(walletAddress)) {
walletAddress = address(0);
}
return IWallet(payable(walletAddress));
}
/**
@@ -120,42 +123,11 @@ contract VerificationGateway
@param messageSenderSignature signature of message containing only the calling address
@param publicKey that signed the caller's address
*/
function setBLSKeyForWallet(
uint256[2] memory messageSenderSignature,
uint256[BLS_KEY_LEN] memory publicKey
function setExternalWallet(
uint256[2] calldata messageSenderSignature,
uint256[BLS_KEY_LEN] calldata publicKey
) public {
require(blsLib.isZeroBLSKey(publicKey) == false, "VG: publicKey must be non-zero");
IWallet wallet = IWallet(msg.sender);
bytes32 existingHash = hashFromWallet(wallet);
if (existingHash == bytes32(0)) { // wallet does not yet have a bls key registered with this gateway
// set it instantly
safeSetWallet(messageSenderSignature, publicKey, wallet);
}
else { // wallet already has a key registered, set after delay
pendingMessageSenderSignatureFromHash[existingHash] = messageSenderSignature;
pendingBLSPublicKeyFromHash[existingHash] = publicKey;
pendingBLSPublicKeyTimeFromHash[existingHash] = block.timestamp + 604800; // 1 week from now
emit PendingBLSKeySet(existingHash, publicKey);
}
}
function setPendingBLSKeyForWallet() public {
IWallet wallet = IWallet(msg.sender);
bytes32 existingHash = hashFromWallet(wallet);
require(existingHash != bytes32(0), "VG: hash does not exist for caller");
if (
(pendingBLSPublicKeyTimeFromHash[existingHash] != 0) &&
(block.timestamp > pendingBLSPublicKeyTimeFromHash[existingHash])
) {
safeSetWallet(
pendingMessageSenderSignatureFromHash[existingHash],
pendingBLSPublicKeyFromHash[existingHash],
wallet
);
pendingMessageSenderSignatureFromHash[existingHash] = [0,0];
pendingBLSPublicKeyTimeFromHash[existingHash] = 0;
pendingBLSPublicKeyFromHash[existingHash] = [0,0,0,0];
}
safeSetWallet(messageSenderSignature, publicKey, msg.sender);
}
/**
@@ -166,9 +138,9 @@ contract VerificationGateway
*/
function walletAdminCall(
bytes32 hash,
bytes memory encodedFunction
bytes calldata encodedFunction
) public onlyWallet(hash) {
IWallet wallet = walletFromHash[hash];
IWallet wallet = walletFromHash(hash);
// ensure first parameter is the calling wallet address
bytes memory encodedAddress = abi.encode(address(wallet));
@@ -207,18 +179,20 @@ contract VerificationGateway
@param newBLSKey to set as the wallet's bls public key
*/
function recoverWallet(
uint256[2] memory walletAddressSignature,
uint256[2] calldata walletAddressSignature,
bytes32 blsKeyHash,
bytes32 salt,
uint256[BLS_KEY_LEN] memory newBLSKey
uint256[BLS_KEY_LEN] calldata newBLSKey
) public {
IWallet wallet = walletFromHash[blsKeyHash];
IWallet wallet = walletFromHash(blsKeyHash);
bytes32 recoveryHash = keccak256(
abi.encodePacked(msg.sender, blsKeyHash, salt)
);
if (recoveryHash == wallet.recoveryHash()) {
safeSetWallet(walletAddressSignature, newBLSKey, wallet);
wallet.recover();
// override mapping of old key hash (takes precedence over create2 address)
externalWalletsFromHash[blsKeyHash] = IWallet(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF);
safeSetWallet(walletAddressSignature, newBLSKey, address(wallet));
wallet.recover(newBLSKey);
}
}
@@ -237,7 +211,7 @@ contract VerificationGateway
"BLSWallet: gateway address param not valid"
);
IWallet wallet = walletFromHash[hash];
IWallet wallet = walletFromHash(hash);
require(
VerificationGateway(blsGateway).walletFromHash(hash) == wallet,
@@ -260,7 +234,7 @@ contract VerificationGateway
Can be called with a single operation with no actions.
*/
function processBundle(
Bundle memory bundle
Bundle calldata bundle
) external returns (
bool[] memory successes,
bytes[][] memory results
@@ -286,9 +260,7 @@ contract VerificationGateway
emit WalletOperationProcessed(
address(wallet),
bundle.operations[i].nonce,
bundle.operations[i].actions,
successes[i],
results[i]
successes[i]
);
}
}
@@ -299,18 +271,18 @@ contract VerificationGateway
needed.
*/
function getOrCreateWallet(
uint256[BLS_KEY_LEN] memory publicKey
uint256[BLS_KEY_LEN] calldata publicKey
) private returns (IWallet) {
bytes32 publicKeyHash = keccak256(abi.encodePacked(publicKey));
IWallet blsWallet = walletFromHash[publicKeyHash];
// publicKeyHash does not yet refer to a wallet, create one then update mappings.
if (address(blsWallet) == address(0)) {
blsWallet = IWallet(address(new TransparentUpgradeableProxy{salt: publicKeyHash}(
address blsWallet = address(walletFromHash(publicKeyHash));
// wallet with publicKeyHash doesn't exist at expected create2 address
if (blsWallet == address(0)) {
blsWallet = address(new TransparentUpgradeableProxy{salt: publicKeyHash}(
address(blsWalletLogic),
address(walletProxyAdmin),
getInitializeData()
)));
updateWalletHashMappings(publicKey, blsWallet);
));
IBLSWallet(payable(blsWallet)).latchBLSPublicKey(publicKey);
emit WalletCreated(
address(blsWallet),
publicKey
@@ -326,11 +298,10 @@ contract VerificationGateway
@param wallet address to set
*/
function safeSetWallet(
uint256[2] memory wallletAddressSignature,
uint256[BLS_KEY_LEN] memory publicKey,
IWallet wallet
uint256[2] calldata wallletAddressSignature,
uint256[BLS_KEY_LEN] calldata publicKey,
address wallet
) private {
// verify the given wallet was signed for by the bls key
uint256[2] memory addressMsg = blsLib.hashToPoint(
BLS_DOMAIN,
abi.encodePacked(wallet)
@@ -339,43 +310,38 @@ contract VerificationGateway
blsLib.verifySingle(wallletAddressSignature, publicKey, addressMsg),
"VG: Signature not verified for wallet address."
);
emit BLSKeySetForWallet(publicKey, wallet);
updateWalletHashMappings(publicKey, wallet);
bytes32 publicKeyHash = keccak256(abi.encodePacked(
publicKey
));
externalWalletsFromHash[publicKeyHash] = IWallet(wallet);
}
/** @dev Only to be called on wallet creation, and in `safeSetWallet` */
function updateWalletHashMappings(
uint256[BLS_KEY_LEN] memory blsKey,
IWallet wallet
) private {
// remove reference from old hash
bytes32 oldHash = hashFromWallet(wallet);
walletFromHash[oldHash] = IWallet(address(0));
// update new hash / wallet mappings
walletFromHash[keccak256(abi.encodePacked(blsKey))] = wallet;
blsKeyFromWallet[wallet] = blsKey;
function hasCode(address a) private view returns (bool) {
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly { size := extcodesize(a) }
return size > 0;
}
function getInitializeData() private view returns (bytes memory) {
return abi.encodeWithSignature("initialize(address,address)", address(this), address(0));
return abi.encodeWithSignature("initialize(address)", address(this));
}
modifier onlyWallet(bytes32 hash) {
require(
(IWallet(msg.sender) == walletFromHash[hash]),
(msg.sender == address(walletFromHash(hash))),
"VG: not called from wallet"
);
_;
}
function messagePoint(
IWallet.Operation memory op
IWallet.Operation calldata op
) internal view returns (
uint256[2] memory
) {
bytes memory encodedActionData;
IWallet.ActionData memory a;
IWallet.ActionData calldata a;
for (uint256 i=0; i<op.actions.length; i++) {
a = op.actions[i];
encodedActionData = abi.encodePacked(
@@ -395,49 +361,4 @@ contract VerificationGateway
);
}
// --- <4337> ---
// These functions seem to exist to allow clients (aka nodes) to rely on logic defined on-chain
// so that they don't need to implement anything specific to any signature aggregation scheme.
// For our prototype we can do this specific implementation and make this work without these
// functions. Later, it might be necessary to add these for compatibility with clients relying
// on this generic technique.
// function validateUserOpSignature(
// UserOperation4337 calldata userOp,
// bool offChainSigCheck
// ) external view returns (
// bytes memory sigForUserOp,
// bytes memory sigForAggregation,
// bytes memory offChainSigInfo
// ) {}
// function aggregateSignatures(
// bytes[] calldata sigsForAggregation
// ) external view returns (bytes memory aggregatesSignature) {}
function validateSignatures(
UserOperation4337[] calldata userOps,
bytes calldata signature
) view external {
uint256[2][] memory messages = new uint256[2][](userOps.length);
uint256[BLS_KEY_LEN][] memory senderPublicKeys = new uint256[BLS_KEY_LEN][](userOps.length);
for (uint256 i = 0; i < userOps.length; i++) {
messages[i] = blsLib.hashToPoint(
BLS_DOMAIN,
abi.encode(userOps[i])
);
senderPublicKeys[i] = blsKeyFromWallet[IWallet(userOps[i].sender)];
}
bool verified = blsLib.verifyMultiple(
abi.decode(signature, (uint256[2])),
senderPublicKeys,
messages
);
require(verified, "VG: Sig not verified");
}
// --- </4337> ---
}
}

View File

@@ -17,7 +17,7 @@ interface IWallet {
bytes encodedFunction;
}
function initialize(address gateway, address entryPoint) external;
function initialize(address gateway) external;
function nonce() external returns (uint256);
function performOperation(
@@ -28,7 +28,7 @@ interface IWallet {
);
function recoveryHash() external returns (bytes32);
function recover() external;
function recover(uint256[4] calldata newBLSKey) external;
// prepares gateway to be set (after pending timestamp)
function setTrustedGateway(address gateway) external;
@@ -39,3 +39,16 @@ interface IWallet {
function approvedProxyAdminFunctionHash() external view returns (bytes32);
function clearApprovedProxyAdminFunctionHash() external;
}
/** Interface for bls-specific functions
*/
interface IBLSWallet is IWallet {
// type BLSPublicKey is uint256[4]; // The underlying type for a user defined value type has to be an elementary value type.
function latchBLSPublicKey(
uint256[4] memory blsKey
) external;
function getBLSPublicKey() external view returns (uint256[4] memory);
}

View File

@@ -1,16 +0,0 @@
//SPDX-License-Identifier: Unlicense
pragma solidity >=0.8.4 <0.9.0;
struct UserOperation4337 {
address sender;
uint256 nonce;
bytes initCode;
bytes callData;
uint256 callGasLimit;
uint256 verificationGasLimit;
uint256 preVerificationGas;
uint256 maxFeePerGas;
uint256 maxPriorityFeePerGas;
bytes paymasterAndData;
bytes signature;
}

View File

@@ -53,12 +53,4 @@ library BLSOpen {
);
}
function isZeroBLSKey(uint256[4] memory blsKey) public pure returns (bool) {
bool isZero = true;
for (uint256 i=0; isZero && i<4; i++) {
isZero = (blsKey[i] == 0);
}
return isZero;
}
}

View File

@@ -20,6 +20,4 @@ interface IBLS {
bytes memory message
) external view returns (uint256[2] memory);
function isZeroBLSKey(uint256[4] memory blsKey) external pure returns (bool);
}

View File

@@ -1,6 +1,6 @@
import * as dotenv from "dotenv";
import { HardhatUserConfig, task, types } from "hardhat/config";
import { HardhatUserConfig, task } from "hardhat/config";
import "@nomiclabs/hardhat-etherscan";
import "@nomiclabs/hardhat-waffle";
import "@typechain/hardhat";
@@ -8,13 +8,12 @@ import * as chai from "chai";
import chaiAsPromised from "chai-as-promised";
import "hardhat-gas-reporter";
import "solidity-coverage";
import defaultDeployerWallets from "./shared/helpers/defaultDeployerWallet";
dotenv.config();
// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => {
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
@@ -22,43 +21,6 @@ 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) => {
const [account0] = await hre.ethers.getSigners();
const deployerAddress = defaultDeployerWallets(hre.ethers).address;
console.log(`${account0.address} -> ${deployerAddress} ${amount} ETH`);
const txnRes = await account0.sendTransaction({
to: deployerAddress,
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);
@@ -75,11 +37,11 @@ const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: "0.8.15",
version: "0.8.10",
settings: {
optimizer: {
enabled: true,
runs: 1,
runs: 1000,
},
},
},
@@ -107,7 +69,6 @@ const config: HardhatUserConfig = {
hardhat: {
initialBaseFeePerGas: 0, // workaround from https://github.com/sc-forks/solidity-coverage/issues/652#issuecomment-896330136 . Remove when that issue is closed.
accounts,
blockGasLimit: 200_000_000,
},
gethDev: {
url: `http://localhost:8545`,
@@ -124,11 +85,6 @@ 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,

View File

@@ -1,19 +0,0 @@
{
"parameters": {},
"addresses": {
"create2Deployer": "0x036d996D6855B83cd80142f2933d8C2617dA5617",
"precompileCostEstimator": "0x22E4a5251C1F02de8369Dd6f192033F6CB7531A4",
"blsLibrary": "0xF8a11BA6eceC43e23c9896b857128a4269290e39",
"verificationGateway": "0xAf96d6e0817Ff8658f0E2a39b641920fA7fF0957",
"blsExpander": "0x376E7c0dA79423F772C2837744F81a7A0ff4bA47",
"utilities": "0x957e58EfEB6cE40F95f3dBFAaCD9465Df5C29E23",
"testToken": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853"
},
"auxiliary": {
"chainid": 421613,
"domain": "0x0054159611832e24cdd64c6a133e71d373c5f8553dde6c762e6bffe707ad83cc",
"genesisBlock": 277661,
"deployedBy": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266",
"version": "03ced09fc2f3b9b8255b05060fcf78c7880a79ca"
}
}

View File

@@ -1,19 +1,19 @@
{
"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"
}
}
"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"
}
}

View File

@@ -3,16 +3,10 @@
"version": "1.0.0",
"description": "BLS Wallet smart contract",
"main": "index.js",
"engines": {
"node": ">=16.0.0",
"yarn": ">=1.0.0"
},
"scripts": {
"build": "hardhat compile",
"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 && hardhat compile && yarn lint && yarn check-ts && yarn --cwd clients premerge && yarn hardhat test"
},
"author": "James Zaki",
"license": "MIT",

View File

@@ -75,8 +75,6 @@ export default class Create2Fixture {
const initCode = factory.bytecode + constructorParamsBytes.substr(2);
const initCodeHash = ethers.utils.solidityKeccak256(["bytes"], [initCode]);
console.log((initCode.length - 2) / 2, "bytes in contract", contractName);
const contractAddress = ethers.utils.getCreate2Address(
create2Deployer.address,
"0x" + salt.toHexString().substr(2).padStart(64, "0"),

View File

@@ -19,7 +19,7 @@ import {
import Range from "./Range";
import assert from "./assert";
import Create2Fixture from "./Create2Fixture";
import { VerificationGateway, BLSOpen, ProxyAdmin } from "../../typechain";
import { VerificationGateway, BLSOpen } from "../../typechain";
export default class Fixture {
static readonly ECDSA_ACCOUNTS_LENGTH = 5;
@@ -68,20 +68,14 @@ export default class Fixture {
} catch (e) {}
const bls = (await create2Fixture.create2Contract("BLSOpen")) as BLSOpen;
const ProxyAdmin = await ethers.getContractFactory("ProxyAdmin");
const proxyAdmin = (await ProxyAdmin.deploy()) as ProxyAdmin;
await proxyAdmin.deployed();
// deploy Verification Gateway
const verificationGateway = (await create2Fixture.create2Contract(
"VerificationGateway",
ethers.utils.defaultAbiCoder.encode(
["address", "address", "address"],
[bls.address, blsWalletImpl.address, proxyAdmin.address],
["address", "address"],
[bls.address, blsWalletImpl.address],
),
)) as VerificationGateway;
await (
await proxyAdmin.transferOwnership(verificationGateway.address)
).wait();
// deploy BLSExpander Gateway
const blsExpander = await create2Fixture.create2Contract(

View File

@@ -1,19 +0,0 @@
/**
* Note: This file cannot have any direct imports
* of hardhat since it is used in hardhat.config.ts.
*/
import { HardhatEthersHelpers } from "@nomiclabs/hardhat-ethers/types";
import { Wallet } from "ethers";
/**
*
* @returns Wallet constructed from DEPLOYER_ env vars
*/
export default function defaultDeployerWallet(
ethers: HardhatEthersHelpers,
): Wallet {
return Wallet.fromMnemonic(
`${process.env.DEPLOYER_MNEMONIC}`,
`m/44'/60'/0'/0/${process.env.DEPLOYER_SET_INDEX}`,
).connect(ethers.provider);
}

View File

@@ -4,7 +4,6 @@ import "@nomiclabs/hardhat-ethers";
import { ethers } from "hardhat";
import { Wallet } from "ethers";
import { Create2Deployer } from "../../typechain";
import defaultDeployerWalletHardhat from "./defaultDeployerWallet";
dotenv.config();
@@ -12,8 +11,15 @@ export function defaultDeployerAddress(): string {
return defaultDeployerWallet().address;
}
/**
*
* @returns Wallet constructed from DEPLOYER_ env vars
*/
export function defaultDeployerWallet(): Wallet {
return defaultDeployerWalletHardhat(ethers);
return ethers.Wallet.fromMnemonic(
`${process.env.DEPLOYER_MNEMONIC}`,
`m/44'/60'/0'/0/${process.env.DEPLOYER_SET_INDEX}`,
).connect(ethers.provider);
}
/**

View File

@@ -3,11 +3,11 @@ import { BigNumber } from "ethers";
import { solidityPack } from "ethers/lib/utils";
import { ethers, network } from "hardhat";
import { BlsWalletWrapper, Signature } from "../clients/src";
import { PublicKey, 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 } from "../typechain";
const signWalletAddress = async (
fx: Fixture,
@@ -43,18 +43,14 @@ describe("Recovery", async function () {
const safetyDelaySeconds = 7 * 24 * 60 * 60;
let fx: Fixture;
let vg: VerificationGateway;
let wallet1: BlsWalletWrapper;
let wallet2: BlsWalletWrapper;
let walletAttacker: BlsWalletWrapper;
let wallet1, wallet2, walletAttacker;
let blsWallet: BLSWallet;
let recoverySigner;
let hash1, hash2, hashAttacker;
let hash1, hash2;
let salt;
let recoveryHash;
beforeEach(async function () {
fx = await Fixture.create();
vg = fx.verificationGateway;
wallet1 = await fx.lazyBlsWallets[0]();
wallet2 = await fx.lazyBlsWallets[1]();
@@ -64,9 +60,6 @@ describe("Recovery", async function () {
hash1 = wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey);
hash2 = wallet2.blsWalletSigner.getPublicKeyHash(wallet2.privateKey);
hashAttacker = wallet2.blsWalletSigner.getPublicKeyHash(
walletAttacker.privateKey,
);
salt = "0x1234567812345678123456781234567812345678123456781234567812345678";
recoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
@@ -75,44 +68,34 @@ describe("Recovery", async function () {
});
it("should update bls key", async function () {
expect(await vg.hashFromWallet(wallet1.address)).to.eql(hash1);
const newKey: PublicKey = [
BigNumber.from(1),
BigNumber.from(2),
BigNumber.from(3),
BigNumber.from(4),
];
const initialKey = await blsWallet.getBLSPublicKey();
const addressSignature = await signWalletAddress(
fx,
wallet1.address,
wallet2.privateKey,
);
await fx.call(wallet1, blsWallet, "setBLSPublicKey", [newKey], 1);
await fx.call(
wallet1,
vg,
"setBLSKeyForWallet",
[addressSignature, wallet2.PublicKey()],
1,
);
expect(await blsWallet.getBLSPublicKey()).to.eql(initialKey);
await fx.advanceTimeBy(safetyDelaySeconds + 1);
await fx.call(wallet1, vg, "setPendingBLSKeyForWallet", [], 2);
await (await blsWallet.setAnyPending()).wait();
expect(await vg.hashFromWallet(wallet1.address)).to.eql(hash2);
expect(await blsWallet.getBLSPublicKey()).to.eql(newKey);
});
it("should NOT override public key hash after creation", async function () {
let walletForHash = await vg.walletFromHash(hash1);
expect(BigNumber.from(walletForHash)).to.not.equal(BigNumber.from(0));
expect(walletForHash).to.equal(wallet1.address);
it("should NOT override public key after creation", async function () {
const initialKey = await blsWallet.getBLSPublicKey();
let hashFromWallet = await vg.hashFromWallet(wallet1.address);
expect(BigNumber.from(hashFromWallet)).to.not.equal(BigNumber.from(0));
expect(hashFromWallet).to.equal(hash1);
const ZERO = ethers.BigNumber.from(0);
expect(initialKey).to.not.eql([ZERO, ZERO, ZERO, ZERO]);
await fx.call(wallet1, vg, "setPendingBLSKeyForWallet", [], 1);
await blsWallet.setAnyPending();
walletForHash = await vg.walletFromHash(hash1);
expect(walletForHash).to.equal(wallet1.address);
hashFromWallet = await vg.hashFromWallet(wallet1.address);
expect(hashFromWallet).to.equal(hash1);
const finalKey = await blsWallet.getBLSPublicKey();
expect(finalKey).to.eql(initialKey);
});
it("should set recovery hash", async function () {
@@ -135,25 +118,14 @@ describe("Recovery", async function () {
it("should recover before bls key update", async function () {
await fx.call(wallet1, blsWallet, "setRecoveryHash", [recoveryHash], 1);
const attackKey = walletAttacker.PublicKey();
const attackSignature = await signWalletAddress(
fx,
wallet1.address,
walletAttacker.privateKey,
);
// Attacker assumed to have compromised wallet1 bls key, and wishes to reset
// the gateway wallet's bls key to their own.
await fx.call(
wallet1,
vg,
"setBLSKeyForWallet",
[attackSignature, walletAttacker.PublicKey()],
1,
);
// Attacker assumed to have compromised current bls key, and wishes to reset
// the contract's bls key to their own.
await fx.call(wallet1, blsWallet, "setBLSPublicKey", [attackKey], 2);
await fx.advanceTimeBy(safetyDelaySeconds / 2); // wait half the time
await fx.call(wallet1, vg, "setPendingBLSKeyForWallet", [], 2);
await (await blsWallet.setAnyPending()).wait();
const addressSignature = await signWalletAddress(
fx,
@@ -169,34 +141,33 @@ describe("Recovery", async function () {
).wait();
// key reset via recovery
expect(await vg.hashFromWallet(wallet1.address)).to.eql(hash2);
expect(await vg.walletFromHash(hash2)).to.eql(wallet1.address);
expect(await blsWallet.getBLSPublicKey()).to.eql(
safeKey.map(BigNumber.from),
);
await fx.advanceTimeBy(safetyDelaySeconds / 2 + 1); // wait remainder the time
// check attacker's key not set after waiting full safety delay
await fx.call(
walletAttacker,
vg,
"setPendingBLSKeyForWallet",
[],
await walletAttacker.Nonce(),
);
await fx.call(
wallet2,
vg,
"setPendingBLSKeyForWallet",
[],
await wallet2.Nonce(),
// attacker's key not set after waiting full safety delay
expect(await blsWallet.getBLSPublicKey()).to.eql(
safeKey.map(BigNumber.from),
);
expect(await vg.walletFromHash(hash1)).to.not.equal(blsWallet.address);
expect(await vg.walletFromHash(hashAttacker)).to.not.equal(
blsWallet.address,
let walletFromKey = await fx.verificationGateway.walletFromHash(
wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey),
);
expect(await vg.walletFromHash(hash2)).to.equal(blsWallet.address);
expect(walletFromKey).to.not.equal(blsWallet.address);
walletFromKey = await fx.verificationGateway.walletFromHash(
walletAttacker.blsWalletSigner.getPublicKeyHash(
walletAttacker.privateKey,
),
);
expect(walletFromKey).to.not.equal(blsWallet.address);
walletFromKey = await fx.verificationGateway.walletFromHash(
wallet2.blsWalletSigner.getPublicKeyHash(wallet2.privateKey),
);
expect(walletFromKey).to.equal(blsWallet.address);
// // verify recovered bls key can successfully call wallet-only function (eg setTrustedGateway)
// verify recovered bls key can successfully call wallet-only function (eg setTrustedGateway)
const res = await fx.callStatic(
wallet2,
fx.verificationGateway,
@@ -213,12 +184,15 @@ describe("Recovery", async function () {
"BLSWallet",
walletAttacker.address,
);
// Attacker users recovery signer to set their recovery hash
const hashAttacker = walletAttacker.blsWalletSigner.getPublicKeyHash(
walletAttacker.privateKey,
);
const attackerRecoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[recoverySigner.address, hashAttacker, salt],
);
// Attacker puts their wallet into recovery
await fx.call(
walletAttacker,
attackerWalletContract,
@@ -230,9 +204,6 @@ describe("Recovery", async function () {
// Attacker waits out safety delay
await fx.advanceTimeBy(safetyDelaySeconds + 1);
await (await attackerWalletContract.setAnyPending()).wait();
expect(await attackerWalletContract.recoveryHash()).to.equal(
attackerRecoveryHash,
);
const addressSignature = await signWalletAddress(
fx,
@@ -241,7 +212,7 @@ describe("Recovery", async function () {
);
const wallet1Key = await wallet1.PublicKey();
// Attacker attempts to overwrite wallet 1's hash in the gateway and fails
// Attacker attempts to overwite wallet 1's public key and fails
await expect(
fx.verificationGateway
.connect(recoverySigner)

View File

@@ -3,7 +3,7 @@ import { BigNumber } from "ethers";
import { solidityPack } from "ethers/lib/utils";
import { ethers, network } from "hardhat";
import { BLSOpen, ProxyAdmin } from "../typechain";
import { BLSOpen } from "../typechain";
import { ActionData, BlsWalletWrapper } from "../clients/src";
import Fixture from "../shared/helpers/Fixture";
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
@@ -76,10 +76,6 @@ describe("Upgrade", async function () {
// Deploy new verification gateway
const create2Fixture = Create2Fixture.create();
const bls = (await create2Fixture.create2Contract("BLSOpen")) as BLSOpen;
const ProxyAdmin = await ethers.getContractFactory("ProxyAdmin");
const proxyAdmin2 = (await ProxyAdmin.deploy()) as ProxyAdmin;
await proxyAdmin2.deployed();
const blsWalletImpl = await create2Fixture.create2Contract("BLSWallet");
const VerificationGateway = await ethers.getContractFactory(
"VerificationGateway",
@@ -87,9 +83,7 @@ describe("Upgrade", async function () {
const vg2 = await VerificationGateway.deploy(
bls.address,
blsWalletImpl.address,
proxyAdmin2.address,
);
await (await proxyAdmin2.transferOwnership(vg2.address)).wait();
// Recreate hubble bls signer
const walletOldVg = await fx.lazyBlsWallets[0]();
@@ -129,7 +123,7 @@ describe("Upgrade", async function () {
const setExternalWalletAction: ActionData = {
ethValue: BigNumber.from(0),
contractAddress: vg2.address,
encodedFunction: vg2.interface.encodeFunctionData("setBLSKeyForWallet", [
encodedFunction: vg2.interface.encodeFunctionData("setExternalWallet", [
addressSignature,
walletOldVg.PublicKey(),
]),
@@ -233,7 +227,6 @@ describe("Upgrade", async function () {
// Direct checks corresponding to each action
expect(await vg2.walletFromHash(hash)).to.equal(walletAddress);
expect(await vg2.hashFromWallet(walletAddress)).to.equal(hash);
expect(await proxyAdmin.getProxyAdmin(walletAddress)).to.equal(
proxyAdmin.address,
);
@@ -274,77 +267,4 @@ describe("Upgrade", async function () {
)[0];
expect(walletFromHashAddress).to.equal(walletAddress);
});
it("should change mapping of an address to hash", async function () {
const vg1 = fx.verificationGateway;
const lazyWallet1 = await fx.lazyBlsWallets[0]();
const lazyWallet2 = await fx.lazyBlsWallets[1]();
const wallet1 = await BlsWalletWrapper.connect(
lazyWallet1.privateKey,
vg1.address,
fx.provider,
);
const wallet2 = await BlsWalletWrapper.connect(
lazyWallet2.privateKey,
vg1.address,
fx.provider,
);
const hash1 = wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey);
expect(await vg1.walletFromHash(hash1)).to.equal(wallet1.address);
expect(await vg1.hashFromWallet(wallet1.address)).to.equal(hash1);
// wallet 2 bls key signs message containing address of wallet 1
const addressMessage = solidityPack(["address"], [wallet1.address]);
const addressSignature = wallet2.signMessage(addressMessage);
const setExternalWalletAction: ActionData = {
ethValue: BigNumber.from(0),
contractAddress: vg1.address,
encodedFunction: vg1.interface.encodeFunctionData("setBLSKeyForWallet", [
addressSignature,
wallet2.PublicKey(),
]),
};
// wallet 1 submits a tx
{
const { successes } = await vg1.callStatic.processBundle(
wallet1.sign({
nonce: BigNumber.from(1),
actions: [setExternalWalletAction],
}),
);
expect(successes).to.deep.equal([true]);
}
await (
await fx.verificationGateway.processBundle(
fx.blsWalletSigner.aggregate([
wallet1.sign({
nonce: BigNumber.from(1),
actions: [setExternalWalletAction],
}),
]),
)
).wait();
// wallet 1's hash is pointed to null address
// wallet 2's hash is now pointed to wallet 1's address
const hash2 = wallet2.blsWalletSigner.getPublicKeyHash(wallet2.privateKey);
await fx.advanceTimeBy(safetyDelaySeconds + 1);
await fx.call(wallet1, vg1, "setPendingBLSKeyForWallet", [], 2);
expect(await vg1.walletFromHash(hash1)).to.equal(
ethers.constants.AddressZero,
);
expect(await vg1.walletFromHash(hash2)).to.equal(wallet1.address);
expect(await vg1.hashFromWallet(wallet1.address)).to.equal(hash2);
});
});

View File

@@ -5,7 +5,7 @@ import { ethers, network } from "hardhat";
import Fixture from "../shared/helpers/Fixture";
import TokenHelper from "../shared/helpers/TokenHelper";
import { BigNumber, ContractReceipt } from "ethers";
import { BigNumber } from "ethers";
import { parseEther, solidityPack } from "ethers/lib/utils";
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
// import splitHex256 from "../shared/helpers/splitHex256";
@@ -118,6 +118,7 @@ describe("WalletActions", async function () {
actions: [
{
ethValue: ethToTransfer,
// TODO: Does wallet contract need to exist?
contractAddress: recvWallet.walletContract.address,
encodedFunction: "0x",
},
@@ -288,21 +289,11 @@ describe("WalletActions", async function () {
const th = new TokenHelper(fx);
const [sender, recipient] = await th.walletTokenSetup();
const r: ContractReceipt = await (
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.
{
@@ -317,21 +308,21 @@ 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();
// Single event "WalletOperationProcessed(address indexed wallet, uint256 nonce, bool success, bytes[] results)"
// Get the first (only) result from "results" argument.
const result = r.events[0].args.results[0]; // For errors this is "Error(string)"
const errorArgBytesString: string = "0x" + result.substring(10); // remove methodId (4bytes after 0x)
const errorString = ethers.utils.defaultAbiCoder.decode(
["string"],
errorArgBytesString,
)[0]; // decoded bytes is a string of the action index that errored.
expect(errorString).to.equal("1 - ERC20: transfer from the zero address");
const recipientBalance = await th.testToken.balanceOf(recipient.address);
// Should be unchanged because the operation that would have added tokens

View File

@@ -28,7 +28,6 @@ services:
command: >
--datadir dev-chain/
--http
--http.api eth,web3,personal,net
--http.addr=0.0.0.0
--http.vhosts='*'
--dev

View File

@@ -1,9 +0,0 @@
## Docs
- [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](./local_development.md)
- [Remote development](./remote_development.md)

View File

@@ -1,24 +0,0 @@
<svg width="1280" height="640" viewBox="0 0 1280 640" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="1280" height="640" fill="#5A9DED"/>
<path d="M209 379.573V144.717H310.729C328.6 144.717 343.607 147.125 355.75 151.941C367.97 156.758 377.173 163.562 383.359 172.354C389.622 181.146 392.753 191.505 392.753 203.431C392.753 212.07 390.844 219.944 387.025 227.054C383.283 234.164 378.013 240.127 371.216 244.944C364.419 249.684 356.476 252.971 347.387 254.806V257.099C357.469 257.482 366.633 260.043 374.882 264.783C383.13 269.446 389.698 275.906 394.586 284.163C399.474 292.343 401.918 301.976 401.918 313.061C401.918 325.905 398.557 337.334 391.836 347.349C385.192 357.364 375.722 365.239 363.426 370.973C351.13 376.706 336.466 379.573 319.435 379.573H209ZM272.695 328.657H302.48C313.172 328.657 321.192 326.669 326.538 322.694C331.884 318.642 334.557 312.679 334.557 304.804C334.557 299.3 333.297 294.637 330.776 290.814C328.256 286.991 324.667 284.086 320.008 282.099C315.425 280.111 309.888 279.117 303.397 279.117H272.695V328.657ZM272.695 239.669H298.814C304.39 239.669 309.316 238.789 313.592 237.031C317.869 235.273 321.192 232.75 323.559 229.462C326.003 226.099 327.225 222.008 327.225 217.192C327.225 209.929 324.628 204.387 319.435 200.564C314.242 196.665 307.674 194.716 299.731 194.716H272.695V239.669Z" fill="#FCFCFC"/>
<path d="M424.486 379.573V144.717H488.181V328.198H575.049V379.573H424.486Z" fill="#FCFCFC"/>
<path d="M727.27 218.109C726.659 210.464 723.795 204.501 718.678 200.22C713.637 195.939 705.962 193.798 695.651 193.798C689.083 193.798 683.699 194.601 679.498 196.206C675.374 197.735 672.319 199.838 670.334 202.513C668.348 205.189 667.317 208.247 667.241 211.688C667.088 214.516 667.584 217.077 668.73 219.371C669.952 221.588 671.861 223.614 674.458 225.449C677.055 227.207 680.377 228.813 684.425 230.265C688.472 231.718 693.284 233.017 698.859 234.164L718.105 238.292C731.088 241.045 742.201 244.676 751.442 249.187C760.683 253.697 768.244 259.011 774.124 265.127C780.005 271.166 784.32 277.97 787.07 285.539C789.895 293.108 791.346 301.364 791.423 310.309C791.346 325.752 787.49 338.825 779.852 349.528C772.215 360.231 761.294 368.373 747.088 373.954C732.959 379.535 715.966 382.325 696.11 382.325C675.718 382.325 657.923 379.306 642.725 373.266C627.603 367.226 615.842 357.938 607.441 345.4C599.116 332.785 594.916 316.654 594.839 297.007H655.327C655.708 304.193 657.503 310.232 660.711 315.125C663.918 320.018 668.424 323.726 674.229 326.249C680.109 328.772 687.098 330.033 695.193 330.033C701.99 330.033 707.68 329.192 712.262 327.51C716.845 325.828 720.32 323.497 722.687 320.515C725.055 317.534 726.277 314.131 726.353 310.309C726.277 306.716 725.093 303.581 722.802 300.906C720.587 298.153 716.921 295.707 711.804 293.566C706.687 291.349 699.775 289.285 691.069 287.374L667.699 282.328C646.926 277.817 630.544 270.287 618.553 259.737C606.639 249.11 600.72 234.623 600.796 216.275C600.72 201.367 604.691 188.332 612.71 177.17C620.806 165.932 631.995 157.178 646.276 150.909C660.634 144.64 677.093 141.506 695.651 141.506C714.592 141.506 730.974 144.679 744.797 151.024C758.621 157.369 769.275 166.314 776.759 177.858C784.32 189.326 788.139 202.743 788.215 218.109H727.27Z" fill="#FCFCFC"/>
<path d="M1142.29 284.637C1095.4 234.764 1069.33 169.175 1069.33 101.059V89.1151C1069.34 87.6533 1068.9 86.2241 1068.07 85.0142C1067.24 83.8041 1066.06 82.8696 1064.69 82.3324C1009.11 60.7152 947.377 60.3857 891.565 81.4083C890.221 81.9288 889.063 82.83 888.235 83.999C887.407 85.1678 886.946 86.5525 886.91 87.9795L958.557 228.3C962.21 225.844 966.332 224.153 970.67 223.333C975.008 222.512 979.47 222.578 983.78 223.529C988.091 224.479 992.159 226.292 995.735 228.858C999.31 231.423 1002.32 234.684 1004.57 238.444C1006.82 242.202 1008.28 246.377 1008.84 250.712C1009.4 255.046 1009.05 259.448 1007.83 263.647C1006.61 267.845 1004.52 271.75 1001.72 275.124C998.909 278.497 995.433 281.265 991.503 283.259C990.178 283.913 989.06 284.918 988.278 286.162C987.495 287.406 987.077 288.842 987.07 290.308V531.664C987.07 532.603 987.265 533.531 987.644 534.392C988.023 535.254 988.578 536.027 989.274 536.666C989.969 537.304 990.791 537.795 991.686 538.106C992.583 538.416 993.533 538.54 994.479 538.47H994.559C995.922 538.369 997.224 537.873 998.303 537.044C999.383 536.215 1000.19 535.091 1000.63 533.81C1031.19 444.852 1084.54 362.552 1142.52 294.796C1143.75 293.367 1144.4 291.545 1144.36 289.674C1144.32 287.801 1143.58 286.009 1142.29 284.637Z" fill="url(#paint0_linear_912_2145)"/>
<path d="M1064.73 82.3345C1009.14 60.6942 947.382 60.3563 891.548 81.3868C890.205 81.9074 889.047 82.8085 888.219 83.9775C887.391 85.1463 886.93 86.531 886.894 87.958C884.519 160.899 864.991 233.535 811.653 286.393C810.236 287.81 809.441 289.723 809.441 291.716C809.441 293.708 810.236 295.621 811.653 297.038C877.664 363.932 923.267 446.53 953.973 533.631C954.449 534.981 955.313 536.165 956.462 537.032C957.611 537.9 958.991 538.414 960.433 538.51H960.511C961.514 538.577 962.518 538.439 963.464 538.106C964.41 537.773 965.276 537.25 966.01 536.571C966.742 535.893 967.327 535.072 967.727 534.161C968.127 533.25 968.332 532.267 968.332 531.274V295.15C968.545 256.478 977.308 218.323 994.007 183.358C1010.71 148.393 1034.93 117.468 1064.99 92.7512C1066.58 91.4747 1067.99 90.2606 1069.32 89.1172C1069.36 87.6569 1068.93 86.2226 1068.11 85.0091C1067.29 83.7956 1066.11 82.8618 1064.73 82.3345Z" fill="url(#paint1_linear_912_2145)"/>
<path d="M254.22 546.761L212.184 404.604H255.334L274.821 492.064H275.934L299.04 404.604H333.003L356.109 492.342H357.223L376.71 404.604H419.86L377.824 546.761H340.798L316.579 467.353H315.465L291.245 546.761H254.22Z" fill="#FCFCFC"/>
<path d="M452.762 546.761H411.004L458.051 404.604H510.945L557.992 546.761H516.234L485.055 443.752H483.941L452.762 546.761ZM444.967 490.676H523.472V519.551H444.967V490.676Z" fill="#FCFCFC"/>
<path d="M571.72 546.761V404.604H610.416V515.664H668.042V546.761H571.72Z" fill="#FCFCFC"/>
<path d="M683.684 546.761V404.604H722.379V515.664H780.005V546.761H683.684Z" fill="#FCFCFC"/>
<path d="M795.647 546.761V404.604H898.372V435.701H834.343V460.134H893.083V491.231H834.343V515.664H898.093V546.761H795.647Z" fill="#FCFCFC"/>
<path d="M913.666 435.701V404.604H1037.55V435.701H994.676V546.761H956.537V435.701H913.666Z" fill="#FCFCFC"/>
<defs>
<linearGradient id="paint0_linear_912_2145" x1="1015.62" y1="538.493" x2="1015.62" y2="65.885" gradientUnits="userSpaceOnUse">
<stop stop-color="#196DD2"/>
<stop offset="1" stop-color="#0D40A1"/>
</linearGradient>
<linearGradient id="paint1_linear_912_2145" x1="939.368" y1="65.8869" x2="939.368" y2="538.566" gradientUnits="userSpaceOnUse">
<stop stop-color="#1E7EE5"/>
<stop offset="1" stop-color="#196DD2"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

View File

@@ -1,139 +0,0 @@
# 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
### Required
- [NodeJS](https://nodejs.org)
- [Yarn](https://yarnpkg.com/getting-started/install) (`npm install -g yarn`)
- [Deno](https://deno.land/#installation)
### Optional (Recomended)
- [nvm](https://github.com/nvm-sh/nvm#installing-and-updating)
- [docker-compose](https://docs.docker.com/compose/install/)
## 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. If you choose to run on Arbitrum Goerli skip ahead until tests.
### Chain (RPC Node)
Start a local Hardhat node for RPC use.
```sh
cd ./contracts
yarn hardhat node
```
### Contracts
Fund the `create2Deployer`.
```sh
yarn hardhat fundDeployer --network gethDev
```
Deploy all `bls-wallet` contracts.
```sh
yarn hardhat run scripts/deploy_all.ts --network gethDev
```
## Aggregator
make these changes in aggregator > .env
RPC_URL=http://localhost:8545
NETWORK_CONFIG_PATH=../contracts/networks/local.json
```sh
docker-compose up -d postgres # Or see local postgres instructions in ./aggregator/README.md#PostgreSQL
cd ./aggregator
./programs/aggregator.ts
```
In a seperate terminal/shell instance
```sh
cd ./extension
yarn run dev:chrome # or dev:firefox, dev:opera
```
## 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).
2. Enable `Developer mode`.
3. Either click `Load unpacked extension...` and select `./extension/extension/chrome` or drag that folder into the page.
### Firefox
1. Go to Firefox's [debugging page](about:debugging#/runtime/this-firefox).
2. Click `Load Temporary Add-on...`.
3. Select `./extension/extension/firefox/manifest.json`.
### Tests
See each components `README.md` for how to run tests.
## Testing/using updates to ./clients
### extension
```sh
cd ./contracts/clients
yarn build
yarn link
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.
You will need write access to the npmjs project to do this. You can request access or request one of the BLS Wallet project developers push up your client changes in the `Discussions` section of this repo.
In `./contracts/clients` with your changes:
```
yarn publish-experimental
```
Note the `x.y.z-abc1234` version that was output.
Then in `./aggregtor/deps.ts`, change all `from` references for that package.
```typescript
...
} from "https://esm.sh/bls-wallet-clients@x.y.z-abc1234";
...
```

View File

@@ -1,105 +0,0 @@
# Remote Development
These steps will setup this repo on your machine for targeting a remote chain, such as an EVM compatible L2.
Follow the instructions for [Local Development](./local_development.md), replacing the sections titled `Chain` and `Contracts` with the steps below.
## Deploy Contracts
### 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.
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.
### Update hardhat.config.ts
If your network is not listed in [hardhat.config.ts](../contracts/hardhat.config.ts), you will need to add it.
### Precompile Cost Estimator
If your network does not already have an instance of the [BNPairingPrecompileCostEstimator contract](../contracts/contracts/lib/hubble-contracts/contracts/libs/BNPairingPrecompileCostEstimator.sol), you will need to deploy that.
```sh
cd ./contracts
yarn hardhat run scripts/0_deploy_precompile_cost_estimator.ts --network YOUR_NETWORK
```
Copy the address that is output.
Update `./contracts/contracts/lib/hubble-contracts/contracts/libs/BLS.sol`'s `COST_ESTIMATOR_ADDRESS` to the value of that address if it is different:
```solidity
...
address private constant COST_ESTIMATOR_ADDRESS = YOUR_NETWORKS_PRECOMPILE_COST_ESTIMATOR_ADDRESS;
...
```
### Remaining Contracts
Deploy all remaining `bls-wallet` contracts.
```sh
cd ./contracts # if not already there
yarn hardhat run scripts/deploy_all.ts --network YOUR_NETWORK
```
A network config file will be generated at `./contracts/networks/local.json`. You should rename it to match your network.
```sh
mv ./networks/local.json ./networks/your-network.json
```
This file can be commited so others can use your deployed contracts.
## Remote RPC
### Aggregator
Update these values in `./aggregator/.env`.
PK0 & PK1 are private keys for funded accounts on your network/chain.
```
RPC_URL=https://your.network.rpc
...
NETWORK_CONFIG_PATH=../contracts/networks/your-network.json
PRIVATE_KEY_AGG=PK0
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.
Then, update this value in `./extension/.env`.
```
...
DEFAULT_CHAIN_ID=YOUR_CHAIN_ID
...
```
## 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.
Update these values in `./aggregator/.env`.
```
RPC_URL=https://rinkeby.arbitrum.io/rpc
...
NETWORK_CONFIG_PATH=../contracts/networks/arbitrum-testnet.json
PRIVATE_KEY_AGG=PK0
PRIVATE_KEY_ADMIN=PK1
...
```
And then update this value in `./extension/.env`.
```
...
DEFAULT_CHAIN_ID=421611
...
```

View File

@@ -1,21 +0,0 @@
# System Overview
## Presentations
- [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
![System Overview](./images/system-overview/system-overview.svg)
## Actions, Bundles, & Aggregator
![Action](./images/system-overview/action-0.jpg)
![Operation](./images/system-overview/operation-1.jpg)
![Bundle](./images/system-overview/bundle-2.jpg)
![Interaction](./images/system-overview/interaction-3.jpg)

View File

@@ -1,95 +0,0 @@
# Use BLS Wallet Client
This walkthrough will show you how to submit an ERC20 transfer to the BLS Wallet Aggregator.
## Add bls-wallet-clients
```sh
# npm
npm install bls-wallet-clients
# yarn
yarn install 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";
```
### Deno
You can use [esm.sh](https://esm.sh/) or a similar service to get Deno compatible modules.
```typescript
import { providers } from "https://esm.sh/ethers@latest";
import { Aggregator, BLSWalletWrapper, getConfig } from "https://esm.sh/bls-wallet-clients@latest";
```
## Get Deployed Contract Addresses
You can find current contract deployments in the [contracts networks folder](../contracts/networks/).
If you would like to deploy locally, see [Local development](./local_development.md).
If you would like to deploy to a remote network, see [Remote development](./remote_development.md).
## Send a transaction
```typescript
import { readFile } from "fs/promises";
// Instantiate a provider via browser extension, such as Metamask
const provider = providers.Web3Provider(window.ethereum);
// Or via RPC
const provider = providers.JsonRpcProvider();
// 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 privateKey = "0x...";
// Note that if a wallet doesn't yet exist, it will be
// lazily created on the first transaction.
const wallet = await BlsWallerWrapper.connect(
privateKey,
netCfg.contracts.verificationGateway,
provider
);
const erc20Address = "0x...";
const erc20Abi = [
"function transfer(address to, uint amount) returns (bool)",
];
const erc20 = new ethers.Contract(erc20Address, erc20Abi, provider);
const recipientAddress = "0x...";
const nonce = await wallet.Nonce();
// All of the actions in a bundle are atomic, if one
// action fails they will all fail.
const bundle = wallet.sign({
nonce,
actions: [
{
contract: erc20,
method: "transfer",
args: [recipientAddress, ethers.utils.parseUnits("1", 18)],
},
],
});
const aggregator = new Aggregator("https://rinkarby.blswallet.org");
await aggregator.add(bundle);
```
## More
See [clients](../contracts/clients/) for additional functionality.

View File

@@ -1,119 +0,0 @@
# 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 = 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
## Coming soon
- Gasless transaction example.

View File

@@ -8,8 +8,10 @@
// Do not transform modules to CJS
"modules": false,
"targets": {
"chrome": "92",
"firefox": "90"
"chrome": "49",
"firefox": "52",
"opera": "36",
"edge": "79"
}
}
],
@@ -17,7 +19,26 @@
["@babel/react", { "runtime": "automatic" }]
],
"plugins": [
"@babel/plugin-proposal-class-properties",
"@babel/plugin-proposal-private-methods"
["@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
}
]
]
}

6
extension/.env.example Normal file
View File

@@ -0,0 +1,6 @@
PRIVATE_KEY_STORAGE_KEY=default-private-key
AGGREGATOR_URL=http://localhost:3000
DEFAULT_CHAIN_ID=31337
CREATE_TX_URL=
ETHERSCAN_KEY=
CRYPTO_COMPARE_API_KEY=

File diff suppressed because it is too large Load Diff

65
extension/.eslintrc.json Normal file
View File

@@ -0,0 +1,65 @@
{
"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
}
}
}

View File

@@ -206,5 +206,3 @@ dist/
# Non-template additions
.env*
!.env.example
/build
/config.json

View File

@@ -0,0 +1,11 @@
{
"bracketSpacing": true,
"jsxBracketSameLine": false,
"printWidth": 80,
"semi": true,
"singleQuote": true,
"tabWidth": 2,
"trailingComma": "all",
"useTabs": false,
"proseWrap": "always"
}

View File

@@ -1,27 +0,0 @@
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),
);

View File

@@ -1,91 +0,0 @@
{
"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://localhost: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
}
}

View File

@@ -1,91 +0,0 @@
{
"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://localhost: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
}
}

View File

@@ -2,7 +2,6 @@
"name": "bls-wallet-extension",
"version": "0.1.0",
"description": "Web extension for managing a BLS wallet",
"license": "MIT",
"private": true,
"repository": "https://github.com/jzaki/bls-wallet-extension.git",
"author": {
@@ -11,7 +10,8 @@
"url": "https://blswallet.org"
},
"engines": {
"yarn": ">=1.0.0"
"node": ">=16.0.0",
"yarn": ">= 1.0.0"
},
"scripts": {
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch",
@@ -21,8 +21,7 @@
"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": "node buildMultiNetworkConfig.js && tsc --noEmit",
"lint": "./scripts/addFileStubsIfNeeded.sh && eslint . --ext .ts,.tsx,.js --max-warnings 0",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix"
},
"resolutions": {
@@ -31,89 +30,95 @@
"dependencies": {
"@babel/runtime": "^7.16.3",
"@ethereumjs/tx": "^3.5.1",
"@tanstack/react-table": "^8.2.3",
"@types/bs58check": "^2.1.0",
"@toruslabs/openlogin-jrpc": "^1.7.2",
"advanced-css-reset": "^1.2.2",
"async-mutex": "^0.3.2",
"axios": "^0.27.2",
"bls-wallet-clients": "0.7.3",
"browser-passworder": "^2.0.3",
"bs58check": "^2.1.2",
"crypto-browserify": "^3.12.0",
"axios": "^0.25.0",
"bls-wallet-clients": "0.6.0",
"dotenv-webpack": "^7.0.3",
"emoji-log": "^1.0.2",
"end-of-stream": "^1.4.4",
"eth-query": "^2.1.2",
"eth-rpc-errors": "^4.0.3",
"ethers": "5.6.9",
"ethers": "5.5.4",
"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",
"qrcode.react": "^3.1.0",
"pump": "^3.0.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",
"stream-browserify": "^3.0.0",
"typed-emitter": "^2.1.0",
"typescript": "^4.7.4",
"typed-emitter": "^1.4.0",
"typescript": "^4.5.3",
"webext-base-css": "^1.3.2",
"webextension-polyfill": "^0.9.0",
"webextension-polyfill": "^0.8.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/webextension-polyfill": "^0.9.0",
"@types/webpack": "^5.28.0",
"@types/readable-stream": "^2.3.12",
"@types/webextension-polyfill": "^0.8.2",
"@types/webpack": "^4.41.29",
"@types/zxcvbn": "^4.4.1",
"@typescript-eslint/eslint-plugin": "^5.30.4",
"@typescript-eslint/parser": "^5.30.4",
"@typescript-eslint/eslint-plugin": "^5.6.0",
"@typescript-eslint/parser": "^5.6.0",
"autoprefixer": "^10.4.2",
"babel-loader": "^8.2.5",
"clean-webpack-plugin": "^4.0.0",
"copy-webpack-plugin": "^11.0.0",
"babel-loader": "^8.2.3",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^6.4.1",
"cross-env": "^7.0.3",
"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",
"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",
"eslint-plugin-node": "^11.1.0",
"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",
"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",
"postcss": "^8.4.5",
"postcss-loader": "^7.0.0",
"postcss-loader": "^4.3.0",
"prettier": "^2.5.1",
"resolve-url-loader": "^5.0.0",
"sass-loader": "^13.0.1",
"resolve-url-loader": "^3.1.3",
"sass-loader": "^10.2.0",
"tailwindcss": "^3.0.13",
"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",
"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",
"wext-manifest-webpack-plugin": "^1.2.1"
}
}

View File

@@ -1,15 +0,0 @@
#!/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

View File

@@ -1,35 +0,0 @@
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;

View File

@@ -1,151 +1,67 @@
import { FunctionComponent, useState } from 'react';
import { ethers } from 'ethers';
import { FunctionComponent, useEffect, useMemo, 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 {
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 CompactQuillHeading from '../components/CompactQuillHeading';
import { DEFAULT_CHAIN_ID_HEX } from '../env';
import { useInputDecode } from '../hooks/useInputDecode';
import formatCompactAddress from '../Popup/helpers/formatCompactAddress';
const Confirm: FunctionComponent = () => {
const quill = useQuill();
const [id, setId] = useState<string>();
const [to, setTo] = useState<string>('0x');
const [value, setValue] = useState<string>('0');
const [data, setData] = useState<string>('0x');
const id = new URL(window.location.href).searchParams.get('id');
const transactions = useCell(quill.cells.transactions);
// 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 [current, setCurrent] = useState<number>(0);
const cleanupTasks = useMemo(() => new TaskQueue(), []);
if (transactions === undefined) {
return <Loading />;
}
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');
const tx = transactions.outgoing.find((t) => t.id === id);
return cleanupTasks.run();
}, [cleanupTasks]);
if (tx === undefined) {
return <>Error: Tx not found</>;
}
const respondTx = (result: PromptMessage['result']) => {
const respondTx = (result: string) => {
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="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 className="confirm">
<div className="section">
<CompactQuillHeading />
</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 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>
</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>
<Button className="btn-primary" onPress={() => respondTx('Yes')}>
Confirm
</Button>
<Button className="btn-secondary" onPress={() => respondTx('No')}>
Reject
</Button>
</>
)}
<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',
'flex',
'justify-between',
'h-20',
].join(' ')}
>
<div className="">Total Transaction Fees</div>
<div className="text-right">
<div className="font-bold">USD $0.0</div>
<div className="">{calculateTotal(tx.actions)} ETH</div>
</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>
);

View File

@@ -1,70 +0,0 @@
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 } = useInputDecode(data || '0x', to);
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-normal">{data}</div>
</div>
</div>
<div className="flex mt-6 gap-3">
<div className="w-60 border-r border-grey-400">
<div>ETH Value</div>
<div className="break-all text-[9.5pt] font-bold">
{ethers.utils.formatEther(value || '0x0')}
</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;

View File

@@ -1,21 +1,7 @@
import '../styles/index.scss';
import ReactDOM from 'react-dom';
import Confirm from './Confirm';
import './styles.scss';
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'),
);
ReactDOM.render(<Confirm />, document.getElementById('confirm-root'));

View File

@@ -0,0 +1,116 @@
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();
}

View File

@@ -0,0 +1,145 @@
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;

View File

@@ -0,0 +1,37 @@
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
}

View File

@@ -0,0 +1,135 @@
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;

View File

@@ -0,0 +1,191 @@
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>);
}
}

View File

@@ -0,0 +1,23 @@
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;

View File

@@ -0,0 +1,98 @@
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;

View File

@@ -0,0 +1,127 @@
import CellCollection from '../../cells/CellCollection';
import ICell from '../../cells/ICell';
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=${
process.env.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);
}
}

View File

@@ -0,0 +1,17 @@
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;
}

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