Compare commits
162 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce515a8635 | |||
|
|
961b12b9f3 | ||
|
|
ff9f3fb7d6 | ||
|
|
297cb67895 | ||
|
|
bcf16144c1 | ||
|
|
16f3e9add6 | ||
|
|
4af882b0ff | ||
|
|
dbaf7feca7 | ||
|
|
735aa75f81 | ||
|
|
910c3759c1 | ||
|
|
9c2d2f404c | ||
|
|
2b51d4c209 | ||
|
|
981231fb47 | ||
|
|
5bc562e810 | ||
|
|
72f0d30b52 | ||
|
|
8ec11d0476 | ||
|
|
8d732adba1 | ||
|
|
296e5c5a22 | ||
|
|
b17fff2b56 | ||
|
|
4eba91ef7e | ||
|
|
1e3aa53ab3 | ||
|
|
a05ff51555 | ||
|
|
52e1c88b58 | ||
|
|
d36dcadd34 | ||
|
|
d543dbb339 | ||
|
|
bd21cc163e | ||
|
|
4a0fc936a1 | ||
|
|
78564afabd | ||
|
|
70a2c30b4c | ||
|
|
06a9586148 | ||
|
|
7379fe4190 | ||
|
|
e59766c5df | ||
|
|
fdb18a1a17 | ||
|
|
4412046f66 | ||
|
|
0ff224ccd3 | ||
|
|
5cedacafe5 | ||
|
|
2e6179f974 | ||
|
|
4f5e3c47be | ||
|
|
4c06329153 | ||
|
|
d53452675e | ||
|
|
c92f80c484 | ||
|
|
5e95b5d8c5 | ||
|
|
6e1f57493a | ||
|
|
8ed6550635 | ||
|
|
c8eb0f1ed8 | ||
|
|
3e5e50c06e | ||
|
|
92e1b597c4 | ||
|
|
8b1a97e673 | ||
|
|
3cf184c391 | ||
|
|
8f72516374 | ||
|
|
98155e3075 | ||
|
|
0d724147bd | ||
|
|
3b959776f3 | ||
|
|
ffbca8b941 | ||
|
|
38f1bae8f5 | ||
|
|
9da1d7ab0a | ||
|
|
78bcd7568b | ||
|
|
429312500a | ||
|
|
59e0bf7565 | ||
|
|
ab51370d5a | ||
|
|
9dfb0510c4 | ||
|
|
f65e8f9244 | ||
|
|
5c8b595382 | ||
|
|
71bf9d5583 | ||
|
|
329ded4beb | ||
|
|
dc70247e2c | ||
|
|
f95beee5dc | ||
|
|
ae83c76be9 | ||
|
|
dc3026e584 | ||
|
|
b6ba0744b9 | ||
|
|
4dba9a50e8 | ||
|
|
818bc4675d | ||
|
|
f93ab330cc | ||
|
|
f64f8b1c91 | ||
|
|
9fd3f6ab82 | ||
|
|
626f9376b2 | ||
|
|
894134ba26 | ||
|
|
e1536755f4 | ||
|
|
0451d6e373 | ||
|
|
409044b8a5 | ||
|
|
5fc52f42d7 | ||
|
|
8a0d9f38b0 | ||
|
|
1aee0a84ef | ||
|
|
811b1a27f1 | ||
|
|
4d468ae107 | ||
|
|
4497a024b1 | ||
|
|
6ce98ff3dd | ||
|
|
04f66b243c | ||
|
|
21581b3c01 | ||
|
|
bbc68f9fa2 | ||
|
|
5327688a20 | ||
|
|
1122daf9a1 | ||
|
|
12269d7426 | ||
|
|
683fa0728d | ||
|
|
dd2075c351 | ||
|
|
ce29b95729 | ||
|
|
eb1069497a | ||
|
|
0228fc7d5f | ||
|
|
f8d85b14de | ||
|
|
5fa9d21246 | ||
|
|
611b8c539d | ||
|
|
389cea3c39 | ||
|
|
fbce0fc035 | ||
|
|
621b20d070 | ||
|
|
48752e8575 | ||
|
|
4efda98f2b | ||
|
|
aff8b777c5 | ||
|
|
74293959f3 | ||
|
|
46daeb6815 | ||
|
|
44ca0d71ce | ||
|
|
fbeb878cdb | ||
|
|
d17ea2ad2b | ||
|
|
4cc87ef61a | ||
|
|
125b66b86d | ||
|
|
7a0ed3f699 | ||
|
|
4e04f2ae1f | ||
|
|
dc377aeb9b | ||
|
|
48dd53622c | ||
|
|
6fe63ae9f4 | ||
|
|
4954c859c3 | ||
|
|
27f059db94 | ||
|
|
686c415a5c | ||
|
|
2e1b022512 | ||
|
|
f252ed2618 | ||
|
|
bea91c0e6e | ||
|
|
c2f6b5e8ba | ||
|
|
f3f226afdf | ||
|
|
1eb8a8b1dc | ||
|
|
8a42cfbe2b | ||
|
|
ab406bc1db | ||
|
|
8cf73d572c | ||
|
|
fa6b37db1c | ||
|
|
64cd258354 | ||
|
|
7a48495118 | ||
|
|
fdfa5cd7af | ||
|
|
77bc6c662a | ||
|
|
dc060387bc | ||
|
|
efc433e9e0 | ||
|
|
ebd97dce5c | ||
|
|
42953ffe30 | ||
|
|
4f6d53964f | ||
|
|
9e6833eb40 | ||
|
|
4c44aa5fcd | ||
|
|
2edd8f2783 | ||
|
|
861c755b09 | ||
|
|
8c268d6f06 | ||
|
|
ffd88f6cd0 | ||
|
|
caf2e2b4d3 | ||
|
|
d5d0c8f56a | ||
|
|
dc27bd6caa | ||
|
|
ab814f831c | ||
|
|
691e4294ae | ||
|
|
4a727dc159 | ||
|
|
2ca07e998a | ||
|
|
0eb7c41278 | ||
|
|
9e9e891db8 | ||
|
|
d228bb7ea9 | ||
|
|
d2606997a3 | ||
|
|
3c956ab9ec | ||
|
|
9b3e6a51a9 | ||
|
|
d512b9850f | ||
|
|
bcdf691000 |
@ -1,321 +0,0 @@
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
tokenbridge-orb:
|
||||
commands:
|
||||
install-chrome:
|
||||
steps:
|
||||
- run:
|
||||
name: Update dpkg
|
||||
command: |
|
||||
sudo apt-get clean
|
||||
sudo apt-get update
|
||||
sudo apt-get install dpkg
|
||||
- run:
|
||||
name: Install Chrome
|
||||
command: |
|
||||
wget -O chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_77.0.3865.120-1_amd64.deb
|
||||
sudo dpkg -i chrome.deb
|
||||
install-node:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Node
|
||||
command: |
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
|
||||
nvm install 10.16.3 && nvm alias default 10.16.3
|
||||
|
||||
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
|
||||
echo ' [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||
install-yarn:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Yarn
|
||||
command: |
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update && sudo apt-get -y install yarn
|
||||
yarn-install-cached-on-machine:
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore Machine Yarn Package Cache
|
||||
keys:
|
||||
- yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Install npm dependencies using Yarn
|
||||
command: nvm use default; yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
name: Save Machine Yarn Package Cache
|
||||
key: yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
wait-for-oracle:
|
||||
parameters:
|
||||
redis-key:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
name: Install redis tools
|
||||
command: sudo apt-get install -y redis-tools
|
||||
- run:
|
||||
name: Wait for the Oracle to start
|
||||
command: |
|
||||
set +e
|
||||
i=0
|
||||
while [[ $(redis-cli GET << parameters.redis-key >> ) ]]; do
|
||||
((i++))
|
||||
if [ "$i" -gt 30 ]
|
||||
then
|
||||
exit -1
|
||||
fi
|
||||
|
||||
echo "Sleeping..."
|
||||
sleep 3
|
||||
done
|
||||
executors:
|
||||
docker-node:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
machine-with-docker-caching:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
|
||||
jobs:
|
||||
initialize:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- restore_cache:
|
||||
name: Restore Yarn Package Cache
|
||||
keys:
|
||||
- yarn-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
- run: git submodule status > submodule.status
|
||||
- restore_cache:
|
||||
name: Restore contracts submodule with compiled contracts
|
||||
keys:
|
||||
- contracts-{{ checksum "submodule.status" }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
name: Save Yarn Package Cache
|
||||
key: yarn-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
- run: touch install_deploy.log; test -d contracts/build/contracts || yarn install:deploy &> install_deploy.log
|
||||
- store_artifacts:
|
||||
path: install_deploy.log
|
||||
- run: test -d contracts/build/contracts || yarn compile:contracts
|
||||
- save_cache:
|
||||
name: Save contracts submodule with compiled contracts
|
||||
key: contracts-{{ checksum "submodule.status" }}
|
||||
paths:
|
||||
- contracts
|
||||
- save_cache:
|
||||
name: Save initialized project for subsequent jobs
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
paths:
|
||||
- ~/project
|
||||
initialize-root:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- checkout
|
||||
- run: sudo su - -c 'export CI=true && cd /home/circleci/project && yarn initialize && yarn test'
|
||||
build:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run build
|
||||
lint:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run lint
|
||||
test:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run test
|
||||
oracle-e2e:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
- run: yarn run oracle-e2e
|
||||
ui-e2e:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- tokenbridge-orb/install-node
|
||||
- tokenbridge-orb/install-yarn
|
||||
- tokenbridge-orb/install-chrome
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/yarn-install-cached-on-machine
|
||||
- run: yarn run ui-e2e
|
||||
monitor-e2e:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run: ./monitor-e2e/run-tests.sh
|
||||
cover:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn workspace ui run coverage
|
||||
- run: yarn workspace ui run coveralls
|
||||
deployment-oracle:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh oracle
|
||||
no_output_timeout: 40m
|
||||
deployment-ui:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh ui
|
||||
no_output_timeout: 40m
|
||||
deployment-monitor:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh monitor
|
||||
no_output_timeout: 40m
|
||||
deployment-repo:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/install-node
|
||||
- tokenbridge-orb/install-yarn
|
||||
- tokenbridge-orb/yarn-install-cached-on-machine
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh repo
|
||||
no_output_timeout: 40m
|
||||
deployment-multiple:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh multiple
|
||||
no_output_timeout: 40m
|
||||
ultimate:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
parameters:
|
||||
scenario-name:
|
||||
description: "Molecule scenario name used to create the infrastructure"
|
||||
type: string
|
||||
redis-key:
|
||||
description: "Redis key checked for non-emptiness to assert if Oracle is running"
|
||||
type: string
|
||||
ui-e2e-grep:
|
||||
description: "Mocha grep string used to run ui-e2e tests specific to given type of bridge"
|
||||
default: ''
|
||||
type: string
|
||||
oracle-e2e-script:
|
||||
description: "Yarn script string used to run oracle-e2e tests specific to given type of bridge"
|
||||
default: ''
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/install-node
|
||||
- tokenbridge-orb/install-chrome
|
||||
- tokenbridge-orb/install-yarn
|
||||
- tokenbridge-orb/yarn-install-cached-on-machine
|
||||
- run:
|
||||
name: Prepare the infrastructure
|
||||
command: e2e-commons/up.sh deploy << parameters.scenario-name >> blocks
|
||||
no_output_timeout: 50m
|
||||
- tokenbridge-orb/wait-for-oracle:
|
||||
redis-key: << parameters.redis-key >>
|
||||
- when:
|
||||
condition: << parameters.ui-e2e-grep >>
|
||||
steps:
|
||||
- run:
|
||||
name: Run the ui-e2e tests
|
||||
command: |
|
||||
nvm use default;
|
||||
cd ui-e2e; yarn mocha -g "<< parameters.ui-e2e-grep >>" -b ./test.js
|
||||
- when:
|
||||
condition: << parameters.oracle-e2e-script >>
|
||||
steps:
|
||||
- run:
|
||||
name: Run the oracle-e2e tests
|
||||
command: cd e2e-commons && docker-compose run e2e yarn workspace oracle-e2e run << parameters.oracle-e2e-script >>
|
||||
workflows:
|
||||
tokenbridge:
|
||||
jobs:
|
||||
- initialize
|
||||
- initialize-root:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- build:
|
||||
requires:
|
||||
- initialize
|
||||
- lint:
|
||||
requires:
|
||||
- initialize
|
||||
- test:
|
||||
requires:
|
||||
- initialize
|
||||
- cover:
|
||||
requires:
|
||||
- initialize
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- oracle-e2e
|
||||
- ui-e2e
|
||||
- monitor-e2e
|
||||
- deployment-oracle
|
||||
- deployment-ui
|
||||
- deployment-monitor
|
||||
- deployment-repo
|
||||
- deployment-multiple
|
||||
- ultimate:
|
||||
name: "ultimate: native to erc"
|
||||
scenario-name: native-to-erc
|
||||
redis-key: native-erc-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "NATIVE TO ERC"
|
||||
- ultimate:
|
||||
name: "ultimate: erc to native"
|
||||
scenario-name: erc-to-native
|
||||
redis-key: erc-native-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "ERC TO NATIVE"
|
||||
- ultimate:
|
||||
name: "ultimate: erc to erc"
|
||||
scenario-name: erc-to-erc
|
||||
redis-key: erc-erc-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "ERC TO ERC"
|
||||
- ultimate:
|
||||
name: "ultimate: amb"
|
||||
scenario-name: amb
|
||||
redis-key: amb-collected-signatures:lastProcessedBlock
|
||||
oracle-e2e-script: "amb"
|
||||
- ultimate:
|
||||
name: "ultimate: amb stake erc to erc"
|
||||
scenario-name: ultimate-amb-stake-erc-to-erc
|
||||
redis-key: amb-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "AMB-STAKE-ERC-TO-ERC"
|
||||
@ -8,9 +8,17 @@
|
||||
**/docs
|
||||
**/*.md
|
||||
|
||||
monitor/**/*.env*
|
||||
oracle/**/*.env*
|
||||
!**/.env.example
|
||||
|
||||
contracts/test
|
||||
contracts/build
|
||||
oracle/test
|
||||
monitor/test
|
||||
monitor/responses
|
||||
monitor/cache/*
|
||||
commons/test
|
||||
oracle/**/*.png
|
||||
oracle/**/*.jpg
|
||||
audit
|
||||
|
||||
@ -3,3 +3,4 @@ submodules
|
||||
coverage
|
||||
lib
|
||||
dist
|
||||
build
|
||||
|
||||
227
.github/workflows/main.yml
vendored
Normal file
227
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,227 @@
|
||||
name: tokenbridge
|
||||
|
||||
on: [push]
|
||||
|
||||
env:
|
||||
DOCKER_REGISTRY: docker.pkg.github.com
|
||||
DOCKER_REPO: poanetwork/tokenbridge
|
||||
DOCKER_IMAGE_BASE: docker.pkg.github.com/poanetwork/tokenbridge
|
||||
|
||||
jobs:
|
||||
initialize:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
cache_key: ${{ steps.get_cache_key.outputs.cache_key }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Set cache key
|
||||
id: get_cache_key
|
||||
run: |
|
||||
git submodule status > submodule.status
|
||||
echo "::set-output name=cache_key::cache-repo-${{ hashFiles('yarn.lock', 'package.json', 'submodule.status') }}"
|
||||
- uses: actions/cache@v2
|
||||
id: cache-repo
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
contracts/build
|
||||
key: ${{ steps.get_cache_key.outputs.cache_key }}
|
||||
- name: Install dependencies and compile contracts
|
||||
if: ${{ !steps.cache-repo.outputs.cache-hit }}
|
||||
run: |
|
||||
yarn install --frozen-lockfile
|
||||
yarn run install:deploy
|
||||
yarn run compile:contracts
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- initialize
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: [build, lint, test]
|
||||
steps:
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 12
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- uses: actions/cache@v2
|
||||
id: cache-repo
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
contracts/build
|
||||
key: ${{ needs.initialize.outputs.cache_key }}
|
||||
- name: yarn run ${{ matrix.task }}
|
||||
run: ${{ steps.cache-repo.outputs.cache-hit }} && yarn run ${{ matrix.task }}
|
||||
build-e2e-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Evaluate e2e docker images tags
|
||||
run: |
|
||||
git submodule status > submodule.status
|
||||
echo "E2E_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e', 'e2e-commons') }}" >> $GITHUB_ENV
|
||||
echo "ORACLE_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}" >> $GITHUB_ENV
|
||||
echo "MONITOR_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}" >> $GITHUB_ENV
|
||||
echo "ALM_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}" >> $GITHUB_ENV
|
||||
- name: Rebuild and push updated images
|
||||
run: |
|
||||
function check_if_image_exists() {
|
||||
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
|
||||
}
|
||||
updated=()
|
||||
if ! check_if_image_exists e2e ${E2E_TAG}; then updated+=("e2e"); fi
|
||||
if ! check_if_image_exists oracle ${ORACLE_TAG}; then updated+=("oracle-amb"); fi
|
||||
if ! check_if_image_exists monitor ${MONITOR_TAG}; then updated+=("monitor-amb"); fi
|
||||
if ! check_if_image_exists alm ${ALM_TAG}; then updated+=("alm"); fi
|
||||
if [ ${#updated[@]} -gt 0 ]; then
|
||||
echo "Updated services: ${updated[@]}"
|
||||
cd e2e-commons
|
||||
docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
docker-compose build ${updated[@]}
|
||||
docker-compose push ${updated[@]}
|
||||
else
|
||||
echo "Nothing relevant was changed in the source"
|
||||
fi
|
||||
build-molecule-runner:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Evaluate e2e molecule runner tag
|
||||
run: echo "MOLECULE_RUNNER_TAG=${{ hashFiles('./deployment-e2e/Dockerfile') }}" >> $GITHUB_ENV
|
||||
- name: Rebuild and push molecule runner e2e image
|
||||
run: |
|
||||
function check_if_image_exists() {
|
||||
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
|
||||
}
|
||||
if check_if_image_exists molecule_runner ${MOLECULE_RUNNER_TAG}; then
|
||||
echo "Image already exists"
|
||||
else
|
||||
cd e2e-commons
|
||||
docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
docker-compose build molecule_runner
|
||||
docker-compose push molecule_runner
|
||||
fi
|
||||
e2e:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- initialize
|
||||
- build-e2e-images
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: [oracle-e2e, monitor-e2e, alm-e2e]
|
||||
include:
|
||||
- task: alm-e2e
|
||||
use-cache: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Evaluate e2e docker images tags
|
||||
run: |
|
||||
git submodule status > submodule.status
|
||||
echo "E2E_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e', 'e2e-commons') }}" >> $GITHUB_ENV
|
||||
echo "ORACLE_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}" >> $GITHUB_ENV
|
||||
echo "MONITOR_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}" >> $GITHUB_ENV
|
||||
echo "ALM_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}" >> $GITHUB_ENV
|
||||
- if: ${{ matrix.use-cache }}
|
||||
uses: actions/cache@v2
|
||||
id: cache-repo
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
contracts/build
|
||||
key: ${{ needs.initialize.outputs.cache_key }}
|
||||
- name: Login to docker registry
|
||||
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
- name: yarn run ${{ matrix.task }}
|
||||
run: ${{ !matrix.use-cache || steps.cache-repo.outputs.cache-hit }} && yarn run ${{ matrix.task }}
|
||||
- name: Upload logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: logs-${{ matrix.task }}
|
||||
path: e2e-commons/logs
|
||||
deployment:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-e2e-images
|
||||
- build-molecule-runner
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: [oracle, monitor, multiple, repo]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Evaluate e2e molecule runner tag
|
||||
run: echo "MOLECULE_RUNNER_TAG=${{ hashFiles('./deployment-e2e/Dockerfile') }}" >> $GITHUB_ENV
|
||||
- name: Login to docker registry
|
||||
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
- run: deployment-e2e/molecule.sh ${{ matrix.task }}
|
||||
ultimate:
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags') || contains(github.event.head_commit.message, 'ultimate')
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- initialize
|
||||
- build-e2e-images
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: [amb, erc-to-native]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- name: Evaluate e2e docker images tags
|
||||
run: |
|
||||
git submodule status > submodule.status
|
||||
echo "E2E_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e', 'e2e-commons') }}" >> $GITHUB_ENV
|
||||
echo "ORACLE_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}" >> $GITHUB_ENV
|
||||
echo "MONITOR_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}" >> $GITHUB_ENV
|
||||
echo "ALM_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}" >> $GITHUB_ENV
|
||||
echo "MOLECULE_RUNNER_TAG=${{ hashFiles('./deployment-e2e/Dockerfile') }}" >> $GITHUB_ENV
|
||||
- uses: actions/cache@v2
|
||||
id: cache-repo
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
contracts/build
|
||||
key: ${{ needs.initialize.outputs.cache_key }}
|
||||
- name: Login to docker registry
|
||||
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
- name: Deploy contracts
|
||||
run: ${{ steps.cache-repo.outputs.cache-hit }} && e2e-commons/up.sh deploy generate-amb-tx blocks
|
||||
- name: Pull e2e oracle image
|
||||
run: |
|
||||
docker-compose -f ./e2e-commons/docker-compose.yml pull oracle-amb
|
||||
docker tag ${DOCKER_IMAGE_BASE}/tokenbridge-e2e-oracle:${ORACLE_TAG} poanetwork/tokenbridge-oracle:latest
|
||||
- name: Deploy oracle
|
||||
run: deployment-e2e/molecule.sh ultimate-${{ matrix.task }}
|
||||
- name: Reset docker socket permissions
|
||||
run: sudo chown -R $USER:docker /var/run/docker.sock
|
||||
- name: Run oracle e2e tests
|
||||
run: docker-compose -f ./e2e-commons/docker-compose.yml run -e ULTIMATE=true e2e yarn workspace oracle-e2e run ${{ matrix.task }}
|
||||
- name: Save logs
|
||||
if: always()
|
||||
run: e2e-commons/down.sh
|
||||
- name: Upload logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: logs-ultimate-${{ matrix.task }}
|
||||
path: e2e-commons/logs
|
||||
13
.gitignore
vendored
13
.gitignore
vendored
@ -10,11 +10,8 @@ dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
*.env*
|
||||
!.env.example
|
||||
.idea
|
||||
.nyc_output
|
||||
logs/
|
||||
@ -49,5 +46,9 @@ __pycache__
|
||||
|
||||
#monitor
|
||||
monitor/responses/*
|
||||
monitor/configs/*.env
|
||||
monitor/cache/*
|
||||
!monitor/cache/.gitkeep
|
||||
!monitor/.gitkeep
|
||||
|
||||
# Local Netlify folder
|
||||
.netlify
|
||||
@ -8,11 +8,11 @@ COMMON_HOME_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in t
|
||||
COMMON_FOREIGN_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Foreign network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
|
||||
COMMON_HOME_BRIDGE_ADDRESS | The address of the bridge contract address in the Home network. It is used to listen to events from and send validators' transactions to the Home network. | hexidecimal beginning with "0x"
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS | The address of the bridge contract address in the Foreign network. It is used to listen to events from and send validators' transactions to the Foreign network. | hexidecimal beginning with "0x"
|
||||
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. | URL
|
||||
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
|
||||
COMMON_HOME_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_HOME_GAS_PRICE_SUPPLIER_URL` is not used. | `instant` / `fast` / `standard` / `slow`
|
||||
COMMON_HOME_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Home Bridge contract are not available. | integer
|
||||
COMMON_HOME_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
|
||||
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. | URL
|
||||
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
|
||||
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL`is not used. | `instant` / `fast` / `standard` / `slow`
|
||||
COMMON_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) used if both the oracle and fall back gas price specified in the Foreign Bridge contract are not available. | integer
|
||||
COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
|
||||
@ -22,7 +22,7 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of th
|
||||
|
||||
name | description | value
|
||||
--- | --- | ---
|
||||
ORACLE_BRIDGE_MODE | The bridge mode. The bridge starts listening to a different set of events based on this parameter. | NATIVE_TO_ERC / ERC_TO_ERC / ERC_TO_NATIVE
|
||||
ORACLE_BRIDGE_MODE | The bridge mode. The bridge starts listening to a different set of events based on this parameter. | ERC_TO_NATIVE / ARBITRARY_MESSAGE
|
||||
ORACLE_ALLOW_HTTP_FOR_RPC | **Only use in test environments - must be omitted in production environments.**. If this parameter is specified and set to `yes`, RPC URLs can be specified in form of HTTP links. A warning that the connection is insecure will be written to the logs. | `yes` / `no`
|
||||
ORACLE_HOME_RPC_POLLING_INTERVAL | The interval in milliseconds used to request the RPC node in the Home network for new blocks. The interval should match the average production time for a new block. | integer
|
||||
ORACLE_FOREIGN_RPC_POLLING_INTERVAL | The interval in milliseconds used to request the RPC node in the Foreign network for new blocks. The interval should match the average production time for a new block. | integer
|
||||
@ -36,29 +36,31 @@ ORACLE_LOG_LEVEL | Set the level of details in the logs. | `trace` / `debug` / `
|
||||
ORACLE_MAX_PROCESSING_TIME | The workers processes will be killed if this amount of time (in milliseconds) is elapsed before they finish processing. It is recommended to set this value to 4 times the value of the longest polling time (set with the `HOME_POLLING_INTERVAL` and `FOREIGN_POLLING_INTERVAL` variables). To disable this, set the time to 0. | integer
|
||||
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY | The private key of the bridge validator used to sign confirmations before sending transactions to the bridge contracts. The validator account is calculated automatically from the private key. Every bridge instance (set of watchers and senders) must have its own unique private key. The specified private key is used to sign transactions on both sides of the bridge. | hexidecimal without "0x"
|
||||
ORACLE_VALIDATOR_ADDRESS | The public address of the bridge validator | hexidecimal with "0x"
|
||||
|
||||
|
||||
## UI configuration
|
||||
|
||||
name | description | value
|
||||
--- | --- | ---
|
||||
UI_TITLE | The title for the bridge UI page. `%c` will be replaced by the name of the network. | string
|
||||
UI_OG_TITLE | The meta title for the deployed bridge page. | string
|
||||
UI_DESCRIPTION | The meta description for the deployed bridge page. | string
|
||||
UI_NATIVE_TOKEN_DISPLAY_NAME | name of the home native coin | string
|
||||
UI_HOME_NETWORK_DISPLAY_NAME | name to be displayed for home network | string
|
||||
UI_FOREIGN_NETWORK_DISPLAY_NAME | name to be displayed for foreign network | string
|
||||
UI_HOME_WITHOUT_EVENTS | `true` if home network doesn't support events | true/false
|
||||
UI_FOREIGN_WITHOUT_EVENTS | `true` if foreign network doesn't support events | true/false
|
||||
UI_HOME_EXPLORER_TX_TEMPLATE | template link to transaction on home explorer. `%s` will be replaced by transaction hash | URL template
|
||||
UI_FOREIGN_EXPLORER_TX_TEMPLATE | template link to transaction on foreign explorer. `%s` will be replaced by transaction hash | URL template
|
||||
UI_HOME_EXPLORER_ADDRESS_TEMPLATE | template link to address on home explorer. `%s` will be replaced by address | URL template
|
||||
UI_FOREIGN_EXPLORER_ADDRESS_TEMPLATE | template link to address on foreign explorer. `%s` will be replaced by address | URL template
|
||||
UI_HOME_GAS_PRICE_UPDATE_INTERVAL | An interval in milliseconds used to get the updated gas price value either from the oracle or from the Home Bridge contract. | integer
|
||||
UI_FOREIGN_GAS_PRICE_UPDATE_INTERVAL | An interval in milliseconds used to get the updated gas price value either from the oracle or from the Foreign Bridge contract. | integer
|
||||
UI_PORT | The port for the UI app. | integer
|
||||
UI_STYLES | The set of styles to render the bridge UI page. | core/classic/stake
|
||||
UI_PUBLIC_URL | The public url for the deployed bridge page | string
|
||||
ORACLE_TX_REDUNDANCY | If set to `true`, instructs oracle to send `eth_sendRawTransaction` requests through all available RPC urls defined in `COMMON_HOME_RPC_URL` and `COMMON_FOREIGN_RPC_URL` variables instead of using first available one
|
||||
ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST | Filename with a list of addresses, separated by newlines. If set, determines the privileged set of accounts whose requests will be automatically processed by the CollectedSignatures watcher. | string
|
||||
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST | Filename with a list of addresses, separated by newlines. If set, determines the blocked set of accounts whose requests will not be automatically processed by the CollectedSignatures watcher. Has a lower priority than the `ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST` | string
|
||||
ORACLE_HOME_TO_FOREIGN_CHECK_SENDER | If set to `true`, instructs the oracle to do an extra check for transaction origin in the block/allowance list. `false` by default. | `true` / `false`
|
||||
ORACLE_ALWAYS_RELAY_SIGNATURES | If set to `true`, the oracle will always relay signatures even if it was not the last who finilized the signatures collecting process. The default is `false`. | `true` / `false`
|
||||
ORACLE_RPC_REQUEST_TIMEOUT | Timeout in milliseconds for a single RPC request. Default value is `ORACLE_*_RPC_POLLING_INTERVAL * 2`. | integer
|
||||
ORACLE_HOME_TX_RESEND_INTERVAL | Interval in milliseconds for automatic resending of stuck transactions for Home sender service. Defaults to 20 minutes. | integer
|
||||
ORACLE_FOREIGN_TX_RESEND_INTERVAL | Interval in milliseconds for automatic resending of stuck transactions for Foreign sender service. Defaults to 20 minutes. | integer
|
||||
ORACLE_SHUTDOWN_SERVICE_URL | Optional external URL to some other service/monitor/configuration manager that controls the remote shutdown process. GET request should return `application/json` message with the following schema: `{ shutdown: true/false }`. | URL
|
||||
ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL | Optional interval in milliseconds used to request the side RPC node or external shutdown service. Default is 120000. | integer
|
||||
ORACLE_SIDE_RPC_URL | Optional HTTPS URL(s) for communication with the external shutdown service or side RPC nodes, used for shutdown manager activities. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
|
||||
ORACLE_FOREIGN_ARCHIVE_RPC_URL | Optional HTTPS URL(s) for communication with the archive nodes on the foreign network. Only used in AMB bridge mode for async information request processing. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
|
||||
ORACLE_SHUTDOWN_CONTRACT_ADDRESS | Optional contract address in the side chain accessible through `ORACLE_SIDE_RPC_URL`, where the method passed in `ORACLE_SHUTDOWN_CONTRACT_METHOD` is implemented. | `address`
|
||||
ORACLE_SHUTDOWN_CONTRACT_METHOD | Method signature to be used in the side chain to identify the current shutdown status. Method should return boolean. Default value is `isShutdown()`. | `function signature`
|
||||
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Foreign chain. Infinite, if not provided. | `integer`
|
||||
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Home chain. Infinite, if not provided. | `integer`
|
||||
ORACLE_JSONRPC_ERROR_CODES | Override default JSON rpc error codes that can trigger RPC fallback to the next URL from the list (or a retry in case of a single RPC URL). Default is `-32603,-32002,-32005`. Should be a comma-separated list of negative integers. | `string`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING | If set to `true`, home events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the home chain. Defaults to `1000` | `integer`
|
||||
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the home chain. Defaults to `500` | `integer`
|
||||
ORACLE_HOME_RPC_SYNC_STATE_CHECK_INTERVAL | Interval for checking JSON RPC sync state, by requesting the latest block number. Oracle will switch to the fallback JSON RPC in case sync process is stuck | `integer`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING | If set to `true`, foreign events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the foreign chain. Defaults to `500` | `integer`
|
||||
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the foreign chain. Defaults to `250` | `integer`
|
||||
ORACLE_FOREIGN_RPC_SYNC_STATE_CHECK_INTERVAL | Interval for checking JSON RPC sync state, by requesting the latest block number. Oracle will switch to the fallback JSON RPC in case sync process is stuck | `integer`
|
||||
|
||||
|
||||
## Monitor configuration
|
||||
@ -72,4 +74,9 @@ MONITOR_VALIDATOR_FOREIGN_TX_LIMIT | Average gas usage of a transaction sent by
|
||||
MONITOR_TX_NUMBER_THRESHOLD | If estimated number of transaction is equal to or below this value, the monitor will report that the validator has less funds than it is required. | integer
|
||||
MONITOR_PORT | The port for the Monitor. | integer
|
||||
MONITOR_BRIDGE_NAME | The name to be used in the url path for the bridge | string
|
||||
MONITOR_CACHE_EVENTS | If set to true, monitor will cache obtained events for other workers runs
|
||||
MONITOR_CACHE_EVENTS | If set to true, monitor will cache obtained events for other workers runs | `true` / `false`
|
||||
MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST | File with a list of addresses, separated by newlines. If set, determines the privileged set of accounts whose requests should be automatically processed by the CollectedSignatures watcher. | string
|
||||
MONITOR_HOME_TO_FOREIGN_BLOCK_LIST | File with a list of addresses, separated by newlines. If set, determines the set of accounts whose requests should be marked as unclaimed. Has a lower priority than the `MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST`. | string
|
||||
MONITOR_HOME_TO_FOREIGN_CHECK_SENDER | If set to `true`, instructs the oracle to do an extra check for transaction origin in the block/allowance list. `false` by default. | `true` / `false`
|
||||
MONITOR_HOME_VALIDATORS_BALANCE_ENABLE | If set, defines the list of home validator addresses for which balance should be checked. | `string`
|
||||
MONITOR_FOREIGN_VALIDATORS_BALANCE_ENABLE | If set, defines the list of foreign validator addresses for which balance should be checked. | `string`
|
||||
|
||||
@ -1,15 +1,33 @@
|
||||
FROM node:10
|
||||
FROM node:12 as contracts
|
||||
|
||||
WORKDIR /mono
|
||||
|
||||
COPY contracts/package.json contracts/package-lock.json ./contracts/
|
||||
|
||||
WORKDIR /mono/contracts
|
||||
RUN npm install --only=prod
|
||||
|
||||
COPY ./contracts/truffle-config.js ./
|
||||
COPY ./contracts/contracts ./contracts
|
||||
RUN npm run compile
|
||||
|
||||
FROM node:12
|
||||
|
||||
WORKDIR /mono
|
||||
COPY package.json .
|
||||
COPY --from=contracts /mono/contracts/build ./contracts/build
|
||||
COPY commons/package.json ./commons/
|
||||
COPY oracle-e2e/package.json ./oracle-e2e/
|
||||
COPY monitor-e2e/package.json ./monitor-e2e/
|
||||
COPY contracts/package.json ./contracts/
|
||||
COPY oracle/src/utils/constants.js ./oracle/src/utils/constants.js
|
||||
|
||||
COPY yarn.lock .
|
||||
RUN yarn install --frozen-lockfile
|
||||
COPY ./contracts ./contracts
|
||||
RUN yarn install:deploy
|
||||
RUN yarn compile:contracts
|
||||
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production
|
||||
|
||||
COPY . .
|
||||
COPY ./contracts/deploy ./contracts/deploy
|
||||
RUN yarn install:deploy
|
||||
|
||||
COPY commons/ ./commons/
|
||||
COPY oracle-e2e/ ./oracle-e2e/
|
||||
COPY monitor-e2e/ ./monitor-e2e/
|
||||
COPY e2e-commons/ ./e2e-commons/
|
||||
|
||||
14
README.md
14
README.md
@ -1,4 +1,4 @@
|
||||
[](https://circleci.com/gh/poanetwork/tokenbridge)
|
||||

|
||||
[](https://gitter.im/poanetwork/poa-bridge?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
@ -19,13 +19,11 @@ Sub-repositories maintained within this monorepo are listed below.
|
||||
|
||||
| Sub-repository | Description |
|
||||
| --- | --- |
|
||||
| [Oracle](oracle/README.md) | Oracle responsible for listening to bridge related events and authorizing asset transfers. |
|
||||
| [UI](ui/README.md) | DApp interface to transfer tokens and coins between chains. |
|
||||
| [Oracle](oracle/README.md) | Responsible for listening to bridge related events and authorizing asset transfers. |
|
||||
| [Monitor](monitor/README.md) | Tool for checking balances and unprocessed events in bridged networks. |
|
||||
| [Deployment](deployment/README.md) | Ansible playbooks for deploying cross-chain bridges. |
|
||||
| [Oracle-E2E](oracle-e2e/README.md) | End to end tests for the Oracle |
|
||||
| [Monitor-E2E](monitor-e2e/README.md) | End to end tests for the Monitor |
|
||||
| [UI-E2E](ui-e2e/README.md) | End to end tests for the UI |
|
||||
| [Deployment-E2E](deployment-e2e/README.md) | End to end tests for the Deployment |
|
||||
| [Commons](commons/README.md) | Interfaces, constants and utilities shared between the sub-repositories |
|
||||
| [E2E-Commons](e2e-commons/README.md) | Common utilities and configuration used in end to end tests |
|
||||
@ -56,8 +54,6 @@ Additionally there are [Smart Contracts](https://github.com/poanetwork/tokenbrid
|
||||
|
||||
The POA TokenBridge provides four operational modes:
|
||||
|
||||
- [x] `Native-to-ERC20` **Coins** on a Home network can be converted to ERC20-compatible **tokens** on a Foreign network. Coins are locked on the Home side and the corresponding amount of ERC20 tokens are minted on the Foreign side. When the operation is reversed, tokens are burnt on the Foreign side and unlocked in the Home network. **More Information: [POA-to-POA20 Bridge](https://medium.com/poa-network/introducing-poa-bridge-and-poa20-55d8b78058ac)**
|
||||
- [x] `ERC20-to-ERC20` ERC20-compatible tokens on the Foreign network are locked and minted as ERC20-compatible tokens (ERC677 tokens) on the Home network. When transferred from Home to Foreign, they are burnt on the Home side and unlocked in the Foreign network. This can be considered a form of atomic swap when a user swaps the token "X" in network "A" to the token "Y" in network "B". **More Information: [ERC20-to-ERC20](https://medium.com/poa-network/introducing-the-erc20-to-erc20-tokenbridge-ce266cc1a2d0)**
|
||||
- [x] `ERC20-to-Native`: Pre-existing **tokens** in the Foreign network are locked and **coins** are minted in the `Home` network. In this mode, the Home network consensus engine invokes [Parity's Block Reward contract](https://wiki.parity.io/Block-Reward-Contract.html) to mint coins per the bridge contract request. **More Information: [xDai Chain](https://medium.com/poa-network/poa-network-partners-with-makerdao-on-xdai-chain-the-first-ever-usd-stable-blockchain-65a078c41e6a)**
|
||||
- [x] `Arbitrary-Message`: Transfer arbitrary data between two networks as so the data could be interpreted as an arbitrary contract method invocation.
|
||||
|
||||
@ -68,7 +64,7 @@ Clone the repository:
|
||||
git clone https://github.com/poanetwork/tokenbridge
|
||||
```
|
||||
|
||||
If there is no need to build docker images for the TokenBridge components (oracle, monitor, UI), initialize submodules, install dependencies, compile the Smart Contracts:
|
||||
If there is no need to build docker images for the TokenBridge components (oracle, monitor), initialize submodules, install dependencies, compile the Smart Contracts:
|
||||
```
|
||||
yarn initialize
|
||||
```
|
||||
@ -91,7 +87,7 @@ Running tests for all projects:
|
||||
yarn test
|
||||
```
|
||||
|
||||
Additionally there are end-to-end tests for [Oracle](oracle-e2e/README.md) and [UI](ui-e2e/README.md).
|
||||
Additionally there are end-to-end tests for [Oracle](oracle-e2e/README.md) and [Monitor](monitor-e2e/README.md).
|
||||
|
||||
For details on building, running and developing please refer to respective READMEs in sub-repositories.
|
||||
|
||||
@ -110,4 +106,4 @@ This project is licensed under the GNU Lesser General Public License v3.0. See t
|
||||
|
||||
## References
|
||||
|
||||
* [TokenBridge Documentation](http://www.tokenbridge.net/)
|
||||
* [TokenBridge Documentation](https://docs.tokenbridge.net/)
|
||||
|
||||
15
alm-e2e/.eslintrc
Normal file
15
alm-e2e/.eslintrc
Normal file
@ -0,0 +1,15 @@
|
||||
{
|
||||
"extends": [
|
||||
"plugin:node/recommended",
|
||||
"airbnb-base",
|
||||
"../.eslintrc"
|
||||
],
|
||||
"plugins": ["node", "jest"],
|
||||
"env": {
|
||||
"jest/globals": true
|
||||
},
|
||||
"globals": {
|
||||
"page": true,
|
||||
"browser": true
|
||||
}
|
||||
}
|
||||
24
alm-e2e/package.json
Normal file
24
alm-e2e/package.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "alm-e2e",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"test": "jest --detectOpenHandles",
|
||||
"lint": "eslint . --ignore-path ../.eslintignore",
|
||||
"lint:fix": "eslint . --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"jest": "24.7.1",
|
||||
"jest-puppeteer": "^4.4.0",
|
||||
"puppeteer": "^5.2.1"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-puppeteer"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-jest": "^23.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.22"
|
||||
}
|
||||
}
|
||||
13
alm-e2e/run-tests.sh
Executable file
13
alm-e2e/run-tests.sh
Executable file
@ -0,0 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
cd $(dirname $0)
|
||||
|
||||
../e2e-commons/up.sh deploy generate-amb-tx blocks alm alm-e2e
|
||||
|
||||
# run oracle amb e2e tests to generate transactions for alm
|
||||
docker-compose -f ../e2e-commons/docker-compose.yml run e2e yarn workspace oracle-e2e run alm
|
||||
|
||||
yarn test
|
||||
rc=$?
|
||||
|
||||
../e2e-commons/down.sh
|
||||
exit $rc
|
||||
50
alm-e2e/src/test.js
Normal file
50
alm-e2e/src/test.js
Normal file
@ -0,0 +1,50 @@
|
||||
const puppeteer = require('puppeteer')
|
||||
const { waitUntil } = require('./utils/utils')
|
||||
|
||||
jest.setTimeout(60000)
|
||||
|
||||
const statusText = 'Success'
|
||||
const statusSelector = 'label[data-id="status"]'
|
||||
|
||||
const homeToForeignTxURL = 'http://localhost:3004/77/0x295efbe6ae98937ef35d939376c9bd752b4dc6f6899a9d5ddd6a57cea3d76c89'
|
||||
const foreignToHomeTxURL = 'http://localhost:3004/42/0x7262f7dbe6c30599edded2137fbbe93c271b37f5c54dd27f713f0cf510e3b4dd'
|
||||
|
||||
describe('ALM', () => {
|
||||
let browser
|
||||
let page
|
||||
|
||||
beforeAll(async () => {
|
||||
browser = await puppeteer.launch()
|
||||
page = await browser.newPage()
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
await browser.close()
|
||||
})
|
||||
|
||||
it('should be titled "AMB Live Monitoring"', async () => {
|
||||
await page.goto(foreignToHomeTxURL)
|
||||
|
||||
await expect(page.title()).resolves.toMatch('AMB Live Monitoring')
|
||||
})
|
||||
it('should display information of foreign to home transaction', async () => {
|
||||
await page.goto(foreignToHomeTxURL)
|
||||
|
||||
await page.waitForSelector(statusSelector)
|
||||
await waitUntil(async () => {
|
||||
const element = await page.$(statusSelector)
|
||||
const text = await page.evaluate(element => element.textContent, element)
|
||||
return text === statusText
|
||||
})
|
||||
})
|
||||
it('should display information of home to foreign transaction', async () => {
|
||||
await page.goto(homeToForeignTxURL)
|
||||
|
||||
await page.waitForSelector(statusSelector)
|
||||
await waitUntil(async () => {
|
||||
const element = await page.$(statusSelector)
|
||||
const text = await page.evaluate(element => element.textContent, element)
|
||||
return text === statusText
|
||||
})
|
||||
})
|
||||
})
|
||||
15
alm-e2e/src/utils/utils.js
Normal file
15
alm-e2e/src/utils/utils.js
Normal file
@ -0,0 +1,15 @@
|
||||
const waitUntil = async (predicate, step = 100, timeout = 20000) => {
|
||||
const stopTime = Date.now() + timeout
|
||||
while (Date.now() <= stopTime) {
|
||||
const result = await predicate()
|
||||
if (result) {
|
||||
return result
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, step)) // sleep
|
||||
}
|
||||
throw new Error(`waitUntil timed out after ${timeout} ms`)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
waitUntil
|
||||
}
|
||||
@ -3,3 +3,13 @@ COMMON_FOREIGN_BRIDGE_ADDRESS=0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560
|
||||
|
||||
COMMON_HOME_RPC_URL=https://sokol.poa.network
|
||||
COMMON_FOREIGN_RPC_URL=https://kovan.infura.io/v3/
|
||||
|
||||
ALM_HOME_NETWORK_NAME=Sokol Testnet
|
||||
ALM_FOREIGN_NETWORK_NAME=Kovan Testnet
|
||||
|
||||
ALM_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx/%s
|
||||
ALM_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
|
||||
|
||||
ALM_HOME_EXPLORER_API=https://blockscout.com/poa/sokol/api
|
||||
ALM_FOREIGN_EXPLORER_API=https://kovan.etherscan.io/api?apikey=YourApiKeyToken
|
||||
PORT=8080
|
||||
|
||||
2
alm/.gitignore
vendored
2
alm/.gitignore
vendored
@ -1,5 +1,7 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
src/snapshots/*.json
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
||||
@ -1,23 +1,38 @@
|
||||
FROM node:12
|
||||
FROM node:12 as contracts
|
||||
|
||||
WORKDIR /mono
|
||||
|
||||
COPY contracts/package.json contracts/package-lock.json ./contracts/
|
||||
|
||||
WORKDIR /mono/contracts
|
||||
RUN npm install --only=prod
|
||||
|
||||
COPY ./contracts/truffle-config.js ./
|
||||
COPY ./contracts/contracts ./contracts
|
||||
RUN npm run compile
|
||||
|
||||
FROM node:12 as alm-builder
|
||||
|
||||
WORKDIR /mono
|
||||
COPY package.json .
|
||||
COPY contracts/package.json ./contracts/
|
||||
COPY --from=contracts /mono/contracts/build ./contracts/build
|
||||
COPY commons/package.json ./commons/
|
||||
COPY alm/package.json ./alm/
|
||||
COPY yarn.lock .
|
||||
RUN yarn install --production --frozen-lockfile
|
||||
|
||||
COPY ./contracts ./contracts
|
||||
RUN yarn run compile:contracts
|
||||
RUN mv ./contracts/build ./ && rm -rf ./contracts/* ./contracts/.[!.]* && mv ./build ./contracts/
|
||||
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile
|
||||
|
||||
COPY ./commons ./commons
|
||||
|
||||
COPY ./alm ./alm
|
||||
|
||||
ARG DOT_ENV_PATH=./alm/.env
|
||||
COPY ${DOT_ENV_PATH} ./alm/.env
|
||||
|
||||
WORKDIR /mono/alm
|
||||
CMD echo "To start the application run:" \
|
||||
"yarn start"
|
||||
RUN yarn run build
|
||||
|
||||
|
||||
FROM node:12 as alm-production
|
||||
RUN yarn global add serve
|
||||
WORKDIR /app
|
||||
COPY --from=alm-builder /mono/alm/build .
|
||||
CMD serve -p $PORT -s .
|
||||
|
||||
@ -5,8 +5,10 @@ services:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: alm/Dockerfile
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
env_file: ./.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
entrypoint: yarn start
|
||||
entrypoint: serve -p ${PORT} -s .
|
||||
|
||||
@ -3,26 +3,44 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@ethersproject/bignumber": ">=5.0.0-beta.130",
|
||||
"@react-hook/window-size": "^3.0.6",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/promise-retry": "^1.1.3",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@use-it/interval": "^0.1.3",
|
||||
"@web3-react/core": "^6.1.1",
|
||||
"@web3-react/injected-connector": "^6.0.7",
|
||||
"customize-cra": "^1.0.0",
|
||||
"date-fns": "^2.14.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"fast-memoize": "^2.5.2",
|
||||
"promise-retry": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.0.1",
|
||||
"typescript": "^3.5.2"
|
||||
"styled-components": "^5.1.1",
|
||||
"typescript": "^3.5.2",
|
||||
"web3": "1.2.11",
|
||||
"web3-eth-contract": "1.2.11",
|
||||
"web3-utils": "1.2.11"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "./load-env.sh react-app-rewired start",
|
||||
"build": "./load-env.sh react-app-rewired build",
|
||||
"start": "yarn createSnapshots && ./load-env.sh react-app-rewired start",
|
||||
"build": "yarn createSnapshots && ./load-env.sh react-app-rewired build",
|
||||
"test": "react-app-rewired test",
|
||||
"eject": "react-app-rewired eject",
|
||||
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore"
|
||||
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore",
|
||||
"createSnapshots": "node scripts/createSnapshots.js"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
@ -40,6 +58,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-prettier": "^3.1.3"
|
||||
"eslint-plugin-prettier": "^3.1.3",
|
||||
"node-fetch": "^2.6.1"
|
||||
}
|
||||
}
|
||||
|
||||
1
alm/public/_redirects
Normal file
1
alm/public/_redirects
Normal file
@ -0,0 +1 @@
|
||||
/* /index.html 200
|
||||
@ -7,8 +7,9 @@
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="Web site created using create-react-app"
|
||||
content="AMB Live Monitoring"
|
||||
/>
|
||||
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
@ -24,7 +25,8 @@
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<title>React App</title>
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,700" rel="stylesheet">
|
||||
<title>AMB Live Monitoring</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 9.4 KiB |
@ -1,21 +1,11 @@
|
||||
{
|
||||
"short_name": "React App",
|
||||
"name": "Create React App Sample",
|
||||
"short_name": "ALM",
|
||||
"name": "AMB Live Monitoring",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
},
|
||||
{
|
||||
"src": "logo192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "logo512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
|
||||
155
alm/scripts/createSnapshots.js
Normal file
155
alm/scripts/createSnapshots.js
Normal file
@ -0,0 +1,155 @@
|
||||
const { BRIDGE_VALIDATORS_ABI, HOME_AMB_ABI } = require('commons')
|
||||
|
||||
const path = require('path')
|
||||
require('dotenv').config()
|
||||
const Web3 = require('web3')
|
||||
const fetch = require('node-fetch')
|
||||
const { URL } = require('url')
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
const {
|
||||
COMMON_HOME_RPC_URL,
|
||||
COMMON_HOME_BRIDGE_ADDRESS,
|
||||
COMMON_FOREIGN_RPC_URL,
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS,
|
||||
ALM_FOREIGN_EXPLORER_API,
|
||||
ALM_HOME_EXPLORER_API
|
||||
} = process.env
|
||||
|
||||
const generateSnapshot = async (side, url, bridgeAddress) => {
|
||||
const snapshotPath = `../src/snapshots/${side}.json`
|
||||
const snapshotFullPath = path.join(__dirname, snapshotPath)
|
||||
const snapshot = {}
|
||||
|
||||
const web3 = new Web3(new Web3.providers.HttpProvider(url))
|
||||
const api = side === 'home' ? ALM_HOME_EXPLORER_API : ALM_FOREIGN_EXPLORER_API
|
||||
|
||||
const getPastEventsWithFallback = (contract, eventName, options) =>
|
||||
contract.getPastEvents(eventName, options).catch(async e => {
|
||||
if (e.message.includes('exceed maximum block range')) {
|
||||
const abi = contract.options.jsonInterface.find(abi => abi.type === 'event' && abi.name === eventName)
|
||||
|
||||
const url = new URL(api)
|
||||
url.searchParams.append('module', 'logs')
|
||||
url.searchParams.append('action', 'getLogs')
|
||||
url.searchParams.append('address', contract.options.address)
|
||||
url.searchParams.append('fromBlock', options.fromBlock)
|
||||
url.searchParams.append('toBlock', options.toBlock || 'latest')
|
||||
url.searchParams.append('topic0', web3.eth.abi.encodeEventSignature(abi))
|
||||
|
||||
const logs = await fetch(url).then(res => res.json())
|
||||
|
||||
return logs.result.map(log => ({
|
||||
transactionHash: log.transactionHash,
|
||||
blockNumber: parseInt(log.blockNumber.slice(2), 16),
|
||||
returnValues: web3.eth.abi.decodeLog(abi.inputs, log.data, log.topics.slice(1))
|
||||
}))
|
||||
}
|
||||
throw e
|
||||
})
|
||||
|
||||
const currentBlockNumber = await web3.eth.getBlockNumber()
|
||||
snapshot.snapshotBlockNumber = currentBlockNumber
|
||||
|
||||
// Save chainId
|
||||
snapshot.chainId = await web3.eth.getChainId()
|
||||
|
||||
const bridgeContract = new web3.eth.Contract(HOME_AMB_ABI, bridgeAddress)
|
||||
|
||||
// Save RequiredBlockConfirmationChanged events
|
||||
let requiredBlockConfirmationChangedEvents = await getPastEventsWithFallback(
|
||||
bridgeContract,
|
||||
'RequiredBlockConfirmationChanged',
|
||||
{
|
||||
fromBlock: 0,
|
||||
toBlock: currentBlockNumber
|
||||
}
|
||||
)
|
||||
|
||||
// In case RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB
|
||||
// manually generate an event for this. Example Sokol - Kovan bridge
|
||||
if (requiredBlockConfirmationChangedEvents.length === 0) {
|
||||
const deployedAtBlock = await bridgeContract.methods.deployedAtBlock().call()
|
||||
const blockConfirmations = await bridgeContract.methods.requiredBlockConfirmations().call()
|
||||
|
||||
requiredBlockConfirmationChangedEvents.push({
|
||||
blockNumber: parseInt(deployedAtBlock),
|
||||
returnValues: {
|
||||
requiredBlockConfirmations: blockConfirmations
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
snapshot.RequiredBlockConfirmationChanged = requiredBlockConfirmationChangedEvents.map(e => ({
|
||||
blockNumber: e.blockNumber,
|
||||
returnValues: {
|
||||
requiredBlockConfirmations: e.returnValues.requiredBlockConfirmations
|
||||
}
|
||||
}))
|
||||
|
||||
const validatorAddress = await bridgeContract.methods.validatorContract().call()
|
||||
const validatorContract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorAddress)
|
||||
|
||||
// Save RequiredSignaturesChanged events
|
||||
const RequiredSignaturesChangedEvents = await getPastEventsWithFallback(
|
||||
validatorContract,
|
||||
'RequiredSignaturesChanged',
|
||||
{
|
||||
fromBlock: 0,
|
||||
toBlock: currentBlockNumber
|
||||
}
|
||||
)
|
||||
snapshot.RequiredSignaturesChanged = RequiredSignaturesChangedEvents.map(e => ({
|
||||
blockNumber: e.blockNumber,
|
||||
returnValues: {
|
||||
requiredSignatures: e.returnValues.requiredSignatures
|
||||
}
|
||||
}))
|
||||
|
||||
// Save ValidatorAdded events
|
||||
const validatorAddedEvents = await getPastEventsWithFallback(validatorContract, 'ValidatorAdded', {
|
||||
fromBlock: 0,
|
||||
toBlock: currentBlockNumber
|
||||
})
|
||||
|
||||
snapshot.ValidatorAdded = validatorAddedEvents.map(e => ({
|
||||
blockNumber: e.blockNumber,
|
||||
returnValues: {
|
||||
validator: e.returnValues.validator
|
||||
},
|
||||
event: 'ValidatorAdded'
|
||||
}))
|
||||
|
||||
// Save ValidatorRemoved events
|
||||
const validatorRemovedEvents = await getPastEventsWithFallback(validatorContract, 'ValidatorRemoved', {
|
||||
fromBlock: 0,
|
||||
toBlock: currentBlockNumber
|
||||
})
|
||||
|
||||
snapshot.ValidatorRemoved = validatorRemovedEvents.map(e => ({
|
||||
blockNumber: e.blockNumber,
|
||||
returnValues: {
|
||||
validator: e.returnValues.validator
|
||||
},
|
||||
event: 'ValidatorRemoved'
|
||||
}))
|
||||
|
||||
// Write snapshot
|
||||
fs.writeFileSync(snapshotFullPath, JSON.stringify(snapshot, null, 2))
|
||||
}
|
||||
|
||||
const main = async () => {
|
||||
await Promise.all([
|
||||
generateSnapshot('home', COMMON_HOME_RPC_URL, COMMON_HOME_BRIDGE_ADDRESS),
|
||||
generateSnapshot('foreign', COMMON_FOREIGN_RPC_URL, COMMON_FOREIGN_BRIDGE_ADDRESS)
|
||||
])
|
||||
}
|
||||
|
||||
main()
|
||||
.then(() => process.exit(0))
|
||||
.catch(error => {
|
||||
console.log('Error while creating snapshots')
|
||||
console.error(error)
|
||||
process.exit(0)
|
||||
})
|
||||
@ -1,14 +0,0 @@
|
||||
.App {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
import React from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import App from './App'
|
||||
|
||||
test('renders learn react link', () => {
|
||||
const { getByText } = render(<App />)
|
||||
const linkElement = getByText(/AMB Live Monitoring/i)
|
||||
expect(linkElement).toBeInTheDocument()
|
||||
})
|
||||
@ -1,13 +1,19 @@
|
||||
import React from 'react'
|
||||
import './App.css'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { Web3ReactProvider } from '@web3-react/core'
|
||||
import Web3 from 'web3'
|
||||
import { MainPage } from './components/MainPage'
|
||||
import { StateProvider } from './state/StateProvider'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<p>AMB Live Monitoring</p>
|
||||
</header>
|
||||
</div>
|
||||
<BrowserRouter>
|
||||
<Web3ReactProvider getLibrary={provider => new Web3(provider)}>
|
||||
<StateProvider>
|
||||
<MainPage />
|
||||
</StateProvider>
|
||||
</Web3ReactProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
319
alm/src/abis/BridgeValidators.ts
Normal file
319
alm/src/abis/BridgeValidators.ts
Normal file
@ -0,0 +1,319 @@
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
const abi: AbiItem[] = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorCount',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeValidatorsInterfacesVersion',
|
||||
outputs: [
|
||||
{
|
||||
name: 'major',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'minor',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'patch',
|
||||
type: 'uint64'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'isInitialized',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorList',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address[]'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_requiredSignatures',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setRequiredSignatures',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredSignatures',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_address',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'getNextValidator',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'isValidatorDuty',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'deployedAtBlock',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'F_ADDR',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'isValidator',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'ValidatorAdded',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'ValidatorRemoved',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'requiredSignatures',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'RequiredSignaturesChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'previousOwner',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_requiredSignatures',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_initialValidators',
|
||||
type: 'address[]'
|
||||
},
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'addValidator',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'removeValidator',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
export default abi
|
||||
607
alm/src/abis/ForeignAMB.ts
Normal file
607
alm/src/abis/ForeignAMB.ts
Normal file
@ -0,0 +1,607 @@
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
const abi: AbiItem[] = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'transactionHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_txHash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'relayedMessages',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_validatorContract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'isInitialized',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredBlockConfirmations',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: '_signatures',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'executeSignatures',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: '_signatures',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'safeExecuteSignaturesWithAutoGasLimit',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'getMinimumGasUsage',
|
||||
outputs: [
|
||||
{
|
||||
name: 'gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageReceiver',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeMode',
|
||||
outputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes4'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setChainIds',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_token',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_to',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'claimTokens',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setMaxGasPerTx',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredSignatures',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorContract',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'deployedAtBlock',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeInterfacesVersion',
|
||||
outputs: [
|
||||
{
|
||||
name: 'major',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'minor',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'patch',
|
||||
type: 'uint64'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSourceChainId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_blockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setRequiredBlockConfirmations',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setGasPrice',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'messageCallStatus',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_contract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: '_gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'requireToPassMessage',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageDataHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'maxGasPerTx',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'gasPrice',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'encodedData',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'UserRequestForAffirmation',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'sender',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'executor',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'status',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
name: 'RelayedMessage',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'GasPriceChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'RequiredBlockConfirmationChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'previousOwner',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event'
|
||||
}
|
||||
]
|
||||
|
||||
export default abi
|
||||
777
alm/src/abis/HomeAMB.ts
Normal file
777
alm/src/abis/HomeAMB.ts
Normal file
@ -0,0 +1,777 @@
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
const abi: AbiItem[] = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'transactionHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_message',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'numMessagesSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
name: '_index',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'signature',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_validatorContract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'isInitialized',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredBlockConfirmations',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'getMinimumGasUsage',
|
||||
outputs: [
|
||||
{
|
||||
name: 'gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageReceiver',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeMode',
|
||||
outputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes4'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setChainIds',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'message',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'signature',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'submitSignature',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_token',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_to',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'claimTokens',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'numAffirmationsSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'affirmationsSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setMaxGasPerTx',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredSignatures',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_message',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'messagesSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorContract',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'deployedAtBlock',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeInterfacesVersion',
|
||||
outputs: [
|
||||
{
|
||||
name: 'major',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'minor',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'patch',
|
||||
type: 'uint64'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSourceChainId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_blockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setRequiredBlockConfirmations',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setGasPrice',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'messageCallStatus',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_contract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: '_gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'requireToPassMessage',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageDataHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'maxGasPerTx',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'message',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'executeAffirmation',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'gasPrice',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_number',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'isAlreadyProcessed',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'encodedData',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'UserRequestForSignature',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'sender',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'executor',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'status',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
name: 'AffirmationCompleted',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'signer',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'messageHash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'SignedForUserRequest',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'signer',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'messageHash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'SignedForAffirmation',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'authorityResponsibleForRelay',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'messageHash',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'NumberOfCollectedSignatures',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'CollectedSignatures',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'GasPriceChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'RequiredBlockConfirmationChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'previousOwner',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event'
|
||||
}
|
||||
]
|
||||
|
||||
export default abi
|
||||
3
alm/src/abis/index.ts
Normal file
3
alm/src/abis/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export { default as HOME_AMB_ABI } from './HomeAMB'
|
||||
export { default as FOREIGN_AMB_ABI } from './ForeignAMB'
|
||||
export { default as BRIDGE_VALIDATORS_ABI } from './BridgeValidators'
|
||||
150
alm/src/components/ConfirmationsContainer.tsx
Normal file
150
alm/src/components/ConfirmationsContainer.tsx
Normal file
@ -0,0 +1,150 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { useMessageConfirmations } from '../hooks/useMessageConfirmations'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
import styled from 'styled-components'
|
||||
import { CONFIRMATIONS_STATUS, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { CONFIRMATIONS_STATUS_LABEL, CONFIRMATIONS_STATUS_LABEL_HOME } from '../config/descriptions'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import { ValidatorsConfirmations } from './ValidatorsConfirmations'
|
||||
import { getConfirmationsStatusDescription } from '../utils/networks'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { ExecutionConfirmation } from './ExecutionConfirmation'
|
||||
import { useValidatorContract } from '../hooks/useValidatorContract'
|
||||
import { useBlockConfirmations } from '../hooks/useBlockConfirmations'
|
||||
import { MultiLine } from './commons/MultiLine'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
|
||||
const StatusLabel = styled.label`
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
`
|
||||
|
||||
const StatusResultLabel = styled.label`
|
||||
font-size: 18px;
|
||||
padding-left: 10px;
|
||||
`
|
||||
|
||||
const StyledConfirmationContainer = styled.div`
|
||||
background-color: var(--bg-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
const StatusDescription = styled.div`
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
export interface ConfirmationsContainerParams {
|
||||
message: MessageObject
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
fromHome: boolean
|
||||
homeStartBlock: Maybe<number>
|
||||
foreignStartBlock: Maybe<number>
|
||||
}
|
||||
|
||||
export const ConfirmationsContainer = ({
|
||||
message,
|
||||
receipt,
|
||||
fromHome,
|
||||
homeStartBlock,
|
||||
foreignStartBlock
|
||||
}: ConfirmationsContainerParams) => {
|
||||
const {
|
||||
home: { name: homeName },
|
||||
foreign: { name: foreignName }
|
||||
} = useStateProvider()
|
||||
const src = useValidatorContract(fromHome, receipt ? receipt.blockNumber : 0)
|
||||
const [executionBlockNumber, setExecutionBlockNumber] = useState(0)
|
||||
const dst = useValidatorContract(!fromHome, executionBlockNumber || 'latest')
|
||||
const { blockConfirmations } = useBlockConfirmations({ fromHome, receipt })
|
||||
const {
|
||||
confirmations,
|
||||
status,
|
||||
executionData,
|
||||
signatureCollected,
|
||||
waitingBlocksResolved,
|
||||
setExecutionData,
|
||||
executionEventsFetched,
|
||||
setPendingExecution
|
||||
} = useMessageConfirmations({
|
||||
message,
|
||||
receipt,
|
||||
fromHome,
|
||||
homeStartBlock,
|
||||
foreignStartBlock,
|
||||
requiredSignatures: src.requiredSignatures,
|
||||
validatorList: src.validatorList,
|
||||
targetValidatorList: dst.validatorList,
|
||||
blockConfirmations
|
||||
})
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (executionBlockNumber || executionData.status !== VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS) return
|
||||
|
||||
setExecutionBlockNumber(executionData.blockNumber)
|
||||
},
|
||||
[executionData.status, executionBlockNumber, executionData.blockNumber]
|
||||
)
|
||||
|
||||
const statusLabel = fromHome ? CONFIRMATIONS_STATUS_LABEL_HOME : CONFIRMATIONS_STATUS_LABEL
|
||||
|
||||
const parseDescription = () => {
|
||||
let description = getConfirmationsStatusDescription(status, homeName, foreignName, fromHome)
|
||||
let link
|
||||
const descArray = description.split('%link')
|
||||
if (descArray.length > 1) {
|
||||
description = descArray[0]
|
||||
link = (
|
||||
<ExplorerTxLink href={descArray[1]} target="_blank" rel="noopener noreferrer">
|
||||
{descArray[1]}
|
||||
</ExplorerTxLink>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{description}
|
||||
{link}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<StyledConfirmationContainer className="col-9">
|
||||
<div className="row is-center">
|
||||
<StatusLabel>Status:</StatusLabel>
|
||||
<StatusResultLabel data-id="status">
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? statusLabel[status] : <SimpleLoading />}
|
||||
</StatusResultLabel>
|
||||
</div>
|
||||
<StatusDescription className="row is-center">
|
||||
<MultiLine className="col-10">
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? parseDescription() : ''}
|
||||
</MultiLine>
|
||||
</StatusDescription>
|
||||
<ValidatorsConfirmations
|
||||
confirmations={fromHome ? confirmations.filter(c => dst.validatorList.includes(c.validator)) : confirmations}
|
||||
requiredSignatures={dst.requiredSignatures}
|
||||
validatorList={dst.validatorList}
|
||||
waitingBlocksResolved={waitingBlocksResolved}
|
||||
/>
|
||||
{signatureCollected && (
|
||||
<ExecutionConfirmation
|
||||
message={message}
|
||||
executionData={executionData}
|
||||
isHome={!fromHome}
|
||||
confirmations={confirmations}
|
||||
setExecutionData={setExecutionData}
|
||||
executionEventsFetched={executionEventsFetched}
|
||||
setPendingExecution={setPendingExecution}
|
||||
dstRequiredSignatures={dst.requiredSignatures}
|
||||
dstValidatorList={dst.validatorList}
|
||||
/>
|
||||
)}
|
||||
</StyledConfirmationContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
172
alm/src/components/ExecutionConfirmation.tsx
Normal file
172
alm/src/components/ExecutionConfirmation.tsx
Normal file
@ -0,0 +1,172 @@
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS, ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION } from '../config/constants'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import styled from 'styled-components'
|
||||
import { ConfirmationParam, ExecutionData } from '../hooks/useMessageConfirmations'
|
||||
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { Thead, AgeTd, StatusTd } from './commons/Table'
|
||||
import { ManualExecutionButton } from './ManualExecutionButton'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { matchesRule, MessageObject, WarnRule } from '../utils/web3'
|
||||
import { WarningAlert } from './commons/WarningAlert'
|
||||
import { ErrorAlert } from './commons/ErrorAlert'
|
||||
|
||||
const StyledExecutionConfirmation = styled.div`
|
||||
margin-top: 30px;
|
||||
`
|
||||
|
||||
export interface ExecutionConfirmationParams {
|
||||
message: MessageObject
|
||||
executionData: ExecutionData
|
||||
setExecutionData: Function
|
||||
confirmations: ConfirmationParam[]
|
||||
isHome: boolean
|
||||
executionEventsFetched: boolean
|
||||
setPendingExecution: Function
|
||||
dstRequiredSignatures: number
|
||||
dstValidatorList: string[]
|
||||
}
|
||||
|
||||
export const ExecutionConfirmation = ({
|
||||
message,
|
||||
executionData,
|
||||
setExecutionData,
|
||||
confirmations,
|
||||
isHome,
|
||||
executionEventsFetched,
|
||||
setPendingExecution,
|
||||
dstRequiredSignatures,
|
||||
dstValidatorList
|
||||
}: ExecutionConfirmationParams) => {
|
||||
const { foreign } = useStateProvider()
|
||||
const [safeExecutionAvailable, setSafeExecutionAvailable] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
const [warning, setWarning] = useState('')
|
||||
const availableManualExecution =
|
||||
!isHome &&
|
||||
(executionData.status === VALIDATOR_CONFIRMATION_STATUS.WAITING ||
|
||||
executionData.status === VALIDATOR_CONFIRMATION_STATUS.FAILED ||
|
||||
(executionData.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED &&
|
||||
executionEventsFetched &&
|
||||
!!executionData.validator))
|
||||
const requiredManualExecution = availableManualExecution && ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION
|
||||
const showAgeColumn = !requiredManualExecution || executionData.status === VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
const windowWidth = useWindowWidth()
|
||||
|
||||
const txExplorerLink = getExplorerTxUrl(executionData.txHash, isHome)
|
||||
const formattedValidator =
|
||||
windowWidth < 850 && executionData.validator ? formatTxHash(executionData.validator) : executionData.validator
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!availableManualExecution || !foreign.bridgeContract) return
|
||||
|
||||
const p = foreign.bridgeContract.methods.getBridgeInterfacesVersion().call()
|
||||
p.then(({ major, minor }: any) => {
|
||||
major = parseInt(major, 10)
|
||||
minor = parseInt(minor, 10)
|
||||
if (major < 5 || (major === 5 && minor < 7)) return
|
||||
|
||||
setSafeExecutionAvailable(true)
|
||||
})
|
||||
},
|
||||
[availableManualExecution, foreign.bridgeContract]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!message.data || !executionData || !availableManualExecution) return
|
||||
|
||||
try {
|
||||
const fileName = 'warnRules'
|
||||
const rules: WarnRule[] = require(`../snapshots/${fileName}.json`)
|
||||
for (let rule of rules) {
|
||||
if (matchesRule(rule, message)) {
|
||||
setWarning(rule.message)
|
||||
return
|
||||
}
|
||||
}
|
||||
} catch (e) {}
|
||||
},
|
||||
[availableManualExecution, executionData, message, message.data, setWarning]
|
||||
)
|
||||
|
||||
const getExecutionStatusElement = (validatorStatus = '') => {
|
||||
switch (validatorStatus) {
|
||||
case VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS:
|
||||
return <SuccessLabel>{validatorStatus}</SuccessLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
|
||||
return <RedLabel>{validatorStatus}</RedLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
|
||||
return <GreyLabel>{validatorStatus}</GreyLabel>
|
||||
default:
|
||||
return executionData.validator ? (
|
||||
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledExecutionConfirmation>
|
||||
{error && <ErrorAlert onClick={() => setError('')} error={error} />}
|
||||
{warning && <WarningAlert onClick={() => setWarning('')} error={warning} />}
|
||||
<table>
|
||||
<Thead>
|
||||
<tr>
|
||||
<th>{requiredManualExecution ? 'Execution info' : 'Executed by'}</th>
|
||||
<th className="text-center">Status</th>
|
||||
{showAgeColumn && <th className="text-center">Age</th>}
|
||||
{availableManualExecution && <th className="text-center">Actions</th>}
|
||||
</tr>
|
||||
</Thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>
|
||||
{requiredManualExecution ? (
|
||||
'Manual user action is required to complete the operation'
|
||||
) : formattedValidator ? (
|
||||
formattedValidator
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)}
|
||||
</td>
|
||||
<StatusTd className="text-center">{getExecutionStatusElement(executionData.status)}</StatusTd>
|
||||
{showAgeColumn && (
|
||||
<AgeTd className="text-center">
|
||||
{executionData.timestamp > 0 ? (
|
||||
<ExplorerTxLink href={txExplorerLink} target="_blank">
|
||||
{formatTimestamp(executionData.timestamp)}
|
||||
</ExplorerTxLink>
|
||||
) : executionData.status === VALIDATOR_CONFIRMATION_STATUS.WAITING ? (
|
||||
''
|
||||
) : (
|
||||
SEARCHING_TX
|
||||
)}
|
||||
</AgeTd>
|
||||
)}
|
||||
{availableManualExecution && (
|
||||
<td>
|
||||
<ManualExecutionButton
|
||||
safeExecutionAvailable={safeExecutionAvailable}
|
||||
messageData={message.data}
|
||||
setExecutionData={setExecutionData}
|
||||
confirmations={confirmations}
|
||||
setPendingExecution={setPendingExecution}
|
||||
setError={setError}
|
||||
requiredSignatures={dstRequiredSignatures}
|
||||
validatorList={dstValidatorList}
|
||||
/>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</StyledExecutionConfirmation>
|
||||
)
|
||||
}
|
||||
69
alm/src/components/Form.tsx
Normal file
69
alm/src/components/Form.tsx
Normal file
@ -0,0 +1,69 @@
|
||||
import React, { useState, FormEvent } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { FormSubmitParams } from './MainPage'
|
||||
import { Button } from './commons/Button'
|
||||
import { TransactionSelector } from './TransactionSelector'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
|
||||
const LabelText = styled.label`
|
||||
line-height: 36px;
|
||||
max-width: 140px;
|
||||
`
|
||||
|
||||
const Input = styled.input`
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
max-width: 100%;
|
||||
border-color: var(--color-primary) !important;
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
border-color: var(--button-color) !important;
|
||||
}
|
||||
`
|
||||
|
||||
export const Form = ({ onSubmit }: { onSubmit: ({ chainId, txHash, receipt }: FormSubmitParams) => void }) => {
|
||||
const [txHash, setTxHash] = useState('')
|
||||
const [searchTx, setSearchTx] = useState(false)
|
||||
|
||||
const formSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSearchTx(true)
|
||||
}
|
||||
|
||||
const onSelected = (chainId: number, receipt: TransactionReceipt) => {
|
||||
onSubmit({ chainId, txHash, receipt })
|
||||
}
|
||||
|
||||
const onBack = () => {
|
||||
setTxHash('')
|
||||
setSearchTx(false)
|
||||
}
|
||||
|
||||
if (searchTx) {
|
||||
return <TransactionSelector txHash={txHash} onSelected={onSelected} onBack={onBack} />
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={formSubmit}>
|
||||
<div className="row is-center">
|
||||
<LabelText className="col-2">Bridgeable tx hash:</LabelText>
|
||||
<div className="col-7">
|
||||
<Input
|
||||
placeholder="Enter transaction hash"
|
||||
type="text"
|
||||
onChange={e => setTxHash(e.target.value)}
|
||||
required
|
||||
pattern="^0x[a-fA-F0-9]{64}$"
|
||||
value={txHash}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-1">
|
||||
<Button className="button outline" type="submit">
|
||||
Check
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
148
alm/src/components/MainPage.tsx
Normal file
148
alm/src/components/MainPage.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Route, useHistory } from 'react-router-dom'
|
||||
import { Form } from './Form'
|
||||
import { StatusContainer } from './StatusContainer'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { InfoAlert } from './commons/InfoAlert'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { FOREIGN_NETWORK_NAME, HOME_NETWORK_NAME } from '../config/constants'
|
||||
|
||||
const StyledMainPage = styled.div`
|
||||
text-align: center;
|
||||
min-height: 100vh;
|
||||
`
|
||||
|
||||
const Header = styled.header`
|
||||
background-color: #001529;
|
||||
color: #ffffff;
|
||||
margin-bottom: 50px;
|
||||
`
|
||||
|
||||
const HeaderContainer = styled.header`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
padding: 0 50px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const AlertP = styled.p`
|
||||
align-items: start;
|
||||
margin-bottom: 0;
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`
|
||||
|
||||
export interface FormSubmitParams {
|
||||
chainId: number
|
||||
txHash: string
|
||||
receipt: TransactionReceipt
|
||||
}
|
||||
|
||||
export const MainPage = () => {
|
||||
const history = useHistory()
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [networkName, setNetworkName] = useState('')
|
||||
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||
const [showInfoAlert, setShowInfoAlert] = useState(false)
|
||||
|
||||
const loadFromStorage = useCallback(() => {
|
||||
const hideAlert = window.localStorage.getItem('hideInfoAlert')
|
||||
setShowInfoAlert(!hideAlert)
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
loadFromStorage()
|
||||
},
|
||||
[loadFromStorage]
|
||||
)
|
||||
|
||||
const onAlertClose = useCallback(
|
||||
() => {
|
||||
window.localStorage.setItem('hideInfoAlert', 'true')
|
||||
loadFromStorage()
|
||||
},
|
||||
[loadFromStorage]
|
||||
)
|
||||
|
||||
const setNetworkData = (chainId: number) => {
|
||||
const network = chainId === home.chainId ? home.name : foreign.name
|
||||
|
||||
setNetworkName(network)
|
||||
}
|
||||
|
||||
const onFormSubmit = ({ chainId, txHash, receipt }: FormSubmitParams) => {
|
||||
setNetworkData(chainId)
|
||||
setReceipt(receipt)
|
||||
history.push(`/${chainId}/${txHash}`)
|
||||
}
|
||||
|
||||
const resetNetworkHeader = () => {
|
||||
setNetworkName('')
|
||||
}
|
||||
|
||||
const setNetworkFromParams = (chainId: number) => {
|
||||
setNetworkData(chainId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const w = window as any
|
||||
if (w.ethereum) {
|
||||
w.ethereum.autoRefreshOnNetworkChange = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<StyledMainPage>
|
||||
<Header>
|
||||
<HeaderContainer>
|
||||
<span>AMB Live Monitoring</span>
|
||||
<span>{networkName}</span>
|
||||
</HeaderContainer>
|
||||
</Header>
|
||||
<div className="container">
|
||||
{showInfoAlert && (
|
||||
<InfoAlert onClick={onAlertClose}>
|
||||
<p className="is-left text-left">
|
||||
The Arbitrary Message Bridge Live Monitoring application provides real-time status updates for messages
|
||||
bridged between {HOME_NETWORK_NAME} and {FOREIGN_NETWORK_NAME}. You can check current tx status, view
|
||||
validator info, and troubleshoot potential issues with bridge transfers.
|
||||
</p>
|
||||
<AlertP className="is-left text-left">
|
||||
For more information refer to
|
||||
<ExplorerTxLink
|
||||
href="https://docs.tokenbridge.net/about-tokenbridge/components/amb-live-monitoring-application"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
the ALM documentation
|
||||
</ExplorerTxLink>
|
||||
</AlertP>
|
||||
</InfoAlert>
|
||||
)}
|
||||
<Route exact path={['/']} children={<Form onSubmit={onFormSubmit} />} />
|
||||
<Route
|
||||
path={['/:chainId/:txHash/:messageIdParam', '/:chainId/:txHash']}
|
||||
children={
|
||||
<StatusContainer
|
||||
onBackToMain={resetNetworkHeader}
|
||||
setNetworkFromParams={setNetworkFromParams}
|
||||
receiptParam={receipt}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</StyledMainPage>
|
||||
)
|
||||
}
|
||||
231
alm/src/components/ManualExecutionButton.tsx
Normal file
231
alm/src/components/ManualExecutionButton.tsx
Normal file
@ -0,0 +1,231 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { InjectedConnector } from '@web3-react/injected-connector'
|
||||
import { useWeb3React } from '@web3-react/core'
|
||||
import {
|
||||
DOUBLE_EXECUTION_ATTEMPT_ERROR,
|
||||
EXECUTION_FAILED_ERROR,
|
||||
EXECUTION_OUT_OF_GAS_ERROR,
|
||||
FOREIGN_EXPLORER_API,
|
||||
INCORRECT_CHAIN_ERROR,
|
||||
VALIDATOR_CONFIRMATION_STATUS
|
||||
} from '../config/constants'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { signatureToVRS, packSignatures } from '../utils/signatures'
|
||||
import { getSuccessExecutionData } from '../utils/getFinalizationEvent'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
const ActionButton = styled.button`
|
||||
color: var(--button-color);
|
||||
border-color: var(--font-color);
|
||||
margin-top: 10px;
|
||||
min-width: 120px;
|
||||
padding: 1rem;
|
||||
&:focus {
|
||||
outline: var(--button-color);
|
||||
}
|
||||
`
|
||||
|
||||
interface ManualExecutionButtonParams {
|
||||
safeExecutionAvailable: boolean
|
||||
messageData: string
|
||||
setExecutionData: Function
|
||||
confirmations: ConfirmationParam[]
|
||||
setPendingExecution: Function
|
||||
setError: Function
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
}
|
||||
|
||||
export const ManualExecutionButton = ({
|
||||
safeExecutionAvailable,
|
||||
messageData,
|
||||
setExecutionData,
|
||||
confirmations,
|
||||
setPendingExecution,
|
||||
setError,
|
||||
requiredSignatures,
|
||||
validatorList
|
||||
}: ManualExecutionButtonParams) => {
|
||||
const { foreign } = useStateProvider()
|
||||
const { library, activate, account, active } = useWeb3React()
|
||||
const [manualExecution, setManualExecution] = useState(false)
|
||||
const [allowFailures, setAllowFailures] = useState(false)
|
||||
const [ready, setReady] = useState(false)
|
||||
const [title, setTitle] = useState('Loading')
|
||||
const [validSignatures, setValidSignatures] = useState<string[]>([])
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
!foreign.bridgeContract ||
|
||||
!foreign.web3 ||
|
||||
!confirmations ||
|
||||
!confirmations.length ||
|
||||
!requiredSignatures ||
|
||||
!validatorList ||
|
||||
!validatorList.length
|
||||
)
|
||||
return
|
||||
|
||||
const signatures = []
|
||||
for (let i = 0; i < confirmations.length && signatures.length < requiredSignatures; i++) {
|
||||
const sig = confirmations[i].signature
|
||||
if (!sig) {
|
||||
continue
|
||||
}
|
||||
const { v, r, s } = signatureToVRS(sig)
|
||||
const signer = foreign.web3.eth.accounts.recover(messageData, `0x${v}`, `0x${r}`, `0x${s}`)
|
||||
if (validatorList.includes(signer)) {
|
||||
signatures.push(sig)
|
||||
}
|
||||
}
|
||||
|
||||
if (signatures.length >= requiredSignatures) {
|
||||
setValidSignatures(signatures.slice(0, requiredSignatures))
|
||||
setTitle('Execute')
|
||||
setReady(true)
|
||||
} else {
|
||||
setTitle('Unavailable')
|
||||
}
|
||||
},
|
||||
[
|
||||
foreign.bridgeContract,
|
||||
foreign.web3,
|
||||
validatorList,
|
||||
requiredSignatures,
|
||||
messageData,
|
||||
setValidSignatures,
|
||||
confirmations
|
||||
]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!manualExecution || !foreign.chainId) return
|
||||
|
||||
if (!active) {
|
||||
activate(new InjectedConnector({ supportedChainIds: [foreign.chainId] }), e => {
|
||||
if (e.message.includes('Unsupported chain id')) {
|
||||
setError(INCORRECT_CHAIN_ERROR)
|
||||
const { ethereum } = window as any
|
||||
|
||||
// remove the error message after chain is correctly changed to the foreign one
|
||||
const listener = (chainId: string) => {
|
||||
if (parseInt(chainId.slice(2), 16) === foreign.chainId) {
|
||||
ethereum.removeListener('chainChanged', listener)
|
||||
setError((error: string) => (error === INCORRECT_CHAIN_ERROR ? '' : error))
|
||||
}
|
||||
}
|
||||
ethereum.on('chainChanged', listener)
|
||||
} else {
|
||||
setError(e.message)
|
||||
}
|
||||
setManualExecution(false)
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (!library || !foreign.bridgeContract || !foreign.web3 || !validSignatures || !validSignatures.length) return
|
||||
|
||||
const signatures = packSignatures(validSignatures.map(signatureToVRS))
|
||||
const messageId = messageData.slice(0, 66)
|
||||
const bridge = foreign.bridgeContract
|
||||
const executeMethod =
|
||||
safeExecutionAvailable && !allowFailures
|
||||
? bridge.methods.safeExecuteSignaturesWithAutoGasLimit
|
||||
: bridge.methods.executeSignatures
|
||||
const data = executeMethod(messageData, signatures).encodeABI()
|
||||
setManualExecution(false)
|
||||
|
||||
library.eth
|
||||
.sendTransaction({
|
||||
from: account,
|
||||
to: foreign.bridgeAddress,
|
||||
data
|
||||
})
|
||||
.on('transactionHash', (txHash: string) => {
|
||||
setExecutionData({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
|
||||
validator: account,
|
||||
txHash,
|
||||
timestamp: Math.floor(new Date().getTime() / 1000.0),
|
||||
executionResult: false
|
||||
})
|
||||
setPendingExecution(true)
|
||||
})
|
||||
.on('error', async (e: Error, receipt: TransactionReceipt) => {
|
||||
if (e.message.includes('Transaction has been reverted by the EVM')) {
|
||||
const successExecutionData = await getSuccessExecutionData(
|
||||
bridge,
|
||||
'RelayedMessage',
|
||||
library,
|
||||
messageId,
|
||||
FOREIGN_EXPLORER_API
|
||||
)
|
||||
if (successExecutionData) {
|
||||
setExecutionData(successExecutionData)
|
||||
setError(DOUBLE_EXECUTION_ATTEMPT_ERROR)
|
||||
} else {
|
||||
const { gas } = await library.eth.getTransaction(receipt.transactionHash)
|
||||
setExecutionData({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
|
||||
validator: account,
|
||||
txHash: receipt.transactionHash,
|
||||
timestamp: Math.floor(new Date().getTime() / 1000.0),
|
||||
executionResult: false
|
||||
})
|
||||
setError(gas === receipt.gasUsed ? EXECUTION_OUT_OF_GAS_ERROR : EXECUTION_FAILED_ERROR)
|
||||
}
|
||||
} else {
|
||||
setError(e.message)
|
||||
}
|
||||
})
|
||||
},
|
||||
[
|
||||
manualExecution,
|
||||
library,
|
||||
activate,
|
||||
active,
|
||||
account,
|
||||
foreign.chainId,
|
||||
foreign.bridgeAddress,
|
||||
foreign.bridgeContract,
|
||||
setError,
|
||||
messageData,
|
||||
setExecutionData,
|
||||
setPendingExecution,
|
||||
safeExecutionAvailable,
|
||||
allowFailures,
|
||||
foreign.web3,
|
||||
validSignatures
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="is-center">
|
||||
<ActionButton disabled={!ready} className="button outline" onClick={() => setManualExecution(true)}>
|
||||
{title}
|
||||
</ActionButton>
|
||||
</div>
|
||||
{safeExecutionAvailable && (
|
||||
<div
|
||||
title="Allow executed message to fail and record its failure on-chain without reverting the whole transaction.
|
||||
Use fixed gas limit for execution."
|
||||
className="is-center"
|
||||
style={{ paddingTop: 10 }}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
id="allow-failures"
|
||||
checked={allowFailures}
|
||||
onChange={e => setAllowFailures(e.target.checked)}
|
||||
/>
|
||||
<label htmlFor="allow-failures">Unsafe mode</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
alm/src/components/MessageSelector.tsx
Normal file
47
alm/src/components/MessageSelector.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from './commons/Button'
|
||||
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { formatTxHashExtended } from '../utils/networks'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
|
||||
export interface MessageSelectorParams {
|
||||
messages: Array<MessageObject>
|
||||
onMessageSelected: (index: number) => void
|
||||
}
|
||||
|
||||
export const MessageSelector = ({ messages, onMessageSelected }: MessageSelectorParams) => {
|
||||
const [messageIndex, setMessageIndex] = useState(0)
|
||||
const windowWidth = useWindowWidth()
|
||||
|
||||
const onSelect = () => {
|
||||
onMessageSelected(messageIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<div className="col-7-lg col-12 is-marginless">
|
||||
{messages.map((message, i) => (
|
||||
<RadioButtonContainer className="row is-center is-vertical-align" key={i} onClick={() => setMessageIndex(i)}>
|
||||
<input
|
||||
className="is-marginless"
|
||||
type="radio"
|
||||
name="message"
|
||||
value={i}
|
||||
checked={i === messageIndex}
|
||||
onChange={() => setMessageIndex(i)}
|
||||
/>
|
||||
<RadioButtonLabel htmlFor={i.toString()}>
|
||||
{windowWidth < 700 ? formatTxHashExtended(message.id) : message.id}
|
||||
</RadioButtonLabel>
|
||||
</RadioButtonContainer>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-1-lg col-12 is-marginless">
|
||||
<Button className="button outline" onClick={onSelect}>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
alm/src/components/NetworkTransactionSelector.tsx
Normal file
47
alm/src/components/NetworkTransactionSelector.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from './commons/Button'
|
||||
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
|
||||
export const NetworkTransactionSelector = ({ onNetworkSelected }: { onNetworkSelected: (chainId: number) => void }) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [chainId, setChainId] = useState(home.chainId)
|
||||
|
||||
const networks = [home, foreign]
|
||||
|
||||
const onSelect = () => {
|
||||
onNetworkSelected(chainId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>The transaction was found in both networks, please select one:</p>
|
||||
<div className="row is-center">
|
||||
<div className="col-3-lg col-12 is-marginless">
|
||||
{networks.map((network, i) => (
|
||||
<RadioButtonContainer
|
||||
className="row is-center is-vertical-align"
|
||||
key={i}
|
||||
onClick={() => setChainId(network.chainId)}
|
||||
>
|
||||
<input
|
||||
className="is-marginless"
|
||||
type="radio"
|
||||
name="message"
|
||||
value={network.chainId}
|
||||
checked={network.chainId === chainId}
|
||||
onChange={() => setChainId(network.chainId)}
|
||||
/>
|
||||
<RadioButtonLabel htmlFor={i.toString()}>{network.name}</RadioButtonLabel>
|
||||
</RadioButtonContainer>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-3-lg col-12 is-marginless">
|
||||
<Button className="button outline" onClick={onSelect}>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
118
alm/src/components/StatusContainer.tsx
Normal file
118
alm/src/components/StatusContainer.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useHistory, useParams } from 'react-router-dom'
|
||||
import { useTransactionStatus } from '../hooks/useTransactionStatus'
|
||||
import { formatTxHash, getExplorerTxUrl, getTransactionStatusDescription, validTxHash } from '../utils/networks'
|
||||
import { TRANSACTION_STATUS } from '../config/constants'
|
||||
import { MessageSelector } from './MessageSelector'
|
||||
import { Loading } from './commons/Loading'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { ConfirmationsContainer } from './ConfirmationsContainer'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { BackButton } from './commons/BackButton'
|
||||
import { useClosestBlock } from '../hooks/useClosestBlock'
|
||||
|
||||
export interface StatusContainerParam {
|
||||
onBackToMain: () => void
|
||||
setNetworkFromParams: (chainId: number) => void
|
||||
receiptParam: Maybe<TransactionReceipt>
|
||||
}
|
||||
|
||||
export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptParam }: StatusContainerParam) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const history = useHistory()
|
||||
const { chainId, txHash, messageIdParam } = useParams()
|
||||
const validChainId = chainId === home.chainId.toString() || chainId === foreign.chainId.toString()
|
||||
const validParameters = validChainId && validTxHash(txHash)
|
||||
const isHome = chainId === home.chainId.toString()
|
||||
|
||||
const { messages, receipt, status, description, timestamp, loading } = useTransactionStatus({
|
||||
txHash: validParameters ? txHash : '',
|
||||
chainId: validParameters ? parseInt(chainId) : 0,
|
||||
receiptParam
|
||||
})
|
||||
const homeStartBlock = useClosestBlock(true, isHome, receipt, timestamp)
|
||||
const foreignStartBlock = useClosestBlock(false, isHome, receipt, timestamp)
|
||||
|
||||
const selectedMessageId = messageIdParam === undefined || messages[messageIdParam] === undefined ? -1 : messageIdParam
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (validChainId) {
|
||||
setNetworkFromParams(parseInt(chainId))
|
||||
}
|
||||
},
|
||||
[validChainId, chainId, setNetworkFromParams]
|
||||
)
|
||||
|
||||
if (!validParameters && home.chainId && foreign.chainId) {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Chain Id: {chainId} and/or Transaction Hash: {txHash} are not valid
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const onMessageSelected = (messageId: number) => {
|
||||
history.push(`/${chainId}/${txHash}/${messageId}`)
|
||||
}
|
||||
|
||||
const displayMessageSelector = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId === -1
|
||||
const multiMessageSelected = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId !== -1
|
||||
const displayReference = multiMessageSelected ? messages[selectedMessageId].id : txHash
|
||||
const formattedMessageId = formatTxHash(displayReference)
|
||||
|
||||
const txExplorerLink = getExplorerTxUrl(txHash, isHome)
|
||||
const displayExplorerLink = status !== TRANSACTION_STATUS.NOT_FOUND
|
||||
|
||||
const displayConfirmations = status === TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE || multiMessageSelected
|
||||
const messageToConfirm =
|
||||
messages.length > 1 ? messages[selectedMessageId] : messages.length > 0 ? messages[0] : { id: '', data: '' }
|
||||
|
||||
let displayedDescription: string = multiMessageSelected
|
||||
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
|
||||
: description
|
||||
let link
|
||||
const descArray = displayedDescription.split('%link')
|
||||
if (descArray.length > 1) {
|
||||
displayedDescription = descArray[0]
|
||||
link = (
|
||||
<ExplorerTxLink href={descArray[1]} target="_blank" rel="noopener noreferrer">
|
||||
{descArray[1]}
|
||||
</ExplorerTxLink>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{status && (
|
||||
<p>
|
||||
The transaction{' '}
|
||||
{displayExplorerLink && (
|
||||
<ExplorerTxLink href={txExplorerLink} target="_blank">
|
||||
{formattedMessageId}
|
||||
</ExplorerTxLink>
|
||||
)}
|
||||
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription} {link}
|
||||
</p>
|
||||
)}
|
||||
{displayMessageSelector && <MessageSelector messages={messages} onMessageSelected={onMessageSelected} />}
|
||||
{displayConfirmations && (
|
||||
<ConfirmationsContainer
|
||||
message={messageToConfirm}
|
||||
receipt={receipt}
|
||||
fromHome={isHome}
|
||||
homeStartBlock={homeStartBlock}
|
||||
foreignStartBlock={foreignStartBlock}
|
||||
/>
|
||||
)}
|
||||
<BackButton onBackToMain={onBackToMain} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
alm/src/components/TransactionSelector.tsx
Normal file
67
alm/src/components/TransactionSelector.tsx
Normal file
@ -0,0 +1,67 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTransactionFinder } from '../hooks/useTransactionFinder'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TRANSACTION_STATUS } from '../config/constants'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { Loading } from './commons/Loading'
|
||||
import { NetworkTransactionSelector } from './NetworkTransactionSelector'
|
||||
import { BackButton } from './commons/BackButton'
|
||||
import { TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
|
||||
import { MultiLine } from './commons/MultiLine'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledMultiLine = styled(MultiLine)`
|
||||
margin-bottom: 40px;
|
||||
`
|
||||
|
||||
export const TransactionSelector = ({
|
||||
txHash,
|
||||
onSelected,
|
||||
onBack
|
||||
}: {
|
||||
txHash: string
|
||||
onSelected: (chainId: number, receipt: TransactionReceipt) => void
|
||||
onBack: () => void
|
||||
}) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const { receipt: homeReceipt, status: homeStatus } = useTransactionFinder({ txHash, web3: home.web3 })
|
||||
const { receipt: foreignReceipt, status: foreignStatus } = useTransactionFinder({ txHash, web3: foreign.web3 })
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!home.chainId || !foreign.chainId) return
|
||||
if (homeStatus === TRANSACTION_STATUS.FOUND && foreignStatus === TRANSACTION_STATUS.NOT_FOUND) {
|
||||
if (!homeReceipt) return
|
||||
onSelected(home.chainId, homeReceipt)
|
||||
} else if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
|
||||
if (!foreignReceipt) return
|
||||
onSelected(foreign.chainId, foreignReceipt)
|
||||
}
|
||||
},
|
||||
[homeReceipt, homeStatus, foreignReceipt, foreignStatus, home.chainId, foreign.chainId, onSelected]
|
||||
)
|
||||
|
||||
const onSelectedNetwork = (chainId: number) => {
|
||||
const chain = chainId === home.chainId ? home.chainId : foreign.chainId
|
||||
const receipt = chainId === home.chainId ? homeReceipt : foreignReceipt
|
||||
|
||||
if (!receipt) return
|
||||
onSelected(chain, receipt)
|
||||
}
|
||||
|
||||
if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.FOUND) {
|
||||
return <NetworkTransactionSelector onNetworkSelected={onSelectedNetwork} />
|
||||
}
|
||||
|
||||
if (foreignStatus === TRANSACTION_STATUS.NOT_FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
|
||||
const message = TRANSACTION_STATUS_DESCRIPTION[TRANSACTION_STATUS.NOT_FOUND]
|
||||
return (
|
||||
<div>
|
||||
<StyledMultiLine>{message}</StyledMultiLine>
|
||||
<BackButton onBackToMain={onBack} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Loading />
|
||||
}
|
||||
105
alm/src/components/ValidatorsConfirmations.tsx
Normal file
105
alm/src/components/ValidatorsConfirmations.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React from 'react'
|
||||
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { RECENT_AGE, SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import styled from 'styled-components'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { Thead, AgeTd, StatusTd } from './commons/Table'
|
||||
|
||||
const RequiredConfirmations = styled.label`
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
export interface ValidatorsConfirmationsParams {
|
||||
confirmations: Array<ConfirmationParam>
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
waitingBlocksResolved: boolean
|
||||
}
|
||||
|
||||
export const ValidatorsConfirmations = ({
|
||||
confirmations,
|
||||
requiredSignatures,
|
||||
validatorList,
|
||||
waitingBlocksResolved
|
||||
}: ValidatorsConfirmationsParams) => {
|
||||
const windowWidth = useWindowWidth()
|
||||
|
||||
const getValidatorStatusElement = (validatorStatus = '') => {
|
||||
switch (validatorStatus) {
|
||||
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.MANUAL:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID:
|
||||
return <SuccessLabel>{VALIDATOR_CONFIRMATION_STATUS.SUCCESS}</SuccessLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
|
||||
return <RedLabel>{validatorStatus}</RedLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
|
||||
return <GreyLabel>{validatorStatus}</GreyLabel>
|
||||
default:
|
||||
return waitingBlocksResolved ? (
|
||||
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
<Thead>
|
||||
<tr>
|
||||
<th>Validator</th>
|
||||
<th className="text-center">Status</th>
|
||||
<th className="text-center">Age</th>
|
||||
</tr>
|
||||
</Thead>
|
||||
<tbody>
|
||||
{confirmations.map((confirmation, i) => {
|
||||
const displayedStatus = confirmation.status
|
||||
const explorerLink = getExplorerTxUrl(confirmation.txHash, true)
|
||||
let elementIfNoTimestamp: any = <SimpleLoading />
|
||||
switch (displayedStatus) {
|
||||
case '':
|
||||
case VALIDATOR_CONFIRMATION_STATUS.UNDEFINED:
|
||||
if (waitingBlocksResolved) {
|
||||
elementIfNoTimestamp = SEARCHING_TX
|
||||
}
|
||||
break
|
||||
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
|
||||
elementIfNoTimestamp = ''
|
||||
break
|
||||
case VALIDATOR_CONFIRMATION_STATUS.MANUAL:
|
||||
elementIfNoTimestamp = RECENT_AGE
|
||||
break
|
||||
}
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td>{windowWidth < 850 ? formatTxHash(confirmation.validator) : confirmation.validator}</td>
|
||||
<StatusTd className="text-center">{getValidatorStatusElement(displayedStatus)}</StatusTd>
|
||||
<AgeTd className="text-center">
|
||||
{confirmation && confirmation.timestamp > 0 ? (
|
||||
<ExplorerTxLink href={explorerLink} target="_blank">
|
||||
{formatTimestamp(confirmation.timestamp)}
|
||||
</ExplorerTxLink>
|
||||
) : (
|
||||
elementIfNoTimestamp
|
||||
)}
|
||||
</AgeTd>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<RequiredConfirmations>
|
||||
At least <strong>{requiredSignatures}</strong> of <strong>{validatorList.length}</strong> confirmations required
|
||||
</RequiredConfirmations>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
alm/src/components/commons/BackButton.tsx
Normal file
35
alm/src/components/commons/BackButton.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { LeftArrow } from './LeftArrow'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledButton = styled.button`
|
||||
color: var(--button-color);
|
||||
border-color: var(--font-color);
|
||||
margin-top: 10px;
|
||||
&:focus {
|
||||
outline: var(--button-color);
|
||||
}
|
||||
`
|
||||
|
||||
const BackLabel = styled.label`
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export interface BackButtonParam {
|
||||
onBackToMain: () => void
|
||||
}
|
||||
|
||||
export const BackButton = ({ onBackToMain }: BackButtonParam) => (
|
||||
<div className="row is-center">
|
||||
<div className="col-9">
|
||||
<Link to="/" onClick={onBackToMain}>
|
||||
<StyledButton className="button outline is-left">
|
||||
<LeftArrow />
|
||||
<BackLabel>Search another transaction</BackLabel>
|
||||
</StyledButton>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
10
alm/src/components/commons/Button.tsx
Normal file
10
alm/src/components/commons/Button.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Button = styled.button`
|
||||
height: 36px;
|
||||
color: var(--button-color);
|
||||
border-color: var(--button-color);
|
||||
&:focus {
|
||||
outline: var(--button-color);
|
||||
}
|
||||
`
|
||||
18
alm/src/components/commons/CloseIcon.tsx
Normal file
18
alm/src/components/commons/CloseIcon.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react'
|
||||
|
||||
export const CloseIcon = ({ color }: { color?: string }) => (
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
data-prefix="fa"
|
||||
data-icon="times"
|
||||
className="svg-inline--fa fa-times fa-w-11 fa-lg "
|
||||
role="img"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 352 512"
|
||||
fill={color || '#1890ff'}
|
||||
height="1em"
|
||||
>
|
||||
<path d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z" />
|
||||
</svg>
|
||||
)
|
||||
48
alm/src/components/commons/ErrorAlert.tsx
Normal file
48
alm/src/components/commons/ErrorAlert.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { InfoIcon } from './InfoIcon'
|
||||
import { CloseIcon } from './CloseIcon'
|
||||
import { ExplorerTxLink } from './ExplorerTxLink'
|
||||
|
||||
const StyledErrorAlert = styled.div`
|
||||
border: 1px solid var(--failed-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
const CloseIconContainer = styled.div`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
white-space: pre-wrap;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export const ErrorAlert = ({ onClick, error }: { onClick: () => void; error: string }) => {
|
||||
const errorArray = error.split('%link')
|
||||
const text = errorArray[0]
|
||||
let link
|
||||
if (errorArray.length > 1) {
|
||||
link = (
|
||||
<ExplorerTxLink href={errorArray[1]} target="_blank" rel="noopener noreferrer">
|
||||
{errorArray[1]}
|
||||
</ExplorerTxLink>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<StyledErrorAlert className="col-12 is-vertical-align row">
|
||||
<InfoIcon color="var(--failed-color)" />
|
||||
<TextContainer className="col-10">
|
||||
{text}
|
||||
{link}
|
||||
</TextContainer>
|
||||
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
|
||||
<CloseIcon color="var(--failed-color)" />
|
||||
</CloseIconContainer>
|
||||
</StyledErrorAlert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
7
alm/src/components/commons/ExplorerTxLink.tsx
Normal file
7
alm/src/components/commons/ExplorerTxLink.tsx
Normal file
@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const ExplorerTxLink = styled.a`
|
||||
color: var(--link-color);
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
`
|
||||
31
alm/src/components/commons/InfoAlert.tsx
Normal file
31
alm/src/components/commons/InfoAlert.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { InfoIcon } from './InfoIcon'
|
||||
import { CloseIcon } from './CloseIcon'
|
||||
|
||||
const StyledInfoAlert = styled.div`
|
||||
border: 1px solid var(--button-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
const CloseIconContainer = styled.div`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export const InfoAlert = ({ onClick, children }: { onClick: () => void; children: React.ReactChild[] }) => (
|
||||
<div className="row is-center">
|
||||
<StyledInfoAlert className="col-10 is-vertical-align row">
|
||||
<InfoIcon />
|
||||
<TextContainer className="col-10">{children}</TextContainer>
|
||||
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
|
||||
<CloseIcon />
|
||||
</CloseIconContainer>
|
||||
</StyledInfoAlert>
|
||||
</div>
|
||||
)
|
||||
16
alm/src/components/commons/InfoIcon.tsx
Normal file
16
alm/src/components/commons/InfoIcon.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
export const InfoIcon = ({ color }: { color?: string }) => (
|
||||
<svg
|
||||
className="col-1 is-left"
|
||||
viewBox="64 64 896 896"
|
||||
focusable="false"
|
||||
data-icon="info-circle"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill={color || '#1890ff'}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z" />
|
||||
</svg>
|
||||
)
|
||||
22
alm/src/components/commons/Labels.tsx
Normal file
22
alm/src/components/commons/Labels.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const SuccessLabel = styled.label`
|
||||
color: var(--success-color);
|
||||
background-color: var(--success-bg-color);
|
||||
padding: 0.4rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export const GreyLabel = styled.label`
|
||||
color: var(--not-required-color);
|
||||
background-color: var(--not-required-bg-color);
|
||||
padding: 0.4rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export const RedLabel = styled.label`
|
||||
color: var(--failed-color);
|
||||
background-color: var(--failed-bg-color);
|
||||
padding: 0.4rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
`
|
||||
20
alm/src/components/commons/LeftArrow.tsx
Normal file
20
alm/src/components/commons/LeftArrow.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { useContext } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
export const LeftArrow = () => {
|
||||
const themeContext = useContext(ThemeContext)
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="mdi-arrow-left"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill={themeContext.buttonColor}
|
||||
>
|
||||
<path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
165
alm/src/components/commons/Loading.tsx
Normal file
165
alm/src/components/commons/Loading.tsx
Normal file
@ -0,0 +1,165 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
export interface LoadingParams {
|
||||
width?: string
|
||||
height?: string
|
||||
displayMessage?: boolean
|
||||
}
|
||||
|
||||
export const Loading = ({ width = '50px', height = '50px', displayMessage = true }: LoadingParams) => {
|
||||
const themeContext = useContext(ThemeContext)
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ background: 'none', display: 'block', shapeRendering: 'auto' }}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.9166666666666666s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(30 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.8333333333333334s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(60 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.75s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(90 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.6666666666666666s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(120 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.5833333333333334s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(150 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(180 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.4166666666666667s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(210 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.3333333333333333s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(240 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.25s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(270 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.16666666666666666s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(300 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.08333333333333333s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(330 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
{displayMessage && <label style={{ color: themeContext.buttonColor }}>Loading...</label>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SimpleLoading = () => <Loading width="30px" height="30px" displayMessage={false} />
|
||||
5
alm/src/components/commons/MultiLine.tsx
Normal file
5
alm/src/components/commons/MultiLine.tsx
Normal file
@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const MultiLine = styled.div`
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
9
alm/src/components/commons/RadioButton.tsx
Normal file
9
alm/src/components/commons/RadioButton.tsx
Normal file
@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const RadioButtonLabel = styled.label`
|
||||
padding-left: 5px;
|
||||
`
|
||||
|
||||
export const RadioButtonContainer = styled.div`
|
||||
padding: 10px;
|
||||
`
|
||||
13
alm/src/components/commons/Table.tsx
Normal file
13
alm/src/components/commons/Table.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Thead = styled.thead`
|
||||
border-bottom: 2px solid #9e9e9e;
|
||||
`
|
||||
|
||||
export const StatusTd = styled.td`
|
||||
width: 150px;
|
||||
`
|
||||
|
||||
export const AgeTd = styled.td`
|
||||
width: 180px;
|
||||
`
|
||||
34
alm/src/components/commons/WarningAlert.tsx
Normal file
34
alm/src/components/commons/WarningAlert.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { InfoIcon } from './InfoIcon'
|
||||
import { CloseIcon } from './CloseIcon'
|
||||
|
||||
const StyledErrorAlert = styled.div`
|
||||
border: 1px solid var(--warning-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
const CloseIconContainer = styled.div`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
white-space: pre-wrap;
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export const WarningAlert = ({ onClick, error }: { onClick: () => void; error: string }) => {
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<StyledErrorAlert className="col-12 is-vertical-align row">
|
||||
<InfoIcon color="var(--warning-color)" />
|
||||
<TextContainer className="col-10">{error}</TextContainer>
|
||||
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
|
||||
<CloseIcon color="var(--warning-color)" />
|
||||
</CloseIconContainer>
|
||||
</StyledErrorAlert>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
alm/src/config/constants.ts
Normal file
81
alm/src/config/constants.ts
Normal file
@ -0,0 +1,81 @@
|
||||
export const HOME_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_HOME_BRIDGE_ADDRESS || ''
|
||||
export const FOREIGN_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_FOREIGN_BRIDGE_ADDRESS || ''
|
||||
|
||||
export const HOME_RPC_URL: string = process.env.REACT_APP_COMMON_HOME_RPC_URL || ''
|
||||
export const FOREIGN_RPC_URL: string = process.env.REACT_APP_COMMON_FOREIGN_RPC_URL || ''
|
||||
|
||||
export const HOME_NETWORK_NAME: string = process.env.REACT_APP_ALM_HOME_NETWORK_NAME || ''
|
||||
export const FOREIGN_NETWORK_NAME: string = process.env.REACT_APP_ALM_FOREIGN_NETWORK_NAME || ''
|
||||
|
||||
export const HOME_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_HOME_EXPLORER_TX_TEMPLATE || ''
|
||||
export const FOREIGN_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_TX_TEMPLATE || ''
|
||||
|
||||
export const HOME_EXPLORER_API: string = process.env.REACT_APP_ALM_HOME_EXPLORER_API || ''
|
||||
export const FOREIGN_EXPLORER_API: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_API || ''
|
||||
|
||||
export const ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION: boolean =
|
||||
(process.env.REACT_APP_ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION || '').toLowerCase() === 'true'
|
||||
|
||||
export const HOME_RPC_POLLING_INTERVAL: number = 5000
|
||||
export const FOREIGN_RPC_POLLING_INTERVAL: number = 5000
|
||||
export const BLOCK_RANGE: number = 500
|
||||
export const MAX_TX_SEARCH_BLOCK_RANGE: number = 10000
|
||||
|
||||
export const EXECUTE_AFFIRMATION_HASH = 'e7a2c01f'
|
||||
export const SUBMIT_SIGNATURE_HASH = '630cea8e'
|
||||
export const EXECUTE_SIGNATURES_HASH = '3f7658fd'
|
||||
|
||||
export const CACHE_KEY_SUCCESS = 'success-confirmation-validator-'
|
||||
export const CACHE_KEY_FAILED = 'failed-confirmation-validator-'
|
||||
export const CACHE_KEY_EXECUTION_FAILED = 'failed-execution-validator-'
|
||||
|
||||
export const TRANSACTION_STATUS = {
|
||||
SUCCESS_MULTIPLE_MESSAGES: 'SUCCESS_MULTIPLE_MESSAGES',
|
||||
SUCCESS_ONE_MESSAGE: 'SUCCESS_ONE_MESSAGE',
|
||||
SUCCESS_NO_MESSAGES: 'SUCCESS_NO_MESSAGES',
|
||||
FAILED: 'FAILED',
|
||||
FOUND: 'FOUND',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const CONFIRMATIONS_STATUS = {
|
||||
SUCCESS: 'SUCCESS',
|
||||
SUCCESS_MESSAGE_FAILED: 'SUCCESS_MESSAGE_FAILED',
|
||||
EXECUTION_FAILED: 'EXECUTION_FAILED',
|
||||
EXECUTION_PENDING: 'EXECUTION_PENDING',
|
||||
EXECUTION_WAITING: 'EXECUTION_WAITING',
|
||||
FAILED: 'FAILED',
|
||||
PENDING: 'PENDING',
|
||||
SEARCHING: 'SEARCHING',
|
||||
WAITING_VALIDATORS: 'WAITING_VALIDATORS',
|
||||
WAITING_CHAIN: 'WAITING_CHAIN',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const VALIDATOR_CONFIRMATION_STATUS = {
|
||||
SUCCESS: 'Confirmed',
|
||||
MANUAL: 'Manual',
|
||||
EXECUTION_SUCCESS: 'Executed',
|
||||
FAILED: 'Failed',
|
||||
FAILED_VALID: 'Failed valid',
|
||||
PENDING: 'Pending',
|
||||
WAITING: 'Waiting',
|
||||
NOT_REQUIRED: 'Not required',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const RECENT_AGE = 'Recent'
|
||||
|
||||
export const SEARCHING_TX = 'Searching Transaction...'
|
||||
|
||||
export const INCORRECT_CHAIN_ERROR = `Incorrect chain chosen. Switch to ${FOREIGN_NETWORK_NAME} in the wallet.`
|
||||
|
||||
export const DOUBLE_EXECUTION_ATTEMPT_ERROR = `Your execution transaction has been reverted.
|
||||
However, the execution completed successfully in the transaction sent by a different party.`
|
||||
|
||||
export const EXECUTION_FAILED_ERROR = `Your execution transaction has been reverted.
|
||||
Please, contact the support by messaging on %linkhttps://forum.poa.network/c/support`
|
||||
|
||||
export const EXECUTION_OUT_OF_GAS_ERROR = `Your execution transaction has been reverted due to Out-of-Gas error.
|
||||
Please, resend the transaction and provide more gas to it.`
|
||||
76
alm/src/config/descriptions.ts
Normal file
76
alm/src/config/descriptions.ts
Normal file
@ -0,0 +1,76 @@
|
||||
// %t will be replaced by the time -> x minutes/hours/days ago
|
||||
import { ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION } from './constants'
|
||||
|
||||
export const TRANSACTION_STATUS_DESCRIPTION: { [key: string]: string } = {
|
||||
SUCCESS_MULTIPLE_MESSAGES: 'was initiated %t and contains several bridge messages. Specify one of them:',
|
||||
SUCCESS_ONE_MESSAGE: 'was initiated %t',
|
||||
SUCCESS_NO_MESSAGES:
|
||||
'successfully mined %t but it does not seem to contain any request to the bridge, \nso nothing needs to be confirmed by the validators. \nIf you are sure that the transaction should contain a request to the bridge,\ncontact to the validators by \nmessaging on %linkhttps://forum.poa.network/c/support',
|
||||
FAILED: 'failed %t',
|
||||
NOT_FOUND:
|
||||
'Transaction not found. \n1. Check that the transaction hash is correct. \n2. Wait several blocks for the transaction to be\nmined, gas price affects mining speed.'
|
||||
}
|
||||
|
||||
export const CONFIRMATIONS_STATUS_LABEL: { [key: string]: string } = {
|
||||
SUCCESS: 'Success',
|
||||
SUCCESS_MESSAGE_FAILED: 'Success',
|
||||
FAILED: 'Failed',
|
||||
PENDING: 'Pending',
|
||||
WAITING_VALIDATORS: 'Waiting',
|
||||
SEARCHING: 'Waiting',
|
||||
WAITING_CHAIN: 'Waiting'
|
||||
}
|
||||
|
||||
export const CONFIRMATIONS_STATUS_LABEL_HOME: { [key: string]: string } = {
|
||||
SUCCESS: 'Success',
|
||||
SUCCESS_MESSAGE_FAILED: 'Success',
|
||||
EXECUTION_FAILED: 'Execution failed',
|
||||
EXECUTION_PENDING: 'Execution pending',
|
||||
EXECUTION_WAITING: ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION ? 'Manual execution waiting' : 'Execution waiting',
|
||||
FAILED: 'Confirmation Failed',
|
||||
PENDING: 'Confirmation Pending',
|
||||
WAITING_VALIDATORS: 'Confirmation Waiting',
|
||||
SEARCHING: 'Confirmation Waiting',
|
||||
WAITING_CHAIN: 'Confirmation Waiting'
|
||||
}
|
||||
|
||||
// use %link to identify a link
|
||||
export const CONFIRMATIONS_STATUS_DESCRIPTION: { [key: string]: string } = {
|
||||
SUCCESS: '',
|
||||
SUCCESS_MESSAGE_FAILED:
|
||||
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
|
||||
FAILED:
|
||||
'The specified transaction was included in a block,\nbut confirmations sent by a majority of validators\nfailed. The cross-chain relay request will not be\nprocessed. Contact to the validators by\nmessaging on %linkhttps://forum.poa.network/c/support',
|
||||
PENDING:
|
||||
'The specified transaction was included in a block. A\nmajority of validators sent confirmations which have\nnot yet been added to a block.',
|
||||
WAITING_VALIDATORS:
|
||||
'The specified transaction was included in a block.\nSome validators have sent confirmations, others are\nwaiting for chain finalization.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
|
||||
SEARCHING:
|
||||
'The specified transaction was included in a block. The app is looking for confirmations. Either\n1. Validators are waiting for chain finalization before sending their signatures.\n2. Validators are not active.\n3. The bridge was stopped.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
|
||||
WAITING_CHAIN:
|
||||
'The specified transaction was included in a block.\nValidators are waiting for chain finalization before\nsending their confirmations.'
|
||||
}
|
||||
|
||||
// use %link to identify a link
|
||||
export const CONFIRMATIONS_STATUS_DESCRIPTION_HOME: { [key: string]: string } = {
|
||||
SUCCESS: '',
|
||||
SUCCESS_MESSAGE_FAILED:
|
||||
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
|
||||
EXECUTION_FAILED:
|
||||
'The specified transaction was included in a block\nand the validators collected signatures. The\n transaction with collected signatures was\nsent but did not succeed. Contact to the validators by messaging\non %linkhttps://forum.poa.network/c/support',
|
||||
EXECUTION_PENDING:
|
||||
'The specified transaction was included in a block\nand the validators collected signatures. The\n transaction with collected signatures was\nsent but is not yet added to a block.',
|
||||
EXECUTION_WAITING: ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION
|
||||
? 'The specified transaction was included in a block\nand the validators collected signatures.\nNow the manual user action is required to complete message execution.\n Please, press the "Execute" button.'
|
||||
: 'The specified transaction was included in a block\nand the validators collected signatures. Either\n1. One of the validators is waiting for chain finalization.\n2. A validator skipped its duty to relay signatures.\n3. The execution transaction is still pending (e.g. due to the gas price spike).\nCheck status again after a few blocks or force execution by pressing the "Execute" button.\nIf the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
|
||||
FAILED:
|
||||
'The specified transaction was included in a block,\nbut transactions with signatures sent by a majority of\nvalidators failed. The cross-chain relay request will\nnot be processed. Contact to the validators by\nmessaging on %linkhttps://forum.poa.network/c/support',
|
||||
PENDING:
|
||||
'The specified transaction was included in a block.\nA majority of validators sent signatures which have not\nyet been added to a block.',
|
||||
WAITING_VALIDATORS:
|
||||
'The specified transaction was included in a block.\nSome validators have sent signatures, others are\nwaiting for chain finalization.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
|
||||
SEARCHING:
|
||||
'The specified transaction was included in a block. The app is looking for confirmations. Either\n1. Validators are waiting for chain finalization before sending their signatures.\n2. Validators are not active.\n3. The bridge was stopped.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
|
||||
WAITING_CHAIN:
|
||||
'The specified transaction was included in a block.\nValidators are waiting for chain finalization\nbefore sending their signatures.'
|
||||
}
|
||||
1
alm/src/global.d.ts
vendored
Normal file
1
alm/src/global.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare type Maybe<T> = T | null
|
||||
47
alm/src/hooks/useBlockConfirmations.ts
Normal file
47
alm/src/hooks/useBlockConfirmations.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { getRequiredBlockConfirmations } from '../utils/contract'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
|
||||
import Web3 from 'web3'
|
||||
import { FOREIGN_EXPLORER_API, HOME_EXPLORER_API } from '../config/constants'
|
||||
|
||||
export interface UseBlockConfirmationsParams {
|
||||
fromHome: boolean
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
}
|
||||
|
||||
export const useBlockConfirmations = ({ receipt, fromHome }: UseBlockConfirmationsParams) => {
|
||||
const [blockConfirmations, setBlockConfirmations] = useState(0)
|
||||
|
||||
const { home, foreign } = useStateProvider()
|
||||
|
||||
const callRequireBlockConfirmations = async (
|
||||
contract: Contract,
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3,
|
||||
api: string
|
||||
) => {
|
||||
const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber, snapshotProvider, web3, api)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
|
||||
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
|
||||
const web3 = fromHome ? home.web3 : foreign.web3
|
||||
const api = fromHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
|
||||
if (!bridgeContract || !receipt || !web3) return
|
||||
callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations, snapshotProvider, web3, api)
|
||||
},
|
||||
[home.bridgeContract, foreign.bridgeContract, receipt, fromHome, home.web3, foreign.web3]
|
||||
)
|
||||
|
||||
return {
|
||||
blockConfirmations
|
||||
}
|
||||
}
|
||||
38
alm/src/hooks/useBridgeContracts.ts
Normal file
38
alm/src/hooks/useBridgeContracts.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
|
||||
import { FOREIGN_BRIDGE_ADDRESS, HOME_BRIDGE_ADDRESS } from '../config/constants'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export interface useBridgeContractsParams {
|
||||
homeWeb3: Web3
|
||||
foreignWeb3: Web3
|
||||
}
|
||||
|
||||
export const useBridgeContracts = ({ homeWeb3, foreignWeb3 }: useBridgeContractsParams) => {
|
||||
const [homeBridge, setHomeBridge] = useState<Maybe<Contract>>(null)
|
||||
const [foreignBridge, setForeignBridge] = useState<Maybe<Contract>>(null)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!homeWeb3) return
|
||||
const homeContract = new homeWeb3.eth.Contract(HOME_AMB_ABI, HOME_BRIDGE_ADDRESS)
|
||||
setHomeBridge(homeContract)
|
||||
},
|
||||
[homeWeb3]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!foreignWeb3) return
|
||||
const foreignContract = new foreignWeb3.eth.Contract(FOREIGN_AMB_ABI, FOREIGN_BRIDGE_ADDRESS)
|
||||
setForeignBridge(foreignContract)
|
||||
},
|
||||
[foreignWeb3]
|
||||
)
|
||||
|
||||
return {
|
||||
homeBridge,
|
||||
foreignBridge
|
||||
}
|
||||
}
|
||||
68
alm/src/hooks/useClosestBlock.ts
Normal file
68
alm/src/hooks/useClosestBlock.ts
Normal file
@ -0,0 +1,68 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { FOREIGN_EXPLORER_API, HOME_EXPLORER_API } from '../config/constants'
|
||||
import { getClosestBlockByTimestamp } from '../utils/explorer'
|
||||
|
||||
export function useClosestBlock(
|
||||
searchHome: boolean,
|
||||
fromHome: boolean,
|
||||
receipt: Maybe<TransactionReceipt>,
|
||||
timestamp: number
|
||||
) {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [blockNumber, setBlockNumber] = useState<number | null>(null)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!receipt || blockNumber || !timestamp) return
|
||||
|
||||
if (fromHome === searchHome) {
|
||||
setBlockNumber(receipt.blockNumber)
|
||||
return
|
||||
}
|
||||
|
||||
const web3 = searchHome ? home.web3 : foreign.web3
|
||||
if (!web3) return
|
||||
|
||||
const getBlock = async () => {
|
||||
// try to fast-fetch closest block number from the chain explorer
|
||||
try {
|
||||
const api = searchHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
|
||||
setBlockNumber(await getClosestBlockByTimestamp(api, timestamp))
|
||||
return
|
||||
} catch {}
|
||||
|
||||
const lastBlock = await web3.eth.getBlock('latest')
|
||||
if (lastBlock.timestamp <= timestamp) {
|
||||
setBlockNumber(lastBlock.number)
|
||||
return
|
||||
}
|
||||
|
||||
const oldBlock = await web3.eth.getBlock(Math.max(lastBlock.number - 10000, 1))
|
||||
const blockDiff = lastBlock.number - oldBlock.number
|
||||
const timeDiff = (lastBlock.timestamp as number) - (oldBlock.timestamp as number)
|
||||
const averageBlockTime = timeDiff / blockDiff
|
||||
let currentBlock = lastBlock
|
||||
|
||||
let prevBlockDiff = Infinity
|
||||
while (true) {
|
||||
const timeDiff = (currentBlock.timestamp as number) - timestamp
|
||||
const blockDiff = Math.ceil(timeDiff / averageBlockTime)
|
||||
if (Math.abs(blockDiff) < 5 || Math.abs(blockDiff) >= Math.abs(prevBlockDiff)) {
|
||||
setBlockNumber(currentBlock.number - blockDiff - 5)
|
||||
break
|
||||
}
|
||||
|
||||
prevBlockDiff = blockDiff
|
||||
currentBlock = await web3.eth.getBlock(currentBlock.number - blockDiff)
|
||||
}
|
||||
}
|
||||
|
||||
getBlock()
|
||||
},
|
||||
[blockNumber, foreign.web3, fromHome, home.web3, receipt, searchHome, timestamp]
|
||||
)
|
||||
|
||||
return blockNumber
|
||||
}
|
||||
444
alm/src/hooks/useMessageConfirmations.ts
Normal file
444
alm/src/hooks/useMessageConfirmations.ts
Normal file
@ -0,0 +1,444 @@
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { EventData } from 'web3-eth-contract'
|
||||
import {
|
||||
BLOCK_RANGE,
|
||||
CONFIRMATIONS_STATUS,
|
||||
FOREIGN_RPC_POLLING_INTERVAL,
|
||||
HOME_RPC_POLLING_INTERVAL,
|
||||
VALIDATOR_CONFIRMATION_STATUS
|
||||
} from '../config/constants'
|
||||
import { homeBlockNumberProvider, foreignBlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
import { getConfirmationsForTx } from '../utils/getConfirmationsForTx'
|
||||
import { getFinalizationEvent } from '../utils/getFinalizationEvent'
|
||||
import {
|
||||
getValidatorFailedTransactionsForMessage,
|
||||
getExecutionFailedTransactionForMessage,
|
||||
getValidatorPendingTransactionsForMessage,
|
||||
getExecutionPendingTransactionsForMessage,
|
||||
getValidatorSuccessTransactionsForMessage
|
||||
} from '../utils/explorer'
|
||||
|
||||
export interface useMessageConfirmationsParams {
|
||||
message: MessageObject
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
fromHome: boolean
|
||||
homeStartBlock: Maybe<number>
|
||||
foreignStartBlock: Maybe<number>
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
targetValidatorList: string[]
|
||||
blockConfirmations: number
|
||||
}
|
||||
|
||||
export interface ConfirmationParam {
|
||||
validator: string
|
||||
status: string
|
||||
txHash: string
|
||||
timestamp: number
|
||||
signature?: string
|
||||
}
|
||||
|
||||
export interface ExecutionData {
|
||||
status: string
|
||||
validator: string
|
||||
txHash: string
|
||||
timestamp: number
|
||||
executionResult: boolean
|
||||
blockNumber: number
|
||||
}
|
||||
|
||||
export const useMessageConfirmations = ({
|
||||
message,
|
||||
receipt,
|
||||
fromHome,
|
||||
homeStartBlock,
|
||||
foreignStartBlock,
|
||||
requiredSignatures,
|
||||
validatorList,
|
||||
targetValidatorList,
|
||||
blockConfirmations
|
||||
}: useMessageConfirmationsParams) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [confirmations, setConfirmations] = useState<ConfirmationParam[]>([])
|
||||
const [status, setStatus] = useState(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
const [waitingBlocks, setWaitingBlocks] = useState(false)
|
||||
const [waitingBlocksResolved, setWaitingBlocksResolved] = useState(false)
|
||||
const [signatureCollected, setSignatureCollected] = useState(false)
|
||||
const [executionEventsFetched, setExecutionEventsFetched] = useState(false)
|
||||
const [collectedSignaturesEvent, setCollectedSignaturesEvent] = useState<Maybe<EventData>>(null)
|
||||
const [executionData, setExecutionData] = useState<ExecutionData>({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator: '',
|
||||
txHash: '',
|
||||
timestamp: 0,
|
||||
executionResult: false,
|
||||
blockNumber: 0
|
||||
})
|
||||
const [waitingBlocksForExecution, setWaitingBlocksForExecution] = useState(false)
|
||||
const [waitingBlocksForExecutionResolved, setWaitingBlocksForExecutionResolved] = useState(false)
|
||||
const [failedConfirmations, setFailedConfirmations] = useState(false)
|
||||
const [failedExecution, setFailedExecution] = useState(false)
|
||||
const [pendingConfirmations, setPendingConfirmations] = useState(false)
|
||||
const [pendingExecution, setPendingExecution] = useState(false)
|
||||
|
||||
const existsConfirmation = (confirmationArray: ConfirmationParam[]) =>
|
||||
confirmationArray.some(
|
||||
c => c.status !== VALIDATOR_CONFIRMATION_STATUS.UNDEFINED && c.status !== VALIDATOR_CONFIRMATION_STATUS.WAITING
|
||||
)
|
||||
|
||||
// start watching blocks at the start
|
||||
useEffect(
|
||||
() => {
|
||||
if (!home.web3 || !foreign.web3) return
|
||||
|
||||
homeBlockNumberProvider.start(home.web3)
|
||||
foreignBlockNumberProvider.start(foreign.web3)
|
||||
},
|
||||
[foreign.web3, home.web3]
|
||||
)
|
||||
|
||||
// Check if the validators are waiting for block confirmations to verify the message
|
||||
useEffect(
|
||||
() => {
|
||||
if (!receipt || !blockConfirmations || waitingBlocksResolved) return
|
||||
|
||||
let timeoutId: number
|
||||
|
||||
const blockProvider = fromHome ? homeBlockNumberProvider : foreignBlockNumberProvider
|
||||
const interval = fromHome ? HOME_RPC_POLLING_INTERVAL : FOREIGN_RPC_POLLING_INTERVAL
|
||||
const targetBlock = receipt.blockNumber + blockConfirmations
|
||||
const validatorsWaiting = validatorList.map(validator => ({
|
||||
validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.WAITING,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
|
||||
const checkSignaturesWaitingForBLocks = () => {
|
||||
const currentBlock = blockProvider.get()
|
||||
|
||||
if (currentBlock && currentBlock >= targetBlock) {
|
||||
setWaitingBlocksResolved(true)
|
||||
setWaitingBlocks(false)
|
||||
} else if (currentBlock) {
|
||||
setWaitingBlocks(true)
|
||||
setConfirmations(validatorsWaiting)
|
||||
timeoutId = setTimeout(checkSignaturesWaitingForBLocks, interval)
|
||||
} else {
|
||||
timeoutId = setTimeout(checkSignaturesWaitingForBLocks, 500)
|
||||
}
|
||||
}
|
||||
|
||||
checkSignaturesWaitingForBLocks()
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
},
|
||||
[blockConfirmations, fromHome, receipt, validatorList, waitingBlocksResolved]
|
||||
)
|
||||
|
||||
// The collected signature event is only fetched once the signatures are collected on tx from home to foreign, to calculate if
|
||||
// the execution tx on the foreign network is waiting for block confirmations
|
||||
// This is executed if the message is in Home to Foreign direction only
|
||||
useEffect(
|
||||
() => {
|
||||
if (!fromHome || !receipt || !home.web3 || !home.bridgeContract || !signatureCollected) return
|
||||
|
||||
let timeoutId: number
|
||||
let isCancelled = false
|
||||
|
||||
const messageHash = home.web3.utils.soliditySha3Raw(message.data)
|
||||
const contract = home.bridgeContract
|
||||
|
||||
const getCollectedSignaturesEvent = async (fromBlock: number, toBlock: number) => {
|
||||
const currentBlock = homeBlockNumberProvider.get()
|
||||
|
||||
if (currentBlock) {
|
||||
// prevent errors if the toBlock parameter is bigger than the latest
|
||||
const securedToBlock = toBlock >= currentBlock ? currentBlock : toBlock
|
||||
const events = await contract.getPastEvents('CollectedSignatures', {
|
||||
fromBlock,
|
||||
toBlock: securedToBlock
|
||||
})
|
||||
const event = events.find(e => e.returnValues.messageHash === messageHash)
|
||||
if (event) {
|
||||
setCollectedSignaturesEvent(event)
|
||||
} else if (!isCancelled) {
|
||||
timeoutId = setTimeout(() => getCollectedSignaturesEvent(securedToBlock, securedToBlock + BLOCK_RANGE), 500)
|
||||
}
|
||||
} else if (!isCancelled) {
|
||||
timeoutId = setTimeout(() => getCollectedSignaturesEvent(fromBlock, toBlock), 500)
|
||||
}
|
||||
}
|
||||
|
||||
getCollectedSignaturesEvent(receipt.blockNumber, receipt.blockNumber + BLOCK_RANGE)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
isCancelled = true
|
||||
}
|
||||
},
|
||||
[fromHome, home.bridgeContract, home.web3, message.data, receipt, signatureCollected]
|
||||
)
|
||||
|
||||
// Check if the responsible validator is waiting for block confirmations to execute the message on foreign network
|
||||
// This is executed if the message is in Home to Foreign direction only
|
||||
useEffect(
|
||||
() => {
|
||||
if (!fromHome || !home.web3 || !collectedSignaturesEvent || !blockConfirmations) return
|
||||
if (waitingBlocksForExecutionResolved) return
|
||||
|
||||
let timeoutId: number
|
||||
|
||||
const targetBlock = collectedSignaturesEvent.blockNumber + blockConfirmations
|
||||
|
||||
const checkWaitingBlocksForExecution = () => {
|
||||
const currentBlock = homeBlockNumberProvider.get()
|
||||
|
||||
if (currentBlock && currentBlock >= targetBlock) {
|
||||
const undefinedExecutionState = {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
|
||||
txHash: '',
|
||||
timestamp: 0,
|
||||
executionResult: false
|
||||
}
|
||||
setExecutionData(
|
||||
(data: any) =>
|
||||
data.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED ||
|
||||
data.status === VALIDATOR_CONFIRMATION_STATUS.WAITING
|
||||
? undefinedExecutionState
|
||||
: data
|
||||
)
|
||||
setWaitingBlocksForExecutionResolved(true)
|
||||
setWaitingBlocksForExecution(false)
|
||||
} else if (currentBlock) {
|
||||
setWaitingBlocksForExecution(true)
|
||||
const waitingExecutionState = {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.WAITING,
|
||||
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
|
||||
txHash: '',
|
||||
timestamp: 0,
|
||||
executionResult: false
|
||||
}
|
||||
setExecutionData(
|
||||
(data: any) =>
|
||||
data.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED ||
|
||||
data.status === VALIDATOR_CONFIRMATION_STATUS.WAITING
|
||||
? waitingExecutionState
|
||||
: data
|
||||
)
|
||||
timeoutId = setTimeout(() => checkWaitingBlocksForExecution(), HOME_RPC_POLLING_INTERVAL)
|
||||
} else {
|
||||
timeoutId = setTimeout(() => checkWaitingBlocksForExecution(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
checkWaitingBlocksForExecution()
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
},
|
||||
[collectedSignaturesEvent, fromHome, blockConfirmations, home.web3, waitingBlocksForExecutionResolved]
|
||||
)
|
||||
|
||||
// Checks if validators verified the message
|
||||
// To avoid making extra requests, this is only executed when validators finished waiting for blocks confirmations
|
||||
useEffect(
|
||||
() => {
|
||||
if (!waitingBlocksResolved || !homeStartBlock || !requiredSignatures || !home.web3 || !home.bridgeContract) return
|
||||
if (!validatorList || !validatorList.length) return
|
||||
|
||||
let timeoutId: number
|
||||
let isCancelled = false
|
||||
|
||||
if (fromHome) {
|
||||
if (!targetValidatorList || !targetValidatorList.length) return
|
||||
const msgHash = home.web3.utils.sha3(message.data)!
|
||||
const allValidators = [...validatorList, ...targetValidatorList].filter((v, i, s) => s.indexOf(v) === i)
|
||||
const manualConfirmations = []
|
||||
for (let i = 0; i < allValidators.length; i++) {
|
||||
try {
|
||||
const overrideSignatures: {
|
||||
[key: string]: string
|
||||
} = require(`../snapshots/signatures_${allValidators[i]}.json`)
|
||||
if (overrideSignatures[msgHash]) {
|
||||
console.log(`Adding manual signature from ${allValidators[i]}`)
|
||||
manualConfirmations.push({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.MANUAL,
|
||||
validator: allValidators[i],
|
||||
timestamp: 0,
|
||||
txHash: '',
|
||||
signature: overrideSignatures[msgHash]
|
||||
})
|
||||
} else {
|
||||
console.log(`No manual signature from ${allValidators[i]} was found`)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`Signatures overrides are not present for ${allValidators[i]}`)
|
||||
}
|
||||
}
|
||||
setConfirmations(manualConfirmations)
|
||||
}
|
||||
|
||||
getConfirmationsForTx(
|
||||
message.data,
|
||||
home.web3,
|
||||
validatorList,
|
||||
home.bridgeContract,
|
||||
fromHome,
|
||||
setConfirmations,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
id => (timeoutId = id),
|
||||
() => isCancelled,
|
||||
homeStartBlock,
|
||||
getValidatorFailedTransactionsForMessage,
|
||||
setFailedConfirmations,
|
||||
getValidatorPendingTransactionsForMessage,
|
||||
setPendingConfirmations,
|
||||
getValidatorSuccessTransactionsForMessage
|
||||
)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
isCancelled = true
|
||||
}
|
||||
},
|
||||
[
|
||||
fromHome,
|
||||
message.data,
|
||||
home.web3,
|
||||
validatorList,
|
||||
home.bridgeContract,
|
||||
requiredSignatures,
|
||||
waitingBlocksResolved,
|
||||
homeStartBlock,
|
||||
targetValidatorList
|
||||
]
|
||||
)
|
||||
|
||||
// Gets finalization event to display the information about the execution of the message
|
||||
// In a message from Home to Foreign it will be executed after finishing waiting for block confirmations for the execution transaction on Foreign
|
||||
// In a message from Foreign to Home it will be executed after finishing waiting for block confirmations of the message request
|
||||
useEffect(
|
||||
() => {
|
||||
if ((fromHome && !waitingBlocksForExecutionResolved) || (!fromHome && !waitingBlocksResolved)) return
|
||||
|
||||
const bridgeContract = fromHome ? foreign.bridgeContract : home.bridgeContract
|
||||
const web3 = fromHome ? foreign.web3 : home.web3
|
||||
const startBlock = fromHome ? foreignStartBlock : homeStartBlock
|
||||
if (!startBlock || !bridgeContract || !web3) return
|
||||
|
||||
let timeoutId: number
|
||||
let isCancelled = false
|
||||
|
||||
getFinalizationEvent(
|
||||
fromHome,
|
||||
bridgeContract,
|
||||
web3,
|
||||
setExecutionData,
|
||||
message,
|
||||
id => (timeoutId = id),
|
||||
() => isCancelled,
|
||||
startBlock,
|
||||
collectedSignaturesEvent,
|
||||
getExecutionFailedTransactionForMessage,
|
||||
setFailedExecution,
|
||||
getExecutionPendingTransactionsForMessage,
|
||||
setPendingExecution,
|
||||
setExecutionEventsFetched
|
||||
)
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId)
|
||||
isCancelled = true
|
||||
}
|
||||
},
|
||||
[
|
||||
fromHome,
|
||||
foreign.bridgeContract,
|
||||
home.bridgeContract,
|
||||
message,
|
||||
foreign.web3,
|
||||
home.web3,
|
||||
waitingBlocksResolved,
|
||||
waitingBlocksForExecutionResolved,
|
||||
collectedSignaturesEvent,
|
||||
foreignStartBlock,
|
||||
homeStartBlock
|
||||
]
|
||||
)
|
||||
|
||||
// Sets the message status based in the collected information
|
||||
useEffect(
|
||||
() => {
|
||||
if (
|
||||
executionData.status === VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS &&
|
||||
existsConfirmation(confirmations)
|
||||
) {
|
||||
const newStatus = executionData.executionResult
|
||||
? CONFIRMATIONS_STATUS.SUCCESS
|
||||
: CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED
|
||||
setStatus(newStatus)
|
||||
|
||||
foreignBlockNumberProvider.stop()
|
||||
homeBlockNumberProvider.stop()
|
||||
} else if (signatureCollected) {
|
||||
if (fromHome) {
|
||||
if (waitingBlocksForExecution) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
|
||||
} else if (failedExecution) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_FAILED)
|
||||
} else if (pendingExecution) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_PENDING)
|
||||
} else if (waitingBlocksForExecutionResolved) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
|
||||
} else {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
|
||||
}
|
||||
} else {
|
||||
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
}
|
||||
} else if (waitingBlocks) {
|
||||
setStatus(CONFIRMATIONS_STATUS.WAITING_CHAIN)
|
||||
} else if (failedConfirmations) {
|
||||
setStatus(CONFIRMATIONS_STATUS.FAILED)
|
||||
} else if (pendingConfirmations) {
|
||||
setStatus(CONFIRMATIONS_STATUS.PENDING)
|
||||
} else if (waitingBlocksResolved && existsConfirmation(confirmations)) {
|
||||
setStatus(CONFIRMATIONS_STATUS.WAITING_VALIDATORS)
|
||||
} else if (waitingBlocksResolved) {
|
||||
setStatus(CONFIRMATIONS_STATUS.SEARCHING)
|
||||
} else {
|
||||
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
}
|
||||
},
|
||||
[
|
||||
executionData,
|
||||
fromHome,
|
||||
signatureCollected,
|
||||
waitingBlocks,
|
||||
waitingBlocksForExecution,
|
||||
failedConfirmations,
|
||||
failedExecution,
|
||||
pendingConfirmations,
|
||||
pendingExecution,
|
||||
waitingBlocksResolved,
|
||||
confirmations,
|
||||
waitingBlocksForExecutionResolved
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
confirmations,
|
||||
status,
|
||||
signatureCollected,
|
||||
executionData,
|
||||
setExecutionData,
|
||||
waitingBlocksResolved,
|
||||
executionEventsFetched,
|
||||
setPendingExecution
|
||||
}
|
||||
}
|
||||
28
alm/src/hooks/useNetwork.ts
Normal file
28
alm/src/hooks/useNetwork.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getChainId, getWeb3 } from '../utils/web3'
|
||||
import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export const useNetwork = (url: string, snapshotProvider: SnapshotProvider) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [chainId, setChainId] = useState(0)
|
||||
const web3 = getWeb3(url)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setLoading(true)
|
||||
const getWeb3ChainId = async () => {
|
||||
const id = await getChainId(web3, snapshotProvider)
|
||||
setChainId(id)
|
||||
setLoading(false)
|
||||
}
|
||||
getWeb3ChainId()
|
||||
},
|
||||
[web3, snapshotProvider]
|
||||
)
|
||||
|
||||
return {
|
||||
web3,
|
||||
chainId,
|
||||
loading
|
||||
}
|
||||
}
|
||||
39
alm/src/hooks/useTransactionFinder.ts
Normal file
39
alm/src/hooks/useTransactionFinder.ts
Normal file
@ -0,0 +1,39 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constants'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export const useTransactionFinder = ({ txHash, web3 }: { txHash: string; web3: Maybe<Web3> }) => {
|
||||
const [status, setStatus] = useState(TRANSACTION_STATUS.UNDEFINED)
|
||||
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!txHash || !web3) return
|
||||
|
||||
let timeoutId: number
|
||||
|
||||
const getReceipt = async () => {
|
||||
const txReceipt = await web3.eth.getTransactionReceipt(txHash)
|
||||
setReceipt(txReceipt)
|
||||
|
||||
if (!txReceipt) {
|
||||
setStatus(TRANSACTION_STATUS.NOT_FOUND)
|
||||
timeoutId = setTimeout(getReceipt, HOME_RPC_POLLING_INTERVAL)
|
||||
} else {
|
||||
setStatus(TRANSACTION_STATUS.FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
getReceipt()
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
},
|
||||
[txHash, web3]
|
||||
)
|
||||
|
||||
return {
|
||||
status,
|
||||
receipt
|
||||
}
|
||||
}
|
||||
120
alm/src/hooks/useTransactionStatus.ts
Normal file
120
alm/src/hooks/useTransactionStatus.ts
Normal file
@ -0,0 +1,120 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constants'
|
||||
import { getTransactionStatusDescription } from '../utils/networks'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { getHomeMessagesFromReceipt, getForeignMessagesFromReceipt, MessageObject, getBlock } from '../utils/web3'
|
||||
import useInterval from '@use-it/interval'
|
||||
|
||||
export const useTransactionStatus = ({
|
||||
txHash,
|
||||
chainId,
|
||||
receiptParam
|
||||
}: {
|
||||
txHash: string
|
||||
chainId: number
|
||||
receiptParam: Maybe<TransactionReceipt>
|
||||
}) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [messages, setMessages] = useState<Array<MessageObject>>([])
|
||||
const [status, setStatus] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||
const [timestamp, setTimestamp] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Update description so the time displayed is accurate
|
||||
useInterval(() => {
|
||||
if (!status || !timestamp || !description) return
|
||||
setDescription(getTransactionStatusDescription(status, timestamp))
|
||||
}, 30000)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!chainId || !txHash || !home.chainId || !foreign.chainId || !home.web3 || !foreign.web3) return
|
||||
const isHome = chainId === home.chainId
|
||||
const web3 = isHome ? home.web3 : foreign.web3
|
||||
|
||||
let timeoutId: number
|
||||
|
||||
const getReceipt = async () => {
|
||||
setLoading(true)
|
||||
|
||||
let txReceipt
|
||||
|
||||
if (receiptParam) {
|
||||
txReceipt = receiptParam
|
||||
} else {
|
||||
txReceipt = await web3.eth.getTransactionReceipt(txHash)
|
||||
}
|
||||
|
||||
setReceipt(txReceipt)
|
||||
|
||||
if (!txReceipt) {
|
||||
setStatus(TRANSACTION_STATUS.NOT_FOUND)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.NOT_FOUND))
|
||||
setMessages([{ id: txHash, data: '' }])
|
||||
timeoutId = setTimeout(() => getReceipt(), HOME_RPC_POLLING_INTERVAL)
|
||||
} else {
|
||||
const blockNumber = txReceipt.blockNumber
|
||||
const block = await getBlock(web3, blockNumber)
|
||||
const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp
|
||||
setTimestamp(blockTimestamp)
|
||||
|
||||
if (txReceipt.status) {
|
||||
let bridgeMessages: Array<MessageObject>
|
||||
if (isHome) {
|
||||
bridgeMessages = getHomeMessagesFromReceipt(txReceipt, web3, home.bridgeAddress)
|
||||
} else {
|
||||
bridgeMessages = getForeignMessagesFromReceipt(txReceipt, web3, foreign.bridgeAddress)
|
||||
}
|
||||
|
||||
if (bridgeMessages.length === 0) {
|
||||
setMessages([{ id: txHash, data: '' }])
|
||||
setStatus(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES, blockTimestamp))
|
||||
} else if (bridgeMessages.length === 1) {
|
||||
setMessages(bridgeMessages)
|
||||
setStatus(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, blockTimestamp))
|
||||
} else {
|
||||
setMessages(bridgeMessages)
|
||||
setStatus(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES)
|
||||
setDescription(
|
||||
getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES, blockTimestamp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setStatus(TRANSACTION_STATUS.FAILED)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.FAILED, blockTimestamp))
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
getReceipt()
|
||||
|
||||
return () => clearTimeout(timeoutId)
|
||||
},
|
||||
[
|
||||
txHash,
|
||||
chainId,
|
||||
home.chainId,
|
||||
foreign.chainId,
|
||||
home.web3,
|
||||
foreign.web3,
|
||||
home.bridgeAddress,
|
||||
foreign.bridgeAddress,
|
||||
receiptParam
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
messages,
|
||||
status,
|
||||
description,
|
||||
receipt,
|
||||
timestamp,
|
||||
loading
|
||||
}
|
||||
}
|
||||
76
alm/src/hooks/useValidatorContract.ts
Normal file
76
alm/src/hooks/useValidatorContract.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '../utils/contract'
|
||||
import { BRIDGE_VALIDATORS_ABI } from '../abis'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
|
||||
import { FOREIGN_EXPLORER_API, HOME_EXPLORER_API } from '../config/constants'
|
||||
|
||||
export const useValidatorContract = (isHome: boolean, blockNumber: number | 'latest') => {
|
||||
const [validatorContract, setValidatorContract] = useState<Maybe<Contract>>(null)
|
||||
const [requiredSignatures, setRequiredSignatures] = useState(0)
|
||||
const [validatorList, setValidatorList] = useState<string[]>([])
|
||||
|
||||
const { home, foreign } = useStateProvider()
|
||||
|
||||
const callValidatorContract = async (bridgeContract: Maybe<Contract>, web3: Web3, setValidatorContract: Function) => {
|
||||
if (!web3 || !bridgeContract) return
|
||||
const address = await getValidatorAddress(bridgeContract)
|
||||
const contract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, address)
|
||||
setValidatorContract(contract)
|
||||
}
|
||||
|
||||
const callRequiredSignatures = async (
|
||||
contract: Maybe<Contract>,
|
||||
blockNumber: number | 'latest',
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3,
|
||||
api: string
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getRequiredSignatures(contract, blockNumber, snapshotProvider, web3, api)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
const callValidatorList = async (
|
||||
contract: Maybe<Contract>,
|
||||
blockNumber: number | 'latest',
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3,
|
||||
api: string
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getValidatorList(contract, blockNumber, snapshotProvider, web3, api)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
const web3 = isHome ? home.web3 : foreign.web3
|
||||
const api = isHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
|
||||
const bridgeContract = isHome ? home.bridgeContract : foreign.bridgeContract
|
||||
const snapshotProvider = isHome ? homeSnapshotProvider : foreignSnapshotProvider
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!web3 || !bridgeContract) return
|
||||
callValidatorContract(bridgeContract, web3, setValidatorContract)
|
||||
},
|
||||
[web3, bridgeContract]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!web3 || !blockNumber) return
|
||||
callRequiredSignatures(validatorContract, blockNumber, setRequiredSignatures, snapshotProvider, web3, api)
|
||||
callValidatorList(validatorContract, blockNumber, setValidatorList, snapshotProvider, web3, api)
|
||||
},
|
||||
[validatorContract, blockNumber, web3, snapshotProvider, api]
|
||||
)
|
||||
|
||||
return {
|
||||
requiredSignatures,
|
||||
validatorList
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@ -1,11 +1,16 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import './index.css'
|
||||
import { ThemeProvider } from 'styled-components'
|
||||
import { GlobalStyle } from './themes/GlobalStyle'
|
||||
import App from './App'
|
||||
import Light from './themes/Light'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
<ThemeProvider theme={Light}>
|
||||
<GlobalStyle />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
|
||||
64
alm/src/services/BlockNumberProvider.ts
Normal file
64
alm/src/services/BlockNumberProvider.ts
Normal file
@ -0,0 +1,64 @@
|
||||
import Web3 from 'web3'
|
||||
import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'
|
||||
import { FOREIGN_RPC_POLLING_INTERVAL, HOME_RPC_POLLING_INTERVAL } from '../config/constants'
|
||||
|
||||
export class BlockNumberProvider {
|
||||
private running: number
|
||||
private web3: Maybe<Web3>
|
||||
private ref: number | undefined
|
||||
private value: Maybe<number>
|
||||
private lastValueTimestamp: Maybe<Date>
|
||||
private readonly interval: number
|
||||
|
||||
constructor(interval = 5000) {
|
||||
this.running = 0
|
||||
this.web3 = null
|
||||
this.ref = undefined
|
||||
this.value = null
|
||||
this.lastValueTimestamp = null
|
||||
this.interval = interval
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
start(web3: Maybe<Web3>) {
|
||||
if (!this.running) {
|
||||
clearTimeout(this.ref)
|
||||
this.web3 = web3
|
||||
this.running = this.running + 1
|
||||
this.fetchLastBlock()
|
||||
} else {
|
||||
this.running = this.running + 1
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = this.running > 0 ? this.running - 1 : 0
|
||||
|
||||
if (!this.running) {
|
||||
clearTimeout(this.ref)
|
||||
this.ref = undefined
|
||||
this.web3 = null
|
||||
}
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.value
|
||||
}
|
||||
|
||||
private async fetchLastBlock() {
|
||||
if (!this.web3) return
|
||||
const now = new Date()
|
||||
const distance = differenceInMilliseconds(now, this.lastValueTimestamp || 0)
|
||||
|
||||
if (distance >= this.interval) {
|
||||
this.value = await this.web3.eth.getBlockNumber()
|
||||
this.lastValueTimestamp = now
|
||||
}
|
||||
|
||||
this.ref = setTimeout(() => this.fetchLastBlock(), this.interval)
|
||||
}
|
||||
}
|
||||
|
||||
export const homeBlockNumberProvider = new BlockNumberProvider(HOME_RPC_POLLING_INTERVAL)
|
||||
export const foreignBlockNumberProvider = new BlockNumberProvider(FOREIGN_RPC_POLLING_INTERVAL)
|
||||
69
alm/src/services/SnapshotProvider.ts
Normal file
69
alm/src/services/SnapshotProvider.ts
Normal file
@ -0,0 +1,69 @@
|
||||
const initialValue = {
|
||||
chainId: 0,
|
||||
RequiredBlockConfirmationChanged: [],
|
||||
RequiredSignaturesChanged: [],
|
||||
ValidatorAdded: [],
|
||||
ValidatorRemoved: [],
|
||||
snapshotBlockNumber: 0
|
||||
}
|
||||
|
||||
export interface SnapshotEvent {
|
||||
blockNumber: number
|
||||
returnValues: any
|
||||
}
|
||||
|
||||
export interface SnapshotValidatorEvent {
|
||||
blockNumber: number
|
||||
returnValues: any
|
||||
event: string
|
||||
}
|
||||
|
||||
export interface Snapshot {
|
||||
chainId: number
|
||||
RequiredBlockConfirmationChanged: SnapshotEvent[]
|
||||
RequiredSignaturesChanged: SnapshotEvent[]
|
||||
ValidatorAdded: SnapshotValidatorEvent[]
|
||||
ValidatorRemoved: SnapshotValidatorEvent[]
|
||||
snapshotBlockNumber: number
|
||||
}
|
||||
|
||||
export class SnapshotProvider {
|
||||
private data: Snapshot
|
||||
|
||||
constructor(side: string) {
|
||||
let data = initialValue
|
||||
try {
|
||||
data = require(`../snapshots/${side}.json`)
|
||||
} catch (e) {
|
||||
console.log('Snapshot not found')
|
||||
}
|
||||
this.data = data
|
||||
}
|
||||
|
||||
chainId() {
|
||||
return this.data.chainId
|
||||
}
|
||||
|
||||
snapshotBlockNumber() {
|
||||
return this.data.snapshotBlockNumber
|
||||
}
|
||||
|
||||
requiredBlockConfirmationEvents(toBlock: number) {
|
||||
return this.data.RequiredBlockConfirmationChanged.filter(e => e.blockNumber <= toBlock)
|
||||
}
|
||||
|
||||
requiredSignaturesEvents(toBlock: number) {
|
||||
return this.data.RequiredSignaturesChanged.filter(e => e.blockNumber <= toBlock)
|
||||
}
|
||||
|
||||
validatorAddedEvents(fromBlock: number) {
|
||||
return this.data.ValidatorAdded.filter(e => e.blockNumber >= fromBlock)
|
||||
}
|
||||
|
||||
validatorRemovedEvents(fromBlock: number) {
|
||||
return this.data.ValidatorRemoved.filter(e => e.blockNumber >= fromBlock)
|
||||
}
|
||||
}
|
||||
|
||||
export const homeSnapshotProvider = new SnapshotProvider('home')
|
||||
export const foreignSnapshotProvider = new SnapshotProvider('foreign')
|
||||
29
alm/src/services/ValidatorsCache.ts
Normal file
29
alm/src/services/ValidatorsCache.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
class ValidatorsCache {
|
||||
private readonly store: { [key: string]: boolean }
|
||||
private readonly dataStore: { [key: string]: ConfirmationParam }
|
||||
|
||||
constructor() {
|
||||
this.store = {}
|
||||
this.dataStore = {}
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this.store[key]
|
||||
}
|
||||
|
||||
set(key: string, value: boolean) {
|
||||
this.store[key] = value
|
||||
}
|
||||
|
||||
getData(key: string) {
|
||||
return this.dataStore[key]
|
||||
}
|
||||
|
||||
setData(key: string, value: ConfirmationParam) {
|
||||
this.dataStore[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
export default new ValidatorsCache()
|
||||
79
alm/src/state/StateProvider.tsx
Normal file
79
alm/src/state/StateProvider.tsx
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { createContext, ReactNode } from 'react'
|
||||
import { useNetwork } from '../hooks/useNetwork'
|
||||
import {
|
||||
HOME_RPC_URL,
|
||||
FOREIGN_RPC_URL,
|
||||
HOME_BRIDGE_ADDRESS,
|
||||
FOREIGN_BRIDGE_ADDRESS,
|
||||
HOME_NETWORK_NAME,
|
||||
FOREIGN_NETWORK_NAME
|
||||
} from '../config/constants'
|
||||
import Web3 from 'web3'
|
||||
import { useBridgeContracts } from '../hooks/useBridgeContracts'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export interface BaseNetworkParams {
|
||||
chainId: number
|
||||
name: string
|
||||
web3: Maybe<Web3>
|
||||
bridgeAddress: string
|
||||
bridgeContract: Maybe<Contract>
|
||||
}
|
||||
|
||||
export interface StateContext {
|
||||
home: BaseNetworkParams
|
||||
foreign: BaseNetworkParams
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
home: {
|
||||
chainId: 0,
|
||||
name: '',
|
||||
web3: null,
|
||||
bridgeAddress: HOME_BRIDGE_ADDRESS,
|
||||
bridgeContract: null
|
||||
},
|
||||
foreign: {
|
||||
chainId: 0,
|
||||
name: '',
|
||||
web3: null,
|
||||
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
|
||||
bridgeContract: null
|
||||
},
|
||||
loading: true
|
||||
}
|
||||
|
||||
const StateContext = createContext<StateContext>(initialState)
|
||||
|
||||
export const StateProvider = ({ children }: { children: ReactNode }) => {
|
||||
const homeNetwork = useNetwork(HOME_RPC_URL, homeSnapshotProvider)
|
||||
const foreignNetwork = useNetwork(FOREIGN_RPC_URL, foreignSnapshotProvider)
|
||||
const { homeBridge, foreignBridge } = useBridgeContracts({
|
||||
homeWeb3: homeNetwork.web3,
|
||||
foreignWeb3: foreignNetwork.web3
|
||||
})
|
||||
|
||||
const value = {
|
||||
home: {
|
||||
bridgeAddress: HOME_BRIDGE_ADDRESS,
|
||||
name: HOME_NETWORK_NAME,
|
||||
bridgeContract: homeBridge,
|
||||
...homeNetwork
|
||||
},
|
||||
foreign: {
|
||||
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
|
||||
name: FOREIGN_NETWORK_NAME,
|
||||
bridgeContract: foreignBridge,
|
||||
...foreignNetwork
|
||||
},
|
||||
loading: homeNetwork.loading || foreignNetwork.loading
|
||||
}
|
||||
|
||||
return <StateContext.Provider value={value}>{children}</StateContext.Provider>
|
||||
}
|
||||
|
||||
export const useStateProvider = (): StateContext => {
|
||||
return React.useContext(StateContext)
|
||||
}
|
||||
22
alm/src/themes/Dark.tsx
Normal file
22
alm/src/themes/Dark.tsx
Normal file
@ -0,0 +1,22 @@
|
||||
const theme = {
|
||||
backgroundColor: '#121212',
|
||||
fontColor: '#f5f5f5',
|
||||
buttonColor: '#f5f5f5',
|
||||
colorPrimary: '#272727',
|
||||
colorGrey: '#272727',
|
||||
colorLightGrey: '#272727',
|
||||
linkColor: '#ffffff',
|
||||
success: {
|
||||
textColor: '#00c9a7',
|
||||
backgroundColor: '#004d40'
|
||||
},
|
||||
notRequired: {
|
||||
textColor: '#bdbdbd',
|
||||
backgroundColor: '#424242'
|
||||
},
|
||||
failed: {
|
||||
textColor: '#EF5350',
|
||||
backgroundColor: '#4E342E'
|
||||
}
|
||||
}
|
||||
export default theme
|
||||
34
alm/src/themes/GlobalStyle.tsx
Normal file
34
alm/src/themes/GlobalStyle.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { createGlobalStyle } from 'styled-components'
|
||||
|
||||
import theme from './Light'
|
||||
|
||||
type ThemeType = typeof theme
|
||||
|
||||
export const GlobalStyle = createGlobalStyle<{ theme: ThemeType }>`
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-color: ${props => props.theme.backgroundColor};
|
||||
--font-color: ${props => props.theme.fontColor};
|
||||
--button-color: ${props => props.theme.buttonColor};
|
||||
--color-primary: ${props => props.theme.colorPrimary};
|
||||
--color-grey: ${props => props.theme.colorGrey};
|
||||
--color-lightGrey: ${props => props.theme.colorLightGrey};
|
||||
--link-color: ${props => props.theme.linkColor};
|
||||
--success-color: ${props => props.theme.success.textColor};
|
||||
--success-bg-color: ${props => props.theme.success.backgroundColor};
|
||||
--not-required-color: ${props => props.theme.notRequired.textColor};
|
||||
--not-required-bg-color: ${props => props.theme.notRequired.backgroundColor};
|
||||
--failed-color: ${props => props.theme.failed.textColor};
|
||||
--failed-bg-color: ${props => props.theme.failed.backgroundColor};
|
||||
--warning-color: ${props => props.theme.warning.textColor};
|
||||
--warning-bg-color: ${props => props.theme.warning.backgroundColor};
|
||||
}
|
||||
`
|
||||
26
alm/src/themes/Light.ts
Normal file
26
alm/src/themes/Light.ts
Normal file
@ -0,0 +1,26 @@
|
||||
const theme = {
|
||||
backgroundColor: '#FFFFFF',
|
||||
fontColor: 'rgba(0, 0, 0, 0.65)',
|
||||
buttonColor: '#1890ff',
|
||||
colorPrimary: '#BDBDBD',
|
||||
colorGrey: '#1890ff',
|
||||
colorLightGrey: '#1890ff',
|
||||
linkColor: '#1890ff',
|
||||
success: {
|
||||
textColor: '#388E3C',
|
||||
backgroundColor: 'rgba(0,201,167,.1)'
|
||||
},
|
||||
notRequired: {
|
||||
textColor: '#77838f',
|
||||
backgroundColor: 'rgba(119,131,143,.1)'
|
||||
},
|
||||
failed: {
|
||||
textColor: '#de4437',
|
||||
backgroundColor: 'rgba(222,68,55,.1)'
|
||||
},
|
||||
warning: {
|
||||
textColor: '#ffa758',
|
||||
backgroundColor: 'rgba(222,68,55,.1)'
|
||||
}
|
||||
}
|
||||
export default theme
|
||||
469
alm/src/utils/__tests__/contracts.test.ts
Normal file
469
alm/src/utils/__tests__/contracts.test.ts
Normal file
@ -0,0 +1,469 @@
|
||||
import 'jest'
|
||||
import { getRequiredBlockConfirmations, getRequiredSignatures, getValidatorList } from '../contract'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { SnapshotProvider } from '../../services/SnapshotProvider'
|
||||
|
||||
describe('getRequiredBlockConfirmations', () => {
|
||||
const methodsBuilder = (value: string) => ({
|
||||
requiredBlockConfirmations: () => {
|
||||
return {
|
||||
call: () => {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Should call requiredBlockConfirmations method if no events present', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: async () => {
|
||||
return []
|
||||
},
|
||||
methods: methodsBuilder('1')
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
requiredBlockConfirmationEvents: () => {
|
||||
return []
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const result = await getRequiredBlockConfirmations(contract, 10, snapshotProvider)
|
||||
|
||||
expect(result).toEqual(1)
|
||||
})
|
||||
test('Should not call to get events if block number was included in the snapshot', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => []),
|
||||
methods: methodsBuilder('3')
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
requiredBlockConfirmationEvents: () => {
|
||||
return [
|
||||
{
|
||||
blockNumber: 8,
|
||||
returnValues: {
|
||||
requiredBlockConfirmations: '1'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 15
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const result = await getRequiredBlockConfirmations(contract, 10, snapshotProvider)
|
||||
|
||||
expect(result).toEqual(1)
|
||||
expect(contract.getPastEvents).toBeCalledTimes(0)
|
||||
})
|
||||
test('Should call to get events if block number was not included in the snapshot', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => [
|
||||
{
|
||||
blockNumber: 9,
|
||||
returnValues: {
|
||||
requiredBlockConfirmations: '2'
|
||||
}
|
||||
}
|
||||
]),
|
||||
methods: methodsBuilder('3')
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
requiredBlockConfirmationEvents: () => {
|
||||
return [
|
||||
{
|
||||
blockNumber: 8,
|
||||
returnValues: {
|
||||
requiredBlockConfirmations: '1'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 8
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const result = await getRequiredBlockConfirmations(contract, 10, snapshotProvider)
|
||||
|
||||
expect(result).toEqual(2)
|
||||
expect(contract.getPastEvents).toBeCalledTimes(1)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('RequiredBlockConfirmationChanged', {
|
||||
fromBlock: 9,
|
||||
toBlock: 10
|
||||
})
|
||||
})
|
||||
test('Should use the most updated event', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => [
|
||||
{
|
||||
blockNumber: 9,
|
||||
returnValues: {
|
||||
requiredBlockConfirmations: '2'
|
||||
}
|
||||
},
|
||||
{
|
||||
blockNumber: 11,
|
||||
returnValues: {
|
||||
requiredBlockConfirmations: '3'
|
||||
}
|
||||
}
|
||||
]),
|
||||
methods: methodsBuilder('3')
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
requiredBlockConfirmationEvents: () => {
|
||||
return []
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 11
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const result = await getRequiredBlockConfirmations(contract, 15, snapshotProvider)
|
||||
|
||||
expect(result).toEqual(3)
|
||||
expect(contract.getPastEvents).toBeCalledTimes(1)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('RequiredBlockConfirmationChanged', {
|
||||
fromBlock: 12,
|
||||
toBlock: 15
|
||||
})
|
||||
})
|
||||
})
|
||||
describe('getRequiredSignatures', () => {
|
||||
test('Should not call to get events if block number was included in the snapshot', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => [])
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
requiredSignaturesEvents: () => {
|
||||
return [
|
||||
{
|
||||
blockNumber: 7,
|
||||
returnValues: {
|
||||
requiredSignatures: '1'
|
||||
}
|
||||
},
|
||||
{
|
||||
blockNumber: 8,
|
||||
returnValues: {
|
||||
requiredSignatures: '2'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const result = await getRequiredSignatures(contract, 10, snapshotProvider)
|
||||
|
||||
expect(result).toEqual(2)
|
||||
expect(contract.getPastEvents).toBeCalledTimes(0)
|
||||
})
|
||||
test('Should call to get events if block number is higher than the snapshot block number', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => [
|
||||
{
|
||||
blockNumber: 15,
|
||||
returnValues: {
|
||||
requiredSignatures: '3'
|
||||
}
|
||||
}
|
||||
])
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
requiredSignaturesEvents: () => {
|
||||
return [
|
||||
{
|
||||
blockNumber: 7,
|
||||
returnValues: {
|
||||
requiredSignatures: '1'
|
||||
}
|
||||
},
|
||||
{
|
||||
blockNumber: 8,
|
||||
returnValues: {
|
||||
requiredSignatures: '2'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const result = await getRequiredSignatures(contract, 20, snapshotProvider)
|
||||
|
||||
expect(result).toEqual(3)
|
||||
expect(contract.getPastEvents).toBeCalledTimes(1)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('RequiredSignaturesChanged', {
|
||||
fromBlock: 11,
|
||||
toBlock: 20
|
||||
})
|
||||
})
|
||||
test('Should use the most updated event before the block number', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => [
|
||||
{
|
||||
blockNumber: 15,
|
||||
returnValues: {
|
||||
requiredSignatures: '4'
|
||||
}
|
||||
}
|
||||
])
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
requiredSignaturesEvents: () => {
|
||||
return [
|
||||
{
|
||||
blockNumber: 5,
|
||||
returnValues: {
|
||||
requiredSignatures: '1'
|
||||
}
|
||||
},
|
||||
{
|
||||
blockNumber: 6,
|
||||
returnValues: {
|
||||
requiredSignatures: '2'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const result = await getRequiredSignatures(contract, 7, snapshotProvider)
|
||||
|
||||
expect(result).toEqual(2)
|
||||
expect(contract.getPastEvents).toBeCalledTimes(0)
|
||||
})
|
||||
})
|
||||
describe('getValidatorList', () => {
|
||||
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
|
||||
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
|
||||
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
|
||||
const methodsBuilder = (value: string[]) => ({
|
||||
validatorList: () => {
|
||||
return {
|
||||
call: () => {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
test('Should return the current validator list if no events found', async () => {
|
||||
const currentValidators = [validator1, validator2, validator3]
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => []),
|
||||
methods: methodsBuilder(currentValidators)
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
validatorAddedEvents: () => {
|
||||
return []
|
||||
},
|
||||
validatorRemovedEvents: () => {
|
||||
return []
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const list = await getValidatorList(contract, 20, snapshotProvider)
|
||||
|
||||
expect(list.length).toEqual(3)
|
||||
expect(list).toEqual(expect.arrayContaining(currentValidators))
|
||||
expect(contract.getPastEvents).toBeCalledTimes(2)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
|
||||
fromBlock: 20
|
||||
})
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
|
||||
fromBlock: 20
|
||||
})
|
||||
})
|
||||
test('If validator was added later from snapshot it should not include it', async () => {
|
||||
const currentValidators = [validator1, validator2, validator3]
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => []),
|
||||
methods: methodsBuilder(currentValidators)
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
validatorAddedEvents: () => {
|
||||
return [
|
||||
{
|
||||
blockNumber: 9,
|
||||
returnValues: {
|
||||
validator: validator3
|
||||
},
|
||||
event: 'ValidatorAdded'
|
||||
}
|
||||
]
|
||||
},
|
||||
validatorRemovedEvents: () => {
|
||||
return []
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const list = await getValidatorList(contract, 5, snapshotProvider)
|
||||
|
||||
expect(list.length).toEqual(2)
|
||||
expect(list).toEqual(expect.arrayContaining([validator1, validator2]))
|
||||
expect(contract.getPastEvents).toBeCalledTimes(2)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
|
||||
fromBlock: 11
|
||||
})
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
|
||||
fromBlock: 11
|
||||
})
|
||||
})
|
||||
test('If validator was added later from chain it should not include it', async () => {
|
||||
const currentValidators = [validator1, validator2, validator3]
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async event => {
|
||||
if (event === 'ValidatorAdded') {
|
||||
return [
|
||||
{
|
||||
blockNumber: 9,
|
||||
returnValues: {
|
||||
validator: validator3
|
||||
},
|
||||
event: 'ValidatorAdded'
|
||||
}
|
||||
]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}),
|
||||
methods: methodsBuilder(currentValidators)
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
validatorAddedEvents: () => {
|
||||
return []
|
||||
},
|
||||
validatorRemovedEvents: () => {
|
||||
return []
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const list = await getValidatorList(contract, 15, snapshotProvider)
|
||||
|
||||
expect(list.length).toEqual(2)
|
||||
expect(list).toEqual(expect.arrayContaining([validator1, validator2]))
|
||||
expect(contract.getPastEvents).toBeCalledTimes(2)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
|
||||
fromBlock: 15
|
||||
})
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
|
||||
fromBlock: 15
|
||||
})
|
||||
})
|
||||
test('If validator was removed later from snapshot it should include it', async () => {
|
||||
const currentValidators = [validator1, validator2]
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async () => []),
|
||||
methods: methodsBuilder(currentValidators)
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
validatorAddedEvents: () => {
|
||||
return []
|
||||
},
|
||||
validatorRemovedEvents: () => {
|
||||
return [
|
||||
{
|
||||
blockNumber: 9,
|
||||
returnValues: {
|
||||
validator: validator3
|
||||
},
|
||||
event: 'ValidatorRemoved'
|
||||
}
|
||||
]
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const list = await getValidatorList(contract, 5, snapshotProvider)
|
||||
|
||||
expect(list.length).toEqual(3)
|
||||
expect(list).toEqual(expect.arrayContaining([validator1, validator2, validator3]))
|
||||
expect(contract.getPastEvents).toBeCalledTimes(2)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
|
||||
fromBlock: 11
|
||||
})
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
|
||||
fromBlock: 11
|
||||
})
|
||||
})
|
||||
test('If validator was removed later from chain it should include it', async () => {
|
||||
const currentValidators = [validator1, validator2]
|
||||
const contract = ({
|
||||
getPastEvents: jest.fn().mockImplementation(async event => {
|
||||
if (event === 'ValidatorRemoved') {
|
||||
return [
|
||||
{
|
||||
blockNumber: 9,
|
||||
returnValues: {
|
||||
validator: validator3
|
||||
},
|
||||
event: 'ValidatorRemoved'
|
||||
}
|
||||
]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}),
|
||||
methods: methodsBuilder(currentValidators)
|
||||
} as unknown) as Contract
|
||||
|
||||
const snapshotProvider = ({
|
||||
validatorAddedEvents: () => {
|
||||
return []
|
||||
},
|
||||
validatorRemovedEvents: () => {
|
||||
return []
|
||||
},
|
||||
snapshotBlockNumber: () => {
|
||||
return 10
|
||||
}
|
||||
} as unknown) as SnapshotProvider
|
||||
|
||||
const list = await getValidatorList(contract, 15, snapshotProvider)
|
||||
|
||||
expect(list.length).toEqual(3)
|
||||
expect(list).toEqual(expect.arrayContaining([validator1, validator2, validator3]))
|
||||
expect(contract.getPastEvents).toBeCalledTimes(2)
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
|
||||
fromBlock: 15
|
||||
})
|
||||
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
|
||||
fromBlock: 15
|
||||
})
|
||||
})
|
||||
})
|
||||
172
alm/src/utils/__tests__/explorer.test.ts
Normal file
172
alm/src/utils/__tests__/explorer.test.ts
Normal file
@ -0,0 +1,172 @@
|
||||
import 'jest'
|
||||
import {
|
||||
getFailedTransactions,
|
||||
getSuccessTransactions,
|
||||
filterValidatorSignatureTransaction,
|
||||
getExecutionFailedTransactionForMessage,
|
||||
APITransaction,
|
||||
getValidatorPendingTransactionsForMessage,
|
||||
getExecutionPendingTransactionsForMessage
|
||||
} from '../explorer'
|
||||
import { EXECUTE_AFFIRMATION_HASH, EXECUTE_SIGNATURES_HASH, SUBMIT_SIGNATURE_HASH } from '../../config/constants'
|
||||
|
||||
const messageData = '0x123456'
|
||||
const OTHER_HASH = 'aabbccdd'
|
||||
const bridgeAddress = '0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560'
|
||||
const otherAddress = '0xD4075FB57fCf038bFc702c915Ef9592534bED5c1'
|
||||
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
|
||||
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
|
||||
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
|
||||
|
||||
describe('getFailedTransactions', () => {
|
||||
test('should only return failed transactions', async () => {
|
||||
const to = otherAddress
|
||||
const transactions = [
|
||||
{ isError: '0', to, from: validator1 },
|
||||
{ isError: '1', to, from: validator1 },
|
||||
{ isError: '0', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator3 }
|
||||
]
|
||||
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
const result = await getFailedTransactions(validator1, to, 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(1)
|
||||
})
|
||||
})
|
||||
describe('getSuccessTransactions', () => {
|
||||
test('should only return success transactions', async () => {
|
||||
const to = otherAddress
|
||||
const transactions = [
|
||||
{ isError: '0', to, from: validator1 },
|
||||
{ isError: '1', to, from: validator1 },
|
||||
{ isError: '0', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator2 },
|
||||
{ isError: '1', to, from: validator3 }
|
||||
]
|
||||
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
const result = await getSuccessTransactions(validator1, to, 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(1)
|
||||
})
|
||||
})
|
||||
describe('filterValidatorSignatureTransaction', () => {
|
||||
test('should return submit signatures related transaction', () => {
|
||||
const transactions = [
|
||||
{ input: `0x${SUBMIT_SIGNATURE_HASH}112233` },
|
||||
{ input: `0x${SUBMIT_SIGNATURE_HASH}123456` },
|
||||
{ input: `0x${OTHER_HASH}123456` },
|
||||
{ input: `0x${OTHER_HASH}112233` }
|
||||
] as APITransaction[]
|
||||
|
||||
const result = filterValidatorSignatureTransaction(transactions, messageData)
|
||||
expect(result.length).toEqual(1)
|
||||
expect(result[0]).toEqual({ input: `0x${SUBMIT_SIGNATURE_HASH}123456` })
|
||||
})
|
||||
test('should return execute affirmation related transaction', () => {
|
||||
const transactions = [
|
||||
{ input: `0x${EXECUTE_AFFIRMATION_HASH}112233` },
|
||||
{ input: `0x${EXECUTE_AFFIRMATION_HASH}123456` },
|
||||
{ input: `0x${OTHER_HASH}123456` },
|
||||
{ input: `0x${OTHER_HASH}112233` }
|
||||
] as APITransaction[]
|
||||
|
||||
const result = filterValidatorSignatureTransaction(transactions, messageData)
|
||||
expect(result.length).toEqual(1)
|
||||
expect(result[0]).toEqual({ input: `0x${EXECUTE_AFFIRMATION_HASH}123456` })
|
||||
})
|
||||
})
|
||||
describe('getExecutionFailedTransactionForMessage', () => {
|
||||
test('should return failed transaction related to signatures execution', async () => {
|
||||
const transactions = [
|
||||
{ input: `0x${EXECUTE_SIGNATURES_HASH}112233` },
|
||||
{ input: `0x${EXECUTE_SIGNATURES_HASH}123456` },
|
||||
{ input: `0x${OTHER_HASH}123456` },
|
||||
{ input: `0x${OTHER_HASH}112233` }
|
||||
] as APITransaction[]
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
|
||||
const result = await getExecutionFailedTransactionForMessage(
|
||||
{
|
||||
account: '',
|
||||
to: '',
|
||||
messageData,
|
||||
startBlock: 0,
|
||||
endBlock: 1
|
||||
},
|
||||
fetchAccountTransactions
|
||||
)
|
||||
expect(result.length).toEqual(1)
|
||||
expect(result[0]).toEqual({ input: `0x${EXECUTE_SIGNATURES_HASH}123456` })
|
||||
})
|
||||
})
|
||||
describe('getValidatorPendingTransactionsForMessage', () => {
|
||||
test('should return pending transaction for submit signature transaction', async () => {
|
||||
const transactions = [
|
||||
{ input: `0x${SUBMIT_SIGNATURE_HASH}112233`, to: bridgeAddress },
|
||||
{ input: `0x${SUBMIT_SIGNATURE_HASH}123456`, to: bridgeAddress },
|
||||
{ input: `0x${SUBMIT_SIGNATURE_HASH}123456`, to: otherAddress },
|
||||
{ input: `0x${OTHER_HASH}123456`, to: bridgeAddress },
|
||||
{ input: `0x${OTHER_HASH}112233`, to: bridgeAddress }
|
||||
] as APITransaction[]
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
|
||||
const result = await getValidatorPendingTransactionsForMessage(
|
||||
{
|
||||
account: '',
|
||||
to: bridgeAddress,
|
||||
messageData
|
||||
},
|
||||
fetchAccountTransactions
|
||||
)
|
||||
|
||||
expect(result.length).toEqual(1)
|
||||
expect(result[0]).toEqual({ input: `0x${SUBMIT_SIGNATURE_HASH}123456`, to: bridgeAddress })
|
||||
})
|
||||
test('should return pending transaction for execute affirmation transaction', async () => {
|
||||
const transactions = [
|
||||
{ input: `0x${EXECUTE_AFFIRMATION_HASH}112233`, to: bridgeAddress },
|
||||
{ input: `0x${EXECUTE_AFFIRMATION_HASH}123456`, to: bridgeAddress },
|
||||
{ input: `0x${EXECUTE_AFFIRMATION_HASH}123456`, to: otherAddress },
|
||||
{ input: `0x${OTHER_HASH}123456`, to: bridgeAddress },
|
||||
{ input: `0x${OTHER_HASH}112233`, to: bridgeAddress }
|
||||
] as APITransaction[]
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
|
||||
const result = await getValidatorPendingTransactionsForMessage(
|
||||
{
|
||||
account: '',
|
||||
to: bridgeAddress,
|
||||
messageData
|
||||
},
|
||||
fetchAccountTransactions
|
||||
)
|
||||
|
||||
expect(result.length).toEqual(1)
|
||||
expect(result[0]).toEqual({ input: `0x${EXECUTE_AFFIRMATION_HASH}123456`, to: bridgeAddress })
|
||||
})
|
||||
})
|
||||
describe('getExecutionPendingTransactionsForMessage', () => {
|
||||
test('should return pending transaction for signatures execution transaction', async () => {
|
||||
const transactions = [
|
||||
{ input: `0x${EXECUTE_SIGNATURES_HASH}112233`, to: bridgeAddress },
|
||||
{ input: `0x${EXECUTE_SIGNATURES_HASH}123456`, to: bridgeAddress },
|
||||
{ input: `0x${EXECUTE_SIGNATURES_HASH}123456`, to: otherAddress },
|
||||
{ input: `0x${OTHER_HASH}123456`, to: bridgeAddress },
|
||||
{ input: `0x${OTHER_HASH}112233`, to: bridgeAddress }
|
||||
] as APITransaction[]
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
|
||||
const result = await getExecutionPendingTransactionsForMessage(
|
||||
{
|
||||
account: '',
|
||||
to: bridgeAddress,
|
||||
messageData
|
||||
},
|
||||
fetchAccountTransactions
|
||||
)
|
||||
|
||||
expect(result.length).toEqual(1)
|
||||
expect(result[0]).toEqual({ input: `0x${EXECUTE_SIGNATURES_HASH}123456`, to: bridgeAddress })
|
||||
})
|
||||
})
|
||||
830
alm/src/utils/__tests__/getConfirmationsForTx.test.ts
Normal file
830
alm/src/utils/__tests__/getConfirmationsForTx.test.ts
Normal file
@ -0,0 +1,830 @@
|
||||
import 'jest'
|
||||
import { getConfirmationsForTx } from '../getConfirmationsForTx'
|
||||
import * as helpers from '../validatorConfirmationHelpers'
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { APIPendingTransaction, APITransaction } from '../explorer'
|
||||
import { VALIDATOR_CONFIRMATION_STATUS } from '../../config/constants'
|
||||
import { ConfirmationParam } from '../../hooks/useMessageConfirmations'
|
||||
|
||||
jest.mock('../validatorConfirmationHelpers')
|
||||
|
||||
const getSuccessExecutionTransaction = helpers.getSuccessExecutionTransaction as jest.Mock<any>
|
||||
const getValidatorConfirmation = helpers.getValidatorConfirmation as jest.Mock<any>
|
||||
const getValidatorFailedTransaction = helpers.getValidatorFailedTransaction as jest.Mock<any>
|
||||
const getValidatorPendingTransaction = helpers.getValidatorPendingTransaction as jest.Mock<any>
|
||||
|
||||
const messageData = '0x111111111'
|
||||
const web3 = {
|
||||
utils: {
|
||||
soliditySha3Raw: (data: string) => `0xaaaa${data.replace('0x', '')}`
|
||||
},
|
||||
eth: {
|
||||
accounts: new Web3().eth.accounts
|
||||
}
|
||||
} as Web3
|
||||
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
|
||||
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
|
||||
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
|
||||
const validatorList = [validator1, validator2, validator3]
|
||||
const signature =
|
||||
'0x6f5b74905669999f1abdb52e1e215506907e1849aac7b31854da458b33a5954e15b165007c3703cfd16e61ca46a96a56727ed11fa47be359d3834515accd016e1b'
|
||||
const bridgeContract = {
|
||||
methods: {
|
||||
signature: () => ({
|
||||
call: () => signature
|
||||
})
|
||||
}
|
||||
} as Contract
|
||||
const requiredSignatures = 2
|
||||
const isCancelled = () => false
|
||||
let subscriptions: Array<number> = []
|
||||
const timestamp = 1594045859
|
||||
const getFailedTransactions = (): Promise<APITransaction[]> => Promise.resolve([])
|
||||
const getPendingTransactions = (): Promise<APIPendingTransaction[]> => Promise.resolve([])
|
||||
const getSuccessTransactions = (): Promise<APITransaction[]> => Promise.resolve([])
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear all instances and calls to constructor and all methods:
|
||||
getSuccessExecutionTransaction.mockClear()
|
||||
getValidatorConfirmation.mockClear()
|
||||
getValidatorFailedTransaction.mockClear()
|
||||
getValidatorPendingTransaction.mockClear()
|
||||
subscriptions = []
|
||||
})
|
||||
describe('getConfirmationsForTx', () => {
|
||||
test('should set validator confirmations status when signatures collected even if validator transactions not found yet and set remaining validator as not required', async () => {
|
||||
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
|
||||
const setResult = jest.fn()
|
||||
const setSignatureCollected = jest.fn()
|
||||
const setFailedConfirmations = jest.fn()
|
||||
const setPendingConfirmations = jest.fn()
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(1)
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
const res1 = setResult.mock.calls[0][0]()
|
||||
const res2 = setResult.mock.calls[1][0](res1)
|
||||
expect(res1).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
|
||||
])
|
||||
)
|
||||
})
|
||||
test('should set validator confirmations status when signatures not collected even if validator transactions not found yet', async () => {
|
||||
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status: validator === validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
|
||||
const setResult = jest.fn()
|
||||
const setSignatureCollected = jest.fn()
|
||||
const setFailedConfirmations = jest.fn()
|
||||
const setPendingConfirmations = jest.fn()
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(1)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
})
|
||||
test('should set validator confirmations status, validator transactions and not retry', async () => {
|
||||
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator !== validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator3 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
|
||||
const setResult = jest.fn()
|
||||
const setSignatureCollected = jest.fn()
|
||||
const setFailedConfirmations = jest.fn()
|
||||
const setPendingConfirmations = jest.fn()
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(3)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
const res1 = setResult.mock.calls[0][0]()
|
||||
const res2 = setResult.mock.calls[1][0](res1)
|
||||
const res3 = setResult.mock.calls[2][0](res2)
|
||||
expect(res1).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
|
||||
])
|
||||
)
|
||||
})
|
||||
test('should set validator confirmations status, validator transactions, keep failed found transaction and not retry', async () => {
|
||||
const validator4 = '0x9d2dC11C342F4eF3C5491A048D0f0eBCd2D8f7C3'
|
||||
const validatorList = [validator1, validator2, validator3, validator4]
|
||||
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status:
|
||||
validator !== validator3 && validator !== validator4
|
||||
? VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator3 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
|
||||
const setResult = jest.fn()
|
||||
const setSignatureCollected = jest.fn()
|
||||
const setFailedConfirmations = jest.fn()
|
||||
const setPendingConfirmations = jest.fn()
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(4)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
const res1 = setResult.mock.calls[0][0]()
|
||||
const res2 = setResult.mock.calls[1][0](res1)
|
||||
const res3 = setResult.mock.calls[2][0](res2)
|
||||
const res4 = setResult.mock.calls[3][0](res3)
|
||||
expect(res1).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res4).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
|
||||
])
|
||||
)
|
||||
})
|
||||
test('should look for failed and pending transactions for not confirmed validators', async () => {
|
||||
// Validator1 success
|
||||
// Validator2 failed
|
||||
// Validator3 Pending
|
||||
|
||||
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator2 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator3 ? 123 : 0
|
||||
}))
|
||||
|
||||
const setResult = jest.fn()
|
||||
const setSignatureCollected = jest.fn()
|
||||
const setFailedConfirmations = jest.fn()
|
||||
const setPendingConfirmations = jest.fn()
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(4)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
|
||||
const res1 = setResult.mock.calls[0][0]()
|
||||
const res2 = setResult.mock.calls[1][0](res1)
|
||||
const res3 = setResult.mock.calls[2][0](res2)
|
||||
const res4 = setResult.mock.calls[3][0](res3)
|
||||
expect(res1).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(res4).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
})
|
||||
test('should set as failed if enough signatures failed', async () => {
|
||||
// Validator1 success
|
||||
// Validator2 failed
|
||||
// Validator3 failed
|
||||
|
||||
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator !== validator1
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator !== validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
|
||||
const setResult = jest.fn()
|
||||
const setSignatureCollected = jest.fn()
|
||||
const setFailedConfirmations = jest.fn()
|
||||
const setPendingConfirmations = jest.fn()
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(3)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
const res1 = setResult.mock.calls[0][0]()
|
||||
const res2 = setResult.mock.calls[1][0](res1)
|
||||
const res3 = setResult.mock.calls[2][0](res2)
|
||||
expect(res1).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
})
|
||||
test('should remove pending state after transaction mined', async () => {
|
||||
const validator4 = '0x9d2dC11C342F4eF3C5491A048D0f0eBCd2D8f7C3'
|
||||
const validatorList = [validator1, validator2, validator3, validator4]
|
||||
|
||||
// Validator1 success (ts=100)
|
||||
// Validator2 failed (ts=200)
|
||||
// Validator3 Pending (ts=300)
|
||||
// Validator4 Excess confirmation (Failed) (ts=400)
|
||||
|
||||
getValidatorConfirmation
|
||||
.mockImplementationOnce(() => async (validator: string) => ({
|
||||
validator,
|
||||
status:
|
||||
validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status:
|
||||
validator === validator1 || validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getSuccessExecutionTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x100' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 100 : 0
|
||||
}))
|
||||
.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash:
|
||||
validatorData.validator === validator1 ? '0x100' : validatorData.validator === validator3 ? '0x300' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 100 : validatorData.validator === validator3 ? 300 : ''
|
||||
}))
|
||||
getValidatorFailedTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator2 ? '0x200' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 200 : 0
|
||||
}))
|
||||
.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2 || validatorData.validator === validator4
|
||||
? validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash:
|
||||
validatorData.validator === validator2 ? '0x200' : validatorData.validator === validator4 ? '0x400' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 200 : validatorData.validator === validator4 ? 400 : ''
|
||||
}))
|
||||
getValidatorPendingTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator3 ? '0x300' : '',
|
||||
timestamp: validatorData.validator === validator3 ? 300 : 0
|
||||
}))
|
||||
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
|
||||
const setResult = jest.fn()
|
||||
const setSignatureCollected = jest.fn()
|
||||
const setFailedConfirmations = jest.fn()
|
||||
const setPendingConfirmations = jest.fn()
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(4)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected).toBeCalledTimes(1)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(1)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
|
||||
const res1 = setResult.mock.calls[0][0]()
|
||||
const res2 = setResult.mock.calls[1][0](res1)
|
||||
const res3 = setResult.mock.calls[2][0](res2)
|
||||
const res4 = setResult.mock.calls[3][0](res3)
|
||||
expect(res1).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res2).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res3).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res4).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
true,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(7)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(2)
|
||||
expect(getSuccessExecutionTransaction).toBeCalledTimes(2)
|
||||
expect(setSignatureCollected).toBeCalledTimes(2)
|
||||
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
|
||||
expect(setSignatureCollected.mock.calls[1][0]).toEqual(true)
|
||||
|
||||
expect(getValidatorFailedTransaction).toBeCalledTimes(2)
|
||||
expect(setFailedConfirmations).toBeCalledTimes(2)
|
||||
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
expect(setFailedConfirmations.mock.calls[1][0]).toEqual(false)
|
||||
|
||||
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
|
||||
expect(setPendingConfirmations).toBeCalledTimes(2)
|
||||
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
|
||||
expect(setPendingConfirmations.mock.calls[1][0]).toEqual(false)
|
||||
|
||||
const res5 = setResult.mock.calls[4][0](res4)
|
||||
const res6 = setResult.mock.calls[5][0](res5)
|
||||
const res7 = setResult.mock.calls[6][0](res6)
|
||||
expect(res5).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res6).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
|
||||
])
|
||||
)
|
||||
expect(res7).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x400', timestamp: 400 }
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
309
alm/src/utils/__tests__/getFinalizationEvent.test.ts
Normal file
309
alm/src/utils/__tests__/getFinalizationEvent.test.ts
Normal file
@ -0,0 +1,309 @@
|
||||
import 'jest'
|
||||
import { Contract, EventData } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
import { getFinalizationEvent } from '../getFinalizationEvent'
|
||||
import { VALIDATOR_CONFIRMATION_STATUS } from '../../config/constants'
|
||||
|
||||
const timestamp = 1594045859
|
||||
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
|
||||
const txHash = '0xdab36c9210e7e45fb82af10ffe4960461e41661dce0c9cd36b2843adaa1df156'
|
||||
|
||||
const web3 = ({
|
||||
eth: {
|
||||
getTransactionReceipt: () => ({
|
||||
from: validator1
|
||||
}),
|
||||
getBlock: () => ({ timestamp })
|
||||
},
|
||||
utils: {
|
||||
toChecksumAddress: (a: string) => a
|
||||
}
|
||||
} as unknown) as Web3
|
||||
const message = {
|
||||
id: '0x123',
|
||||
data: '0x123456789'
|
||||
}
|
||||
const isCancelled = () => false
|
||||
let subscriptions: Array<number> = []
|
||||
|
||||
const event = {
|
||||
transactionHash: txHash,
|
||||
blockNumber: 5523145,
|
||||
returnValues: {
|
||||
status: true
|
||||
}
|
||||
}
|
||||
|
||||
const bridgeAddress = '0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560'
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
subscriptions = []
|
||||
})
|
||||
describe('getFinalizationEvent', () => {
|
||||
test('should get finalization event and not try to get failed or pending transactions', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: async () => {
|
||||
return [event]
|
||||
}
|
||||
} as unknown) as Contract
|
||||
|
||||
const collectedSignaturesEvent = null
|
||||
const setResult = jest.fn()
|
||||
const getFailedExecution = jest.fn()
|
||||
const setFailedExecution = jest.fn()
|
||||
const getPendingExecution = jest.fn()
|
||||
const setPendingExecution = jest.fn()
|
||||
const setExecutionEventsFetched = jest.fn()
|
||||
|
||||
await getFinalizationEvent(
|
||||
true,
|
||||
contract,
|
||||
web3,
|
||||
setResult,
|
||||
message,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution,
|
||||
setExecutionEventsFetched
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(1)
|
||||
expect(setResult.mock.calls[0][0]).toEqual({
|
||||
validator: validator1,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS,
|
||||
txHash,
|
||||
timestamp,
|
||||
executionResult: true,
|
||||
blockNumber: 5523145
|
||||
})
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(0)
|
||||
expect(setFailedExecution).toBeCalledTimes(0)
|
||||
|
||||
expect(getPendingExecution).toBeCalledTimes(0)
|
||||
expect(setPendingExecution).toBeCalledTimes(0)
|
||||
})
|
||||
test('should retry to get finalization event and not try to get failed or pending transactions if foreign to home transaction', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: async () => {
|
||||
return []
|
||||
}
|
||||
} as unknown) as Contract
|
||||
|
||||
const collectedSignaturesEvent = null
|
||||
const setResult = jest.fn()
|
||||
const getFailedExecution = jest.fn()
|
||||
const setFailedExecution = jest.fn()
|
||||
const getPendingExecution = jest.fn()
|
||||
const setPendingExecution = jest.fn()
|
||||
const setExecutionEventsFetched = jest.fn()
|
||||
|
||||
await getFinalizationEvent(
|
||||
true,
|
||||
contract,
|
||||
web3,
|
||||
setResult,
|
||||
message,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution,
|
||||
setExecutionEventsFetched
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(1)
|
||||
expect(setResult).toBeCalledTimes(0)
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(0)
|
||||
expect(setFailedExecution).toBeCalledTimes(0)
|
||||
|
||||
expect(getPendingExecution).toBeCalledTimes(0)
|
||||
expect(setPendingExecution).toBeCalledTimes(0)
|
||||
})
|
||||
test('should retry to get finalization event and try to get failed and pending transactions if home to foreign transaction', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: async () => {
|
||||
return []
|
||||
},
|
||||
options: {
|
||||
address: bridgeAddress
|
||||
}
|
||||
} as unknown) as Contract
|
||||
|
||||
const collectedSignaturesEvent = ({
|
||||
returnValues: {
|
||||
authorityResponsibleForRelay: validator1
|
||||
}
|
||||
} as unknown) as EventData
|
||||
const setResult = jest.fn()
|
||||
const getFailedExecution = jest.fn().mockResolvedValue([])
|
||||
const setFailedExecution = jest.fn()
|
||||
const getPendingExecution = jest.fn().mockResolvedValue([])
|
||||
const setPendingExecution = jest.fn()
|
||||
const setExecutionEventsFetched = jest.fn()
|
||||
|
||||
await getFinalizationEvent(
|
||||
true,
|
||||
contract,
|
||||
web3,
|
||||
setResult,
|
||||
message,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution,
|
||||
setExecutionEventsFetched
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(1)
|
||||
expect(setResult).toBeCalledTimes(0)
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(1)
|
||||
expect(setFailedExecution).toBeCalledTimes(0)
|
||||
|
||||
expect(getPendingExecution).toBeCalledTimes(1)
|
||||
expect(setPendingExecution).toBeCalledTimes(0)
|
||||
})
|
||||
test('should retry to get finalization event and not to try to get failed transaction if pending transactions found if home to foreign transaction', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: async () => {
|
||||
return []
|
||||
},
|
||||
options: {
|
||||
address: bridgeAddress
|
||||
}
|
||||
} as unknown) as Contract
|
||||
|
||||
const collectedSignaturesEvent = ({
|
||||
returnValues: {
|
||||
authorityResponsibleForRelay: validator1
|
||||
}
|
||||
} as unknown) as EventData
|
||||
const setResult = jest.fn()
|
||||
const getFailedExecution = jest.fn().mockResolvedValue([])
|
||||
const setFailedExecution = jest.fn()
|
||||
const getPendingExecution = jest.fn().mockResolvedValue([{ hash: txHash }])
|
||||
const setPendingExecution = jest.fn()
|
||||
const setExecutionEventsFetched = jest.fn()
|
||||
|
||||
await getFinalizationEvent(
|
||||
true,
|
||||
contract,
|
||||
web3,
|
||||
setResult,
|
||||
message,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution,
|
||||
setExecutionEventsFetched
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(1)
|
||||
expect(setResult).toBeCalledTimes(1)
|
||||
expect(setResult.mock.calls[0][0]).toEqual({
|
||||
validator: validator1,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
|
||||
txHash,
|
||||
timestamp: expect.any(Number),
|
||||
executionResult: false,
|
||||
blockNumber: 0
|
||||
})
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(0)
|
||||
expect(setFailedExecution).toBeCalledTimes(0)
|
||||
|
||||
expect(getPendingExecution).toBeCalledTimes(1)
|
||||
expect(setPendingExecution).toBeCalledTimes(1)
|
||||
})
|
||||
test('should retry to get finalization event even if failed transaction found if home to foreign transaction', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: async () => {
|
||||
return []
|
||||
},
|
||||
options: {
|
||||
address: bridgeAddress
|
||||
}
|
||||
} as unknown) as Contract
|
||||
|
||||
const collectedSignaturesEvent = ({
|
||||
returnValues: {
|
||||
authorityResponsibleForRelay: validator1
|
||||
}
|
||||
} as unknown) as EventData
|
||||
const setResult = jest.fn()
|
||||
const getFailedExecution = jest.fn().mockResolvedValue([{ timeStamp: timestamp, hash: txHash }])
|
||||
const setFailedExecution = jest.fn()
|
||||
const getPendingExecution = jest.fn().mockResolvedValue([])
|
||||
const setPendingExecution = jest.fn()
|
||||
const setExecutionEventsFetched = jest.fn()
|
||||
|
||||
await getFinalizationEvent(
|
||||
true,
|
||||
contract,
|
||||
web3,
|
||||
setResult,
|
||||
message,
|
||||
subscriptions.push.bind(subscriptions),
|
||||
isCancelled,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution,
|
||||
setExecutionEventsFetched
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(1)
|
||||
expect(setResult).toBeCalledTimes(1)
|
||||
expect(setResult.mock.calls[0][0]).toEqual({
|
||||
validator: validator1,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
|
||||
txHash,
|
||||
timestamp: expect.any(Number),
|
||||
executionResult: false,
|
||||
blockNumber: expect.any(Number)
|
||||
})
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(1)
|
||||
expect(setFailedExecution).toBeCalledTimes(1)
|
||||
|
||||
expect(getPendingExecution).toBeCalledTimes(1)
|
||||
expect(setPendingExecution).toBeCalledTimes(0)
|
||||
})
|
||||
})
|
||||
150
alm/src/utils/contract.ts
Normal file
150
alm/src/utils/contract.ts
Normal file
@ -0,0 +1,150 @@
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { EventData } from 'web3-eth-contract'
|
||||
import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
import { getLogs } from './explorer'
|
||||
import Web3 from 'web3'
|
||||
|
||||
const getPastEventsWithFallback = (
|
||||
api: string,
|
||||
web3: Web3 | null,
|
||||
contract: Contract,
|
||||
eventName: string,
|
||||
options: any
|
||||
) =>
|
||||
contract
|
||||
.getPastEvents(eventName, options)
|
||||
.catch(() => (api && web3 ? getLogs(api, web3, contract, eventName, options) : []))
|
||||
|
||||
export const getRequiredBlockConfirmations = async (
|
||||
contract: Contract,
|
||||
blockNumber: number,
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3 | null = null,
|
||||
api: string = ''
|
||||
) => {
|
||||
let blockConfirmations
|
||||
|
||||
try {
|
||||
blockConfirmations = await contract.methods.requiredBlockConfirmations().call()
|
||||
} catch {}
|
||||
|
||||
if (blockConfirmations) {
|
||||
return parseInt(blockConfirmations)
|
||||
}
|
||||
|
||||
const eventsFromSnapshot = snapshotProvider.requiredBlockConfirmationEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
||||
let contractEvents: EventData[] = []
|
||||
if (blockNumber > snapshotBlockNumber) {
|
||||
contractEvents = await getPastEventsWithFallback(api, web3, contract, 'RequiredBlockConfirmationChanged', {
|
||||
fromBlock: snapshotBlockNumber + 1,
|
||||
toBlock: blockNumber
|
||||
})
|
||||
}
|
||||
|
||||
const events = [...eventsFromSnapshot, ...contractEvents]
|
||||
|
||||
// Use the value from last event before the transaction
|
||||
const event = events[events.length - 1]
|
||||
blockConfirmations = event.returnValues.requiredBlockConfirmations
|
||||
|
||||
return parseInt(blockConfirmations)
|
||||
}
|
||||
|
||||
export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call()
|
||||
|
||||
export const getRequiredSignatures = async (
|
||||
contract: Contract,
|
||||
blockNumber: number | 'latest',
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3 | null = null,
|
||||
api: string = ''
|
||||
) => {
|
||||
let requiredSignatures
|
||||
|
||||
try {
|
||||
requiredSignatures = await contract.methods.requiredSignatures().call()
|
||||
} catch {}
|
||||
|
||||
if (requiredSignatures) {
|
||||
return parseInt(requiredSignatures)
|
||||
}
|
||||
|
||||
if (blockNumber === 'latest') {
|
||||
return contract.methods.requiredSignatures().call()
|
||||
}
|
||||
|
||||
const eventsFromSnapshot = snapshotProvider.requiredSignaturesEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
||||
let contractEvents: EventData[] = []
|
||||
if (blockNumber > snapshotBlockNumber) {
|
||||
contractEvents = await getPastEventsWithFallback(api, web3, contract, 'RequiredSignaturesChanged', {
|
||||
fromBlock: snapshotBlockNumber + 1,
|
||||
toBlock: blockNumber
|
||||
})
|
||||
}
|
||||
|
||||
const events = [...eventsFromSnapshot, ...contractEvents]
|
||||
|
||||
// Use the value form last event before the transaction
|
||||
const event = events[events.length - 1]
|
||||
;({ requiredSignatures } = event.returnValues)
|
||||
return parseInt(requiredSignatures)
|
||||
}
|
||||
|
||||
export const getValidatorList = async (
|
||||
contract: Contract,
|
||||
blockNumber: number | 'latest',
|
||||
snapshotProvider: SnapshotProvider,
|
||||
web3: Web3 | null = null,
|
||||
api: string = ''
|
||||
) => {
|
||||
try {
|
||||
const currentList = await contract.methods.validatorList().call()
|
||||
|
||||
if (currentList) {
|
||||
return currentList
|
||||
}
|
||||
} catch {}
|
||||
|
||||
const addedEventsFromSnapshot = snapshotProvider.validatorAddedEvents(blockNumber)
|
||||
const removedEventsFromSnapshot = snapshotProvider.validatorRemovedEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
||||
const fromBlock = snapshotBlockNumber > blockNumber ? snapshotBlockNumber + 1 : blockNumber
|
||||
const [currentList, added, removed] = await Promise.all([
|
||||
contract.methods.validatorList().call(),
|
||||
getPastEventsWithFallback(api, web3, contract, 'ValidatorAdded', {
|
||||
fromBlock
|
||||
}),
|
||||
getPastEventsWithFallback(api, web3, contract, 'ValidatorRemoved', {
|
||||
fromBlock
|
||||
})
|
||||
])
|
||||
|
||||
// Ordered desc
|
||||
const orderedEvents = [...addedEventsFromSnapshot, ...added, ...removedEventsFromSnapshot, ...removed].sort(
|
||||
({ blockNumber: prev }, { blockNumber: next }) => next - prev
|
||||
)
|
||||
|
||||
// Stored as a Set to avoid duplicates
|
||||
const validatorList = new Set(currentList)
|
||||
|
||||
orderedEvents.forEach(e => {
|
||||
const { validator } = e.returnValues
|
||||
if (e.event === 'ValidatorRemoved') {
|
||||
validatorList.add(validator)
|
||||
} else if (e.event === 'ValidatorAdded') {
|
||||
validatorList.delete(validator)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(validatorList)
|
||||
}
|
||||
|
||||
export const getMessagesSigned = (contract: Contract, hash: string) => contract.methods.messagesSigned(hash).call()
|
||||
|
||||
export const getAffirmationsSigned = (contract: Contract, hash: string) =>
|
||||
contract.methods.affirmationsSigned(hash).call()
|
||||
312
alm/src/utils/explorer.ts
Normal file
312
alm/src/utils/explorer.ts
Normal file
@ -0,0 +1,312 @@
|
||||
import {
|
||||
BLOCK_RANGE,
|
||||
EXECUTE_AFFIRMATION_HASH,
|
||||
EXECUTE_SIGNATURES_HASH,
|
||||
FOREIGN_EXPLORER_API,
|
||||
HOME_EXPLORER_API,
|
||||
MAX_TX_SEARCH_BLOCK_RANGE,
|
||||
SUBMIT_SIGNATURE_HASH
|
||||
} from '../config/constants'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
|
||||
export interface APITransaction {
|
||||
from: string
|
||||
timeStamp: string
|
||||
isError: string
|
||||
input: string
|
||||
to: string
|
||||
hash: string
|
||||
blockNumber: string
|
||||
}
|
||||
|
||||
export interface APIPendingTransaction {
|
||||
input: string
|
||||
to: string
|
||||
hash: string
|
||||
}
|
||||
|
||||
export interface PendingTransactionsParams {
|
||||
account: string
|
||||
api: string
|
||||
}
|
||||
|
||||
export interface AccountTransactionsParams {
|
||||
account: string
|
||||
startBlock: number
|
||||
endBlock: number
|
||||
api: string
|
||||
}
|
||||
|
||||
export interface GetPendingTransactionParams {
|
||||
account: string
|
||||
to: string
|
||||
messageData: string
|
||||
}
|
||||
|
||||
export interface GetTransactionParams extends GetPendingTransactionParams {
|
||||
startBlock: number
|
||||
endBlock: number
|
||||
}
|
||||
|
||||
export const fetchAccountTransactions = async ({ account, startBlock, endBlock, api }: AccountTransactionsParams) => {
|
||||
const url = new URL(api)
|
||||
url.searchParams.append('module', 'account')
|
||||
url.searchParams.append('action', 'txlist')
|
||||
url.searchParams.append('address', account)
|
||||
url.searchParams.append('filterby', 'to')
|
||||
url.searchParams.append('startblock', startBlock.toString())
|
||||
url.searchParams.append('endblock', endBlock.toString())
|
||||
|
||||
const result = await fetch(url.toString()).then(res => res.json())
|
||||
|
||||
if (result.message === 'No transactions found') {
|
||||
return []
|
||||
}
|
||||
|
||||
return result.result || []
|
||||
}
|
||||
|
||||
export const fetchPendingTransactions = async ({
|
||||
account,
|
||||
api
|
||||
}: PendingTransactionsParams): Promise<APIPendingTransaction[]> => {
|
||||
if (!api.includes('blockscout')) {
|
||||
return []
|
||||
}
|
||||
const url = new URL(api)
|
||||
url.searchParams.append('module', 'account')
|
||||
url.searchParams.append('action', 'pendingtxlist')
|
||||
url.searchParams.append('address', account)
|
||||
|
||||
try {
|
||||
const result = await fetch(url.toString()).then(res => res.json())
|
||||
if (result.status === '0') {
|
||||
return []
|
||||
}
|
||||
|
||||
return result.result
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const getClosestBlockByTimestamp = async (api: string, timestamp: number): Promise<number> => {
|
||||
if (api.includes('blockscout')) {
|
||||
throw new Error('Blockscout does not support getblocknobytime')
|
||||
}
|
||||
|
||||
const url = new URL(api)
|
||||
url.searchParams.append('module', 'block')
|
||||
url.searchParams.append('action', 'getblocknobytime')
|
||||
url.searchParams.append('timestamp', timestamp.toString())
|
||||
url.searchParams.append('closest', 'before')
|
||||
|
||||
const blockNumber = await fetch(url.toString()).then(res => res.json())
|
||||
|
||||
return parseInt(blockNumber.result)
|
||||
}
|
||||
|
||||
// fast version of fetchAccountTransactions
|
||||
// sequentially fetches transactions in small batches
|
||||
// caches the result
|
||||
const transactionsCache: { [key: string]: { lastBlock: number; transactions: APITransaction[] } } = {}
|
||||
export const getAccountTransactions = async ({
|
||||
account,
|
||||
startBlock,
|
||||
endBlock,
|
||||
api
|
||||
}: AccountTransactionsParams): Promise<APITransaction[]> => {
|
||||
const key = `${account}-${startBlock}-${api}`
|
||||
|
||||
// initialize empty cache if it doesn't exist yet
|
||||
if (!transactionsCache[key]) {
|
||||
transactionsCache[key] = { lastBlock: startBlock - 1, transactions: [] }
|
||||
}
|
||||
|
||||
// if cache contains events up to block X,
|
||||
// new batch is fetched for range [X + 1, X + 1 + BLOCK_RANGE]
|
||||
const newStartBlock = transactionsCache[key].lastBlock + 1
|
||||
const newEndBlock = newStartBlock + BLOCK_RANGE
|
||||
|
||||
// search for new transactions only if max allowed block range is not yet exceeded
|
||||
if (newEndBlock <= startBlock + MAX_TX_SEARCH_BLOCK_RANGE) {
|
||||
const newTransactions = await fetchAccountTransactions({
|
||||
account,
|
||||
startBlock: newStartBlock,
|
||||
endBlock: newEndBlock,
|
||||
api
|
||||
})
|
||||
|
||||
const transactions = transactionsCache[key].transactions.concat(...newTransactions)
|
||||
|
||||
// cache updated transactions list
|
||||
transactionsCache[key].transactions = transactions
|
||||
|
||||
// enbBlock is assumed to be the current block number of the chain
|
||||
// if the whole range is finalized, last block can be safely updated to the end of the range
|
||||
// this works even if there are no transactions in the list
|
||||
if (newEndBlock < endBlock) {
|
||||
transactionsCache[key].lastBlock = newEndBlock
|
||||
} else if (transactions.length > 0) {
|
||||
transactionsCache[key].lastBlock = parseInt(transactions[transactions.length - 1].blockNumber, 10)
|
||||
}
|
||||
|
||||
return transactions
|
||||
}
|
||||
|
||||
console.warn(`Reached max transaction searching range, returning previously cached transactions for ${account}`)
|
||||
return transactionsCache[key].transactions
|
||||
}
|
||||
|
||||
export const getLogs = async (
|
||||
api: string,
|
||||
web3: Web3,
|
||||
contract: Contract,
|
||||
event: string,
|
||||
options: { fromBlock: number; toBlock: number | 'latest'; topics: (string | null)[] }
|
||||
) => {
|
||||
const abi = contract.options.jsonInterface.find((abi: AbiItem) => abi.type === 'event' && abi.name === event)!
|
||||
|
||||
const url = new URL(api)
|
||||
url.searchParams.append('module', 'logs')
|
||||
url.searchParams.append('action', 'getLogs')
|
||||
url.searchParams.append('address', contract.options.address)
|
||||
url.searchParams.append('fromBlock', options.fromBlock.toString())
|
||||
url.searchParams.append('toBlock', (options.toBlock || 'latest').toString())
|
||||
|
||||
const topics = [web3.eth.abi.encodeEventSignature(abi), ...(options.topics || [])]
|
||||
for (let i = 0; i < topics.length; i++) {
|
||||
if (topics[i] !== null) {
|
||||
url.searchParams.append(`topic${i}`, topics[i] as string)
|
||||
for (let j = 0; j < i; j++) {
|
||||
if (topics[j] !== null) {
|
||||
url.searchParams.append(`topic${j}_${i}_opr`, 'and')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const logs = await fetch(url.toString()).then(res => res.json())
|
||||
|
||||
return logs.result.map((log: any) => ({
|
||||
transactionHash: log.transactionHash,
|
||||
blockNumber: parseInt(log.blockNumber.slice(2), 16),
|
||||
returnValues: web3.eth.abi.decodeLog(abi.inputs!, log.data, log.topics.slice(1))
|
||||
}))
|
||||
}
|
||||
|
||||
const filterSender = (from: string) => (tx: APITransaction) => tx.from.toLowerCase() === from.toLowerCase()
|
||||
|
||||
export const getFailedTransactions = async (
|
||||
account: string,
|
||||
to: string,
|
||||
startBlock: number,
|
||||
endBlock: number,
|
||||
api: string,
|
||||
getAccountTransactionsMethod = getAccountTransactions
|
||||
): Promise<APITransaction[]> => {
|
||||
const transactions = await getAccountTransactionsMethod({ account: to, startBlock, endBlock, api })
|
||||
|
||||
return transactions.filter(t => t.isError !== '0').filter(filterSender(account))
|
||||
}
|
||||
|
||||
export const getSuccessTransactions = async (
|
||||
account: string,
|
||||
to: string,
|
||||
startBlock: number,
|
||||
endBlock: number,
|
||||
api: string,
|
||||
getAccountTransactionsMethod = getAccountTransactions
|
||||
): Promise<APITransaction[]> => {
|
||||
const transactions = await getAccountTransactionsMethod({ account: to, startBlock, endBlock, api })
|
||||
|
||||
return transactions.filter(t => t.isError === '0').filter(filterSender(account))
|
||||
}
|
||||
|
||||
export const filterValidatorSignatureTransaction = (
|
||||
transactions: APITransaction[],
|
||||
messageData: string
|
||||
): APITransaction[] => {
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
return transactions.filter(
|
||||
t =>
|
||||
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
|
||||
t.input.includes(messageDataValue)
|
||||
)
|
||||
}
|
||||
|
||||
export const getValidatorFailedTransactionsForMessage = async ({
|
||||
account,
|
||||
to,
|
||||
messageData,
|
||||
startBlock,
|
||||
endBlock
|
||||
}: GetTransactionParams): Promise<APITransaction[]> => {
|
||||
const failedTransactions = await getFailedTransactions(account, to, startBlock, endBlock, HOME_EXPLORER_API)
|
||||
|
||||
return filterValidatorSignatureTransaction(failedTransactions, messageData)
|
||||
}
|
||||
|
||||
export const getValidatorSuccessTransactionsForMessage = async ({
|
||||
account,
|
||||
to,
|
||||
messageData,
|
||||
startBlock,
|
||||
endBlock
|
||||
}: GetTransactionParams): Promise<APITransaction[]> => {
|
||||
const transactions = await getSuccessTransactions(account, to, startBlock, endBlock, HOME_EXPLORER_API)
|
||||
|
||||
return filterValidatorSignatureTransaction(transactions, messageData)
|
||||
}
|
||||
|
||||
export const getExecutionFailedTransactionForMessage = async (
|
||||
{ account, to, messageData, startBlock, endBlock }: GetTransactionParams,
|
||||
getFailedTransactionsMethod = getFailedTransactions
|
||||
): Promise<APITransaction[]> => {
|
||||
const failedTransactions = await getFailedTransactionsMethod(account, to, startBlock, endBlock, FOREIGN_EXPLORER_API)
|
||||
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
return failedTransactions.filter(t => t.input.includes(EXECUTE_SIGNATURES_HASH) && t.input.includes(messageDataValue))
|
||||
}
|
||||
|
||||
export const getValidatorPendingTransactionsForMessage = async (
|
||||
{ account, to, messageData }: GetPendingTransactionParams,
|
||||
fetchPendingTransactionsMethod = fetchPendingTransactions
|
||||
): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactionsMethod({
|
||||
account,
|
||||
api: HOME_EXPLORER_API
|
||||
})
|
||||
|
||||
const toAddressLowerCase = to.toLowerCase()
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
|
||||
return pendingTransactions.filter(
|
||||
t =>
|
||||
t.to.toLowerCase() === toAddressLowerCase &&
|
||||
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
|
||||
t.input.includes(messageDataValue)
|
||||
)
|
||||
}
|
||||
|
||||
export const getExecutionPendingTransactionsForMessage = async (
|
||||
{ account, to, messageData }: GetPendingTransactionParams,
|
||||
fetchPendingTransactionsMethod = fetchPendingTransactions
|
||||
): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactionsMethod({
|
||||
account,
|
||||
api: FOREIGN_EXPLORER_API
|
||||
})
|
||||
|
||||
const toAddressLowerCase = to.toLowerCase()
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
|
||||
return pendingTransactions.filter(
|
||||
t =>
|
||||
t.to.toLowerCase() === toAddressLowerCase &&
|
||||
t.input.includes(EXECUTE_SIGNATURES_HASH) &&
|
||||
t.input.includes(messageDataValue)
|
||||
)
|
||||
}
|
||||
201
alm/src/utils/getConfirmationsForTx.ts
Normal file
201
alm/src/utils/getConfirmationsForTx.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { HOME_RPC_POLLING_INTERVAL, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { GetTransactionParams, APITransaction, APIPendingTransaction, GetPendingTransactionParams } from './explorer'
|
||||
import {
|
||||
getValidatorConfirmation,
|
||||
getValidatorFailedTransaction,
|
||||
getValidatorPendingTransaction,
|
||||
getSuccessExecutionTransaction
|
||||
} from './validatorConfirmationHelpers'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
import { signatureToVRS } from './signatures'
|
||||
|
||||
const mergeConfirmations = (oldConfirmations: ConfirmationParam[], newConfirmations: ConfirmationParam[]) => {
|
||||
const confirmations = [...oldConfirmations]
|
||||
newConfirmations.forEach(validatorData => {
|
||||
const index = confirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
if (index === -1) {
|
||||
confirmations.push(validatorData)
|
||||
return
|
||||
}
|
||||
const currentStatus = confirmations[index].status
|
||||
const newStatus = validatorData.status
|
||||
if (
|
||||
validatorData.txHash ||
|
||||
!!validatorData.signature ||
|
||||
(newStatus !== currentStatus && newStatus !== VALIDATOR_CONFIRMATION_STATUS.UNDEFINED)
|
||||
) {
|
||||
confirmations[index] = {
|
||||
status: validatorData.status,
|
||||
validator: validatorData.validator,
|
||||
timestamp: confirmations[index].timestamp || validatorData.timestamp,
|
||||
txHash: confirmations[index].txHash || validatorData.txHash,
|
||||
signature: confirmations[index].signature || validatorData.signature
|
||||
}
|
||||
}
|
||||
})
|
||||
return confirmations
|
||||
}
|
||||
|
||||
export const getConfirmationsForTx = async (
|
||||
messageData: string,
|
||||
web3: Web3,
|
||||
validatorList: string[],
|
||||
bridgeContract: Contract,
|
||||
fromHome: boolean,
|
||||
setResult: Function,
|
||||
requiredSignatures: number,
|
||||
setSignatureCollected: Function,
|
||||
setTimeoutId: (timeoutId: number) => void,
|
||||
isCancelled: () => boolean,
|
||||
startBlock: number,
|
||||
getFailedTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>,
|
||||
setFailedConfirmations: Function,
|
||||
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>,
|
||||
setPendingConfirmations: Function,
|
||||
getSuccessTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
|
||||
) => {
|
||||
const hashMsg = web3.utils.soliditySha3Raw(messageData)
|
||||
let validatorConfirmations = await Promise.all(
|
||||
validatorList.map(getValidatorConfirmation(web3, hashMsg, bridgeContract, fromHome))
|
||||
)
|
||||
|
||||
const updateConfirmations = (confirmations: ConfirmationParam[]) => {
|
||||
if (confirmations.length === 0) {
|
||||
return
|
||||
}
|
||||
validatorConfirmations = mergeConfirmations(validatorConfirmations, confirmations)
|
||||
setResult((currentConfirmations: ConfirmationParam[]) => {
|
||||
if (currentConfirmations && currentConfirmations.length) {
|
||||
return mergeConfirmations(currentConfirmations, confirmations)
|
||||
}
|
||||
return confirmations
|
||||
})
|
||||
}
|
||||
|
||||
const successConfirmations = validatorConfirmations.filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
|
||||
const notSuccessConfirmations = validatorConfirmations.filter(c => c.status !== VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
|
||||
const hasEnoughSignatures = successConfirmations.length >= requiredSignatures
|
||||
|
||||
updateConfirmations(validatorConfirmations)
|
||||
setSignatureCollected(hasEnoughSignatures)
|
||||
|
||||
if (hasEnoughSignatures) {
|
||||
setPendingConfirmations(false)
|
||||
if (fromHome) {
|
||||
// fetch collected signatures for possible manual processing
|
||||
const signatures = await Promise.all(
|
||||
Array.from(Array(requiredSignatures).keys()).map(i => bridgeContract.methods.signature(hashMsg, i).call())
|
||||
)
|
||||
const confirmations = signatures.flatMap(sig => {
|
||||
const { v, r, s } = signatureToVRS(sig)
|
||||
const address = web3.eth.accounts.recover(messageData, `0x${v}`, `0x${r}`, `0x${s}`)
|
||||
return successConfirmations.filter(c => c.validator === address).map(c => ({ ...c, signature: sig }))
|
||||
})
|
||||
updateConfirmations(confirmations)
|
||||
}
|
||||
}
|
||||
|
||||
// get transactions from success signatures
|
||||
const successConfirmationWithData = await Promise.all(
|
||||
successConfirmations.map(
|
||||
getSuccessExecutionTransaction(web3, bridgeContract, fromHome, messageData, startBlock, getSuccessTransactions)
|
||||
)
|
||||
)
|
||||
|
||||
const successConfirmationWithTxFound = successConfirmationWithData.filter(v => v.txHash !== '')
|
||||
updateConfirmations(successConfirmationWithTxFound)
|
||||
|
||||
// If signatures not collected, look for pending transactions
|
||||
if (!hasEnoughSignatures) {
|
||||
// Check if confirmation is pending
|
||||
const validatorPendingConfirmationsChecks = await Promise.all(
|
||||
notSuccessConfirmations.map(getValidatorPendingTransaction(bridgeContract, messageData, getPendingTransactions))
|
||||
)
|
||||
|
||||
const validatorPendingConfirmations = validatorPendingConfirmationsChecks.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
)
|
||||
updateConfirmations(validatorPendingConfirmations)
|
||||
setPendingConfirmations(validatorPendingConfirmations.length > 0)
|
||||
}
|
||||
|
||||
const undefinedConfirmations = validatorConfirmations.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
)
|
||||
|
||||
// Check if confirmation failed
|
||||
const validatorFailedConfirmationsChecks = await Promise.all(
|
||||
undefinedConfirmations.map(
|
||||
getValidatorFailedTransaction(web3, bridgeContract, messageData, startBlock, getFailedTransactions)
|
||||
)
|
||||
)
|
||||
let validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED || c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
)
|
||||
if (hasEnoughSignatures && !fromHome) {
|
||||
const lastTS = Math.max(...successConfirmationWithTxFound.map(c => c.timestamp || 0))
|
||||
validatorFailedConfirmations = validatorFailedConfirmations.map(
|
||||
c =>
|
||||
c.timestamp < lastTS
|
||||
? c
|
||||
: {
|
||||
...c,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
}
|
||||
)
|
||||
}
|
||||
setFailedConfirmations(
|
||||
!hasEnoughSignatures && validatorFailedConfirmations.some(c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED)
|
||||
)
|
||||
updateConfirmations(validatorFailedConfirmations)
|
||||
|
||||
const missingConfirmations = validatorConfirmations.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
)
|
||||
|
||||
if (hasEnoughSignatures) {
|
||||
// If signatures collected, it should set other signatures not found as not required
|
||||
const notRequiredConfirmations = missingConfirmations.map(c => ({
|
||||
validator: c.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED,
|
||||
timestamp: 0,
|
||||
txHash: ''
|
||||
}))
|
||||
updateConfirmations(notRequiredConfirmations)
|
||||
}
|
||||
|
||||
// retry if not all signatures are collected and some confirmations are still missing
|
||||
// or some success transactions were not fetched successfully
|
||||
if (
|
||||
(!hasEnoughSignatures && missingConfirmations.length > 0) ||
|
||||
successConfirmationWithTxFound.length < successConfirmationWithData.length
|
||||
) {
|
||||
if (!isCancelled()) {
|
||||
const timeoutId = setTimeout(
|
||||
() =>
|
||||
getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
fromHome,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
setTimeoutId,
|
||||
isCancelled,
|
||||
startBlock,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
),
|
||||
HOME_RPC_POLLING_INTERVAL
|
||||
)
|
||||
setTimeoutId(timeoutId)
|
||||
}
|
||||
}
|
||||
}
|
||||
182
alm/src/utils/getFinalizationEvent.ts
Normal file
182
alm/src/utils/getFinalizationEvent.ts
Normal file
@ -0,0 +1,182 @@
|
||||
import { Contract, EventData } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
import {
|
||||
CACHE_KEY_EXECUTION_FAILED,
|
||||
FOREIGN_EXPLORER_API,
|
||||
FOREIGN_RPC_POLLING_INTERVAL,
|
||||
HOME_EXPLORER_API,
|
||||
HOME_RPC_POLLING_INTERVAL,
|
||||
VALIDATOR_CONFIRMATION_STATUS
|
||||
} from '../config/constants'
|
||||
import { ExecutionData } from '../hooks/useMessageConfirmations'
|
||||
import {
|
||||
APIPendingTransaction,
|
||||
APITransaction,
|
||||
GetTransactionParams,
|
||||
GetPendingTransactionParams,
|
||||
getLogs
|
||||
} from './explorer'
|
||||
import { getBlock, MessageObject } from './web3'
|
||||
import validatorsCache from '../services/ValidatorsCache'
|
||||
import { foreignBlockNumberProvider, homeBlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
|
||||
const getPastEventsWithFallback = (api: string, web3: Web3, contract: Contract, eventName: string, options: any) =>
|
||||
contract.getPastEvents(eventName, options).catch(
|
||||
() =>
|
||||
api
|
||||
? getLogs(api, web3, contract, eventName, {
|
||||
fromBlock: options.fromBlock,
|
||||
toBlock: options.toBlock,
|
||||
topics: [null, null, options.filter.messageId]
|
||||
})
|
||||
: []
|
||||
)
|
||||
|
||||
export const getSuccessExecutionData = async (
|
||||
contract: Contract,
|
||||
eventName: string,
|
||||
web3: Web3,
|
||||
messageId: string,
|
||||
api: string = ''
|
||||
) => {
|
||||
// Since it filters by the message id, only one event will be fetched
|
||||
// so there is no need to limit the range of the block to reduce the network traffic
|
||||
const events: EventData[] = await getPastEventsWithFallback(api, web3, contract, eventName, {
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
filter: {
|
||||
messageId
|
||||
}
|
||||
})
|
||||
if (events.length > 0) {
|
||||
const event = events[0]
|
||||
const [txReceipt, block] = await Promise.all([
|
||||
web3.eth.getTransactionReceipt(event.transactionHash),
|
||||
getBlock(web3, event.blockNumber)
|
||||
])
|
||||
|
||||
const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp
|
||||
const validatorAddress = web3.utils.toChecksumAddress(txReceipt.from)
|
||||
|
||||
return {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS,
|
||||
validator: validatorAddress,
|
||||
txHash: event.transactionHash,
|
||||
timestamp: blockTimestamp,
|
||||
executionResult: event.returnValues.status,
|
||||
blockNumber: event.blockNumber
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export const getFinalizationEvent = async (
|
||||
fromHome: boolean,
|
||||
contract: Contract,
|
||||
web3: Web3,
|
||||
setResult: React.Dispatch<React.SetStateAction<ExecutionData>>,
|
||||
message: MessageObject,
|
||||
setTimeoutId: (timeoutId: number) => void,
|
||||
isCancelled: () => boolean,
|
||||
startBlock: number,
|
||||
collectedSignaturesEvent: Maybe<EventData>,
|
||||
getFailedExecution: (args: GetTransactionParams) => Promise<APITransaction[]>,
|
||||
setFailedExecution: Function,
|
||||
getPendingExecution: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>,
|
||||
setPendingExecution: Function,
|
||||
setExecutionEventsFetched: Function
|
||||
) => {
|
||||
const eventName = fromHome ? 'RelayedMessage' : 'AffirmationCompleted'
|
||||
const api = fromHome ? FOREIGN_EXPLORER_API : HOME_EXPLORER_API
|
||||
|
||||
const successExecutionData = await getSuccessExecutionData(contract, eventName, web3, message.id, api)
|
||||
|
||||
if (successExecutionData) {
|
||||
setResult(successExecutionData)
|
||||
} else {
|
||||
setExecutionEventsFetched(true)
|
||||
// If event is defined, it means it is a message from Home to Foreign
|
||||
if (collectedSignaturesEvent) {
|
||||
const validator = collectedSignaturesEvent.returnValues.authorityResponsibleForRelay
|
||||
|
||||
const pendingTransactions = await getPendingExecution({
|
||||
account: validator,
|
||||
messageData: message.data,
|
||||
to: contract.options.address
|
||||
})
|
||||
|
||||
// If the transaction is pending it sets the status and avoid making the request for failed transactions
|
||||
if (pendingTransactions.length > 0) {
|
||||
const pendingTx = pendingTransactions[0]
|
||||
|
||||
const nowTimestamp = Math.floor(new Date().getTime() / 1000.0)
|
||||
|
||||
setResult({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
|
||||
validator: validator,
|
||||
txHash: pendingTx.hash,
|
||||
timestamp: nowTimestamp,
|
||||
executionResult: false,
|
||||
blockNumber: 0
|
||||
})
|
||||
setPendingExecution(true)
|
||||
} else {
|
||||
const validatorExecutionCacheKey = `${CACHE_KEY_EXECUTION_FAILED}${validator}-${message.id}`
|
||||
const failedFromCache = validatorsCache.get(validatorExecutionCacheKey)
|
||||
const blockProvider = fromHome ? foreignBlockNumberProvider : homeBlockNumberProvider
|
||||
|
||||
if (!failedFromCache) {
|
||||
const failedTransactions = await getFailedExecution({
|
||||
account: validator,
|
||||
to: contract.options.address,
|
||||
messageData: message.data,
|
||||
startBlock,
|
||||
endBlock: blockProvider.get() || 0
|
||||
})
|
||||
|
||||
if (failedTransactions.length > 0) {
|
||||
const failedTx = failedTransactions[0]
|
||||
|
||||
// If validator execution failed, we cache the result to avoid doing future requests for a result that won't change
|
||||
validatorsCache.set(validatorExecutionCacheKey, true)
|
||||
|
||||
const timestamp = parseInt(failedTx.timeStamp)
|
||||
setResult({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
|
||||
validator: validator,
|
||||
txHash: failedTx.hash,
|
||||
timestamp,
|
||||
executionResult: false,
|
||||
blockNumber: parseInt(failedTx.blockNumber)
|
||||
})
|
||||
setFailedExecution(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isCancelled()) {
|
||||
const timeoutId = setTimeout(
|
||||
() =>
|
||||
getFinalizationEvent(
|
||||
fromHome,
|
||||
contract,
|
||||
web3,
|
||||
setResult,
|
||||
message,
|
||||
setTimeoutId,
|
||||
isCancelled,
|
||||
startBlock,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution,
|
||||
setExecutionEventsFetched
|
||||
),
|
||||
fromHome ? FOREIGN_RPC_POLLING_INTERVAL : HOME_RPC_POLLING_INTERVAL
|
||||
)
|
||||
setTimeoutId(timeoutId)
|
||||
}
|
||||
}
|
||||
}
|
||||
42
alm/src/utils/networks.ts
Normal file
42
alm/src/utils/networks.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { formatDistance } from 'date-fns'
|
||||
import {
|
||||
CONFIRMATIONS_STATUS_DESCRIPTION,
|
||||
CONFIRMATIONS_STATUS_DESCRIPTION_HOME,
|
||||
TRANSACTION_STATUS_DESCRIPTION
|
||||
} from '../config/descriptions'
|
||||
import { FOREIGN_EXPLORER_TX_TEMPLATE, HOME_EXPLORER_TX_TEMPLATE } from '../config/constants'
|
||||
|
||||
export const validTxHash = (txHash: string) => /^0x[a-fA-F0-9]{64}$/.test(txHash)
|
||||
|
||||
export const formatTxHash = (txHash: string) => `${txHash.substring(0, 6)}...${txHash.substring(txHash.length - 4)}`
|
||||
|
||||
export const getExplorerTxUrl = (txHash: string, isHome: boolean) => {
|
||||
const template = isHome ? HOME_EXPLORER_TX_TEMPLATE : FOREIGN_EXPLORER_TX_TEMPLATE
|
||||
return template.replace('%s', txHash)
|
||||
}
|
||||
|
||||
export const formatTxHashExtended = (txHash: string) =>
|
||||
`${txHash.substring(0, 10)}...${txHash.substring(txHash.length - 8)}`
|
||||
|
||||
export const formatTimestamp = (timestamp: number): string => {
|
||||
const txDate = new Date(0).setUTCSeconds(timestamp)
|
||||
return formatDistance(txDate, new Date(), {
|
||||
addSuffix: true
|
||||
})
|
||||
}
|
||||
|
||||
export const getTransactionStatusDescription = (status: string, timestamp: Maybe<number> = null) => {
|
||||
let description = TRANSACTION_STATUS_DESCRIPTION[status]
|
||||
|
||||
if (timestamp) {
|
||||
description = description.replace('%t', formatTimestamp(timestamp))
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
export const getConfirmationsStatusDescription = (status: string, home: string, foreign: string, fromHome: boolean) => {
|
||||
const statusDescription = fromHome ? CONFIRMATIONS_STATUS_DESCRIPTION_HOME : CONFIRMATIONS_STATUS_DESCRIPTION
|
||||
|
||||
return statusDescription[status]
|
||||
}
|
||||
26
alm/src/utils/signatures.ts
Normal file
26
alm/src/utils/signatures.ts
Normal file
@ -0,0 +1,26 @@
|
||||
import Web3 from 'web3'
|
||||
|
||||
function strip0x(s: string) {
|
||||
return Web3.utils.isHexStrict(s) ? s.substr(2) : s
|
||||
}
|
||||
|
||||
export interface Signature {
|
||||
v: string
|
||||
r: string
|
||||
s: string
|
||||
}
|
||||
|
||||
export function signatureToVRS(rawSignature: string): Signature {
|
||||
const signature = strip0x(rawSignature)
|
||||
const v = signature.substr(64 * 2)
|
||||
const r = signature.substr(0, 32 * 2)
|
||||
const s = signature.substr(32 * 2, 32 * 2)
|
||||
return { v, r, s }
|
||||
}
|
||||
|
||||
export function packSignatures(array: Array<Signature>): string {
|
||||
const length = strip0x(Web3.utils.toHex(array.length))
|
||||
const msgLength = length.length === 1 ? `0${length}` : length
|
||||
const [v, r, s] = array.reduce(([vs, rs, ss], { v, r, s }) => [vs + v, rs + r, ss + s], ['', '', ''])
|
||||
return `0x${msgLength}${v}${r}${s}`
|
||||
}
|
||||
176
alm/src/utils/validatorConfirmationHelpers.ts
Normal file
176
alm/src/utils/validatorConfirmationHelpers.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
import validatorsCache from '../services/ValidatorsCache'
|
||||
import { CACHE_KEY_FAILED, CACHE_KEY_SUCCESS, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { APIPendingTransaction, APITransaction, GetTransactionParams, GetPendingTransactionParams } from './explorer'
|
||||
import { homeBlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
import { getAffirmationsSigned, getMessagesSigned } from './contract'
|
||||
|
||||
export const getValidatorConfirmation = (
|
||||
web3: Web3,
|
||||
hashMsg: string,
|
||||
bridgeContract: Contract,
|
||||
fromHome: boolean
|
||||
) => async (validator: string): Promise<ConfirmationParam> => {
|
||||
const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg)
|
||||
|
||||
const fromCache = validatorsCache.getData(hashSenderMsg)
|
||||
if (fromCache) {
|
||||
return fromCache
|
||||
}
|
||||
|
||||
const confirmationContractMethod = fromHome ? getMessagesSigned : getAffirmationsSigned
|
||||
const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg)
|
||||
|
||||
// If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change
|
||||
if (confirmed) {
|
||||
const confirmation: ConfirmationParam = {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
validator,
|
||||
timestamp: 0,
|
||||
txHash: ''
|
||||
}
|
||||
validatorsCache.setData(hashSenderMsg, confirmation)
|
||||
return confirmation
|
||||
}
|
||||
|
||||
return {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator,
|
||||
timestamp: 0,
|
||||
txHash: ''
|
||||
}
|
||||
}
|
||||
|
||||
export const getSuccessExecutionTransaction = (
|
||||
web3: Web3,
|
||||
bridgeContract: Contract,
|
||||
fromHome: boolean,
|
||||
messageData: string,
|
||||
startBlock: number,
|
||||
getSuccessTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
|
||||
const { validator } = validatorData
|
||||
const validatorCacheKey = `${CACHE_KEY_SUCCESS}${validatorData.validator}-${messageData}`
|
||||
const fromCache = validatorsCache.getData(validatorCacheKey)
|
||||
|
||||
if (fromCache && fromCache.txHash) {
|
||||
return fromCache
|
||||
}
|
||||
|
||||
const transactions = await getSuccessTransactions({
|
||||
account: validatorData.validator,
|
||||
to: bridgeContract.options.address,
|
||||
messageData,
|
||||
startBlock,
|
||||
endBlock: homeBlockNumberProvider.get() || 0
|
||||
})
|
||||
|
||||
let txHashTimestamp = 0
|
||||
let txHash = ''
|
||||
const status = VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
|
||||
if (transactions.length > 0) {
|
||||
const tx = transactions[0]
|
||||
txHashTimestamp = parseInt(tx.timeStamp)
|
||||
txHash = tx.hash
|
||||
|
||||
// cache the result
|
||||
validatorsCache.setData(validatorCacheKey, {
|
||||
validator,
|
||||
status,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
validator,
|
||||
status,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidatorFailedTransaction = (
|
||||
web3: Web3,
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
startBlock: number,
|
||||
getFailedTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
|
||||
const validatorCacheKey = `${CACHE_KEY_FAILED}${validatorData.validator}-${messageData}`
|
||||
const failedFromCache = validatorsCache.getData(validatorCacheKey)
|
||||
|
||||
if (failedFromCache && failedFromCache.txHash) {
|
||||
return failedFromCache
|
||||
}
|
||||
|
||||
const failedTransactions = await getFailedTransactions({
|
||||
account: validatorData.validator,
|
||||
to: bridgeContract.options.address,
|
||||
messageData,
|
||||
startBlock,
|
||||
endBlock: homeBlockNumberProvider.get() || 0
|
||||
})
|
||||
// If validator signature failed, we cache the result to avoid doing future requests for a result that won't change
|
||||
if (failedTransactions.length > 0) {
|
||||
const failedTx = failedTransactions[0]
|
||||
const confirmation: ConfirmationParam = {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
|
||||
validator: validatorData.validator,
|
||||
txHash: failedTx.hash,
|
||||
timestamp: parseInt(failedTx.timeStamp)
|
||||
}
|
||||
|
||||
if (failedTx.input && failedTx.input.length > 10) {
|
||||
try {
|
||||
const res = web3.eth.abi.decodeParameters(['bytes', 'bytes'], `0x${failedTx.input.slice(10)}`)
|
||||
confirmation.signature = res[0]
|
||||
confirmation.status = VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
|
||||
console.log(`Adding manual signature from failed message from ${validatorData.validator}`)
|
||||
} catch {}
|
||||
}
|
||||
validatorsCache.setData(validatorCacheKey, confirmation)
|
||||
return confirmation
|
||||
}
|
||||
|
||||
return {
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator: validatorData.validator,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidatorPendingTransaction = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>
|
||||
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
|
||||
const failedTransactions = await getPendingTransactions({
|
||||
account: validatorData.validator,
|
||||
to: bridgeContract.options.address,
|
||||
messageData
|
||||
})
|
||||
|
||||
const newStatus =
|
||||
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.PENDING : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
|
||||
let timestamp = 0
|
||||
let txHash = ''
|
||||
|
||||
if (failedTransactions.length > 0) {
|
||||
const failedTx = failedTransactions[0]
|
||||
timestamp = Math.floor(new Date().getTime() / 1000.0)
|
||||
txHash = failedTx.hash
|
||||
}
|
||||
|
||||
return {
|
||||
validator: validatorData.validator,
|
||||
status: newStatus,
|
||||
txHash,
|
||||
timestamp
|
||||
}
|
||||
}
|
||||
121
alm/src/utils/web3.ts
Normal file
121
alm/src/utils/web3.ts
Normal file
@ -0,0 +1,121 @@
|
||||
import Web3 from 'web3'
|
||||
import { BlockTransactionString } from 'web3-eth'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
import memoize from 'fast-memoize'
|
||||
import promiseRetry from 'promise-retry'
|
||||
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
|
||||
import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export interface MessageObject {
|
||||
id: string
|
||||
data: string
|
||||
sender?: string
|
||||
executor?: string
|
||||
obToken?: string
|
||||
obReceiver?: string
|
||||
}
|
||||
|
||||
export interface WarnRule {
|
||||
message: string
|
||||
sender?: string
|
||||
executor?: string
|
||||
obToken?: string
|
||||
obReceiver?: string
|
||||
}
|
||||
|
||||
export const matchesRule = (rule: WarnRule, msg: MessageObject) => {
|
||||
if (!msg.executor || !msg.sender) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.executor && rule.executor.toLowerCase() !== msg.executor.toLowerCase()) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.sender && rule.sender.toLowerCase() !== msg.sender.toLowerCase()) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.obToken && (!msg.obToken || rule.obToken.toLowerCase() !== msg.obToken.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
if (!!rule.obReceiver && (!msg.obReceiver || rule.obReceiver.toLowerCase() !== msg.obReceiver.toLowerCase())) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const rawGetWeb3 = (url: string) => new Web3(new Web3.providers.HttpProvider(url))
|
||||
const memoized = memoize(rawGetWeb3)
|
||||
|
||||
export const getWeb3 = (url: string) => memoized(url)
|
||||
|
||||
export const filterEventsByAbi = (
|
||||
txReceipt: TransactionReceipt,
|
||||
web3: Web3,
|
||||
bridgeAddress: string,
|
||||
eventAbi: AbiItem
|
||||
): MessageObject[] => {
|
||||
const eventHash = web3.eth.abi.encodeEventSignature(eventAbi)
|
||||
const events = txReceipt.logs.filter(e => e.address === bridgeAddress && e.topics[0] === eventHash)
|
||||
|
||||
if (!eventAbi || !eventAbi.inputs || !eventAbi.inputs.length) {
|
||||
return []
|
||||
}
|
||||
const inputs = eventAbi.inputs
|
||||
return events.map(e => {
|
||||
const { messageId, encodedData } = web3.eth.abi.decodeLog(inputs, e.data, [e.topics[1]])
|
||||
let sender, executor, obToken, obReceiver
|
||||
if (encodedData.length >= 160) {
|
||||
sender = `0x${encodedData.slice(66, 106)}`
|
||||
executor = `0x${encodedData.slice(106, 146)}`
|
||||
const dataOffset =
|
||||
160 + (parseInt(encodedData.slice(154, 156), 16) + parseInt(encodedData.slice(156, 158), 16)) * 2 + 8
|
||||
if (encodedData.length >= dataOffset + 64) {
|
||||
obToken = `0x${encodedData.slice(dataOffset + 24, dataOffset + 64)}`
|
||||
}
|
||||
if (encodedData.length >= dataOffset + 128) {
|
||||
obReceiver = `0x${encodedData.slice(dataOffset + 88, dataOffset + 128)}`
|
||||
}
|
||||
}
|
||||
return {
|
||||
id: messageId || '',
|
||||
data: encodedData || '',
|
||||
sender,
|
||||
executor,
|
||||
obToken,
|
||||
obReceiver
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const getHomeMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
|
||||
const UserRequestForSignatureAbi: AbiItem = HOME_AMB_ABI.filter(
|
||||
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForSignature'
|
||||
)[0]
|
||||
return filterEventsByAbi(txReceipt, web3, bridgeAddress, UserRequestForSignatureAbi)
|
||||
}
|
||||
|
||||
export const getForeignMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
|
||||
const userRequestForAffirmationAbi: AbiItem = FOREIGN_AMB_ABI.filter(
|
||||
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForAffirmation'
|
||||
)[0]
|
||||
return filterEventsByAbi(txReceipt, web3, bridgeAddress, userRequestForAffirmationAbi)
|
||||
}
|
||||
|
||||
// In some rare cases the block data is not available yet for the block of a new event detected
|
||||
// so this logic retry to get the block in case it fails
|
||||
export const getBlock = async (web3: Web3, blockNumber: number): Promise<BlockTransactionString> =>
|
||||
promiseRetry(async retry => {
|
||||
const result = await web3.eth.getBlock(blockNumber)
|
||||
if (!result) {
|
||||
return retry('Error getting block data')
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
export const getChainId = async (web3: Web3, snapshotProvider: SnapshotProvider) => {
|
||||
let id = snapshotProvider.chainId()
|
||||
if (id === 0) {
|
||||
id = await web3.eth.getChainId()
|
||||
}
|
||||
return id
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -2,19 +2,37 @@ import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import BurnerCore from '@burner-wallet/core'
|
||||
import { InjectedSigner, LocalSigner } from '@burner-wallet/core/signers'
|
||||
import { InfuraGateway, InjectedGateway } from '@burner-wallet/core/gateways'
|
||||
import { XDaiBridge } from '@burner-wallet/exchange'
|
||||
import { xdai } from '@burner-wallet/assets'
|
||||
import { InfuraGateway, InjectedGateway, XDaiGateway } from '@burner-wallet/core/gateways'
|
||||
import Exchange from '@burner-wallet/exchange'
|
||||
import ModernUI from '@burner-wallet/modern-ui'
|
||||
import { Etc, Wetc, TokenBridgeGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
|
||||
import {
|
||||
Etc,
|
||||
Wetc,
|
||||
Dai,
|
||||
qDai,
|
||||
MOON,
|
||||
xMOON,
|
||||
TokenBridgeGateway,
|
||||
WETCBridge,
|
||||
QDAIBridge,
|
||||
MOONBridge
|
||||
} from '@poanet/tokenbridge-bw-exchange'
|
||||
import MetamaskPlugin from '@burner-wallet/metamask-plugin'
|
||||
|
||||
const core = new BurnerCore({
|
||||
signers: [new InjectedSigner(), new LocalSigner()],
|
||||
gateways: [new InjectedGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY), new TokenBridgeGateway()],
|
||||
assets: [Wetc, Etc]
|
||||
gateways: [
|
||||
new InjectedGateway(),
|
||||
new XDaiGateway(),
|
||||
new InfuraGateway(process.env.REACT_APP_INFURA_KEY),
|
||||
new TokenBridgeGateway()
|
||||
],
|
||||
assets: [xdai, Wetc, Etc, Dai, qDai, MOON, xMOON]
|
||||
})
|
||||
|
||||
const exchange = new Exchange([new WETCBridge()])
|
||||
const exchange = new Exchange([new XDaiBridge(), new WETCBridge(), new QDAIBridge(), new MOONBridge()])
|
||||
|
||||
const BurnerWallet = () => <ModernUI title="Staging Wallet" core={core} plugins={[exchange, new MetamaskPlugin()]} />
|
||||
|
||||
|
||||
@ -16,8 +16,7 @@
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-scripts": "3.0.1",
|
||||
"typescript": "3.5.1",
|
||||
"web3": "1.0.0-beta.55"
|
||||
"typescript": "3.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
||||
53
burner-wallet-plugin/testing/src/LocalhostGateway.ts
Normal file
53
burner-wallet-plugin/testing/src/LocalhostGateway.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Gateway } from '@burner-wallet/core/gateways'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export default class LocalhostGateway extends Gateway {
|
||||
private readonly providers: object
|
||||
private readonly providerStrings: { [id: string]: string }
|
||||
constructor() {
|
||||
super()
|
||||
this.providerStrings = {
|
||||
'111': 'http://localhost:8545',
|
||||
'1337': 'http://localhost:8546'
|
||||
}
|
||||
this.providers = {}
|
||||
}
|
||||
|
||||
isAvailable() {
|
||||
return true
|
||||
}
|
||||
|
||||
getNetworks() {
|
||||
return ['111', '1337']
|
||||
}
|
||||
|
||||
_provider(network) {
|
||||
if (!this.providers[network]) {
|
||||
this._makeProvider(network)
|
||||
}
|
||||
return this.providers[network]
|
||||
}
|
||||
|
||||
_makeProvider(network) {
|
||||
if (!this.providerStrings[network]) {
|
||||
throw new Error(`Network ${network} not supported by LocalhostGateway`)
|
||||
}
|
||||
|
||||
this.providers[network] = new Web3.providers.HttpProvider(this.providerStrings[network])
|
||||
}
|
||||
|
||||
send(network, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (this.getNetworks().indexOf(network) === -1) {
|
||||
return reject(new Error('LocalhostGateway does not support this network'))
|
||||
}
|
||||
|
||||
this._provider(network).send(payload, (err, response) => {
|
||||
if (err) {
|
||||
return reject(err)
|
||||
}
|
||||
return resolve(response.result)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -6,13 +6,23 @@ import { InjectedSigner, LocalSigner } from '@burner-wallet/core/signers'
|
||||
import { InfuraGateway, InjectedGateway } from '@burner-wallet/core/gateways'
|
||||
import ModernUI from '@burner-wallet/modern-ui'
|
||||
import Exchange from '@burner-wallet/exchange'
|
||||
import { Mediator, sPOA, ERC677Asset, TokenBridgeGateway } from '@poanet/tokenbridge-bw-exchange'
|
||||
import {
|
||||
Mediator,
|
||||
sPOA,
|
||||
ERC677Asset,
|
||||
TokenBridgeGateway,
|
||||
NativeMediatorAsset,
|
||||
MediatorErcToNative,
|
||||
BridgeableERC20Asset
|
||||
} from '@poanet/tokenbridge-bw-exchange'
|
||||
import MetamaskPlugin from '@burner-wallet/metamask-plugin'
|
||||
import LocalhostGateway from './LocalhostGateway'
|
||||
|
||||
let assetIdAtHome = 'assetAtHome'
|
||||
const assetIdAtForeign = 'assetAtForeign'
|
||||
let assetAtHome: Asset
|
||||
let assetAtForeign: Asset
|
||||
let testBridge: Mediator
|
||||
|
||||
if (process.env.REACT_APP_MODE === 'AMB_NATIVE_TO_ERC677') {
|
||||
sPOA.setMediatorAddress(process.env.REACT_APP_HOME_MEDIATOR_ADDRESS)
|
||||
@ -28,8 +38,16 @@ if (process.env.REACT_APP_MODE === 'AMB_NATIVE_TO_ERC677') {
|
||||
// @ts-ignore
|
||||
address: process.env.REACT_APP_FOREIGN_TOKEN_ADDRESS
|
||||
})
|
||||
} else {
|
||||
// process.env.REACT_APP_MODE === 'AMB_ERC677_TO_ERC677'
|
||||
|
||||
testBridge = new Mediator({
|
||||
assetA: assetIdAtHome,
|
||||
// @ts-ignore
|
||||
assetABridge: process.env.REACT_APP_HOME_MEDIATOR_ADDRESS,
|
||||
assetB: assetIdAtForeign,
|
||||
// @ts-ignore
|
||||
assetBBridge: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
|
||||
})
|
||||
} else if (process.env.REACT_APP_MODE === 'AMB_ERC677_TO_ERC677') {
|
||||
assetAtHome = new ERC677Asset({
|
||||
id: 'assetAtHome',
|
||||
// @ts-ignore
|
||||
@ -49,20 +67,53 @@ if (process.env.REACT_APP_MODE === 'AMB_NATIVE_TO_ERC677') {
|
||||
// @ts-ignore
|
||||
address: process.env.REACT_APP_FOREIGN_TOKEN_ADDRESS
|
||||
})
|
||||
}
|
||||
|
||||
const testBridge = new Mediator({
|
||||
assetA: assetIdAtHome,
|
||||
// @ts-ignore
|
||||
assetABridge: process.env.REACT_APP_HOME_MEDIATOR_ADDRESS,
|
||||
assetB: assetIdAtForeign,
|
||||
// @ts-ignore
|
||||
assetBBridge: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
|
||||
})
|
||||
testBridge = new Mediator({
|
||||
assetA: assetIdAtHome,
|
||||
// @ts-ignore
|
||||
assetABridge: process.env.REACT_APP_HOME_MEDIATOR_ADDRESS,
|
||||
assetB: assetIdAtForeign,
|
||||
// @ts-ignore
|
||||
assetBBridge: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
|
||||
})
|
||||
} else {
|
||||
// process.env.REACT_APP_MODE === 'AMB_ERC20_TO_NATIVE'
|
||||
assetAtHome = new NativeMediatorAsset({
|
||||
id: assetIdAtHome,
|
||||
name: 'qDAI',
|
||||
network: process.env.REACT_APP_HOME_NETWORK as string,
|
||||
mediatorAddress: process.env.REACT_APP_HOME_MEDIATOR_ADDRESS
|
||||
})
|
||||
|
||||
assetAtForeign = new BridgeableERC20Asset({
|
||||
id: 'assetAtForeign',
|
||||
// @ts-ignore
|
||||
name: process.env.REACT_APP_FOREIGN_TOKEN_NAME,
|
||||
// @ts-ignore
|
||||
network: process.env.REACT_APP_FOREIGN_NETWORK,
|
||||
// @ts-ignore
|
||||
address: process.env.REACT_APP_FOREIGN_TOKEN_ADDRESS,
|
||||
bridgeModes: ['erc-to-native-amb']
|
||||
})
|
||||
|
||||
testBridge = new MediatorErcToNative({
|
||||
assetA: assetIdAtHome,
|
||||
// @ts-ignore
|
||||
assetABridge: process.env.REACT_APP_HOME_MEDIATOR_ADDRESS,
|
||||
assetB: assetIdAtForeign,
|
||||
// @ts-ignore
|
||||
assetBBridge: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
|
||||
})
|
||||
}
|
||||
|
||||
const core = new BurnerCore({
|
||||
signers: [new InjectedSigner(), new LocalSigner({ privateKey: process.env.REACT_APP_PK, saveKey: false })],
|
||||
gateways: [new InjectedGateway(), new TokenBridgeGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
|
||||
gateways: [
|
||||
new InjectedGateway(),
|
||||
new LocalhostGateway(),
|
||||
new TokenBridgeGateway(),
|
||||
new InfuraGateway(process.env.REACT_APP_INFURA_KEY)
|
||||
],
|
||||
assets: [assetAtHome, assetAtForeign]
|
||||
})
|
||||
|
||||
|
||||
@ -17,6 +17,7 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"noImplicitAny": false,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
|
||||
@ -4,12 +4,16 @@ This plugin defines a Bridge trading pair to be used in the Exchange Plugin.
|
||||
|
||||
Bridge trading pairs and assets supported:
|
||||
* ETC - WETC Bridge
|
||||
* MOON - xMOON Bridge
|
||||
* DAI - qDAI Bridge (For qDAI Bridge, it's necessary to use a custom DAI token from this repo instead of the DAI asset provided by burner-wallet)
|
||||
|
||||
It also provides some generic resources that can be used and extended:
|
||||
* **ERC677Asset** - A representation of an Erc677 token
|
||||
* **ERC677Asset** - A representation of an Erc677 token.
|
||||
* **BridgeableERC20Asset** - A representation of Erc20 token with a possibility of bridging it via a call to `relayTokens`.
|
||||
* **NativeMediatorAsset** - Represents a native token that interacts with a Mediator extension.
|
||||
* **Mediator Pair** - Represents an Exchange Pair that interacts with mediators extensions.
|
||||
* **TokenBridgeGateway** - A gateway to operate with ETC, POA Sokol and POA Core networks.
|
||||
* **MediatorErcToNative Pair** - Represents a modified Mediator Pair that interacts with a tokenbridge erc-to-native mediators contracts.
|
||||
* **TokenBridgeGateway** - A gateway to operate with ETC, POA Sokol, POA Core and qDAI networks.
|
||||
|
||||
### Install package
|
||||
```
|
||||
@ -18,13 +22,22 @@ yarn add @poanet/tokenbridge-bw-exchange
|
||||
|
||||
### Usage
|
||||
|
||||
#### WETCBridge example
|
||||
In this example, we use `TokenBridgeGateway` for connecting to the Ethereum Classic and `InfuraGateway` for connecting to the Ethereum Mainnet.
|
||||
|
||||
`WETCBridge` operates with two assets: `WETC` (Ethereum Mainnet) and `ETC` (Ethereum Classic), they should be added in the assets list.
|
||||
|
||||
```javascript
|
||||
import { Etc, Wetc, EtcGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
|
||||
import BurnerCore from '@burner-wallet/core'
|
||||
import Exchange from '@burner-wallet/exchange'
|
||||
import { LocalSigner } from '@burner-wallet/core/signers'
|
||||
import { Etc, Wetc, TokenBridgeGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
|
||||
import { InfuraGateway } from '@burner-wallet/core/gateways'
|
||||
|
||||
const core = new BurnerCore({
|
||||
...
|
||||
gateways: [new EtcGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
|
||||
assets: [Etc, Wetc]
|
||||
signers: [new LocalSigner()],
|
||||
gateways: [new TokenBridgeGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
|
||||
assets: [Wetc, Etc]
|
||||
})
|
||||
|
||||
const exchange = new Exchange({
|
||||
@ -32,6 +45,33 @@ const exchange = new Exchange({
|
||||
})
|
||||
```
|
||||
|
||||
### Using several exchanges simultaneously
|
||||
In this example, we use `TokenBridgeGateway` for connecting to the qDAI chain, `XDaiGatewai` for connecting to the xDAI chain and `InfuraGateway` for connecting to the Ethereum Mainnet and Rinkeby Network.
|
||||
|
||||
`QDAIBridge` operates with two assets: `qDAI` (qDAI chain) and `DAI` (Ethereum Mainnet). Note that we use a custom DAI token from the `@poanet/tokenbridge-bw-exchange`, this is necessary for allowing bridge operations on this token.
|
||||
|
||||
`MOONBridge` operates with two assets: `MOON` (Rinkeby network) and `xMOON` (xDAI chain).
|
||||
|
||||
All four assets should be added to the assets list.
|
||||
|
||||
```javascript
|
||||
import BurnerCore from '@burner-wallet/core'
|
||||
import Exchange from '@burner-wallet/exchange'
|
||||
import { LocalSigner } from '@burner-wallet/core/signers'
|
||||
import { InfuraGateway, XDaiGateway } from '@burner-wallet/core/gateways'
|
||||
import { Dai, qDai, MOON, xMOON, TokenBridgeGateway, QDAIBridge, MOONBridge } from '@poanet/tokenbridge-bw-exchange'
|
||||
|
||||
const core = new BurnerCore({
|
||||
signers: [new LocalSigner()],
|
||||
gateways: [new TokenBridgeGateway(), new XDaiGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
|
||||
assets: [Dai, qDai, MOON, xMOON]
|
||||
})
|
||||
|
||||
const exchange = new Exchange({
|
||||
pairs: [new QDAIBridge(), new MOONBridge()]
|
||||
})
|
||||
```
|
||||
|
||||
This is how the exchange plugin will look like:
|
||||
|
||||

|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@poanet/tokenbridge-bw-exchange",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"license": "GPL-3.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { Mediator } from '../burner-wallet'
|
||||
|
||||
export default class MOONBridge extends Mediator {
|
||||
constructor() {
|
||||
super({
|
||||
assetA: 'xmoon',
|
||||
assetABridge: '0x1E0507046130c31DEb20EC2f870ad070Ff266079',
|
||||
assetB: 'moon',
|
||||
assetBBridge: '0xFEaB457D95D9990b7eb6c943c839258245541754'
|
||||
})
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user