Compare commits
36 Commits
cells-walk
...
v0.3.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17ccc5b00c | ||
|
|
ce7f958fb7 | ||
|
|
4dae24f4ec | ||
|
|
471be6f0e0 | ||
|
|
0ef2799980 | ||
|
|
f31ee249b8 | ||
|
|
935bdf16c3 | ||
|
|
94c9060d57 | ||
|
|
e671e73e0f | ||
|
|
140f5e7094 | ||
|
|
1b5e2eaec4 | ||
|
|
3982007d76 | ||
|
|
b39ed6653f | ||
|
|
cbdb771f33 | ||
|
|
dd944ccf54 | ||
|
|
e58903eb9b | ||
|
|
b538fa70ec | ||
|
|
fdd1c020c6 | ||
|
|
7165a4c2aa | ||
|
|
0cbe1ceaff | ||
|
|
7c8b8383e4 | ||
|
|
d2b18f3f50 | ||
|
|
fafc15c897 | ||
|
|
b52c8e6e90 | ||
|
|
c8e7c7370a | ||
|
|
53c8e62cf6 | ||
|
|
436094914f | ||
|
|
745aed060f | ||
|
|
df53392ce7 | ||
|
|
7d82903f69 | ||
|
|
5e8827b595 | ||
|
|
d2c1e6af1e | ||
|
|
21dd23953d | ||
|
|
0b7580a442 | ||
|
|
074d58b8a8 | ||
|
|
114a5fdf6c |
47
.github/actions/build-upload-extension/action.yml
vendored
Normal 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 }}
|
||||
24
.github/actions/setup-contracts-clients/action.yml
vendored
Normal 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
@@ -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
@@ -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: |
|
||||
## What’s Changed
|
||||
|
||||
$CHANGES
|
||||
34
.github/workflows/aggregator-proxy.yml
vendored
Normal 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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
2
.github/workflows/labeler.yml
vendored
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,173 +1,45 @@
|
||||
# bls-wallet
|
||||

|
||||
|
||||
An Ethereum Layer 2 smart contract wallet that uses [BLS signatures](https://en.wikipedia.org/wiki/BLS_digital_signature) and aggregated transactions to reduce gas costs.
|
||||
|
||||
You can watch a full end-to-end demo of the project [here](https://www.youtube.com/watch?v=MOQ3sCLP56g)
|
||||
|
||||
## Getting Started
|
||||
|
||||
- [See an overview of BLS Wallet & how the components work together](./docs/system_overview.md)
|
||||
- [Use BLS Wallet in a browser/NodeJS/Deno app](./docs/use_bls_wallet_clients.md)
|
||||
- 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/)
|
||||
|
||||

|
||||
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.
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
# 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',
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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();
|
||||
13
aggregator/programs/helpers/git.ts
Normal 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",
|
||||
),
|
||||
];
|
||||
}
|
||||
22
aggregator/programs/helpers/lint.ts
Normal 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
|
||||
}
|
||||
}
|
||||
25
aggregator/programs/helpers/typescript.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
7
aggregator/programs/lintTodos.ts
Executable 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
|
||||
@@ -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",
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -35,5 +35,7 @@ module.exports = {
|
||||
ignores: [],
|
||||
},
|
||||
],
|
||||
// TODO (merge-ok) Remove and fix lint error
|
||||
"node/no-unpublished-import": ["warn"],
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# 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"
|
||||
```
|
||||
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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(
|
||||
|
||||
19
contracts/shared/helpers/defaultDeployerWallet.ts
Normal 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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
@@ -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)
|
||||
24
docs/images/bls-github-banner.svg
Normal 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 |
BIN
docs/images/system-overview/action-0.jpg
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
docs/images/system-overview/bundle-2.jpg
Normal file
|
After Width: | Height: | Size: 75 KiB |
BIN
docs/images/system-overview/interaction-3.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
docs/images/system-overview/operation-1.jpg
Normal file
|
After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 65 KiB |
104
docs/local_development.md
Normal 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
@@ -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
@@ -0,0 +1,19 @@
|
||||
# System Overview
|
||||
|
||||
## Layer 2 Amsterdam April 2022 Presentation
|
||||
|
||||
https://youtu.be/Ke4L_PXIi8M?t=22380
|
||||
|
||||
## Overview Diagram
|
||||
|
||||

|
||||
|
||||
## Actions, Bundles, & Aggregator
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
95
docs/use_bls_wallet_clients.md
Normal 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.
|
||||
@@ -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
@@ -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
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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}"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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] : [];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -333,5 +333,3 @@ export class QuillInPageProvider extends BaseProvider<InPageProviderState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default QuillInPageProvider;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
121
extension/source/QuillPage/QuillContext.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
18
extension/source/QuillPage/Theme.tsx
Normal 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;
|
||||
17
extension/source/QuillPage/TimeCell.ts
Normal 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;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 ${
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
20
extension/source/QuillPage/approximate.ts
Normal 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;
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
44
extension/source/QuillPage/getWindowQuillProvider.ts
Normal 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');
|
||||
}
|
||||
@@ -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'));
|
||||
|
||||
7
extension/source/cells/AsyncIteratee.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
type AsyncIteratee<I extends AsyncIterable<unknown>> = I extends AsyncIterable<
|
||||
infer T
|
||||
>
|
||||
? T
|
||||
: never;
|
||||
|
||||
export default AsyncIteratee;
|
||||
206
extension/source/cells/CellCollection.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
69
extension/source/cells/CellIterator.ts
Normal 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 };
|
||||
}
|
||||
}
|
||||
65
extension/source/cells/CellsDemo/BalanceWidget.tsx
Normal 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;
|
||||
126
extension/source/cells/CellsDemo/CellsDemoPage.tsx
Normal 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: </td>
|
||||
<td>
|
||||
<Counter cell={cells.a} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>b: </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: </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>
|
||||
);
|
||||
};
|
||||
15
extension/source/cells/CellsDemo/CellsDemoPage2.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
22
extension/source/cells/CellsDemo/CheckBox.tsx
Normal 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;
|
||||
33
extension/source/cells/CellsDemo/Counter.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
25
extension/source/cells/CellsDemo/DemoTable.tsx
Normal 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;
|
||||
23
extension/source/cells/CellsDemo/DemosContainer.tsx
Normal 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;
|
||||
11
extension/source/cells/CellsDemo/Display.tsx
Normal 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}</>;
|
||||
};
|
||||
16
extension/source/cells/CellsDemo/DisplayJson.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
30
extension/source/cells/CellsDemo/Selector.tsx
Normal 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;
|
||||