36 Commits

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

* Fixes, add iteration

* Use default to ensure there's always a value

* Define IReadableCell, split out ReadableCellIterator

* FormulaCell

* Remove unused functions

* Rename to cells/ExtensionLocalStorage.ts

* StorageCell -> ExtensionLocalCell

* Split out ICell

* Split out CellIterator

* MemoryCell.ts

* Split out FormulaCell

* ReadableCellIterator -> CellIterator

* Define IAsyncStorage abstraction and use it to generalize ExtensionLocalStorage

* MemoryCellCollection.ts

* Enable removing cells

* useCell.ts, useReadableCell.ts

* Add cells demo

* CellDisplay

* Add counters

* Allow mixing unknowns in CellCollection

* Quill cells

* Allow async formulas

* Use a cell for CurrencyController.state

* Use versionedType when reading

* QuillContext

* Fix window.QuillController() in WalletWrapper

* Replace internal rpc with public+private

* Move eth_accounts into typed rpc

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

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

* Fix .KeyringController in WalletPage

* Fix CellIterator undefined bug

* useNewCell

* Avoid useCellWithFallback

* Remove useNewCell

* useCellState

* tmp

* Add events to IAsyncStorage, change extensionLocalStorage to singleton

* Use change events in CellCollection

* Allow cleaning up the change handler

* localCellCollection

* Use extensionLocalCellCollection in demo

* Fix demo page

* Make FormulaCell lazy

* Stop iteration more actively

* Fix cleanup of end handler

* Use elcc

* Add blockNumber cell

* Page selection, ethersProvider

* nitpicks

* Balance demo

* More event cleanup fixes

* Improve label

* Add dark-theme

* Remove obsoleted rpc-based cell collection

* Cleanup

* DemoTable

* Add cells readme

* Fix theme persistence

* Remove regex check

* Avoid casting window

* Use ChangeEvent to avoid cast

* Move cells demo into its own page

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

View File

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

View File

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

22
.github/labeler.yml vendored
View File

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

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

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

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

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

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

@@ -0,0 +1,78 @@
name: aggregator
on:
push:
branches:
- 'main'
paths:
- 'aggregator/**'
pull_request:
branches:
- 'main'
paths:
- 'aggregator/**'
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.example .env
- run: deno test --allow-net --allow-env --allow-read --unstable
# Cleanup
- working-directory: ./
run: docker-compose down

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

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

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

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

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

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

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

@@ -0,0 +1,52 @@
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
# Valid network config not needed to test build.
- working-directory: ./contracts/networks
run: echo "{}" > "local.json"
- run: cp .env.example .env
- 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@main
- uses: actions/labeler@v4
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

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

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

60
CONTRIBUTING.md Normal file
View File

@@ -0,0 +1,60 @@
# 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 Normal file
View File

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

180
README.md
View File

@@ -1,173 +1,45 @@
# bls-wallet
![BLS Wallet](./docs/images/bls-github-banner.svg)
An Ethereum Layer 2 smart contract wallet that uses [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature) and aggregated transactions to reduce gas costs.
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)
- Setup the BLS Wallet components for:
- [Local develeopment](./docs/local_development.md)
- [Remote development](./docs/remote_development.md)
## Components
See each component's directory `README` for more details.
[contracts](./contracts/)
![System Overview](images/system-overview.svg)
Solidity smart contracts for wallets, BLS signature verification, and deployment/testing tools.
### Aggregator
[aggregator](./aggregator/)
Service which aggregates BLS wallet transactions.
Service which accepts BLS signed transactions and bundles them into one for submission.
### Clients
[aggregator-proxy](./aggregator-proxy/)
TS/JS Client libraries for web apps and services.
npm package for proxying to another aggregator instance.
### Contracts
[bls-wallet-clients](./contracts/clients/)
`bls-wallet` Solidity contracts.
npm package which provides easy to use constructs to interact with the contracts and aggregator.
### Extension
[extension](./extension/)
Quill browser extension used to manage BLS Wallets and sign transactions.
Prototype browser extension used to manage BLS Wallets and sign transactions.
### Signer
## Ways to Contribute
TS/JS BLS Signing lib.
- [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)
## 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";
...
```
See our [contribution instructions & guidelines](./CONTRIBUTING.md) for more details.

View File

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

View File

@@ -2,10 +2,14 @@
"name": "bls-wallet-aggregator-proxy",
"version": "0.1.1",
"main": "dist/src/index.js",
"repository": "https://github.com/web3well/bls-wallet",
"repository": "https://github.com/web3well/bls-wallet/aggregator-proxy",
"author": "Andrew Morris",
"license": "MIT",
"private": false,
"engines": {
"node": ">=16.0.0",
"yarn": ">=1.0.0"
},
"scripts": {
"build": "rm -rf dist && tsc"
},

View File

@@ -6,8 +6,8 @@ ORIGIN=http://localhost:3000
PORT=3000
NETWORK_CONFIG_PATH=../contracts/networks/local.json
PRIVATE_KEY_AGG=0x0000000000000000000000000000000000000000000000000000000000000a99
PRIVATE_KEY_ADMIN=
PRIVATE_KEY_AGG=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
PRIVATE_KEY_ADMIN=0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d
TEST_BLS_WALLETS_SECRET=test-bls-wallets-secret
PG_HOST=localhost

View File

@@ -8,7 +8,7 @@ Verification Gateway.
## Installation
Install [Deno](deno.land).
Install [Deno](deno.land)
### Configuration
@@ -168,6 +168,12 @@ 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?
### Notable Components
- **src/chain**: Should contain all of the contract interactions, exposing more

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -91,10 +91,6 @@ 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,5 +1,7 @@
# 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
@@ -18,20 +20,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.addTransaction(...);
await aggregator.add(...);
```
## BlsWalletWrapper
Wraps a BLS wallet, storing the private key and providing `.sign(...)` to
produce a `Bundle`, that can be used with `aggregator.addTransaction(...)`.
produce a `Bundle`, that can be used with `aggregator.add(...)`.
```ts
import { BlsWalletWrapper } from 'bls-wallet-clients';
@@ -46,16 +48,19 @@ const bundle = wallet.sign({
nonce: await wallet.Nonce(),
actions: [
{
contract: someToken, // An ethers.Contract
method: 'transfer',
args: [recipientAddress, ethers.utils.parseUnits('1', 18)],
ethValue: 0,
contractAddress: someToken.address, // An ethers.Contract
encodedFunction: someToken.interface.encodeFunctionData(
"transfer",
["0x...some address...", ethers.BigNumber.from(1).pow(18)],
),
},
// Additional actions can go here. When using multiple actions, they'll
// either all succeed or all fail.
],
});
await aggregator.addTransaction(bundle);
await aggregator.add(bundle);
```
## VerificationGateway
@@ -118,3 +123,32 @@ 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

@@ -8,11 +8,16 @@
"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",
"pretest": "yarn build",
"test": "mocha dist/**/*.test.js",
"premerge": "yarn build && yarn test",
"premerge": "yarn test",
"publish-experimental": "node scripts/showVersion.js >.version && npm version $(node scripts/showBaseVersion.js)-$(git rev-parse HEAD | head -c7) --allow-same-version && npm publish --tag experimental && npm version $(cat .version) && rm .version",
"publish-experimental-dry-run": "node scripts/showVersion.js >.version && npm version $(node scripts/showBaseVersion.js)-$(git rev-parse HEAD | head -c7) --allow-same-version && npm publish --tag experimental --dry-run && npm version $(cat .version) && rm .version"
},

View File

@@ -11,7 +11,7 @@ import "./interfaces/IWallet.sol";
/** Minimal upgradable smart contract wallet.
Generic calls can only be requested by its trusted gateway.
*/
contract BLSWallet is Initializable, IBLSWallet
contract BLSWallet is Initializable, IWallet
{
uint256 public nonce;
bytes32 public recoveryHash;
@@ -22,9 +22,6 @@ contract BLSWallet is Initializable, IBLSWallet
uint256 pendingPAFunctionTime;
// BLS variables
uint256[4] public blsPublicKey;
uint256[4] pendingBLSPublicKey;
uint256 pendingBLSPublicKeyTime;
address public trustedBLSGateway;
address pendingBLSGateway;
uint256 pendingGatewayTime;
@@ -32,9 +29,6 @@ contract BLSWallet is Initializable, IBLSWallet
event PendingRecoveryHashSet(
bytes32 pendingRecoveryHash
);
event PendingBLSKeySet(
uint256[4] pendingBLSKey
);
event PendingGatewaySet(
address pendingGateway
);
@@ -46,10 +40,6 @@ contract BLSWallet is Initializable, IBLSWallet
bytes32 oldHash,
bytes32 newHash
);
event BLSKeySet(
uint256[4] oldBLSKey,
uint256[4] newBLSKey
);
event GatewayUpdated(
address oldGateway,
address newGateway
@@ -63,38 +53,11 @@ contract BLSWallet is Initializable, IBLSWallet
) external initializer {
nonce = 0;
trustedBLSGateway = blsGateway;
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
*/
@@ -111,16 +74,6 @@ contract BLSWallet is Initializable, IBLSWallet
}
}
/**
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
*/
@@ -143,51 +96,45 @@ contract BLSWallet is Initializable, IBLSWallet
Set results of any pending set operation if their respective timestamp has elapsed.
*/
function setAnyPending() public {
if (block.timestamp > pendingRecoveryHashTime) {
if (pendingRecoveryHashTime != 0 &&
block.timestamp > pendingRecoveryHashTime
) {
bytes32 previousRecoveryHash = recoveryHash;
recoveryHash = pendingRecoveryHash;
clearPendingRecoveryHash();
emit RecoveryHashUpdated(previousRecoveryHash, recoveryHash);
}
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) {
if (pendingGatewayTime != 0 &&
block.timestamp > pendingGatewayTime
) {
address previousGateway = trustedBLSGateway;
trustedBLSGateway = pendingBLSGateway;
pendingGatewayTime = type(uint256).max;
pendingGatewayTime = 0;
pendingBLSGateway = address(0);
emit GatewayUpdated(previousGateway, trustedBLSGateway);
}
if (block.timestamp > pendingPAFunctionTime) {
if (
pendingPAFunctionTime != 0 &&
block.timestamp > pendingPAFunctionTime
) {
approvedProxyAdminFunctionHash = pendingPAFunctionHash;
pendingPAFunctionTime = type(uint256).max;
pendingPAFunctionTime = 0;
pendingPAFunctionHash = 0;
emit ProxyAdminFunctionHashApproved(approvedProxyAdminFunctionHash);
}
}
function clearPendingRecoveryHash() internal {
pendingRecoveryHashTime = type(uint256).max;
pendingRecoveryHashTime = 0;
pendingRecoveryHash = bytes32(0);
}
function recover(
uint256[4] calldata newBLSKey
) public onlyTrustedGateway {
// set new bls key
blsPublicKey = newBLSKey;
function recover() public onlyTrustedGateway {
// clear any pending operations
clearPendingRecoveryHash();
pendingBLSPublicKeyTime = type(uint256).max;
pendingBLSPublicKey = [0,0,0,0];
pendingGatewayTime = type(uint256).max;
pendingGatewayTime = 0;
pendingBLSGateway = address(0);
pendingPAFunctionTime = type(uint256).max;
pendingPAFunctionTime = 0;
pendingPAFunctionHash = 0;
}

View File

@@ -22,11 +22,16 @@ contract VerificationGateway
bytes32 BLS_DOMAIN = keccak256(abi.encodePacked(uint32(0xfeedbee5)));
uint8 constant BLS_KEY_LEN = 4;
IBLS public blsLib;
IBLS public immutable blsLib;
ProxyAdmin public immutable walletProxyAdmin;
address public blsWalletLogic;
mapping(bytes32 => IWallet) externalWalletsFromHash;
address public immutable blsWalletLogic;
mapping(bytes32 => IWallet) public walletFromHash;
mapping(IWallet => bytes32) public hashFromWallet;
//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 {
@@ -46,27 +51,36 @@ contract VerificationGateway
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 blsWalletImpl,
address proxyAdmin
) {
blsLib = bls;
blsWalletLogic = blsWalletImpl;
walletProxyAdmin = new ProxyAdmin();
walletProxyAdmin = ProxyAdmin(proxyAdmin);
}
/** Throw if bundle not valid or signature verification fails */
function verify(
Bundle calldata bundle
Bundle memory bundle
) public view {
uint256 opLength = bundle.operations.length;
require(
opLength == bundle.senderPublicKeys.length,
"VG: Sender and operation length mismatch"
"VG: Sender/op length mismatch"
);
uint256[2][] memory messages = new uint256[2][](opLength);
@@ -81,37 +95,7 @@ contract VerificationGateway
messages
);
require(verified, "VG: All sigs not verified");
}
/**
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];
}
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));
require(verified, "VG: Sig not verified");
}
/**
@@ -123,11 +107,42 @@ contract VerificationGateway
@param messageSenderSignature signature of message containing only the calling address
@param publicKey that signed the caller's address
*/
function setExternalWallet(
uint256[2] calldata messageSenderSignature,
uint256[BLS_KEY_LEN] calldata publicKey
function setBLSKeyForWallet(
uint256[2] memory messageSenderSignature,
uint256[BLS_KEY_LEN] memory publicKey
) public {
safeSetWallet(messageSenderSignature, publicKey, msg.sender);
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];
}
}
/**
@@ -138,9 +153,9 @@ contract VerificationGateway
*/
function walletAdminCall(
bytes32 hash,
bytes calldata encodedFunction
bytes memory 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));
@@ -179,20 +194,18 @@ contract VerificationGateway
@param newBLSKey to set as the wallet's bls public key
*/
function recoverWallet(
uint256[2] calldata walletAddressSignature,
uint256[2] memory walletAddressSignature,
bytes32 blsKeyHash,
bytes32 salt,
uint256[BLS_KEY_LEN] calldata newBLSKey
uint256[BLS_KEY_LEN] memory newBLSKey
) public {
IWallet wallet = walletFromHash(blsKeyHash);
IWallet wallet = walletFromHash[blsKeyHash];
bytes32 recoveryHash = keccak256(
abi.encodePacked(msg.sender, blsKeyHash, salt)
);
if (recoveryHash == wallet.recoveryHash()) {
// override mapping of old key hash (takes precedence over create2 address)
externalWalletsFromHash[blsKeyHash] = IWallet(0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF);
safeSetWallet(walletAddressSignature, newBLSKey, address(wallet));
wallet.recover(newBLSKey);
safeSetWallet(walletAddressSignature, newBLSKey, wallet);
wallet.recover();
}
}
@@ -211,7 +224,7 @@ contract VerificationGateway
"BLSWallet: gateway address param not valid"
);
IWallet wallet = walletFromHash(hash);
IWallet wallet = walletFromHash[hash];
require(
VerificationGateway(blsGateway).walletFromHash(hash) == wallet,
@@ -234,7 +247,7 @@ contract VerificationGateway
Can be called with a single operation with no actions.
*/
function processBundle(
Bundle calldata bundle
Bundle memory bundle
) external returns (
bool[] memory successes,
bytes[][] memory results
@@ -271,18 +284,18 @@ contract VerificationGateway
needed.
*/
function getOrCreateWallet(
uint256[BLS_KEY_LEN] calldata publicKey
uint256[BLS_KEY_LEN] memory publicKey
) private returns (IWallet) {
bytes32 publicKeyHash = keccak256(abi.encodePacked(publicKey));
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}(
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(blsWalletLogic),
address(walletProxyAdmin),
getInitializeData()
));
IBLSWallet(payable(blsWallet)).latchBLSPublicKey(publicKey);
)));
updateWalletHashMappings(publicKeyHash, blsWallet);
emit WalletCreated(
address(blsWallet),
publicKey
@@ -298,10 +311,11 @@ contract VerificationGateway
@param wallet address to set
*/
function safeSetWallet(
uint256[2] calldata wallletAddressSignature,
uint256[BLS_KEY_LEN] calldata publicKey,
address wallet
uint256[2] memory wallletAddressSignature,
uint256[BLS_KEY_LEN] memory publicKey,
IWallet wallet
) private {
// verify the given wallet was signed for by the bls key
uint256[2] memory addressMsg = blsLib.hashToPoint(
BLS_DOMAIN,
abi.encodePacked(wallet)
@@ -313,14 +327,22 @@ contract VerificationGateway
bytes32 publicKeyHash = keccak256(abi.encodePacked(
publicKey
));
externalWalletsFromHash[publicKeyHash] = IWallet(wallet);
emit BLSKeySetForWallet(publicKey, wallet);
updateWalletHashMappings(publicKeyHash, wallet);
}
function hasCode(address a) private view returns (bool) {
uint256 size;
// solhint-disable-next-line no-inline-assembly
assembly { size := extcodesize(a) }
return size > 0;
/** @dev Only to be called on wallet creation, and in `safeSetWallet` */
function updateWalletHashMappings(
bytes32 publicKeyHash,
IWallet wallet
) private {
// remove reference from old hash
bytes32 oldHash = hashFromWallet[wallet];
walletFromHash[oldHash] = IWallet(address(0));
// update new hash / wallet mappings
walletFromHash[publicKeyHash] = wallet;
hashFromWallet[wallet] = publicKeyHash;
}
function getInitializeData() private view returns (bytes memory) {
@@ -329,19 +351,19 @@ contract VerificationGateway
modifier onlyWallet(bytes32 hash) {
require(
(msg.sender == address(walletFromHash(hash))),
(IWallet(msg.sender) == walletFromHash[hash]),
"VG: not called from wallet"
);
_;
}
function messagePoint(
IWallet.Operation calldata op
IWallet.Operation memory op
) internal view returns (
uint256[2] memory
) {
bytes memory encodedActionData;
IWallet.ActionData calldata a;
IWallet.ActionData memory a;
for (uint256 i=0; i<op.actions.length; i++) {
a = op.actions[i];
encodedActionData = abi.encodePacked(

View File

@@ -28,7 +28,7 @@ interface IWallet {
);
function recoveryHash() external returns (bytes32);
function recover(uint256[4] calldata newBLSKey) external;
function recover() external;
// prepares gateway to be set (after pending timestamp)
function setTrustedGateway(address gateway) external;
@@ -39,16 +39,3 @@ 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

@@ -53,4 +53,12 @@ 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,4 +20,6 @@ interface IBLS {
bytes memory message
) external view returns (uint256[2] memory);
function isZeroBLSKey(uint256[4] memory blsKey) external pure returns (bool);
}

View File

@@ -8,12 +8,13 @@ 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) {
@@ -21,6 +22,21 @@ task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
}
});
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);
@@ -37,11 +53,11 @@ const config: HardhatUserConfig = {
solidity: {
compilers: [
{
version: "0.8.10",
version: "0.8.15",
settings: {
optimizer: {
enabled: true,
runs: 1000,
runs: 1,
},
},
},
@@ -69,6 +85,7 @@ 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: 30_000_000,
},
gethDev: {
url: `http://localhost:8545`,

View File

@@ -3,10 +3,16 @@
"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",
"premerge": "rm -rf artifacts cache typechain && hardhat compile && yarn lint && yarn check-ts && yarn --cwd clients premerge && yarn hardhat test"
"test": "hardhat test",
"premerge": "rm -rf artifacts cache typechain && hardhat compile && lint && check-ts && yarn --cwd clients premerge && test"
},
"author": "James Zaki",
"license": "MIT",

View File

@@ -75,6 +75,8 @@ 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 } from "../../typechain";
import { VerificationGateway, BLSOpen, ProxyAdmin } from "../../typechain";
export default class Fixture {
static readonly ECDSA_ACCOUNTS_LENGTH = 5;
@@ -68,14 +68,20 @@ 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"],
[bls.address, blsWalletImpl.address],
["address", "address", "address"],
[bls.address, blsWalletImpl.address, proxyAdmin.address],
),
)) as VerificationGateway;
await (
await proxyAdmin.transferOwnership(verificationGateway.address)
).wait();
// deploy BLSExpander Gateway
const blsExpander = await create2Fixture.create2Contract(

View File

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

View File

@@ -3,11 +3,11 @@ import { BigNumber } from "ethers";
import { solidityPack } from "ethers/lib/utils";
import { ethers, network } from "hardhat";
import { PublicKey, BlsWalletWrapper, Signature } from "../clients/src";
import { BlsWalletWrapper, Signature } from "../clients/src";
import Fixture from "../shared/helpers/Fixture";
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
import { defaultDeployerAddress } from "../shared/helpers/deployDeployer";
import { BLSWallet } from "../typechain";
import { BLSWallet, VerificationGateway } from "../typechain";
const signWalletAddress = async (
fx: Fixture,
@@ -43,14 +43,18 @@ describe("Recovery", async function () {
const safetyDelaySeconds = 7 * 24 * 60 * 60;
let fx: Fixture;
let wallet1, wallet2, walletAttacker;
let vg: VerificationGateway;
let wallet1: BlsWalletWrapper;
let wallet2: BlsWalletWrapper;
let walletAttacker: BlsWalletWrapper;
let blsWallet: BLSWallet;
let recoverySigner;
let hash1, hash2;
let hash1, hash2, hashAttacker;
let salt;
let recoveryHash;
beforeEach(async function () {
fx = await Fixture.create();
vg = fx.verificationGateway;
wallet1 = await fx.lazyBlsWallets[0]();
wallet2 = await fx.lazyBlsWallets[1]();
@@ -60,6 +64,9 @@ 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"],
@@ -68,34 +75,44 @@ describe("Recovery", async function () {
});
it("should update bls key", async function () {
const newKey: PublicKey = [
BigNumber.from(1),
BigNumber.from(2),
BigNumber.from(3),
BigNumber.from(4),
];
const initialKey = await blsWallet.getBLSPublicKey();
expect(await vg.hashFromWallet(wallet1.address)).to.eql(hash1);
await fx.call(wallet1, blsWallet, "setBLSPublicKey", [newKey], 1);
const addressSignature = await signWalletAddress(
fx,
wallet1.address,
wallet2.privateKey,
);
expect(await blsWallet.getBLSPublicKey()).to.eql(initialKey);
await fx.call(
wallet1,
vg,
"setBLSKeyForWallet",
[addressSignature, wallet2.PublicKey()],
1,
);
await fx.advanceTimeBy(safetyDelaySeconds + 1);
await (await blsWallet.setAnyPending()).wait();
await fx.call(wallet1, vg, "setPendingBLSKeyForWallet", [], 2);
expect(await blsWallet.getBLSPublicKey()).to.eql(newKey);
expect(await vg.hashFromWallet(wallet1.address)).to.eql(hash2);
});
it("should NOT override public key after creation", async function () {
const initialKey = await blsWallet.getBLSPublicKey();
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);
const ZERO = ethers.BigNumber.from(0);
expect(initialKey).to.not.eql([ZERO, ZERO, ZERO, ZERO]);
let hashFromWallet = await vg.hashFromWallet(wallet1.address);
expect(BigNumber.from(hashFromWallet)).to.not.equal(BigNumber.from(0));
expect(hashFromWallet).to.equal(hash1);
await blsWallet.setAnyPending();
await fx.call(wallet1, vg, "setPendingBLSKeyForWallet", [], 1);
const finalKey = await blsWallet.getBLSPublicKey();
expect(finalKey).to.eql(initialKey);
walletForHash = await vg.walletFromHash(hash1);
expect(walletForHash).to.equal(wallet1.address);
hashFromWallet = await vg.hashFromWallet(wallet1.address);
expect(hashFromWallet).to.equal(hash1);
});
it("should set recovery hash", async function () {
@@ -118,14 +135,25 @@ 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();
// 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);
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,
);
await fx.advanceTimeBy(safetyDelaySeconds / 2); // wait half the time
await (await blsWallet.setAnyPending()).wait();
await fx.call(wallet1, vg, "setPendingBLSKeyForWallet", [], 2);
const addressSignature = await signWalletAddress(
fx,
@@ -141,33 +169,34 @@ describe("Recovery", async function () {
).wait();
// key reset via recovery
expect(await blsWallet.getBLSPublicKey()).to.eql(
safeKey.map(BigNumber.from),
);
expect(await vg.hashFromWallet(wallet1.address)).to.eql(hash2);
expect(await vg.walletFromHash(hash2)).to.eql(wallet1.address);
await fx.advanceTimeBy(safetyDelaySeconds / 2 + 1); // wait remainder the time
// attacker's key not set after waiting full safety delay
expect(await blsWallet.getBLSPublicKey()).to.eql(
safeKey.map(BigNumber.from),
// 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(),
);
let walletFromKey = await fx.verificationGateway.walletFromHash(
wallet1.blsWalletSigner.getPublicKeyHash(wallet1.privateKey),
expect(await vg.walletFromHash(hash1)).to.not.equal(blsWallet.address);
expect(await vg.walletFromHash(hashAttacker)).to.not.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);
expect(await vg.walletFromHash(hash2)).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,
@@ -184,15 +213,12 @@ describe("Recovery", async function () {
"BLSWallet",
walletAttacker.address,
);
const hashAttacker = walletAttacker.blsWalletSigner.getPublicKeyHash(
walletAttacker.privateKey,
);
// Attacker users recovery signer to set their recovery hash
const attackerRecoveryHash = ethers.utils.solidityKeccak256(
["address", "bytes32", "bytes32"],
[recoverySigner.address, hashAttacker, salt],
);
// Attacker puts their wallet into recovery
await fx.call(
walletAttacker,
attackerWalletContract,
@@ -204,6 +230,9 @@ 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,
@@ -212,7 +241,7 @@ describe("Recovery", async function () {
);
const wallet1Key = await wallet1.PublicKey();
// Attacker attempts to overwite wallet 1's public key and fails
// Attacker attempts to overwrite wallet 1's hash in the gateway 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 } from "../typechain";
import { BLSOpen, ProxyAdmin } from "../typechain";
import { ActionData, BlsWalletWrapper } from "../clients/src";
import Fixture from "../shared/helpers/Fixture";
import deployAndRunPrecompileCostEstimator from "../shared/helpers/deployAndRunPrecompileCostEstimator";
@@ -76,6 +76,10 @@ 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",
@@ -83,7 +87,9 @@ 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]();
@@ -123,7 +129,7 @@ describe("Upgrade", async function () {
const setExternalWalletAction: ActionData = {
ethValue: BigNumber.from(0),
contractAddress: vg2.address,
encodedFunction: vg2.interface.encodeFunctionData("setExternalWallet", [
encodedFunction: vg2.interface.encodeFunctionData("setBLSKeyForWallet", [
addressSignature,
walletOldVg.PublicKey(),
]),
@@ -227,6 +233,7 @@ 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,
);
@@ -267,4 +274,77 @@ 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

@@ -118,7 +118,6 @@ describe("WalletActions", async function () {
actions: [
{
ethValue: ethToTransfer,
// TODO: Does wallet contract need to exist?
contractAddress: recvWallet.walletContract.address,
encodedFunction: "0x",
},

7
docs/README.md Normal file
View File

@@ -0,0 +1,7 @@
## Docs
- [See an overview of BLS Wallet & how the components work together](./docs/system_overview.md)
- [Use BLS Wallet in a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md)
- Setup the BLS Wallet components for:
- [Local develeopment](./docs/local_development.md)
- [Remote development](./docs/remote_development.md)

View File

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

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

View File

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

104
docs/local_development.md Normal file
View File

@@ -0,0 +1,104 @@
# Local Development
These steps will setup this repo on your machine for local development for the majority of the components in this repo.
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
Run the repo setup script
```sh
./setup.ts
```
Then choose to target either a local Hardhat node or the Arbitrum Testnet.
### 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
```
## 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`.
### 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
```
### 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";
...
```

105
docs/remote_development.md Normal file
View File

@@ -0,0 +1,105 @@
# 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
...
```

19
docs/system_overview.md Normal file
View File

@@ -0,0 +1,19 @@
# System Overview
## Layer 2 Amsterdam April 2022 Presentation
https://youtu.be/Ke4L_PXIi8M?t=22380
## 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

@@ -0,0 +1,95 @@
# 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,6 +1,4 @@
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=

4
extension/.env.release Normal file
View File

@@ -0,0 +1,4 @@
PRIVATE_KEY_STORAGE_KEY=quill-private-key
AGGREGATOR_URL=https://arbitrum-testnet.blswallet.org
DEFAULT_CHAIN_ID=421611
CRYPTO_COMPARE_API_KEY=injected-from-github-actions-environment

View File

@@ -2,6 +2,7 @@
"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": {
@@ -10,8 +11,8 @@
"url": "https://blswallet.org"
},
"engines": {
"node": ">=16.0.0",
"yarn": ">= 1.0.0"
"node": ">=16.0.0 <18.0.0",
"yarn": ">=1.0.0"
},
"scripts": {
"dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch",
@@ -21,6 +22,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": "tsc --noEmit",
"lint": "eslint . --ext .ts,.tsx",
"lint:fix": "eslint . --ext .ts,.tsx --fix"
},

View File

@@ -1,4 +1,6 @@
import BaseController from '../BaseController';
import CellCollection from '../../cells/CellCollection';
import ICell from '../../cells/ICell';
import { CRYPTO_COMPARE_API_KEY } from '../../env';
import {
CurrencyControllerConfig,
CurrencyControllerState,
@@ -7,99 +9,48 @@ import {
// every ten minutes
const POLLING_INTERVAL = 600_000;
export default class CurrencyController extends BaseController<
CurrencyControllerConfig,
CurrencyControllerState
> {
private conversionInterval: number;
const defaultConfig: CurrencyControllerConfig = {
pollInterval: POLLING_INTERVAL,
};
constructor({
config = {},
state,
}: {
config: Partial<CurrencyControllerConfig>;
state?: Partial<CurrencyControllerState>;
}) {
super({ config, state });
this.defaultState = {
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',
} as CurrencyControllerState;
this.defaultConfig = {
pollInterval: POLLING_INTERVAL,
} as CurrencyControllerConfig;
this.initialize();
});
}
//
// PUBLIC METHODS
//
public getNativeCurrency(): string {
return this.state.nativeCurrency;
}
public setNativeCurrency(nativeCurrency: string): void {
this.update({
nativeCurrency,
ticker: nativeCurrency,
} as CurrencyControllerState);
}
public getCurrentCurrency(): string {
return this.state.currentCurrency;
}
public setCurrentCurrency(currentCurrency: string): void {
this.update({ currentCurrency } as CurrencyControllerState);
}
/**
* A getter for the conversionRate property
*
* @returns The conversion rate from ETH to the selected currency.
*
*/
public getConversionRate(): number {
return this.state.conversionRate;
}
public setConversionRate(conversionRate: number): void {
this.update({ conversionRate } as CurrencyControllerState);
}
/**
* A getter for the conversionDate property
*
* @returns The date at which the conversion rate was set. Expressed in milliseconds since midnight of
* January 1, 1970
*
*/
public getConversionDate(): string {
return this.state.conversionDate;
}
public setConversionDate(conversionDate: string): void {
this.update({ conversionDate } as CurrencyControllerState);
public async update(stateUpdates: Partial<CurrencyControllerState>) {
await this.state.write({
...(await this.state.read()),
...stateUpdates,
});
}
async updateConversionRate(): Promise<void> {
let currentCurrency = '';
let nativeCurrency = '';
try {
// fiat
currentCurrency = this.getCurrentCurrency();
let state: CurrencyControllerState | undefined;
// crypto
nativeCurrency = this.getNativeCurrency();
try {
state = await this.state.read();
const apiUrl = `${
this.config.api
}?fsym=${nativeCurrency.toUpperCase()}&tsyms=${currentCurrency.toUpperCase()}&api_key=${
process.env.CRYPTO_COMPARE_API_KEY
}`;
}?fsym=${state.nativeCurrency.toUpperCase()}&tsyms=${state.currentCurrency.toUpperCase()}&api_key=${CRYPTO_COMPARE_API_KEY}`;
let response: Response;
try {
response = await fetch(apiUrl);
@@ -128,30 +79,38 @@ export default class CurrencyController extends BaseController<
// this.setConversionRate(Number(parsedResponse.bid))
// this.setConversionDate(Number(parsedResponse.timestamp))
// } else
if (parsedResponse[currentCurrency.toUpperCase()]) {
if (parsedResponse[state.currentCurrency.toUpperCase()]) {
// ETC
this.setConversionRate(
Number(parsedResponse[currentCurrency.toUpperCase()]),
);
this.setConversionDate((Date.now() / 1000).toString());
this.update({
conversionRate: Number(
parsedResponse[state.currentCurrency.toUpperCase()],
),
conversionDate: (Date.now() / 1000).toString(),
});
} else {
this.setConversionRate(0);
this.setConversionDate('N/A');
this.update({
conversionRate: 0,
conversionDate: 'N/A',
});
}
} catch (error) {
// reset current conversion rate
console.warn(
'Quill - Failed to query currency conversion:',
nativeCurrency,
currentCurrency,
state?.nativeCurrency,
state?.currentCurrency,
error,
);
this.setConversionRate(0);
this.setConversionDate('N/A');
this.update({
conversionRate: 0,
conversionDate: 'N/A',
});
// throw error
console.error(
error,
`CurrencyController - Failed to query rate for currency "${currentCurrency}"`,
`CurrencyController - Failed to query rate for currency "${state?.currentCurrency}"`,
);
}
}

View File

@@ -1,14 +1,17 @@
import { BaseConfig, BaseState } from '../interfaces';
import * as io from 'io-ts';
export interface CurrencyControllerState extends BaseState {
currentCurrency: string;
conversionRate: number;
conversionDate: string;
nativeCurrency: string;
ticker: string;
}
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;
api?: string;
}

View File

@@ -99,7 +99,6 @@ export function providerFromEngine(
return res.result as U;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider.send = <T, U>(
req: JRPCRequest<T>,
callback: (error: any, providerRes: U | undefined) => void,

View File

@@ -208,7 +208,7 @@ export default class NetworkController
blockTracker: PollingBlockTracker;
}): void {
if (this._providerProxy) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* eslint @typescript-eslint/ban-ts-comment: "warn" -- TODO (merge-ok) Fix typing */
// @ts-ignore
this._providerProxy.setTarget(provider);
} else {
@@ -217,7 +217,7 @@ export default class NetworkController
}
if (this._blockTrackerProxy) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
/* eslint @typescript-eslint/ban-ts-comment: "warn" -- TODO (merge-ok) Fix typing */
// @ts-ignore
this._blockTrackerProxy.setTarget(blockTracker);
} else {

View File

@@ -13,7 +13,7 @@ export function createOriginMiddleware(options: OriginMiddlewareOptions) {
_: JRPCResponse<unknown>,
next: JRPCEngineNextCallback,
): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// TODO (merge-ok) Add JRPCRequest type with origin property added.
(request as any).origin = options.origin;
next();
};

View File

@@ -1,5 +1,6 @@
import { version } from '../../../package.json';
// Matches exact property for web3 client.
// eslint-disable-next-line @typescript-eslint/naming-convention
const web3_clientVersion = `Quill/v${version}`;

View File

@@ -57,10 +57,11 @@ import createMetaRPCHandler from './streamHelpers/MetaRPCHandler';
import { PROVIDER_NOTIFICATIONS } from '../common/constants';
import { AGGREGATOR_URL } from '../env';
import knownTransactions from './knownTransactions';
import CellCollection from '../cells/CellCollection';
import assert from '../helpers/assert';
import mapValues from '../helpers/mapValues';
import ExplicitAny from '../types/ExplicitAny';
import Rpc, { rpcMap } from '../types/Rpc';
import mapValues from '../helpers/mapValues';
export const DEFAULT_CONFIG = {
CurrencyControllerConfig: {
@@ -122,13 +123,16 @@ export default class QuillController extends BaseController<
// private txController!: TransactionController;
constructor({
config,
state,
}: {
config: Partial<QuillControllerConfig>;
state: Partial<QuillControllerState>;
}) {
constructor(
{
config,
state,
}: {
config: Partial<QuillControllerConfig>;
state: Partial<QuillControllerState>;
},
public storage: CellCollection,
) {
super({ config, state });
}
@@ -207,10 +211,10 @@ export default class QuillController extends BaseController<
state: this.state.NetworkControllerState,
});
this.initializeProvider();
this.currencyController = new CurrencyController({
config: this.config.CurrencyControllerConfig,
state: this.state.CurrencyControllerState,
});
this.currencyController = new CurrencyController(
this.config.CurrencyControllerConfig,
this.storage,
);
this.currencyController.updateConversionRate();
this.currencyController.scheduleConversionInterval();
@@ -292,8 +296,8 @@ export default class QuillController extends BaseController<
return {
// etc
getState: () => this.state,
setCurrentCurrency:
currencyController.setCurrentCurrency.bind(currencyController),
setCurrentCurrency: (currentCurrency) =>
currencyController.update({ currentCurrency }),
setCurrentLocale: preferencesController.setUserLocale.bind(
preferencesController,
),
@@ -355,9 +359,9 @@ export default class QuillController extends BaseController<
async setDefaultCurrency(currency: string): Promise<void> {
const { ticker } = this.networkController.getProviderConfig();
// This is ETH
this.currencyController.setNativeCurrency(ticker);
this.currencyController.update({ nativeCurrency: ticker });
// This is USD
this.currencyController.setCurrentCurrency(currency);
this.currencyController.update({ currentCurrency: currency });
await this.currencyController.updateConversionRate();
this.preferencesController.setSelectedCurrency(currency);
}
@@ -466,10 +470,6 @@ export default class QuillController extends BaseController<
this.update({ PreferencesControllerState: state });
});
this.currencyController.on('store', (state) => {
this.update({ CurrencyControllerState: state });
});
this.networkController.on('store', (state) => {
this.update({ NetworkControllerState: state });
});
@@ -523,7 +523,7 @@ export default class QuillController extends BaseController<
},
eth_setPreferredAggregator: async (req: any) => {
// eslint-disable-next-line prefer-destructuring
/* eslint prefer-destructuring: "warn" -- TODO (merge-ok) Destructure properly */
this.tabPreferredAggregators[req.tabId] = req.params[0];
return 'ok';
@@ -588,10 +588,10 @@ export default class QuillController extends BaseController<
);
}
// Expose no accounts if this origin has not been approved, preventing
// account-requiring RPC methods from completing successfully
// TODO (merge-ok) Expose no accounts if this origin has not been approved,
// preventing account-requiring RPC methods from completing successfully
// only show address if account is unlocked
// FIXME: The comment above is not yet implemented.
// https://github.com/web3well/bls-wallet/issues/224
return this.selectedAddress ? [this.selectedAddress] : [];
},
};

View File

@@ -20,6 +20,7 @@ import { getDefaultProviderConfig } from './utils';
import ControllerStoreStream from './streamHelpers/ControllerStoreStream';
import ControllerStreamSink from './streamHelpers/ControllerStreamSink';
import PortDuplexStream from '../common/PortStream';
import extensionLocalCellCollection from '../cells/extensionLocalCellCollection';
// const notificationManager = new NotificationManager();
@@ -94,11 +95,14 @@ function setupController(initState: unknown): void {
//
console.log(initState, 'initstate');
const controller = new QuillController({
// initial state
state: initState as QuillControllerState,
config: DEFAULT_CONFIG,
});
const controller = new QuillController(
{
// initial state
state: initState as QuillControllerState,
config: DEFAULT_CONFIG,
},
extensionLocalCellCollection,
);
controller.init({
state: initState as QuillControllerState,

View File

@@ -41,10 +41,8 @@ export default function createEventEmitterProxy<
.eventNames()
.filter(eventFilter as (name: string | symbol) => boolean)
.forEach((name: string | symbol) => {
getRawListeners(oldTarget, name as string).forEach(
// eslint-disable-next-line @typescript-eslint/ban-types
(handler: Function) =>
newTarget.on(name, handler as unknown as (...args: any[]) => void),
getRawListeners(oldTarget, name as string).forEach((handler: unknown) =>
newTarget.on(name, handler as (...args: any[]) => void),
);
});

View File

@@ -5,7 +5,7 @@ EthQuery.prototype.request = function request<T>(opts: {
params?: unknown;
}): Promise<T> {
return new Promise((resolve, reject) => {
this.sendAsync(opts, (error, result) => {
this.sendAsync(opts, (error: Error, result: unknown) => {
if (error) return reject(error);
resolve(result as T);
});

View File

@@ -333,5 +333,3 @@ export class QuillInPageProvider extends BaseProvider<InPageProviderState> {
}
}
}
export default QuillInPageProvider;

View File

@@ -1,7 +1,7 @@
import { BasePostMessageStream } from '@toruslabs/openlogin-jrpc';
import type { Duplex } from 'readable-stream';
import { CONTENT_SCRIPT, INPAGE } from '../common/constants';
import QuillInPageProvider from './InPageProvider';
import { QuillInPageProvider } from './InPageProvider';
import { ProviderOptions } from './interfaces';
interface InitializeProviderOptions extends ProviderOptions {

View File

@@ -77,7 +77,7 @@ const Carousel: FunctionComponent<{
return () => clearInterval(intervalId);
// eslint-disable-next-line react-hooks/exhaustive-deps
/* eslint react-hooks/exhaustive-deps: "warn" -- TODO (merge-ok) Add hook deps */
}, []);
return (

View File

@@ -1,24 +1,23 @@
import { FunctionComponent, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import QuillContext from '../QuillContext';
import { useQuill } from '../QuillContext';
import OnboardingActionPanel from './OnboardingActionPanel';
import OnboardingInfoPanel from './OnboardingInfoPanel';
const OnboardingPage: FunctionComponent = () => {
const quillCtx = QuillContext.use();
const { rpc } = useQuill();
const navigate = useNavigate();
useEffect(() => {
(async () => {
const onboardingComplete =
await quillCtx.rpc.private.quill_isOnboardingComplete();
const onboardingComplete = await rpc.private.quill_isOnboardingComplete();
if (onboardingComplete) {
navigate('/wallet/');
}
})();
}, [navigate, quillCtx]);
}, [navigate, rpc]);
return (
<div className="flex h-screen">

View File

@@ -4,7 +4,7 @@ import { ArrowRight } from 'phosphor-react';
import { useNavigate } from 'react-router-dom';
import Button from '../../components/Button';
import Range from '../../helpers/Range';
import QuillContext from '../QuillContext';
import { useQuill } from '../QuillContext';
const WordInReview: FunctionComponent<{
index: number;
@@ -50,7 +50,7 @@ const ReviewSecretPhrasePanel: FunctionComponent<{
sampleIndexes?: number[];
onBack: () => void;
}> = ({ secretPhrase, sampleIndexes = [0, 3, 9, 11], onBack }) => {
const quillCtx = QuillContext.use();
const { rpc } = useQuill();
const len = sampleIndexes.length;
@@ -70,9 +70,9 @@ const ReviewSecretPhrasePanel: FunctionComponent<{
const navigate = useNavigate();
const setHDWalletPhrase = async () => {
await quillCtx.rpc.private.quill_setHDPhrase(secretPhrase.join(' '));
const address = await quillCtx.rpc.private.quill_createHDAccount();
await quillCtx.rpc.private.quill_setSelectedAddress(address);
await rpc.private.quill_setHDPhrase(secretPhrase.join(' '));
const address = await rpc.private.quill_createHDAccount();
await rpc.private.quill_setSelectedAddress(address);
navigate('/wallet');
};

View File

@@ -1,46 +0,0 @@
import { createContext, useContext } from 'react';
import assert from '../helpers/assert';
import mapValues from '../helpers/mapValues';
import { QuillInPageProvider } from '../PageContentScript/InPageProvider';
import ExplicitAny from '../types/ExplicitAny';
import Rpc, { rpcMap } from '../types/Rpc';
export default class QuillContext {
rpc: Rpc;
constructor(public ethereum: QuillInPageProvider) {
this.rpc = {
public: mapValues(
rpcMap.public,
({ params: paramsType, output }, method) => {
return async (...params: unknown[]) => {
assert(paramsType.is(params));
const response = await this.ethereum.request({ method, params });
assert(output.is(response));
return response as ExplicitAny;
};
},
),
private: mapValues(
rpcMap.private,
({ params: paramsType, output }, method) => {
return async (...params: unknown[]) => {
assert(paramsType.is(params));
const response = await this.ethereum.request({ method, params });
assert(output.is(response));
return response as ExplicitAny;
};
},
),
};
}
private static context = createContext<QuillContext>({} as QuillContext);
static Provider = QuillContext.context.Provider;
static use() {
// eslint-disable-next-line react-hooks/rules-of-hooks
return useContext(QuillContext.context);
}
}

View File

@@ -0,0 +1,121 @@
import * as io from 'io-ts';
import React from 'react';
import { ethers } from 'ethers';
import getWindowQuillProvider from './getWindowQuillProvider';
import assert from '../helpers/assert';
import mapValues from '../helpers/mapValues';
import { QuillInPageProvider } from '../PageContentScript/InPageProvider';
import ExplicitAny from '../types/ExplicitAny';
import Rpc, { rpcMap } from '../types/Rpc';
import CellCollection from '../cells/CellCollection';
import ICell, { IReadableCell } from '../cells/ICell';
import elcc from '../cells/extensionLocalCellCollection';
import TimeCell from './TimeCell';
import { FormulaCell } from '../cells/FormulaCell';
import approximate from './approximate';
type QuillContextValue = {
provider: QuillInPageProvider;
ethersProvider: ethers.providers.Web3Provider;
rpc: Rpc;
Cell: CellCollection['Cell'];
time: IReadableCell<number>;
blockNumber: IReadableCell<number>;
theme: ICell<string>;
};
function getQuillContextValue(
provider: QuillInPageProvider,
): QuillContextValue {
const Cell = elcc.Cell.bind(elcc);
const time = TimeCell(100);
const blockNumber = new FormulaCell(
{ _: approximate(time, 5000) },
async () => {
console.log('getting block number');
return Number(await provider.request({ method: 'eth_blockNumber' }));
},
);
// FIXME: This cell has an awkward name due to an apparent collision with
// theming coming from the old controller system. It should simply be named
// 'theme', but this requires updating the controllers, which is out of scope
// for now.
const theme = Cell('cell-based-theme', io.string, 'light');
return {
provider,
ethersProvider: new ethers.providers.Web3Provider(provider),
rpc: {
public: mapValues(
rpcMap.public,
({ params: paramsType, output }, method) => {
return async (...params: unknown[]) => {
assert(paramsType.is(params));
const response = await provider.request({
method,
params,
});
assert(output.is(response));
return response as ExplicitAny;
};
},
),
private: mapValues(
rpcMap.private,
({ params: paramsType, output }, method) => {
return async (...params: unknown[]) => {
assert(paramsType.is(params));
const response = await provider.request({
method,
params,
});
assert(output.is(response));
return response as ExplicitAny;
};
},
),
},
Cell,
time,
blockNumber,
theme,
};
}
const QuillContext = React.createContext<QuillContextValue>(
// QuillProvider render will ensure this is set properly
// before other components load.
{} as QuillContextValue,
);
export function useQuill() {
return React.useContext(QuillContext);
}
type Props = {
children: React.ReactNode;
};
export function QuillProvider({ children }: Props) {
const [ctxVal, setCtxVal] = React.useState<QuillContextValue | undefined>();
React.useEffect(() => {
(async () => {
const provider = await getWindowQuillProvider();
const val = getQuillContextValue(provider);
setCtxVal(val);
})();
}, []);
if (!ctxVal) {
// This could be replaced by a nicer splash screen
return <div>Loading...</div>;
}
return (
<QuillContext.Provider value={ctxVal}>{children}</QuillContext.Provider>
);
}

View File

@@ -1,17 +1,23 @@
import { FunctionComponent } from 'react';
import React, { FunctionComponent } from 'react';
import { HashRouter, Route, Routes } from 'react-router-dom';
import OnboardingPage from './Onboarding/OnboardingPage';
import { WalletPage } from './Wallet/WalletPage';
import { QuillProvider } from './QuillContext';
import Theme from './Theme';
const QuillPage: FunctionComponent = () => {
return (
<HashRouter>
<Routes>
<Route path="/onboarding" element={<OnboardingPage />} />
<Route path="/wallet/*" element={<WalletPage />} />
</Routes>
</HashRouter>
<QuillProvider>
<Theme>
<HashRouter>
<Routes>
<Route path="/onboarding" element={<OnboardingPage />} />
<Route path="/wallet/*" element={<WalletPage />} />
</Routes>
</HashRouter>
</Theme>
</QuillProvider>
);
};

View File

@@ -0,0 +1,18 @@
import { FunctionComponent } from 'react';
import useCell from '../cells/useCell';
import { useQuill } from './QuillContext';
const Theme: FunctionComponent = ({ children }) => {
const quill = useQuill();
const theme = useCell(quill.theme);
return (
<div className={`themable1 ${theme === 'dark' && 'dark-theme'}`}>
<div className={`themable2 ${theme === 'dark' && 'dark-theme'}`}>
{children}
</div>
</div>
);
};
export default Theme;

View File

@@ -0,0 +1,17 @@
import { IReadableCell } from '../cells/ICell';
import MemoryCell from '../cells/MemoryCell';
import delay from '../helpers/delay';
export default function TimeCell(accuracy: number): IReadableCell<number> {
const cell = new MemoryCell(Math.floor(Date.now() / accuracy));
(async () => {
while (true) {
const now = Date.now();
await delay(accuracy * Math.ceil(now / accuracy) - now);
await cell.write(accuracy * Math.round(Date.now() / accuracy));
}
})();
return cell;
}

View File

@@ -11,50 +11,51 @@ interface IConnectionsSummary {
expanded?: boolean;
}
export const ConnectionsSummary: React.FunctionComponent<IConnectionsSummary> =
({ onClick, expanded = true }) => {
return (
<div
className={`p-4 rounded-lg
export const ConnectionsSummary: React.FunctionComponent<
IConnectionsSummary
> = ({ onClick, expanded = true }) => {
return (
<div
className={`p-4 rounded-lg
${expanded && 'bg-white border-2 border-blue-500 shadow-xl'}
`}
>
<div className="flex place-items-center gap-4 ">
<div className="w-5 h-5">
<input
type="radio"
checked={expanded}
readOnly
className="h-5 w-5 cursor-pointer"
onClick={onClick}
/>
</div>
<div className="flex-grow">All Wallets</div>
<div className="text-body">2.089 ETH</div>
>
<div className="flex place-items-center gap-4 ">
<div className="w-5 h-5">
<input
type="radio"
checked={expanded}
readOnly
className="h-5 w-5 cursor-pointer"
onClick={onClick}
/>
</div>
{/* Details */}
{expanded && (
<div className="mt-6">
<div className="mt-4 flex flex-col gap-1">
<div className="flex gap-2 place-items-center">
<CurrencyDollar className="text-blue-400 icon-md" /> USD $
{2.089 * 3000.1}
</div>
<div className="flex gap-2 place-items-center">
<ShareNetwork className="text-blue-400 icon-md" />3 Networks
</div>
<div className="flex gap-2 place-items-center">
<Wallet className="text-blue-400 icon-md" />2 Wallets
</div>
<div className="flex gap-2 place-items-center">
<PokerChip className="text-blue-400 icon-md" /> 2 Tokens
</div>
<div className="flex-grow">All Wallets</div>
<div className="text-body">2.089 ETH</div>
</div>
{/* Details */}
{expanded && (
<div className="mt-6">
<div className="mt-4 flex flex-col gap-1">
<div className="flex gap-2 place-items-center">
<CurrencyDollar className="text-blue-400 icon-md" /> USD $
{2.089 * 3000.1}
</div>
<div className="flex gap-2 place-items-center">
<ShareNetwork className="text-blue-400 icon-md" />3 Networks
</div>
<div className="flex gap-2 place-items-center">
<Wallet className="text-blue-400 icon-md" />2 Wallets
</div>
<div className="flex gap-2 place-items-center">
<PokerChip className="text-blue-400 icon-md" /> 2 Tokens
</div>
</div>
)}
</div>
);
};
</div>
)}
</div>
);
};

View File

@@ -1,6 +1,6 @@
import * as React from 'react';
import { Route, Routes, useNavigate } from 'react-router-dom';
import QuillContext from '../QuillContext';
import { useQuill } from '../QuillContext';
import { ConnectionsWrapper } from './Connections/ConnectionWrapper';
import { ContactsWrapper } from './Contacts/ContactsWrapper';
import { Navigation } from './Navigation';
@@ -11,8 +11,8 @@ import { WalletsWrapper } from './Wallets/WalletWrapper';
interface IRoutes {
name: string;
path: string;
summaryComponent: JSX.Element;
detailComponent: JSX.Element;
summaryComponent: React.ReactElement;
detailComponent: React.ReactElement;
}
const routes: IRoutes[] = [
@@ -43,20 +43,19 @@ const routes: IRoutes[] = [
];
export const WalletPage: React.FunctionComponent = () => {
const quillCtx = QuillContext.use();
const { rpc } = useQuill();
const navigate = useNavigate();
React.useEffect(() => {
(async () => {
const onboardingComplete =
await quillCtx.rpc.private.quill_isOnboardingComplete();
const onboardingComplete = await rpc.private.quill_isOnboardingComplete();
console.debug('onboardingComplete', onboardingComplete);
if (!onboardingComplete) {
navigate('/onboarding?p=1');
}
})();
}, [navigate, quillCtx]);
}, [navigate, rpc]);
return (
<div className="flex h-screen">

View File

@@ -1,4 +1,5 @@
// @ts-nocheck // TODO: fix types
/* eslint @typescript-eslint/ban-ts-comment: "warn", react/jsx-key: "warn" -- TODO (merge-ok) Fix types, linting */
// @ts-nocheck
import * as React from 'react';
import { useTable, usePagination } from 'react-table';
import {

View File

@@ -62,6 +62,7 @@ const WalletTabs: React.FunctionComponent = () => {
return (
<div className="flex border-b border-grey-300 gap-4 mb-4">
{tabs.map((tab) => (
/* eslint jsx-a11y/click-events-have-key-events: "warn" -- TODO (merge-ok) Add keyboard listener */
<div
key={tab.name}
className={`py-2 px-4 cursor-pointer ${

View File

@@ -9,6 +9,7 @@ import {
// Circle,
} from 'phosphor-react';
import Button from '../../../components/Button';
/* eslint import/no-cycle: "warn" -- TODO (merge-ok) Fix import cycle */
import { IWallet } from './WalletWrapper';
interface IWalletSummary {
@@ -71,12 +72,14 @@ export const WalletSummary: React.FunctionComponent<IWalletSummary> = ({
<div className="flex gap-2">
<Button
onPress={() => {}}
/* eslint react/no-children-prop: "warn" -- TODO (merge-ok) Pass 'Send' as child */
children={'Send'}
className="btn-primary"
icon={<PaperPlaneTilt className="icon-md" />}
/>
<Button
onPress={() => {}}
/* eslint react/no-children-prop: "warn" -- TODO (merge-ok) Pass 'Receive' as child */
children={'Receive'}
className="btn-secondary"
/>

View File

@@ -1,6 +1,7 @@
import { FunctionComponent, useEffect, useState } from 'react';
import Button from '../../../components/Button';
import QuillContext from '../../QuillContext';
import { useQuill } from '../../QuillContext';
/* eslint import/no-cycle: "warn" -- TODO (merge-ok) Fix import cycle */
import { WalletSummary } from './WalletSummary';
export interface IWallet {
@@ -12,7 +13,7 @@ export interface IWallet {
}
export const WalletsWrapper: FunctionComponent = () => {
const quillCtx = QuillContext.use();
const { rpc } = useQuill();
const [selected, setSelected] = useState<number>(0);
const [wallets, setWallets] = useState<IWallet[]>([]);
@@ -22,7 +23,7 @@ export const WalletsWrapper: FunctionComponent = () => {
(async () => {
setLoading(true);
const accounts = await quillCtx.rpc.public.eth_accounts();
const accounts = await rpc.public.eth_accounts();
setWallets(
accounts.map((address: string, index: number) => {
@@ -38,10 +39,10 @@ export const WalletsWrapper: FunctionComponent = () => {
setLoading(false);
if (accounts[0]) {
quillCtx.rpc.private.quill_setSelectedAddress(accounts[0]);
rpc.private.quill_setSelectedAddress(accounts[0]);
}
})();
}, [quillCtx]);
}, [rpc]);
return (
<div className="">
@@ -49,9 +50,10 @@ export const WalletsWrapper: FunctionComponent = () => {
<div className="text-body">Wallets</div>
<Button
onPress={async () => {
await quillCtx.rpc.private.quill_createHDAccount();
await rpc.private.quill_createHDAccount();
window.location.reload();
}}
/* eslint react/no-children-prop: "warn" -- TODO (merge-ok) Pass 'Add Wallet' as child */
children={'Add Wallet'}
className="btn-secondary"
/>
@@ -65,7 +67,7 @@ export const WalletsWrapper: FunctionComponent = () => {
<WalletSummary
onClick={() => {
setSelected(index);
quillCtx.rpc.private.quill_setSelectedAddress(wallet.address);
rpc.private.quill_setSelectedAddress(wallet.address);
}}
key={wallet.name}
wallet={wallet}

View File

@@ -0,0 +1,20 @@
import { FormulaCell } from '../cells/FormulaCell';
import { IReadableCell } from '../cells/ICell';
export default function approximate(
value: IReadableCell<number>,
accuracy: number,
): IReadableCell<number> {
return new FormulaCell<{ value: IReadableCell<number> }, number>(
{ value },
// eslint-disable-next-line @typescript-eslint/no-shadow
({ value }) => value,
(previous, latest) => {
if (previous === undefined) {
return true;
}
return Math.abs(latest - previous) >= accuracy;
},
);
}

View File

@@ -1,33 +0,0 @@
import assert from '../helpers/assert';
import { QuillInPageProvider } from '../PageContentScript/InPageProvider';
export default function getWindowEthereum() {
const windowAny = window as any;
if (windowAny.ethereum?.isQuill) {
return Promise.resolve(windowAny.ethereum);
}
return new Promise<QuillInPageProvider>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timed out waiting for window.ethereum'));
}, 1000);
function handleEthereumInitialized() {
clearTimeout(timeout);
window.removeEventListener(
'ethereum#initialized',
handleEthereumInitialized,
);
const { ethereum } = windowAny;
assert(ethereum?.isQuill === true);
resolve(ethereum);
}
window.addEventListener('ethereum#initialized', handleEthereumInitialized);
});
}

View File

@@ -0,0 +1,44 @@
import assert from '../helpers/assert';
import { QuillInPageProvider } from '../PageContentScript/InPageProvider';
const ethereumInitialziedEvent = 'ethereum#initialized';
function waitForWindowEthererum(): Promise<void> {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Timed out waiting for window.ethereum'));
}, 1000);
function handleEthereumInitialized() {
clearTimeout(timeout);
window.removeEventListener(
ethereumInitialziedEvent,
handleEthereumInitialized,
);
resolve();
}
window.addEventListener(
ethereumInitialziedEvent,
handleEthereumInitialized,
);
});
}
export default async function getWindowQuillProvider(): Promise<QuillInPageProvider> {
if (!window.ethereum) {
await waitForWindowEthererum();
}
if (!window.ethereum) {
throw new Error('window.ethereum failed to initialize');
}
if ('isQuill' in window.ethereum) {
assert(window.ethereum.isQuill);
return window.ethereum;
}
throw new Error('window.ethereum is not Quill provider');
}

View File

@@ -1,20 +1,11 @@
import ReactDOM from 'react-dom';
import Browser from 'webextension-polyfill';
import '../ContentScript';
import '../styles/index.scss';
import './styles.scss';
import ReactDOM from 'react-dom';
import QuillPage from './QuillPage';
import QuillContext from './QuillContext';
import getWindowEthereum from './getWindowEthereum';
getWindowEthereum().then((ethereum) => {
const quillContext = new QuillContext(ethereum);
window.Browser ??= Browser;
ReactDOM.render(
<QuillContext.Provider value={quillContext}>
<QuillPage />
</QuillContext.Provider>,
document.getElementById('quill-page-root'),
);
});
ReactDOM.render(<QuillPage />, document.getElementById('quill-page-root'));

View File

@@ -0,0 +1,7 @@
type AsyncIteratee<I extends AsyncIterable<unknown>> = I extends AsyncIterable<
infer T
>
? T
: never;
export default AsyncIteratee;

View File

@@ -0,0 +1,206 @@
import { EventEmitter } from 'events';
import * as io from 'io-ts';
import ExplicitAny from '../types/ExplicitAny';
import ICell, { CellEmitter } from './ICell';
import CellIterator from './CellIterator';
import jsonHasChanged from './jsonHasChanged';
import assert from '../helpers/assert';
import IAsyncStorage from './IAsyncStorage';
export default class CellCollection {
cells: Record<string, CollectionCell<ExplicitAny> | undefined> = {};
#asyncStorageChangeHandler = (keys: string[]) => {
for (const key of keys) {
const cell = this.cells[key];
if (cell) {
cell.versionedRead();
}
}
};
constructor(public asyncStorage: IAsyncStorage) {
asyncStorage.events.on('change', this.#asyncStorageChangeHandler);
}
/**
* A `CellCollection` is usually intended to live for the life of your
* application. In that case, calling .end is not required. However, if you're
* doing something different and you'd like to clean up the change event
* handler, this will take care of that.
*/
end() {
this.asyncStorage.events.removeListener(
'change',
this.#asyncStorageChangeHandler,
);
}
Cell<T>(
key: string,
type: io.Type<T>,
defaultValue: T,
hasChanged = jsonHasChanged,
): CollectionCell<T> {
let cell = this.cells[key];
if (cell) {
cell.applyType(type);
return cell;
}
cell = new CollectionCell(
this.asyncStorage,
key,
type,
defaultValue,
hasChanged,
);
this.cells[key] = cell;
return cell;
}
async remove(key: string) {
const cell = this.cells[key];
cell?.end();
delete this.cells[key];
await this.asyncStorage.write(key, io.undefined, undefined);
}
}
export type Versioned<T> = { version: number; value: T };
export class CollectionCell<T> implements ICell<T> {
events = new EventEmitter() as CellEmitter<T>;
ended = false;
versionedType: io.Type<Versioned<T>>;
lastSeen?: Versioned<T>;
initialRead: Promise<void>;
constructor(
public asyncStorage: IAsyncStorage,
public key: string,
public type: io.Type<T>,
public defaultValue: T,
public hasChanged: ICell<T>['hasChanged'] = jsonHasChanged,
) {
this.versionedType = io.type({ version: io.number, value: type });
this.initialRead = this.versionedRead().then((versionedReadResult) => {
this.lastSeen = versionedReadResult;
});
}
applyType<X>(type: io.Type<X>) {
if (type === io.unknown) {
return;
}
if (this.type !== io.unknown && this.type.name !== type.name) {
throw new Error(
[
'Tried to get existing storage cell with a different type',
`(type: ${type.name}, existing: ${this.type.name})`,
].join(' '),
);
}
if (this.lastSeen && !type.is(this.lastSeen.value)) {
throw new Error(
[
`Type mismatch at storage key ${this.key}`,
`contents: ${JSON.stringify(this.lastSeen.value)}`,
`expected: ${type.name}`,
].join(' '),
);
}
if (this.type === io.unknown) {
this.type = type as unknown as io.Type<T>;
this.versionedType = io.type({ version: io.number, value: this.type });
}
}
async read(): Promise<T> {
const latest = await this.versionedRead();
return latest.value;
}
async write(newValue: T): Promise<void> {
assert(!this.ended);
assert(this.type.is(newValue));
await this.initialRead;
const newVersionedValue = {
version: (this.lastSeen?.version ?? 0) + 1,
value: newValue,
};
const latest = await this.versionedRead();
if (!(newVersionedValue.version > latest.version)) {
throw new Error('Rejecting write which is not newer than remote');
}
await this.asyncStorage.write(
this.key,
this.versionedType,
newVersionedValue,
);
const { lastSeen: previous } = this;
this.lastSeen = newVersionedValue;
if (this.hasChanged(previous?.value, newValue)) {
this.events.emit('change', {
previous: previous?.value,
latest: newVersionedValue.value,
});
}
}
end() {
this.events.emit('end');
this.ended = true;
}
[Symbol.asyncIterator](): AsyncIterator<T> {
return new CellIterator(this);
}
async versionedRead(): Promise<Versioned<T>> {
const readResult = (await this.asyncStorage.read(
this.key,
this.versionedType,
)) ?? { version: 0, value: this.defaultValue };
if (!this.versionedType.is(readResult)) {
throw new Error(
[
`Type mismatch at storage key ${this.key}`,
`contents: ${JSON.stringify(readResult)}`,
`expected: ${this.versionedType.name}`,
].join(' '),
);
}
if (this.hasChanged(this.lastSeen?.value, readResult.value)) {
this.events.emit('change', {
previous: this.lastSeen?.value,
latest: readResult.value,
});
}
this.lastSeen = readResult;
return readResult;
}
}

View File

@@ -0,0 +1,69 @@
import { EventEmitter } from 'events';
import TypedEmitter from 'typed-emitter';
import { ChangeEvent, IReadableCell } from './ICell';
export default class CellIterator<T> implements AsyncIterator<T> {
lastProvided?: { value: T };
cleanup = () => {};
endHandler = () => {};
endListener = () => {
this.endHandler();
};
events = new EventEmitter() as TypedEmitter<{
finished(): void;
}>;
constructor(public cell: IReadableCell<T>) {
this.cell.events.once('end', this.endListener);
this.cleanup = () => {
this.cell.events.off('end', this.endListener);
};
}
async next() {
const latestRead = await this.cell.read();
if (
this.lastProvided === undefined ||
this.cell.hasChanged(this.lastProvided.value, latestRead)
) {
this.lastProvided = { value: latestRead };
return { value: latestRead, done: false };
}
if (this.cell.ended) {
return { value: undefined, done: true as const };
}
return new Promise<IteratorResult<T>>((resolve) => {
const changeHandler = ({ latest }: ChangeEvent<T>) => {
this.cleanup();
this.lastProvided = { value: latest };
resolve({ value: latest });
};
this.cell.events.once('change', changeHandler);
this.endHandler = () => {
this.cleanup();
resolve({ value: undefined, done: true });
};
this.cleanup = () => {
this.cell.events.off('change', changeHandler);
this.cell.events.off('end', this.endListener);
this.cleanup = () => {};
};
});
}
async return() {
this.cleanup();
this.events.emit('finished');
return { value: undefined, done: true as const };
}
}

View File

@@ -0,0 +1,65 @@
import { ethers } from 'ethers';
import { FunctionComponent, useMemo } from 'react';
import { FormulaCell } from '../FormulaCell';
import MemoryCell from '../MemoryCell';
import { useQuill } from '../../QuillPage/QuillContext';
import { Display } from './Display';
import TextBox from './TextBox';
const BalanceWidget: FunctionComponent = () => {
const quillCtx = useQuill();
const cells = useMemo(() => {
const address = new MemoryCell('');
const balanceDisplay = new FormulaCell(
{ address, _: quillCtx.blockNumber },
// eslint-disable-next-line @typescript-eslint/no-shadow
async ({ address }) => {
const addressError = (() => {
try {
// Handles mixed case checksums
ethers.utils.getAddress(address);
return undefined;
} catch (err) {
const error = err as Error;
if (error.message.includes('bad address checksum')) {
return new Error('bad address checksum');
}
return error;
}
})();
if (addressError) {
return `(${addressError.message})`;
}
const balance = await quillCtx.ethersProvider.getBalance(address);
return `ETH: ${(+ethers.utils.formatEther(balance)).toFixed(3)}`;
},
);
return { address, balanceDisplay };
}, [quillCtx]);
return (
<>
<tr>
<td>Address:</td>
<td>
<TextBox value={cells.address} />
</td>
</tr>
<tr>
<td>Balance:</td>
<td>
<Display cell={cells.balanceDisplay} />
</td>
</tr>
</>
);
};
export default BalanceWidget;

View File

@@ -0,0 +1,126 @@
import * as io from 'io-ts';
import { FunctionComponent, useMemo } from 'react';
import { FormulaCell } from '../FormulaCell';
import MemoryCell from '../MemoryCell';
import useCell from '../useCell';
import delay from '../../helpers/delay';
import Range from '../../helpers/Range';
import { useQuill } from '../../QuillPage/QuillContext';
import BalanceWidget from './BalanceWidget';
import CheckBox from './CheckBox';
import { Counter } from './Counter';
import DemoTable from './DemoTable';
import { DisplayJson } from './DisplayJson';
import Selector from './Selector';
// Accessible at chrome-extension://<insert extension id>/cellsDemo.html
export const CellsDemoPage: FunctionComponent = () => {
const quill = useQuill();
const cells = useMemo(() => {
const page = new MemoryCell('math');
const a = quill.Cell('a', io.number, 3);
const b = new MemoryCell(5);
const c = new MemoryCell(0);
const includeSlow = new MemoryCell(true);
// eslint-disable-next-line @typescript-eslint/no-shadow
const ab = new FormulaCell({ a, b }, ({ a, b }) => a * b);
// eslint-disable-next-line @typescript-eslint/no-shadow
const abSlow = new FormulaCell({ a, b }, async ({ a, b }) => {
console.log(`calculating ${a}*${b}...`);
await delay(500);
const res = a * b;
console.log(`...${res}`);
return res;
});
return { page, a, b, c, includeSlow, ab, abSlow };
}, [quill]);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(window as any).cells = cells;
const includeSlowValue = useCell(cells.includeSlow);
const cValue = useCell(cells.c);
const pageValue = useCell(cells.page);
if (pageValue === undefined) {
return <></>;
}
return (
<DemoTable>
<tr>
<td style={{ height: '3em' }}>page</td>
<td>
<Selector
options={['math', 'blockNumber', 'balance', 'settings']}
selection={cells.page}
/>
</td>
</tr>
{pageValue === 'math' && (
<>
<tr>
<td>a:&nbsp;</td>
<td>
<Counter cell={cells.a} />
</td>
</tr>
<tr>
<td>b:&nbsp;</td>
<td>
<Counter cell={cells.b} />
</td>
</tr>
<tr>
<td>ab: </td>
<td>
<DisplayJson cell={cells.ab} />
</td>
</tr>
<tr>
<td>
abSlow: <CheckBox cell={cells.includeSlow} />
</td>
<td>{includeSlowValue && <DisplayJson cell={cells.abSlow} />}</td>
</tr>
</>
)}
{pageValue === 'blockNumber' && (
<>
<tr>
<td>components:&nbsp;</td>
<td>
<Counter cell={cells.c} />
</td>
</tr>
{Range(cValue ?? 0).map((i) => (
<tr key={i}>
<td>blockNumber: </td>
<td>
<DisplayJson cell={quill.blockNumber} />
</td>
</tr>
))}
</>
)}
{pageValue === 'balance' && <BalanceWidget />}
{pageValue === 'settings' && (
<>
<tr>
<td>Theme</td>
<td>
<Selector options={['light', 'dark']} selection={quill.theme} />
</td>
</tr>
</>
)}
</DemoTable>
);
};

View File

@@ -0,0 +1,15 @@
import { FunctionComponent } from 'react';
import DemoTable from './DemoTable';
// Accessible at chrome-extension://<insert extension id>/cellsDemo.html#/demo2
export const CellsDemoPage2: FunctionComponent = () => {
return (
<DemoTable>
<tr>
<td>Hello:</td>
<td>World</td>
</tr>
</DemoTable>
);
};

View File

@@ -0,0 +1,22 @@
import { FunctionComponent } from 'react';
import ICell from '../ICell';
import useCell from '../useCell';
const CheckBox: FunctionComponent<{ cell: ICell<boolean> }> = ({ cell }) => {
const value = useCell(cell);
if (value === undefined) {
return <></>;
}
return (
<input
type="checkbox"
checked={value}
onChange={() => cell.write(!value)}
style={{ width: 'initial' }}
/>
);
};
export default CheckBox;

View File

@@ -0,0 +1,33 @@
import { FunctionComponent } from 'react';
import ICell from '../ICell';
import useCell from '../useCell';
import Button from '../../components/Button';
export const Counter: FunctionComponent<{
cell: ICell<number>;
}> = ({ cell }) => {
const value = useCell(cell);
if (value === undefined) {
return <></>;
}
return (
<div style={{ display: 'flex', flexDirection: 'row' }}>
<Button
className="btn-secondary"
onPress={() => value !== undefined && cell.write(value - 1)}
>
-
</Button>
<div>{value}</div>
<Button
className="btn-secondary"
onPress={() => value !== undefined && cell.write(value + 1)}
>
+
</Button>
</div>
);
};

View File

@@ -0,0 +1,25 @@
import { FunctionComponent } from 'react';
const DemoTable: FunctionComponent = ({ children }) => {
return (
<div
style={{
padding: '2em',
fontSize: '3em',
lineHeight: '1.5em',
fontFamily: 'monospace',
}}
>
<table className="demo-table">
<tbody>
<tr>
<td style={{ width: '380px', height: '0' }} />
</tr>
{children}
</tbody>
</table>
</div>
);
};
export default DemoTable;

View File

@@ -0,0 +1,23 @@
import React, { FunctionComponent } from 'react';
import { HashRouter, Route, Routes } from 'react-router-dom';
import { QuillProvider } from '../../QuillPage/QuillContext';
import Theme from '../../QuillPage/Theme';
import { CellsDemoPage } from './CellsDemoPage';
import { CellsDemoPage2 } from './CellsDemoPage2';
const DemosContainer: FunctionComponent = () => {
return (
<QuillProvider>
<Theme>
<HashRouter>
<Routes>
<Route path="/demo2" element={<CellsDemoPage2 />} />
<Route path="*" element={<CellsDemoPage />} />
</Routes>
</HashRouter>
</Theme>
</QuillProvider>
);
};
export default DemosContainer;

View File

@@ -0,0 +1,11 @@
import React, { FunctionComponent } from 'react';
import { IReadableCell } from '../ICell';
import useCell from '../useCell';
export const Display: FunctionComponent<{
cell: IReadableCell<unknown>;
}> = ({ cell }) => {
const value = useCell(cell);
return <>{value}</>;
};

View File

@@ -0,0 +1,16 @@
import { FunctionComponent } from 'react';
import { IReadableCell } from '../ICell';
import useCell from '../useCell';
export const DisplayJson: FunctionComponent<{
cell: IReadableCell<unknown>;
}> = ({ cell }) => {
const value = useCell(cell);
return (
<pre style={{ display: 'inline-block' }}>
{JSON.stringify(value, null, 2)}
</pre>
);
};

View File

@@ -0,0 +1,30 @@
import { FunctionComponent } from 'react';
import ICell from '../ICell';
import useCell from '../useCell';
const Selector: FunctionComponent<{
options: string[];
selection: ICell<string>;
}> = ({ options, selection }) => {
const selectionValue = useCell(selection);
if (selectionValue === undefined) {
return <></>;
}
return (
<select
value={selectionValue}
onChange={(evt) => {
selection.write(options[evt.target.selectedIndex]);
}}
style={{ border: '1px solid black' }}
>
{options.map((option) => (
<option key={option}>{option}</option>
))}
</select>
);
};
export default Selector;

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