Compare commits
38 Commits
2.3.0-rc0
...
fix-bw-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c02ecad8fd | ||
|
|
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 |
@@ -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"
|
||||
@@ -11,6 +11,9 @@
|
||||
contracts/test
|
||||
contracts/build
|
||||
oracle/test
|
||||
monitor/test
|
||||
monitor/responses
|
||||
commons/test
|
||||
oracle/**/*.png
|
||||
oracle/**/*.jpg
|
||||
audit
|
||||
|
||||
231
.github/workflows/main.yml
vendored
Normal file
231
.github/workflows/main.yml
vendored
Normal file
@@ -0,0 +1,231 @@
|
||||
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: 10
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- run: git submodule status > submodule.status
|
||||
- id: get_cache_key
|
||||
run: 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 }}
|
||||
- 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: 10
|
||||
- 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 }}
|
||||
- run: ${{ steps.cache-repo.outputs.cache-hit }} && yarn run ${{ matrix.task }}
|
||||
ui-coverage:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- initialize
|
||||
if: github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/tags')
|
||||
steps:
|
||||
- uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 10
|
||||
- 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 }}
|
||||
- run: ${{ steps.cache-repo.outputs.cache-hit }} && yarn workspace ui run coverage
|
||||
- uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ github.token }}
|
||||
path-to-lcov: ./ui/coverage/lcov.info
|
||||
build-e2e-images:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- run: |
|
||||
git submodule status > submodule.status
|
||||
echo "::set-env name=E2E_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e') }}"
|
||||
echo "::set-env name=ORACLE_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}"
|
||||
echo "::set-env name=MONITOR_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}"
|
||||
echo "::set-env name=UI_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'ui') }}"
|
||||
echo "::set-env name=ALM_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}"
|
||||
- run: |
|
||||
function check_if_image_exists() {
|
||||
curl -fsSlL -H 'Authorization: bearer ${{ github.token }}' "https://${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"); fi
|
||||
if ! check_if_image_exists monitor ${MONITOR_TAG}; then updated+=("monitor"); fi
|
||||
if ! check_if_image_exists ui ${UI_TAG}; then updated+=("ui"); 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
|
||||
- run: echo "::set-env name=MOLECULE_RUNNER_TAG::${{ hashFiles('./deployment-e2e/Dockerfile') }}"
|
||||
- run: |
|
||||
function check_if_image_exists() {
|
||||
curl -fsSlL -H 'Authorization: bearer ${{ github.token }}' "https://${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, 'ui-e2e:ci']
|
||||
include:
|
||||
- task: alm-e2e
|
||||
use-cache: true
|
||||
- task: 'ui-e2e:ci'
|
||||
use-cache: true
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- run: |
|
||||
git submodule status > submodule.status
|
||||
echo "::set-env name=E2E_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e') }}"
|
||||
echo "::set-env name=ORACLE_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}"
|
||||
echo "::set-env name=MONITOR_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}"
|
||||
echo "::set-env name=UI_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'ui') }}"
|
||||
echo "::set-env name=ALM_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}"
|
||||
- if: ${{ matrix.use-cache }}
|
||||
uses: actions/cache@v2
|
||||
id: cache-repo
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
contracts/build
|
||||
key: ${{ needs.initialize.outputs.cache_key }}
|
||||
- run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
- run: ${{ !matrix.use-cache || steps.cache-repo.outputs.cache-hit }} && yarn run ${{ matrix.task }}
|
||||
deployment:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-molecule-runner
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: [oracle, ui, monitor, multiple, repo]
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- run: echo "::set-env name=MOLECULE_RUNNER_TAG::${{ hashFiles('./deployment-e2e/Dockerfile') }}"
|
||||
- run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
- run: deployment-e2e/molecule.sh ${{ matrix.task }}
|
||||
ultimate:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- initialize
|
||||
- build-e2e-images
|
||||
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags')
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
task: [amb, erc-to-erc, erc-to-native, native-to-erc, amb-stake-erc-to-erc]
|
||||
include:
|
||||
- task: erc-to-erc
|
||||
ui-e2e-grep: 'ERC TO ERC'
|
||||
- task: erc-to-native
|
||||
ui-e2e-grep: 'ERC TO NATIVE'
|
||||
- task: native-to-erc
|
||||
ui-e2e-grep: 'NATIVE TO ERC'
|
||||
- task: amb-stake-erc-to-erc
|
||||
ui-e2e-grep: 'AMB-STAKE-ERC-TO-ERC'
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: true
|
||||
- run: |
|
||||
git submodule status > submodule.status
|
||||
echo "::set-env name=E2E_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e') }}"
|
||||
echo "::set-env name=ORACLE_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}"
|
||||
echo "::set-env name=MONITOR_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}"
|
||||
echo "::set-env name=UI_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'ui') }}"
|
||||
echo "::set-env name=ALM_TAG::${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}"
|
||||
echo "::set-env name=MOLECULE_RUNNER_TAG::${{ hashFiles('./deployment-e2e/Dockerfile') }}"
|
||||
- uses: actions/cache@v2
|
||||
id: cache-repo
|
||||
with:
|
||||
path: |
|
||||
**/node_modules
|
||||
contracts/build
|
||||
key: ${{ needs.initialize.outputs.cache_key }}
|
||||
- run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
|
||||
- run: ${{ steps.cache-repo.outputs.cache-hit }} && e2e-commons/up.sh deploy blocks
|
||||
- run: docker-compose -f ./e2e-commons/docker-compose.yml pull oracle
|
||||
- run: deployment-e2e/molecule.sh ultimate-${{ matrix.task }}
|
||||
- run: sudo chown -R $USER:docker /var/run/docker.sock
|
||||
- if: ${{ matrix.ui-e2e-grep }}
|
||||
run: cd ui-e2e && xvfb-run yarn mocha -g "${{ matrix.ui-e2e-grep }}" -b ./test.js
|
||||
- if: ${{ !matrix.ui-e2e-grep }}
|
||||
run: docker-compose -f ./e2e-commons/docker-compose.yml run e2e yarn workspace oracle-e2e run ${{ matrix.task }}
|
||||
@@ -12,7 +12,7 @@ COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from th
|
||||
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. | 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. | NATIVE_TO_ERC / ERC_TO_ERC / 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,6 +36,11 @@ 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"
|
||||
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`
|
||||
|
||||
|
||||
## UI configuration
|
||||
|
||||
@@ -1,15 +1,32 @@
|
||||
FROM node:10 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:10
|
||||
|
||||
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 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/
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -110,4 +110,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": ">= 10.18"
|
||||
}
|
||||
}
|
||||
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 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/0x58e7d63368335b9591d4dbb43889084f698fcee93ab7656fd7a39d8c66bc4b60'
|
||||
const foreignToHomeTxURL = 'http://localhost:3004/42/0x592bf28fc896419d2838f71cd0388775814b692688f1ecd5b1519081566b994a'
|
||||
|
||||
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
|
||||
}
|
||||
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,20 +1,29 @@
|
||||
FROM node:10 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 --production
|
||||
|
||||
COPY ./commons ./commons
|
||||
|
||||
COPY ./alm ./alm
|
||||
|
||||
ARG DOT_ENV_PATH=./alm/.env
|
||||
COPY ${DOT_ENV_PATH} ./alm/.env
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
"@use-it/interval": "^0.1.3",
|
||||
"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",
|
||||
@@ -30,11 +31,12 @@
|
||||
"web3-eth-contract": "1.2.7"
|
||||
},
|
||||
"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"
|
||||
|
||||
118
alm/scripts/createSnapshots.js
Normal file
118
alm/scripts/createSnapshots.js
Normal file
@@ -0,0 +1,118 @@
|
||||
const { BRIDGE_VALIDATORS_ABI, HOME_AMB_ABI } = require('commons')
|
||||
|
||||
const path = require('path')
|
||||
require('dotenv').config()
|
||||
const Web3 = require('web3')
|
||||
|
||||
const fs = require('fs')
|
||||
|
||||
const {
|
||||
COMMON_HOME_RPC_URL,
|
||||
COMMON_HOME_BRIDGE_ADDRESS,
|
||||
COMMON_FOREIGN_RPC_URL,
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS
|
||||
} = 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 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 bridgeContract.getPastEvents('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 validatorContract.getPastEvents('RequiredSignaturesChanged', {
|
||||
fromBlock: 0,
|
||||
toBlock: currentBlockNumber
|
||||
})
|
||||
snapshot.RequiredSignaturesChanged = RequiredSignaturesChangedEvents.map(e => ({
|
||||
blockNumber: e.blockNumber,
|
||||
returnValues: {
|
||||
requiredSignatures: e.returnValues.requiredSignatures
|
||||
}
|
||||
}))
|
||||
|
||||
// Save ValidatorAdded events
|
||||
const validatorAddedEvents = await validatorContract.getPastEvents('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 validatorContract.getPastEvents('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,5 +0,0 @@
|
||||
import React from 'react'
|
||||
|
||||
test('renders learn react link', () => {
|
||||
// Removed basic test from setup. Keeping this so CI passes until we add unit tests.
|
||||
})
|
||||
@@ -4,7 +4,7 @@ import { useMessageConfirmations } from '../hooks/useMessageConfirmations'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
import styled from 'styled-components'
|
||||
import { CONFIRMATIONS_STATUS } from '../config/constants'
|
||||
import { CONFIRMATIONS_STATUS_LABEL } from '../config/descriptions'
|
||||
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'
|
||||
@@ -12,6 +12,8 @@ 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;
|
||||
@@ -47,7 +49,7 @@ export const ConfirmationsContainer = ({ message, receipt, fromHome, timestamp }
|
||||
} = useStateProvider()
|
||||
const { requiredSignatures, validatorList } = useValidatorContract({ fromHome, receipt })
|
||||
const { blockConfirmations } = useBlockConfirmations({ fromHome, receipt })
|
||||
const { confirmations, status, executionData, signatureCollected } = useMessageConfirmations({
|
||||
const { confirmations, status, executionData, signatureCollected, waitingBlocksResolved } = useMessageConfirmations({
|
||||
message,
|
||||
receipt,
|
||||
fromHome,
|
||||
@@ -57,26 +59,48 @@ export const ConfirmationsContainer = ({ message, receipt, fromHome, timestamp }
|
||||
blockConfirmations
|
||||
})
|
||||
|
||||
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>
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? CONFIRMATIONS_STATUS_LABEL[status] : <SimpleLoading />}
|
||||
<StatusResultLabel data-id="status">
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? statusLabel[status] : <SimpleLoading />}
|
||||
</StatusResultLabel>
|
||||
</div>
|
||||
<StatusDescription className="row is-center">
|
||||
<p className="col-10">
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED
|
||||
? getConfirmationsStatusDescription(status, homeName, foreignName)
|
||||
: ''}
|
||||
</p>
|
||||
<MultiLine className="col-10">
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? parseDescription() : ''}
|
||||
</MultiLine>
|
||||
</StatusDescription>
|
||||
<ValidatorsConfirmations
|
||||
confirmations={confirmations}
|
||||
requiredSignatures={requiredSignatures}
|
||||
validatorList={validatorList}
|
||||
waitingBlocksResolved={waitingBlocksResolved}
|
||||
/>
|
||||
{signatureCollected && <ExecutionConfirmation executionData={executionData} isHome={!fromHome} />}
|
||||
</StyledConfirmationContainer>
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from 'react'
|
||||
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import styled from 'styled-components'
|
||||
import { ExecutionData } from '../hooks/useMessageConfirmations'
|
||||
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
|
||||
const Thead = styled.thead`
|
||||
border-bottom: 2px solid #9e9e9e;
|
||||
`
|
||||
import { Thead, AgeTd, StatusTd } from './commons/Table'
|
||||
|
||||
const StyledExecutionConfirmation = styled.div`
|
||||
margin-top: 30px;
|
||||
@@ -38,7 +35,11 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir
|
||||
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
|
||||
return <GreyLabel>{validatorStatus}</GreyLabel>
|
||||
default:
|
||||
return <SimpleLoading />
|
||||
return executionData.validator ? (
|
||||
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,12 +56,18 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{formattedValidator ? formattedValidator : <SimpleLoading />}</td>
|
||||
<td className="text-center">{getExecutionStatusElement(executionData.status)}</td>
|
||||
<td className="text-center">
|
||||
<ExplorerTxLink href={txExplorerLink} target="_blank">
|
||||
{executionData.timestamp > 0 ? formatTimestamp(executionData.timestamp) : ''}
|
||||
</ExplorerTxLink>
|
||||
</td>
|
||||
<StatusTd className="text-center">{getExecutionStatusElement(executionData.status)}</StatusTd>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -35,8 +35,13 @@ export const Form = ({ onSubmit }: { onSubmit: ({ chainId, txHash, receipt }: Fo
|
||||
onSubmit({ chainId, txHash, receipt })
|
||||
}
|
||||
|
||||
const onBack = () => {
|
||||
setTxHash('')
|
||||
setSearchTx(false)
|
||||
}
|
||||
|
||||
if (searchTx) {
|
||||
return <TransactionSelector txHash={txHash} onSelected={onSelected} />
|
||||
return <TransactionSelector txHash={txHash} onSelected={onSelected} onBack={onBack} />
|
||||
}
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import React, { useState } from 'react'
|
||||
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;
|
||||
@@ -32,6 +35,14 @@ const HeaderContainer = styled.header`
|
||||
}
|
||||
`
|
||||
|
||||
const AlertP = styled.p`
|
||||
align-items: start;
|
||||
margin-bottom: 0;
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`
|
||||
|
||||
export interface FormSubmitParams {
|
||||
chainId: number
|
||||
txHash: string
|
||||
@@ -43,6 +54,27 @@ export const MainPage = () => {
|
||||
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
|
||||
@@ -64,6 +96,13 @@ export const MainPage = () => {
|
||||
setNetworkData(chainId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const w = window as any
|
||||
if (w.ethereum) {
|
||||
w.ethereum.autoRefreshOnNetworkChange = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<StyledMainPage>
|
||||
<Header>
|
||||
@@ -73,6 +112,25 @@ export const MainPage = () => {
|
||||
</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']}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { Link, useHistory, useParams } from 'react-router-dom'
|
||||
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'
|
||||
@@ -8,23 +8,8 @@ import { Loading } from './commons/Loading'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { ConfirmationsContainer } from './ConfirmationsContainer'
|
||||
import { LeftArrow } from './commons/LeftArrow'
|
||||
import styled from 'styled-components'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
|
||||
const BackButton = 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;
|
||||
`
|
||||
import { BackButton } from './commons/BackButton'
|
||||
|
||||
export interface StatusContainerParam {
|
||||
onBackToMain: () => void
|
||||
@@ -79,10 +64,6 @@ export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptPar
|
||||
const displayReference = multiMessageSelected ? messages[selectedMessageId].id : txHash
|
||||
const formattedMessageId = formatTxHash(displayReference)
|
||||
|
||||
const displayedDescription = multiMessageSelected
|
||||
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
|
||||
: description
|
||||
|
||||
const isHome = chainId === home.chainId.toString()
|
||||
const txExplorerLink = getExplorerTxUrl(txHash, isHome)
|
||||
const displayExplorerLink = status !== TRANSACTION_STATUS.NOT_FOUND
|
||||
@@ -90,33 +71,39 @@ export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptPar
|
||||
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 request{' '}
|
||||
The transaction{' '}
|
||||
{displayExplorerLink && (
|
||||
<ExplorerTxLink href={txExplorerLink} target="blank">
|
||||
<ExplorerTxLink href={txExplorerLink} target="_blank">
|
||||
{formattedMessageId}
|
||||
</ExplorerTxLink>
|
||||
)}
|
||||
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription}
|
||||
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription} {link}
|
||||
</p>
|
||||
)}
|
||||
{displayMessageSelector && <MessageSelector messages={messages} onMessageSelected={onMessageSelected} />}
|
||||
{displayConfirmations && (
|
||||
<ConfirmationsContainer message={messageToConfirm} receipt={receipt} fromHome={isHome} timestamp={timestamp} />
|
||||
)}
|
||||
<div className="row is-center">
|
||||
<div className="col-9">
|
||||
<Link to="/" onClick={onBackToMain}>
|
||||
<BackButton className="button outline is-left">
|
||||
<LeftArrow />
|
||||
<BackLabel>Search another transaction</BackLabel>
|
||||
</BackButton>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<BackButton onBackToMain={onBackToMain} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -5,13 +5,23 @@ 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
|
||||
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 })
|
||||
@@ -43,5 +53,15 @@ export const TransactionSelector = ({
|
||||
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 />
|
||||
}
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import React from 'react'
|
||||
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { 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'
|
||||
|
||||
const Thead = styled.thead`
|
||||
border-bottom: 2px solid #9e9e9e;
|
||||
`
|
||||
import { Thead, AgeTd, StatusTd } from './commons/Table'
|
||||
|
||||
const RequiredConfirmations = styled.label`
|
||||
font-size: 14px;
|
||||
@@ -20,12 +17,14 @@ export interface ValidatorsConfirmationsParams {
|
||||
confirmations: Array<ConfirmationParam>
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
waitingBlocksResolved: boolean
|
||||
}
|
||||
|
||||
export const ValidatorsConfirmations = ({
|
||||
confirmations,
|
||||
requiredSignatures,
|
||||
validatorList
|
||||
validatorList,
|
||||
waitingBlocksResolved
|
||||
}: ValidatorsConfirmationsParams) => {
|
||||
const windowWidth = useWindowWidth()
|
||||
|
||||
@@ -40,7 +39,11 @@ export const ValidatorsConfirmations = ({
|
||||
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
|
||||
return <GreyLabel>{validatorStatus}</GreyLabel>
|
||||
default:
|
||||
return <SimpleLoading />
|
||||
return waitingBlocksResolved ? (
|
||||
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,21 +66,28 @@ export const ValidatorsConfirmations = ({
|
||||
const elementIfNoTimestamp =
|
||||
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.WAITING &&
|
||||
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED ? (
|
||||
<SimpleLoading />
|
||||
(displayedStatus === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || displayedStatus === '') &&
|
||||
waitingBlocksResolved ? (
|
||||
SEARCHING_TX
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td>{windowWidth < 850 ? formatTxHash(validator) : validator}</td>
|
||||
<td className="text-center">{getValidatorStatusElement(displayedStatus)}</td>
|
||||
<td className="text-center">
|
||||
<ExplorerTxLink href={explorerLink} target="_blank">
|
||||
{confirmation && confirmation.timestamp > 0
|
||||
? formatTimestamp(confirmation.timestamp)
|
||||
: elementIfNoTimestamp}
|
||||
</ExplorerTxLink>
|
||||
</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>
|
||||
)
|
||||
})}
|
||||
|
||||
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>
|
||||
)
|
||||
21
alm/src/components/commons/CloseIcon.tsx
Normal file
21
alm/src/components/commons/CloseIcon.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from 'react'
|
||||
|
||||
export const CloseIcon = () => (
|
||||
<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="#1890ff"
|
||||
height="1em"
|
||||
>
|
||||
<path
|
||||
fill="#1890ff"
|
||||
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>
|
||||
)
|
||||
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 = () => (
|
||||
<svg
|
||||
className="col-1 is-left"
|
||||
viewBox="64 64 896 896"
|
||||
focusable="false"
|
||||
data-icon="info-circle"
|
||||
width="1em"
|
||||
height="1em"
|
||||
fill="#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>
|
||||
)
|
||||
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;
|
||||
`
|
||||
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;
|
||||
`
|
||||
@@ -45,7 +45,9 @@ export const CONFIRMATIONS_STATUS = {
|
||||
EXECUTION_WAITING: 'EXECUTION_WAITING',
|
||||
FAILED: 'FAILED',
|
||||
PENDING: 'PENDING',
|
||||
WAITING: 'WAITING',
|
||||
SEARCHING: 'SEARCHING',
|
||||
WAITING_VALIDATORS: 'WAITING_VALIDATORS',
|
||||
WAITING_CHAIN: 'WAITING_CHAIN',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
@@ -57,3 +59,5 @@ export const VALIDATOR_CONFIRMATION_STATUS = {
|
||||
NOT_REQUIRED: 'Not required',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const SEARCHING_TX = 'Searching Transaction...'
|
||||
|
||||
@@ -2,35 +2,72 @@
|
||||
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: 'execution succeeded %t but it does not contain any bridge messages',
|
||||
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: 'was not found'
|
||||
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: 'Execution waiting',
|
||||
FAILED: 'Failed',
|
||||
PENDING: 'Pending',
|
||||
WAITING: 'Waiting'
|
||||
FAILED: 'Confirmation Failed',
|
||||
PENDING: 'Confirmation Pending',
|
||||
WAITING_VALIDATORS: 'Confirmation Waiting',
|
||||
SEARCHING: 'Confirmation Waiting',
|
||||
WAITING_CHAIN: 'Confirmation Waiting'
|
||||
}
|
||||
|
||||
// %homeChain will be replaced by the home network name
|
||||
// %foreignChain will be replaced by the foreign network name
|
||||
// use %link to identify a link
|
||||
export const CONFIRMATIONS_STATUS_DESCRIPTION: { [key: string]: string } = {
|
||||
SUCCESS: '',
|
||||
SUCCESS_MESSAGE_FAILED:
|
||||
'Signatures have been collected in the %homeChain and they were successfully sent to the %foreignChain but the contained message execution failed.',
|
||||
EXECUTION_FAILED:
|
||||
'Signatures have been collected in the %homeChain and they were sent to the %foreignChain but the transaction with signatures failed',
|
||||
EXECUTION_PENDING:
|
||||
'Signatures have been collected in the %homeChain and they were sent to the %foreignChain but the transaction is in the pending state (transactions congestion or low gas price)',
|
||||
EXECUTION_WAITING: 'Execution waiting',
|
||||
'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:
|
||||
'Some validators sent improper transactions as so they were failed, collected confirmations are not enough to execute the relay request',
|
||||
PENDING: 'Some confirmations are in pending state',
|
||||
WAITING: 'Validators are waiting for the chain finalization'
|
||||
'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\nvalidator’s 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\nvalidator’s transaction with collected signatures was\nsent but is not yet added to a block.',
|
||||
EXECUTION_WAITING:
|
||||
'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. If 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.'
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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'
|
||||
|
||||
export interface UseBlockConfirmationsParams {
|
||||
fromHome: boolean
|
||||
@@ -17,17 +18,19 @@ export const useBlockConfirmations = ({ receipt, fromHome }: UseBlockConfirmatio
|
||||
const callRequireBlockConfirmations = async (
|
||||
contract: Contract,
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber)
|
||||
const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber, snapshotProvider)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
|
||||
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
|
||||
if (!bridgeContract || !receipt) return
|
||||
callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations)
|
||||
callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations, snapshotProvider)
|
||||
},
|
||||
[home.bridgeContract, foreign.bridgeContract, receipt, fromHome]
|
||||
)
|
||||
|
||||
@@ -83,6 +83,13 @@ export const useMessageConfirmations = ({
|
||||
const [pendingConfirmations, setPendingConfirmations] = useState(false)
|
||||
const [pendingExecution, setPendingExecution] = useState(false)
|
||||
|
||||
const existsConfirmation = (confirmationArray: ConfirmationParam[]) => {
|
||||
const filteredList = confirmationArray.filter(
|
||||
c => c.status !== VALIDATOR_CONFIRMATION_STATUS.UNDEFINED && c.status !== VALIDATOR_CONFIRMATION_STATUS.WAITING
|
||||
)
|
||||
return filteredList.length > 0
|
||||
}
|
||||
|
||||
// Check if the validators are waiting for block confirmations to verify the message
|
||||
useEffect(
|
||||
() => {
|
||||
@@ -306,7 +313,7 @@ export const useMessageConfirmations = ({
|
||||
// Sets the message status based in the collected information
|
||||
useEffect(
|
||||
() => {
|
||||
if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS) {
|
||||
if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS && existsConfirmation(confirmations)) {
|
||||
const newStatus = executionData.executionResult
|
||||
? CONFIRMATIONS_STATUS.SUCCESS
|
||||
: CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED
|
||||
@@ -319,18 +326,24 @@ export const useMessageConfirmations = ({
|
||||
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.UNDEFINED)
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
|
||||
}
|
||||
} else {
|
||||
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
}
|
||||
} else if (waitingBlocks) {
|
||||
setStatus(CONFIRMATIONS_STATUS.WAITING)
|
||||
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)
|
||||
}
|
||||
@@ -344,7 +357,10 @@ export const useMessageConfirmations = ({
|
||||
failedConfirmations,
|
||||
failedExecution,
|
||||
pendingConfirmations,
|
||||
pendingExecution
|
||||
pendingExecution,
|
||||
waitingBlocksResolved,
|
||||
confirmations,
|
||||
waitingBlocksForExecutionResolved
|
||||
]
|
||||
)
|
||||
|
||||
@@ -352,6 +368,7 @@ export const useMessageConfirmations = ({
|
||||
confirmations,
|
||||
status,
|
||||
signatureCollected,
|
||||
executionData
|
||||
executionData,
|
||||
waitingBlocksResolved
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getWeb3 } from '../utils/web3'
|
||||
import { getChainId, getWeb3 } from '../utils/web3'
|
||||
import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export const useNetwork = (url: string) => {
|
||||
export const useNetwork = (url: string, snapshotProvider: SnapshotProvider) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [chainId, setChainId] = useState(0)
|
||||
const web3 = getWeb3(url)
|
||||
@@ -9,14 +10,14 @@ export const useNetwork = (url: string) => {
|
||||
useEffect(
|
||||
() => {
|
||||
setLoading(true)
|
||||
const getChainId = async () => {
|
||||
const id = await web3.eth.getChainId()
|
||||
const getWeb3ChainId = async () => {
|
||||
const id = await getChainId(web3, snapshotProvider)
|
||||
setChainId(id)
|
||||
setLoading(false)
|
||||
}
|
||||
getChainId()
|
||||
getWeb3ChainId()
|
||||
},
|
||||
[web3.eth]
|
||||
[web3, snapshotProvider]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '..
|
||||
import { BRIDGE_VALIDATORS_ABI } from '../abis'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export interface useValidatorContractParams {
|
||||
fromHome: boolean
|
||||
@@ -28,16 +29,22 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract
|
||||
const callRequiredSignatures = async (
|
||||
contract: Maybe<Contract>,
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getRequiredSignatures(contract, receipt.blockNumber)
|
||||
const result = await getRequiredSignatures(contract, receipt.blockNumber, snapshotProvider)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
const callValidatorList = async (contract: Maybe<Contract>, receipt: TransactionReceipt, setResult: Function) => {
|
||||
const callValidatorList = async (
|
||||
contract: Maybe<Contract>,
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getValidatorList(contract, receipt.blockNumber)
|
||||
const result = await getValidatorList(contract, receipt.blockNumber, snapshotProvider)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
@@ -55,10 +62,11 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract
|
||||
useEffect(
|
||||
() => {
|
||||
if (!receipt) return
|
||||
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures)
|
||||
callValidatorList(validatorContract, receipt, setValidatorList)
|
||||
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
|
||||
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures, snapshotProvider)
|
||||
callValidatorList(validatorContract, receipt, setValidatorList, snapshotProvider)
|
||||
},
|
||||
[validatorContract, receipt]
|
||||
[validatorContract, receipt, fromHome]
|
||||
)
|
||||
|
||||
return {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class BlockNumberProvider {
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = this.running - 1
|
||||
this.running = this.running > 0 ? this.running - 1 : 0
|
||||
|
||||
if (!this.running) {
|
||||
clearTimeout(this.ref)
|
||||
|
||||
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')
|
||||
0
alm/src/snapshots/.gitkeep
Normal file
0
alm/src/snapshots/.gitkeep
Normal file
@@ -11,6 +11,7 @@ import {
|
||||
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
|
||||
@@ -47,8 +48,8 @@ const initialState = {
|
||||
const StateContext = createContext<StateContext>(initialState)
|
||||
|
||||
export const StateProvider = ({ children }: { children: ReactNode }) => {
|
||||
const homeNetwork = useNetwork(HOME_RPC_URL)
|
||||
const foreignNetwork = useNetwork(FOREIGN_RPC_URL)
|
||||
const homeNetwork = useNetwork(HOME_RPC_URL, homeSnapshotProvider)
|
||||
const foreignNetwork = useNetwork(FOREIGN_RPC_URL, foreignSnapshotProvider)
|
||||
const { homeBridge, foreignBridge } = useBridgeContracts({
|
||||
homeWeb3: homeNetwork.web3,
|
||||
foreignWeb3: foreignNetwork.web3
|
||||
|
||||
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: () => {
|
||||
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(() => []),
|
||||
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(() => [
|
||||
{
|
||||
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(() => [
|
||||
{
|
||||
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(() => [])
|
||||
} 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(() => [
|
||||
{
|
||||
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(() => [
|
||||
{
|
||||
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(() => []),
|
||||
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(() => []),
|
||||
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(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(() => []),
|
||||
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(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
|
||||
})
|
||||
})
|
||||
})
|
||||
155
alm/src/utils/__tests__/explorer.test.ts
Normal file
155
alm/src/utils/__tests__/explorer.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
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'
|
||||
|
||||
describe('getFailedTransactions', () => {
|
||||
test('should only return failed transactions', async () => {
|
||||
const transactions = [{ isError: '0' }, { isError: '1' }, { isError: '0' }, { isError: '1' }, { isError: '1' }]
|
||||
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
const result = await getFailedTransactions('', '', 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(3)
|
||||
})
|
||||
})
|
||||
describe('getSuccessTransactions', () => {
|
||||
test('should only return success transactions', async () => {
|
||||
const transactions = [{ isError: '0' }, { isError: '1' }, { isError: '0' }, { isError: '1' }, { isError: '1' }]
|
||||
|
||||
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
|
||||
const result = await getSuccessTransactions('', '', 0, 1, '', fetchAccountTransactions)
|
||||
expect(result.length).toEqual(2)
|
||||
})
|
||||
})
|
||||
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,
|
||||
startTimestamp: 0,
|
||||
endTimestamp: 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 })
|
||||
})
|
||||
})
|
||||
705
alm/src/utils/__tests__/getConfirmationsForTx.test.ts
Normal file
705
alm/src/utils/__tests__/getConfirmationsForTx.test.ts
Normal file
@@ -0,0 +1,705 @@
|
||||
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 { BasicConfirmationParam } from '../../hooks/useMessageConfirmations'
|
||||
|
||||
jest.mock('../validatorConfirmationHelpers')
|
||||
|
||||
const getValidatorSuccessTransaction = helpers.getValidatorSuccessTransaction 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', '')}`
|
||||
}
|
||||
} as Web3
|
||||
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
|
||||
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
|
||||
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
|
||||
const validatorList = [validator1, validator2, validator3]
|
||||
const bridgeContract = {} as Contract
|
||||
const confirmationContractMethod = () => {}
|
||||
const requiredSignatures = 2
|
||||
const waitingBlocksResolved = true
|
||||
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:
|
||||
getValidatorSuccessTransaction.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
|
||||
}))
|
||||
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(1)
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getValidatorSuccessTransaction).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)
|
||||
|
||||
expect(setResult.mock.calls[0][0]()).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 }
|
||||
])
|
||||
)
|
||||
expect(setResult.mock.calls[1][0]).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 }
|
||||
])
|
||||
)
|
||||
})
|
||||
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
|
||||
}))
|
||||
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getValidatorSuccessTransaction).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
|
||||
}))
|
||||
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator !== validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator3 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: '',
|
||||
timestamp: 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getValidatorSuccessTransaction).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)
|
||||
|
||||
expect(setResult.mock.calls[0][0]()).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 }
|
||||
])
|
||||
)
|
||||
expect(setResult.mock.calls[1][0]).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 }
|
||||
])
|
||||
)
|
||||
})
|
||||
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
|
||||
}))
|
||||
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator3
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator3 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator3 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getValidatorSuccessTransaction).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)
|
||||
|
||||
expect(setResult.mock.calls[0][0]()).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
|
||||
])
|
||||
)
|
||||
expect(setResult.mock.calls[1][0]).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, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
|
||||
])
|
||||
)
|
||||
})
|
||||
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
|
||||
}))
|
||||
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator2 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getValidatorSuccessTransaction).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)
|
||||
|
||||
expect(setResult.mock.calls[0][0]()).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(setResult.mock.calls[1][0]).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.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
|
||||
}))
|
||||
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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: BasicConfirmationParam) => ({
|
||||
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,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getValidatorSuccessTransaction).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)
|
||||
|
||||
expect(setResult.mock.calls[0][0]()).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(setResult.mock.calls[1][0]).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 () => {
|
||||
// Validator1 success
|
||||
// Validator2 failed
|
||||
// Validator3 Pending
|
||||
|
||||
getValidatorConfirmation
|
||||
.mockImplementationOnce(() => async (validator: string) => ({
|
||||
validator,
|
||||
status:
|
||||
validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
.mockImplementation(() => async (validator: string) => ({
|
||||
validator,
|
||||
status:
|
||||
validator !== validator2 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
}))
|
||||
getValidatorSuccessTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator === validator1 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator1 ? 123 : 0
|
||||
}))
|
||||
.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash: validatorData.validator !== validator2 ? '0x123' : '',
|
||||
timestamp: validatorData.validator !== validator2 ? 123 : 0
|
||||
}))
|
||||
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
validator: validatorData.validator,
|
||||
status:
|
||||
validatorData.validator === validator2
|
||||
? VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
txHash: validatorData.validator === validator2 ? '0x123' : '',
|
||||
timestamp: validatorData.validator === validator2 ? 123 : 0
|
||||
}))
|
||||
getValidatorPendingTransaction
|
||||
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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
|
||||
}))
|
||||
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
|
||||
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,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(2)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(1)
|
||||
expect(getValidatorSuccessTransaction).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)
|
||||
|
||||
expect(setResult.mock.calls[0][0]()).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
expect(setResult.mock.calls[1][0]).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.PENDING, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
|
||||
await getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(setResult).toBeCalledTimes(4)
|
||||
expect(getValidatorConfirmation).toBeCalledTimes(2)
|
||||
expect(getValidatorSuccessTransaction).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(false)
|
||||
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)
|
||||
|
||||
expect(setResult.mock.calls[2][0]()).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
|
||||
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
|
||||
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS }
|
||||
])
|
||||
)
|
||||
expect(setResult.mock.calls[3][0]).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.SUCCESS, txHash: '0x123', timestamp: 123 }
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
303
alm/src/utils/__tests__/getFinalizationEvent.test.ts
Normal file
303
alm/src/utils/__tests__/getFinalizationEvent.test.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
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 eventName = 'RelayedMessage'
|
||||
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 waitingBlocksResolved = true
|
||||
const message = {
|
||||
id: '0x123',
|
||||
data: '0x123456789'
|
||||
}
|
||||
const interval = 10000
|
||||
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: () => {
|
||||
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()
|
||||
|
||||
await getFinalizationEvent(
|
||||
contract,
|
||||
eventName,
|
||||
web3,
|
||||
setResult,
|
||||
waitingBlocksResolved,
|
||||
message,
|
||||
interval,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution
|
||||
)
|
||||
|
||||
unsubscribe()
|
||||
|
||||
expect(subscriptions.length).toEqual(0)
|
||||
expect(setResult).toBeCalledTimes(1)
|
||||
expect(setResult.mock.calls[0][0]).toEqual({
|
||||
validator: validator1,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
txHash,
|
||||
timestamp,
|
||||
executionResult: true
|
||||
})
|
||||
|
||||
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: () => {
|
||||
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()
|
||||
|
||||
await getFinalizationEvent(
|
||||
contract,
|
||||
eventName,
|
||||
web3,
|
||||
setResult,
|
||||
waitingBlocksResolved,
|
||||
message,
|
||||
interval,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution
|
||||
)
|
||||
|
||||
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: () => {
|
||||
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()
|
||||
|
||||
await getFinalizationEvent(
|
||||
contract,
|
||||
eventName,
|
||||
web3,
|
||||
setResult,
|
||||
waitingBlocksResolved,
|
||||
message,
|
||||
interval,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution
|
||||
)
|
||||
|
||||
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: () => {
|
||||
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()
|
||||
|
||||
await getFinalizationEvent(
|
||||
contract,
|
||||
eventName,
|
||||
web3,
|
||||
setResult,
|
||||
waitingBlocksResolved,
|
||||
message,
|
||||
interval,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
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: () => {
|
||||
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()
|
||||
|
||||
await getFinalizationEvent(
|
||||
contract,
|
||||
eventName,
|
||||
web3,
|
||||
setResult,
|
||||
waitingBlocksResolved,
|
||||
message,
|
||||
interval,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution
|
||||
)
|
||||
|
||||
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
|
||||
})
|
||||
|
||||
expect(getFailedExecution).toBeCalledTimes(1)
|
||||
expect(setFailedExecution).toBeCalledTimes(1)
|
||||
|
||||
expect(getPendingExecution).toBeCalledTimes(1)
|
||||
expect(setPendingExecution).toBeCalledTimes(0)
|
||||
})
|
||||
})
|
||||
@@ -1,10 +1,24 @@
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { EventData } from 'web3-eth-contract'
|
||||
import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export const getRequiredBlockConfirmations = async (contract: Contract, blockNumber: number) => {
|
||||
const events = await contract.getPastEvents('RequiredBlockConfirmationChanged', {
|
||||
fromBlock: 0,
|
||||
toBlock: blockNumber
|
||||
})
|
||||
export const getRequiredBlockConfirmations = async (
|
||||
contract: Contract,
|
||||
blockNumber: number,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
const eventsFromSnapshot = snapshotProvider.requiredBlockConfirmationEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
||||
let contractEvents: EventData[] = []
|
||||
if (blockNumber > snapshotBlockNumber) {
|
||||
contractEvents = await contract.getPastEvents('RequiredBlockConfirmationChanged', {
|
||||
fromBlock: snapshotBlockNumber + 1,
|
||||
toBlock: blockNumber
|
||||
})
|
||||
}
|
||||
|
||||
const events = [...eventsFromSnapshot, ...contractEvents]
|
||||
|
||||
let blockConfirmations
|
||||
if (events.length > 0) {
|
||||
@@ -21,11 +35,23 @@ export const getRequiredBlockConfirmations = async (contract: Contract, blockNum
|
||||
|
||||
export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call()
|
||||
|
||||
export const getRequiredSignatures = async (contract: Contract, blockNumber: number) => {
|
||||
const events = await contract.getPastEvents('RequiredSignaturesChanged', {
|
||||
fromBlock: 0,
|
||||
toBlock: blockNumber
|
||||
})
|
||||
export const getRequiredSignatures = async (
|
||||
contract: Contract,
|
||||
blockNumber: number,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
const eventsFromSnapshot = snapshotProvider.requiredSignaturesEvents(blockNumber)
|
||||
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
|
||||
|
||||
let contractEvents: EventData[] = []
|
||||
if (blockNumber > snapshotBlockNumber) {
|
||||
contractEvents = await contract.getPastEvents('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]
|
||||
@@ -33,19 +59,26 @@ export const getRequiredSignatures = async (contract: Contract, blockNumber: num
|
||||
return parseInt(requiredSignatures)
|
||||
}
|
||||
|
||||
export const getValidatorList = async (contract: Contract, blockNumber: number) => {
|
||||
let currentList: string[] = await contract.methods.validatorList().call()
|
||||
const [added, removed] = await Promise.all([
|
||||
export const getValidatorList = async (contract: Contract, blockNumber: number, snapshotProvider: SnapshotProvider) => {
|
||||
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(),
|
||||
contract.getPastEvents('ValidatorAdded', {
|
||||
fromBlock: blockNumber
|
||||
fromBlock
|
||||
}),
|
||||
contract.getPastEvents('ValidatorRemoved', {
|
||||
fromBlock: blockNumber
|
||||
fromBlock
|
||||
})
|
||||
])
|
||||
|
||||
// Ordered desc
|
||||
const orderedEvents = [...added, ...removed].sort(({ blockNumber: prev }, { blockNumber: next }) => next - prev)
|
||||
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)
|
||||
|
||||
@@ -22,8 +22,8 @@ export const checkWaitingBlocksForExecution = async (
|
||||
timestamp: 0,
|
||||
executionResult: false
|
||||
})
|
||||
setWaitingBlocksForExecution(false)
|
||||
setWaitingBlocksForExecutionResolved(true)
|
||||
setWaitingBlocksForExecution(false)
|
||||
blockProvider.stop()
|
||||
} else {
|
||||
let nextInterval = interval
|
||||
|
||||
@@ -217,14 +217,11 @@ export const getValidatorSuccessTransactionsForMessage = async ({
|
||||
return filterValidatorSignatureTransaction(transactions, messageData)
|
||||
}
|
||||
|
||||
export const getExecutionFailedTransactionForMessage = async ({
|
||||
account,
|
||||
to,
|
||||
messageData,
|
||||
startTimestamp,
|
||||
endTimestamp
|
||||
}: GetFailedTransactionParams): Promise<APITransaction[]> => {
|
||||
const failedTransactions = await getFailedTransactions(
|
||||
export const getExecutionFailedTransactionForMessage = async (
|
||||
{ account, to, messageData, startTimestamp, endTimestamp }: GetFailedTransactionParams,
|
||||
getFailedTransactionsMethod = getFailedTransactions
|
||||
): Promise<APITransaction[]> => {
|
||||
const failedTransactions = await getFailedTransactionsMethod(
|
||||
account,
|
||||
to,
|
||||
startTimestamp,
|
||||
@@ -237,12 +234,11 @@ export const getExecutionFailedTransactionForMessage = async ({
|
||||
return failedTransactions.filter(t => t.input.includes(EXECUTE_SIGNATURES_HASH) && t.input.includes(messageDataValue))
|
||||
}
|
||||
|
||||
export const getValidatorPendingTransactionsForMessage = async ({
|
||||
account,
|
||||
to,
|
||||
messageData
|
||||
}: GetPendingTransactionParams): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactions({
|
||||
export const getValidatorPendingTransactionsForMessage = async (
|
||||
{ account, to, messageData }: GetPendingTransactionParams,
|
||||
fetchPendingTransactionsMethod = fetchPendingTransactions
|
||||
): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactionsMethod({
|
||||
account,
|
||||
api: HOME_EXPLORER_API
|
||||
})
|
||||
@@ -258,12 +254,11 @@ export const getValidatorPendingTransactionsForMessage = async ({
|
||||
)
|
||||
}
|
||||
|
||||
export const getExecutionPendingTransactionsForMessage = async ({
|
||||
account,
|
||||
to,
|
||||
messageData
|
||||
}: GetPendingTransactionParams): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactions({
|
||||
export const getExecutionPendingTransactionsForMessage = async (
|
||||
{ account, to, messageData }: GetPendingTransactionParams,
|
||||
fetchPendingTransactionsMethod = fetchPendingTransactions
|
||||
): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactionsMethod({
|
||||
account,
|
||||
api: FOREIGN_EXPLORER_API
|
||||
})
|
||||
|
||||
@@ -1,176 +1,19 @@
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import validatorsCache from '../services/ValidatorsCache'
|
||||
import {
|
||||
CACHE_KEY_FAILED,
|
||||
CACHE_KEY_SUCCESS,
|
||||
HOME_RPC_POLLING_INTERVAL,
|
||||
ONE_DAY_TIMESTAMP,
|
||||
VALIDATOR_CONFIRMATION_STATUS
|
||||
} from '../config/constants'
|
||||
import { HOME_RPC_POLLING_INTERVAL, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import {
|
||||
GetFailedTransactionParams,
|
||||
APITransaction,
|
||||
APIPendingTransaction,
|
||||
GetPendingTransactionParams
|
||||
} from './explorer'
|
||||
import { BasicConfirmationParam, ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
export const getValidatorConfirmation = (
|
||||
web3: Web3,
|
||||
hashMsg: string,
|
||||
bridgeContract: Contract,
|
||||
confirmationContractMethod: Function
|
||||
) => async (validator: string): Promise<BasicConfirmationParam> => {
|
||||
const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg)
|
||||
|
||||
const signatureFromCache = validatorsCache.get(hashSenderMsg)
|
||||
if (signatureFromCache) {
|
||||
return {
|
||||
validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg)
|
||||
const status = confirmed ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
|
||||
// If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change
|
||||
if (confirmed) {
|
||||
validatorsCache.set(hashSenderMsg, confirmed)
|
||||
}
|
||||
|
||||
return {
|
||||
validator,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidatorSuccessTransaction = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
timestamp: number,
|
||||
getSuccessTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: BasicConfirmationParam): 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,
|
||||
startTimestamp: timestamp,
|
||||
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
|
||||
})
|
||||
|
||||
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 = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
timestamp: number,
|
||||
getFailedTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: BasicConfirmationParam): 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,
|
||||
startTimestamp: timestamp,
|
||||
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
|
||||
})
|
||||
const newStatus =
|
||||
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.FAILED : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
|
||||
let txHashTimestamp = 0
|
||||
let txHash = ''
|
||||
// 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]
|
||||
txHashTimestamp = parseInt(failedTx.timeStamp)
|
||||
txHash = failedTx.hash
|
||||
|
||||
validatorsCache.setData(validatorCacheKey, {
|
||||
validator: validatorData.validator,
|
||||
status: newStatus,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
validator: validatorData.validator,
|
||||
status: newStatus,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidatorPendingTransaction = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>
|
||||
) => async (validatorData: BasicConfirmationParam): 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
|
||||
}
|
||||
}
|
||||
import {
|
||||
getValidatorConfirmation,
|
||||
getValidatorFailedTransaction,
|
||||
getValidatorPendingTransaction,
|
||||
getValidatorSuccessTransaction
|
||||
} from './validatorConfirmationHelpers'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
export const getConfirmationsForTx = async (
|
||||
messageData: string,
|
||||
@@ -201,9 +44,22 @@ export const getConfirmationsForTx = async (
|
||||
|
||||
const successConfirmations = validatorConfirmations.filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
|
||||
|
||||
setResult((prevConfirmations: ConfirmationParam[]) => {
|
||||
if (prevConfirmations && prevConfirmations.length) {
|
||||
successConfirmations.forEach(validatorData => {
|
||||
const index = prevConfirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
validatorConfirmations[index] = validatorData
|
||||
})
|
||||
return prevConfirmations
|
||||
} else {
|
||||
return validatorConfirmations
|
||||
}
|
||||
})
|
||||
|
||||
const notSuccessConfirmations = validatorConfirmations.filter(c => c.status !== VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
|
||||
|
||||
// If signatures not collected, it needs to retry in the next blocks
|
||||
// If signatures not collected, look for pending transactions
|
||||
let pendingConfirmationsResult = false
|
||||
if (successConfirmations.length !== requiredSignatures) {
|
||||
// Check if confirmation is pending
|
||||
const validatorPendingConfirmationsChecks = await Promise.all(
|
||||
@@ -219,51 +75,56 @@ export const getConfirmationsForTx = async (
|
||||
})
|
||||
|
||||
if (validatorPendingConfirmations.length > 0) {
|
||||
setPendingConfirmations(true)
|
||||
pendingConfirmationsResult = true
|
||||
}
|
||||
}
|
||||
|
||||
const undefinedConfirmations = validatorConfirmations.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
)
|
||||
// Check if confirmation failed
|
||||
const validatorFailedConfirmationsChecks = await Promise.all(
|
||||
undefinedConfirmations.map(
|
||||
getValidatorFailedTransaction(bridgeContract, messageData, timestamp, getFailedTransactions)
|
||||
)
|
||||
)
|
||||
const validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
)
|
||||
validatorFailedConfirmations.forEach(validatorData => {
|
||||
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
validatorConfirmations[index] = validatorData
|
||||
})
|
||||
const messageConfirmationsFailed = validatorFailedConfirmations.length > validatorList.length - requiredSignatures
|
||||
if (messageConfirmationsFailed) {
|
||||
setFailedConfirmations(true)
|
||||
}
|
||||
const undefinedConfirmations = validatorConfirmations.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
)
|
||||
|
||||
const missingConfirmations = validatorConfirmations.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
// Check if confirmation failed
|
||||
let failedConfirmationsResult = false
|
||||
const validatorFailedConfirmationsChecks = await Promise.all(
|
||||
undefinedConfirmations.map(
|
||||
getValidatorFailedTransaction(bridgeContract, messageData, timestamp, getFailedTransactions)
|
||||
)
|
||||
)
|
||||
const validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED
|
||||
)
|
||||
validatorFailedConfirmations.forEach(validatorData => {
|
||||
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
validatorConfirmations[index] = validatorData
|
||||
})
|
||||
const messageConfirmationsFailed = validatorFailedConfirmations.length > validatorList.length - requiredSignatures
|
||||
if (messageConfirmationsFailed) {
|
||||
failedConfirmationsResult = true
|
||||
}
|
||||
|
||||
if (missingConfirmations.length > 0) {
|
||||
shouldRetry = true
|
||||
}
|
||||
} else {
|
||||
// If signatures collected, it should set other signatures as not required
|
||||
const notRequiredConfirmations = notSuccessConfirmations.map(c => ({
|
||||
const missingConfirmations = validatorConfirmations.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
)
|
||||
|
||||
if (successConfirmations.length !== requiredSignatures && missingConfirmations.length > 0) {
|
||||
shouldRetry = true
|
||||
}
|
||||
|
||||
let signatureCollectedResult = false
|
||||
if (successConfirmations.length === requiredSignatures) {
|
||||
// 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
|
||||
}))
|
||||
|
||||
validatorConfirmations = [...successConfirmations, ...notRequiredConfirmations]
|
||||
setSignatureCollected(true)
|
||||
notRequiredConfirmations.forEach(validatorData => {
|
||||
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
validatorConfirmations[index] = validatorData
|
||||
})
|
||||
signatureCollectedResult = true
|
||||
}
|
||||
|
||||
// Set confirmations to update UI and continue requesting the transactions for the signatures
|
||||
setResult(validatorConfirmations)
|
||||
|
||||
// get transactions from success signatures
|
||||
const successConfirmationWithData = await Promise.all(
|
||||
validatorConfirmations
|
||||
@@ -282,7 +143,11 @@ export const getConfirmationsForTx = async (
|
||||
})
|
||||
}
|
||||
|
||||
// Set results
|
||||
setResult(updatedValidatorConfirmations)
|
||||
setFailedConfirmations(failedConfirmationsResult)
|
||||
setPendingConfirmations(pendingConfirmationsResult)
|
||||
setSignatureCollected(signatureCollectedResult)
|
||||
|
||||
// Retry if not all transaction were found for validator confirmations
|
||||
if (successConfirmationWithTxFound.length < successConfirmationWithData.length) {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { formatDistance } from 'date-fns'
|
||||
import { CONFIRMATIONS_STATUS_DESCRIPTION, TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
|
||||
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)
|
||||
@@ -31,11 +35,8 @@ export const getTransactionStatusDescription = (status: string, timestamp: Maybe
|
||||
return description
|
||||
}
|
||||
|
||||
export const getConfirmationsStatusDescription = (status: string, home: string, foreign: string) => {
|
||||
let description = CONFIRMATIONS_STATUS_DESCRIPTION[status]
|
||||
export const getConfirmationsStatusDescription = (status: string, home: string, foreign: string, fromHome: boolean) => {
|
||||
const statusDescription = fromHome ? CONFIRMATIONS_STATUS_DESCRIPTION_HOME : CONFIRMATIONS_STATUS_DESCRIPTION
|
||||
|
||||
description = description.replace('%homeChain', home)
|
||||
description = description.replace('%foreignChain', foreign)
|
||||
|
||||
return description
|
||||
return statusDescription[status]
|
||||
}
|
||||
|
||||
@@ -14,8 +14,8 @@ export const checkSignaturesWaitingForBLocks = async (
|
||||
const currentBlock = blockProvider.get()
|
||||
|
||||
if (currentBlock && currentBlock >= targetBlock) {
|
||||
setWaitingStatus(false)
|
||||
setWaitingBlocksResolved(true)
|
||||
setWaitingStatus(false)
|
||||
blockProvider.stop()
|
||||
} else {
|
||||
let nextInterval = interval
|
||||
|
||||
172
alm/src/utils/validatorConfirmationHelpers.ts
Normal file
172
alm/src/utils/validatorConfirmationHelpers.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { BasicConfirmationParam, ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
import validatorsCache from '../services/ValidatorsCache'
|
||||
import {
|
||||
CACHE_KEY_FAILED,
|
||||
CACHE_KEY_SUCCESS,
|
||||
ONE_DAY_TIMESTAMP,
|
||||
VALIDATOR_CONFIRMATION_STATUS
|
||||
} from '../config/constants'
|
||||
import {
|
||||
APIPendingTransaction,
|
||||
APITransaction,
|
||||
GetFailedTransactionParams,
|
||||
GetPendingTransactionParams
|
||||
} from './explorer'
|
||||
|
||||
export const getValidatorConfirmation = (
|
||||
web3: Web3,
|
||||
hashMsg: string,
|
||||
bridgeContract: Contract,
|
||||
confirmationContractMethod: Function
|
||||
) => async (validator: string): Promise<BasicConfirmationParam> => {
|
||||
const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg)
|
||||
|
||||
const signatureFromCache = validatorsCache.get(hashSenderMsg)
|
||||
if (signatureFromCache) {
|
||||
return {
|
||||
validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg)
|
||||
const status = confirmed ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
|
||||
// If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change
|
||||
if (confirmed) {
|
||||
validatorsCache.set(hashSenderMsg, confirmed)
|
||||
}
|
||||
|
||||
return {
|
||||
validator,
|
||||
status
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidatorSuccessTransaction = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
timestamp: number,
|
||||
getSuccessTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: BasicConfirmationParam): 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,
|
||||
startTimestamp: timestamp,
|
||||
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
|
||||
})
|
||||
|
||||
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 = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
timestamp: number,
|
||||
getFailedTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
|
||||
) => async (validatorData: BasicConfirmationParam): 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,
|
||||
startTimestamp: timestamp,
|
||||
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
|
||||
})
|
||||
const newStatus =
|
||||
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.FAILED : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
|
||||
let txHashTimestamp = 0
|
||||
let txHash = ''
|
||||
// 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]
|
||||
txHashTimestamp = parseInt(failedTx.timeStamp)
|
||||
txHash = failedTx.hash
|
||||
|
||||
validatorsCache.setData(validatorCacheKey, {
|
||||
validator: validatorData.validator,
|
||||
status: newStatus,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
validator: validatorData.validator,
|
||||
status: newStatus,
|
||||
txHash,
|
||||
timestamp: txHashTimestamp
|
||||
}
|
||||
}
|
||||
|
||||
export const getValidatorPendingTransaction = (
|
||||
bridgeContract: Contract,
|
||||
messageData: string,
|
||||
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>
|
||||
) => async (validatorData: BasicConfirmationParam): 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
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -61,3 +62,11 @@ export const getBlock = async (web3: Web3, blockNumber: number): Promise<BlockTr
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
export const getChainId = async (web3: Web3, snapshotProvider: SnapshotProvider) => {
|
||||
let id = snapshotProvider.chainId()
|
||||
if (id === 0) {
|
||||
id = await web3.eth.getChainId()
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
@@ -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()]} />
|
||||
|
||||
|
||||
@@ -93,8 +93,7 @@ if (process.env.REACT_APP_MODE === 'AMB_NATIVE_TO_ERC677') {
|
||||
network: process.env.REACT_APP_FOREIGN_NETWORK,
|
||||
// @ts-ignore
|
||||
address: process.env.REACT_APP_FOREIGN_TOKEN_ADDRESS,
|
||||
// @ts-ignore
|
||||
bridgeAddress: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
|
||||
bridgeModes: ['erc-to-native-amb']
|
||||
})
|
||||
|
||||
testBridge = new MediatorErcToNative({
|
||||
|
||||
@@ -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'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { MediatorErcToNative } from '../burner-wallet'
|
||||
|
||||
export default class QDAIBridge extends MediatorErcToNative {
|
||||
constructor() {
|
||||
super({
|
||||
assetA: 'qdai',
|
||||
assetABridge: '0xFEaB457D95D9990b7eb6c943c839258245541754',
|
||||
assetB: 'dai',
|
||||
assetBBridge: '0xf6edFA16926f30b0520099028A145F4E06FD54ed'
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Mediator } from '../burner-wallet'
|
||||
import { HOME_NATIVE_TO_ERC_ABI, FOREIGN_NATIVE_TO_ERC_ABI } from '../utils'
|
||||
import { waitForEvent, isBridgeContract, constants } from '../utils'
|
||||
import { waitForEvent, isVanillaBridgeContract, constants } from '../utils'
|
||||
import { ValueTypes } from '@burner-wallet/exchange'
|
||||
import { toBN, fromWei } from 'web3-utils'
|
||||
|
||||
@@ -19,7 +19,7 @@ export default class WETCBridge extends Mediator {
|
||||
.getAsset(this.assetA)
|
||||
.getWeb3()
|
||||
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.assetABridge)
|
||||
const listenToBridgeEvent = await isBridgeContract(contract)
|
||||
const listenToBridgeEvent = await isVanillaBridgeContract(contract)
|
||||
if (listenToBridgeEvent) {
|
||||
await waitForEvent(web3, contract, 'AffirmationCompleted', this.processBridgeEvents(sendResult.txHash))
|
||||
} else {
|
||||
@@ -32,7 +32,7 @@ export default class WETCBridge extends Mediator {
|
||||
.getAsset(this.assetB)
|
||||
.getWeb3()
|
||||
const contract = new web3.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, this.assetBBridge)
|
||||
const listenToBridgeEvent = await isBridgeContract(contract)
|
||||
const listenToBridgeEvent = await isVanillaBridgeContract(contract)
|
||||
if (listenToBridgeEvent) {
|
||||
await waitForEvent(web3, contract, 'RelayedMessage', this.processBridgeEvents(sendResult.txHash))
|
||||
} else {
|
||||
@@ -53,7 +53,7 @@ export default class WETCBridge extends Mediator {
|
||||
.getWeb3()
|
||||
const contract = new web3.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, this.assetBBridge)
|
||||
|
||||
const useBridgeContract = await isBridgeContract(contract)
|
||||
const useBridgeContract = await isVanillaBridgeContract(contract)
|
||||
|
||||
if (useBridgeContract) {
|
||||
const fee = toBN(await contract.methods.getHomeFee().call())
|
||||
@@ -79,7 +79,7 @@ export default class WETCBridge extends Mediator {
|
||||
.getWeb3()
|
||||
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.assetABridge)
|
||||
|
||||
const useBridgeContract = await isBridgeContract(contract)
|
||||
const useBridgeContract = await isVanillaBridgeContract(contract)
|
||||
|
||||
if (useBridgeContract) {
|
||||
const fee = toBN(await contract.methods.getForeignFee().call())
|
||||
@@ -0,0 +1,3 @@
|
||||
export { default as WETCBridge } from './WETCBridge'
|
||||
export { default as QDAIBridge } from './QDAIBridge'
|
||||
export { default as MOONBridge } from './MOONBridge'
|
||||
@@ -1,42 +1,55 @@
|
||||
import { ERC20Asset } from '@burner-wallet/assets'
|
||||
import { MEDIATOR_ABI, constants } from '../../utils'
|
||||
import { toBN } from 'web3-utils'
|
||||
import { AssetConstructor } from '@burner-wallet/assets/Asset'
|
||||
import { MEDIATOR_ABI, constants, isBridgeContract } from '../../utils'
|
||||
import { toBN, soliditySha3 } from 'web3-utils'
|
||||
|
||||
interface BridgeableERC20Constructor {
|
||||
interface BridgeableERC20Constructor extends AssetConstructor {
|
||||
abi?: object
|
||||
address: string
|
||||
id: string
|
||||
name: string
|
||||
network: string
|
||||
bridgeAddress: string
|
||||
bridgeModes: string[]
|
||||
}
|
||||
|
||||
export default class BridgeableERC20Asset extends ERC20Asset {
|
||||
protected bridgeAddress: string
|
||||
private _bridge
|
||||
private _bridgeModes
|
||||
private _bridges: { [addr: string]: object | false }
|
||||
|
||||
constructor({ bridgeAddress, ...params }: BridgeableERC20Constructor) {
|
||||
super({ ...params })
|
||||
this.bridgeAddress = bridgeAddress.toLowerCase()
|
||||
public set bridgeModes(bridgeModes: string[]) {
|
||||
this._bridgeModes = bridgeModes.map(s => soliditySha3(s)!.slice(0, 10))
|
||||
}
|
||||
|
||||
getBridgeContract() {
|
||||
if (!this._bridge) {
|
||||
public get bridgeModes() {
|
||||
return this._bridgeModes
|
||||
}
|
||||
|
||||
constructor({
|
||||
bridgeModes = ['erc-to-native-core', 'erc-to-native-amb', 'erc-to-erc-core', 'erc-to-erc-amb'],
|
||||
...params
|
||||
}: BridgeableERC20Constructor) {
|
||||
super(params)
|
||||
this._bridges = {}
|
||||
this.bridgeModes = bridgeModes
|
||||
}
|
||||
|
||||
async getBridgeContract(addr: string) {
|
||||
if (typeof this._bridges[addr] === 'undefined') {
|
||||
const Contract = this.getWeb3().eth.Contract
|
||||
this._bridge = new Contract(MEDIATOR_ABI, this.bridgeAddress)
|
||||
const bridge = new Contract(MEDIATOR_ABI, addr)
|
||||
if (await isBridgeContract(bridge, this.bridgeModes)) {
|
||||
return (this._bridges[addr] = bridge)
|
||||
}
|
||||
return (this._bridges[addr] = false)
|
||||
}
|
||||
return this._bridge
|
||||
return this._bridges[addr]
|
||||
}
|
||||
|
||||
async _send({ from, to, value }) {
|
||||
if (to.toLowerCase() === this.bridgeAddress) {
|
||||
const bridge = await this.getBridgeContract(to)
|
||||
if (bridge) {
|
||||
const allowance = await this.allowance(from, to)
|
||||
if (toBN(allowance).lt(toBN(value))) {
|
||||
await this.approve(from, to, value)
|
||||
}
|
||||
const receipt = await this.getBridgeContract()
|
||||
.methods.relayTokens(from, value)
|
||||
.send({ from })
|
||||
const receipt = await bridge.methods.relayTokens(from, value).send({ from })
|
||||
const transferLog = Object.values(receipt.events as object).find(
|
||||
e => e.raw.topics[0] === constants.TRANSFER_TOPIC
|
||||
)
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import BridgeableERC20Asset from './BridgeableERC20Asset'
|
||||
|
||||
export default new BridgeableERC20Asset({
|
||||
id: 'dai',
|
||||
name: 'Dai',
|
||||
network: '1',
|
||||
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
|
||||
usdPrice: 1,
|
||||
icon: 'https://static.burnerfactory.com/icons/mcd.svg',
|
||||
bridgeModes: ['erc-to-native-amb']
|
||||
})
|
||||
@@ -1,14 +1,12 @@
|
||||
import { ERC20Asset } from '@burner-wallet/assets'
|
||||
import { AssetConstructor } from '@burner-wallet/assets/Asset'
|
||||
import { ERC677_ABI, constants } from '../../utils'
|
||||
|
||||
const BLOCK_LOOKBACK = 250
|
||||
|
||||
interface ERC677Constructor {
|
||||
interface ERC677Constructor extends AssetConstructor {
|
||||
abi?: object
|
||||
address: string
|
||||
id: string
|
||||
name: string
|
||||
network: string
|
||||
}
|
||||
|
||||
export default class ERC677Asset extends ERC20Asset {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import NativeMediatorAsset from './NativeMediatorAsset'
|
||||
import { isBridgeContract, HOME_NATIVE_TO_ERC_ABI } from '../../utils'
|
||||
import { isVanillaBridgeContract, HOME_NATIVE_TO_ERC_ABI } from '../../utils'
|
||||
|
||||
class EtcNativeAsset extends NativeMediatorAsset {
|
||||
constructor(props) {
|
||||
@@ -9,7 +9,7 @@ class EtcNativeAsset extends NativeMediatorAsset {
|
||||
async scanMediatorEvents(address, fromBlock, toBlock) {
|
||||
const web3 = this.getWeb3()
|
||||
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.mediatorAddress)
|
||||
const listenToBridgeEvent = await isBridgeContract(contract)
|
||||
const listenToBridgeEvent = await isVanillaBridgeContract(contract)
|
||||
if (listenToBridgeEvent && this.mediatorAddress != '') {
|
||||
const events = await contract.getPastEvents('AffirmationCompleted', {
|
||||
fromBlock,
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import BridgeableERC20Asset from './BridgeableERC20Asset'
|
||||
|
||||
export default new BridgeableERC20Asset({
|
||||
id: 'moon',
|
||||
name: 'MOON',
|
||||
network: '4',
|
||||
address: '0xDF82c9014F127243CE1305DFE54151647d74B27A',
|
||||
icon: 'https://blockscout.com/poa/xdai/images/icons/moon.png',
|
||||
bridgeModes: ['erc-to-erc-amb']
|
||||
})
|
||||
@@ -1,12 +1,10 @@
|
||||
import { NativeAsset } from '@burner-wallet/assets'
|
||||
import { Contract, EventData } from 'web3-eth-contract'
|
||||
import { MEDIATOR_ABI } from '../../utils'
|
||||
import { AssetConstructor } from '@burner-wallet/assets/Asset'
|
||||
|
||||
interface NativeMediatorConstructor {
|
||||
interface NativeMediatorConstructor extends AssetConstructor {
|
||||
mediatorAddress?: string
|
||||
id: string
|
||||
name: string
|
||||
network: string
|
||||
}
|
||||
|
||||
export default class NativeMediatorAsset extends NativeAsset {
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import NativeMediatorAsset from './NativeMediatorAsset'
|
||||
|
||||
export default new NativeMediatorAsset({
|
||||
id: 'qdai',
|
||||
name: 'qDai',
|
||||
network: '181',
|
||||
usdPrice: 1,
|
||||
mediatorAddress: '0xFEaB457D95D9990b7eb6c943c839258245541754'
|
||||
})
|
||||
@@ -0,0 +1,9 @@
|
||||
import { default as ERC677Asset } from './ERC677Asset'
|
||||
|
||||
export default new ERC677Asset({
|
||||
id: 'xmoon',
|
||||
name: 'xMOON',
|
||||
network: '100',
|
||||
address: '0x1e16aa4Df73d29C029d94CeDa3e3114EC191E25A',
|
||||
icon: 'https://blockscout.com/poa/xdai/images/icons/moon.png'
|
||||
})
|
||||
@@ -9,7 +9,8 @@ export default class TokenBridgeGateway extends Gateway {
|
||||
this.providerStrings = {
|
||||
'61': `https://www.ethercluster.com/etc`,
|
||||
'77': 'https://sokol.poa.network',
|
||||
'99': 'https://core.poa.network'
|
||||
'99': 'https://core.poa.network',
|
||||
'181': 'https://quorum-rpc.tokenbridge.net'
|
||||
}
|
||||
this.providers = {}
|
||||
}
|
||||
@@ -19,7 +20,7 @@ export default class TokenBridgeGateway extends Gateway {
|
||||
}
|
||||
|
||||
getNetworks() {
|
||||
return ['61', '77', '99']
|
||||
return ['61', '77', '99', '181']
|
||||
}
|
||||
|
||||
_provider(network) {
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export { default as sPOA } from './assets/sPOA'
|
||||
export { default as Etc } from './assets/Etc'
|
||||
export { default as Wetc } from './assets/Wetc'
|
||||
export { default as Dai } from './assets/Dai'
|
||||
export { default as qDai } from './assets/qDai'
|
||||
export { default as MOON } from './assets/MOON'
|
||||
export { default as xMOON } from './assets/xMOON'
|
||||
export { default as ERC677Asset } from './assets/ERC677Asset'
|
||||
export { default as BridgeableERC20Asset } from './assets/BridgeableERC20Asset'
|
||||
export { default as NativeMediatorAsset } from './assets/NativeMediatorAsset'
|
||||
|
||||
@@ -5,8 +5,12 @@ export {
|
||||
sPOA,
|
||||
Etc,
|
||||
Wetc,
|
||||
qDai,
|
||||
Dai,
|
||||
MOON,
|
||||
xMOON,
|
||||
TokenBridgeGateway,
|
||||
Mediator,
|
||||
MediatorErcToNative
|
||||
} from './burner-wallet'
|
||||
export { WETCBridge } from './wetc-bridge'
|
||||
export { WETCBridge, QDAIBridge, MOONBridge } from './bridges'
|
||||
|
||||
@@ -52,5 +52,19 @@ export default [
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeMode',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes4'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { constants, wait, waitForEvent, isBridgeContract } from './utils'
|
||||
export { constants, wait, waitForEvent, isVanillaBridgeContract, isBridgeContract } from './utils'
|
||||
export {
|
||||
ERC677_ABI,
|
||||
FOREIGN_NATIVE_TO_ERC_ABI,
|
||||
|
||||
@@ -34,7 +34,7 @@ export const waitForEvent = async (web3, contract: Contract, event: string, call
|
||||
}
|
||||
}
|
||||
|
||||
export const isBridgeContract = async (contract: Contract): Promise<boolean> => {
|
||||
export const isVanillaBridgeContract = async (contract: Contract): Promise<boolean> => {
|
||||
try {
|
||||
await contract.methods.deployedAtBlock().call()
|
||||
return true
|
||||
@@ -42,3 +42,18 @@ export const isBridgeContract = async (contract: Contract): Promise<boolean> =>
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const isBridgeContract = async (contract: Contract, allowedModes?: string[]): Promise<boolean> => {
|
||||
try {
|
||||
const mode = await contract.methods.getBridgeMode().call()
|
||||
if (typeof allowedModes === 'undefined') {
|
||||
return true
|
||||
}
|
||||
return allowedModes.includes(mode)
|
||||
} catch (e) {
|
||||
if (e.message.includes("Returned values aren't valid")) {
|
||||
return false
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export { default as WETCBridge } from './WETCBridge'
|
||||
@@ -13,7 +13,6 @@ const REWARDABLE_VALIDATORS_ABI = require('../contracts/build/contracts/Rewardab
|
||||
const HOME_AMB_ABI = require('../contracts/build/contracts/HomeAMB').abi
|
||||
const FOREIGN_AMB_ABI = require('../contracts/build/contracts/ForeignAMB').abi
|
||||
const BOX_ABI = require('../contracts/build/contracts/Box').abi
|
||||
const SAI_TOP = require('../contracts/build/contracts/SaiTopMock').abi
|
||||
const HOME_AMB_ERC_TO_ERC_ABI = require('../contracts/build/contracts/HomeAMBErc677ToErc677').abi
|
||||
const FOREIGN_AMB_ERC_TO_ERC_ABI = require('../contracts/build/contracts/ForeignAMBErc677ToErc677').abi
|
||||
const HOME_STAKE_ERC_TO_ERC_ABI = require('../contracts/build/contracts/HomeStakeTokenMediator').abi
|
||||
@@ -136,7 +135,6 @@ module.exports = {
|
||||
OLD_AMB_USER_REQUEST_FOR_AFFIRMATION_ABI,
|
||||
OLD_AMB_USER_REQUEST_FOR_SIGNATURE_ABI,
|
||||
BOX_ABI,
|
||||
SAI_TOP,
|
||||
HOME_STAKE_ERC_TO_ERC_ABI,
|
||||
FOREIGN_STAKE_ERC_TO_ERC_ABI
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
"test": "NODE_ENV=test mocha"
|
||||
},
|
||||
"dependencies": {
|
||||
"gas-price-oracle": "^0.1.5",
|
||||
"web3-utils": "1.0.0-beta.34"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
const { toWei, toBN } = require('web3-utils')
|
||||
const { GasPriceOracle } = require('gas-price-oracle')
|
||||
const { BRIDGE_MODES, FEE_MANAGER_MODE, ERC_TYPES } = require('./constants')
|
||||
const { REWARDABLE_VALIDATORS_ABI } = require('./abis')
|
||||
|
||||
const gasPriceOracle = new GasPriceOracle()
|
||||
|
||||
function decodeBridgeMode(bridgeModeHash) {
|
||||
switch (bridgeModeHash) {
|
||||
case '0x92a8d7fe':
|
||||
@@ -235,8 +238,13 @@ const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => {
|
||||
// we use built-in 'fetch' on browser side, and `node-fetch` package in Node.
|
||||
const gasPriceFromSupplier = async (fetchFn, options = {}) => {
|
||||
try {
|
||||
const response = await fetchFn()
|
||||
const json = await response.json()
|
||||
let json
|
||||
if (fetchFn) {
|
||||
const response = await fetchFn()
|
||||
json = await response.json()
|
||||
} else {
|
||||
json = await gasPriceOracle.fetchGasPricesOffChain()
|
||||
}
|
||||
const oracleGasPrice = json[options.speedType]
|
||||
|
||||
if (!oracleGasPrice) {
|
||||
|
||||
Submodule contracts updated: d67761d938...dd46135248
@@ -13,7 +13,7 @@ git push
|
||||
Alternatively, if there are no changes except the playbooks, you can use the `master` branch:
|
||||
|
||||
```
|
||||
CIRCLE_BRANCH=master ./molecule.sh <scenario_name>
|
||||
./molecule.sh <scenario_name>
|
||||
```
|
||||
|
||||
In this case `master` branch will be used as a codebase for Monitor, UI, Oracle and Contracts deployed by your local playbook.
|
||||
@@ -21,7 +21,7 @@ In this case `master` branch will be used as a codebase for Monitor, UI, Oracle
|
||||
## Run the tests
|
||||
|
||||
```
|
||||
CIRCLE_BRANCH=master ./molecule.sh <scenario_name>
|
||||
./molecule.sh <scenario_name>
|
||||
```
|
||||
|
||||
Available scenarios:
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
---
|
||||
version: '3.0'
|
||||
services:
|
||||
molecule_runner:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deployment-e2e/Dockerfile
|
||||
restart: 'no'
|
||||
privileged: true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ..:/mono
|
||||
@@ -1,9 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
cd $(dirname $0)
|
||||
cd ./e2e-commons
|
||||
set -e # exit when any command fails
|
||||
|
||||
if [ -z "$CI" ]; then
|
||||
docker-compose build molecule_runner
|
||||
else
|
||||
docker-compose pull molecule_runner
|
||||
fi
|
||||
docker network create --driver bridge ultimate || true
|
||||
while [ "$1" != "" ]; do
|
||||
docker-compose build && docker-compose run molecule_runner /bin/bash -c "molecule test --scenario-name $1"
|
||||
docker-compose run molecule_runner /bin/bash -c "molecule test --scenario-name $1"
|
||||
|
||||
shift # Shift all the parameters down by one
|
||||
done
|
||||
|
||||
@@ -6,12 +6,10 @@
|
||||
- name: stop the service
|
||||
shell: service poabridge stop
|
||||
|
||||
- name: Build current oracle image
|
||||
shell: docker build -t oracle:ultimate-testing --file oracle/Dockerfile .
|
||||
- name: ReTag current oracle image
|
||||
shell: docker tag $(docker images --format '{{ '{{' }}.Repository{{ '}}' }}:{{ '{{' }}.Tag{{ '}}' }}' | grep -m 1 tokenbridge-e2e-oracle) oracle:ultimate-testing
|
||||
delegate_to: 127.0.0.1
|
||||
become: false
|
||||
args:
|
||||
chdir: "{{ lookup('env', 'PWD') }}/.."
|
||||
|
||||
- name: Replace oracle image
|
||||
replace:
|
||||
|
||||
@@ -4,7 +4,7 @@ ORACLE_BRIDGE_MODE: "NATIVE_TO_ERC"
|
||||
UI_NATIVE_TOKEN_DISPLAY_NAME: "ETC"
|
||||
|
||||
## Home contract
|
||||
COMMON_HOME_RPC_URL: "https://ethereumclassic.network"
|
||||
COMMON_HOME_RPC_URL: "https://www.ethercluster.com/etc"
|
||||
UI_HOME_NETWORK_DISPLAY_NAME: "Ethereum Classic"
|
||||
UI_HOME_WITHOUT_EVENTS: false
|
||||
COMMON_HOME_BRIDGE_ADDRESS: "0x073081832B4Ecdce79d4D6753565c85Ba4b3BeA9"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
copy:
|
||||
src: ../../../../{{ item }}
|
||||
dest: "{{ bridge_path }}/"
|
||||
mode: '0640'
|
||||
with_items:
|
||||
- monorepo.tar.gz
|
||||
- contracts.tar.gz
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
template:
|
||||
src: .env.j2
|
||||
dest: "{{ bridge_path }}/monitor/.env"
|
||||
owner: "{{ compose_service_user }}"
|
||||
mode: '0640'
|
||||
when: skip_task != true
|
||||
|
||||
- name: Copy docker-compose file
|
||||
@@ -45,3 +47,5 @@
|
||||
template:
|
||||
src: config.env.j2
|
||||
dest: "{{ bridge_path }}/monitor/configs/{{ MONITOR_BRIDGE_NAME }}.env"
|
||||
owner: "{{ compose_service_user }}"
|
||||
mode: '0640'
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
template:
|
||||
src: .env.j2
|
||||
dest: "{{ bridge_path }}/oracle/.env"
|
||||
owner: "{{ compose_service_user }}"
|
||||
mode: '0640'
|
||||
|
||||
- name: Copy docker-compose files
|
||||
copy:
|
||||
|
||||
@@ -47,6 +47,9 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR={{ COMMON_FOREIGN_GAS_PRICE_FACTOR }}
|
||||
ORACLE_ALLOW_HTTP_FOR_RPC={{ "yes" if ORACLE_ALLOW_HTTP_FOR_RPC else "no" }}
|
||||
ORACLE_QUEUE_URL={{ ORACLE_QUEUE_URL }}
|
||||
ORACLE_REDIS_URL={{ ORACLE_REDIS_URL }}
|
||||
{% if ORACLE_TX_REDUNDANCY | default('') != '' %}
|
||||
ORACLE_TX_REDUNDANCY={{ ORACLE_TX_REDUNDANCY }}
|
||||
{% endif %}
|
||||
|
||||
{% if ORACLE_HOME_START_BLOCK | default('') != '' %}
|
||||
ORACLE_HOME_START_BLOCK={{ ORACLE_HOME_START_BLOCK }}
|
||||
|
||||
@@ -3,3 +3,5 @@
|
||||
template:
|
||||
src: .env.j2
|
||||
dest: "{{ bridge_path }}/ui/.env"
|
||||
owner: "{{ compose_service_user }}"
|
||||
mode: '0640'
|
||||
|
||||
7
e2e-commons/Dockerfile.ui
Normal file
7
e2e-commons/Dockerfile.ui
Normal file
@@ -0,0 +1,7 @@
|
||||
ARG DOCKER_IMAGE_BASE
|
||||
ARG UI_TAG
|
||||
FROM ${DOCKER_IMAGE_BASE}/tokenbridge-e2e-ui:${UI_TAG}
|
||||
|
||||
ARG DOT_ENV_PATH
|
||||
|
||||
COPY ${DOT_ENV_PATH} ./.env
|
||||
@@ -22,9 +22,16 @@ Shut down and cleans up containers, networks, services, running scripts:
|
||||
| --- | --- |
|
||||
| deploy | Deploys the Smart Contracts |
|
||||
| oracle | Launches Oracle containers |
|
||||
| oracle-validator-2 | Launches Oracle containers for second validator |
|
||||
| oracle-validator-3 | Launches Oracle containers for third validator |
|
||||
| ui | Launches UI containers |
|
||||
| blocks | Auto mines blocks |
|
||||
| monitor | Launches Monitor containers |
|
||||
| native-to-erc | Creates infrastructure for ultimate e2e testing, for native-to-erc type of bridge |
|
||||
| erc-to-native | Creates infrastructure for ultimate e2e testing, for erc-to-native type of bridge |
|
||||
| erc-to-erc | Creates infrastructure for ultimate e2e testing, for erc-to-erc type of bridge |
|
||||
| amb | Creates infrastructure for ultimate e2e testing, for arbitrary message type of bridge |
|
||||
| ultimate-amb-stake-erc-to-erc | Creates infrastructure for ultimate e2e testing, for stake token bridge |
|
||||
|
||||
#### Ultimate e2e testing
|
||||
|
||||
|
||||
@@ -15,13 +15,13 @@ It runs the e2e tests on components deployed using the deployment playbooks.
|
||||
Run the Parity nodes, deploy the bridge contracts, deploy Oracle using the deployment playbook.
|
||||
|
||||
```bash
|
||||
./up.sh deploy native-to-erc
|
||||
./up.sh deploy native-to-erc blocks
|
||||
```
|
||||
|
||||
### 2. Run the E2E tests
|
||||
|
||||
```
|
||||
docker-compose run e2e yarn workspace oracle-e2e run native-to-erc
|
||||
cd ui-e2e; yarn mocha -g "NATIVE_TO_ERC" -b ./test.js
|
||||
```
|
||||
|
||||
## Diagram
|
||||
|
||||
0
e2e-commons/access-lists/allowance_list.txt
Normal file
0
e2e-commons/access-lists/allowance_list.txt
Normal file
1
e2e-commons/access-lists/block_list.txt
Normal file
1
e2e-commons/access-lists/block_list.txt
Normal file
@@ -0,0 +1 @@
|
||||
0xc9e38bfdB9c635F0796ad83CC8705dc379F41c04
|
||||
17
e2e-commons/build.sh
Executable file
17
e2e-commons/build.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
cd $(dirname $0)
|
||||
set -e # exit when any command fails
|
||||
|
||||
docker-compose build e2e
|
||||
while [ "$1" != "" ]; do
|
||||
if [ "$1" == "oracle" ]; then
|
||||
docker-compose build oracle
|
||||
elif [ "$1" == "monitor" ]; then
|
||||
docker-compose build monitor
|
||||
elif [ "$1" == "ui" ]; then
|
||||
docker-compose build ui
|
||||
elif [ "$1" == "alm" ]; then
|
||||
docker-compose build alm
|
||||
fi
|
||||
shift
|
||||
done
|
||||
10
e2e-commons/components-envs/alm.env
Normal file
10
e2e-commons/components-envs/alm.env
Normal file
@@ -0,0 +1,10 @@
|
||||
COMMON_HOME_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
|
||||
|
||||
COMMON_HOME_RPC_URL=http://localhost:8541
|
||||
COMMON_FOREIGN_RPC_URL=http://localhost:8542
|
||||
|
||||
ALM_HOME_NETWORK_NAME=Parity1
|
||||
ALM_FOREIGN_NETWORK_NAME=Parity2
|
||||
|
||||
PORT=3000
|
||||
@@ -23,3 +23,4 @@ ORACLE_FOREIGN_RPC_POLLING_INTERVAL=500
|
||||
ORACLE_ALLOW_HTTP_FOR_RPC=yes
|
||||
ORACLE_HOME_START_BLOCK=1
|
||||
ORACLE_FOREIGN_START_BLOCK=1
|
||||
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST=/mono/oracle/access-lists/block_list.txt
|
||||
|
||||
@@ -15,6 +15,10 @@
|
||||
"address": "0x3CC5baAB679eC0732C175760079Bf48F564ad26B",
|
||||
"privateKey": "0xedb53ee050631b7914d5f1a66c2f0d2df3ec85a9ed2a9616b16a7b1b7a10b8d1"
|
||||
},
|
||||
"blockedUser": {
|
||||
"address": "0xc9e38bfdB9c635F0796ad83CC8705dc379F41c04",
|
||||
"privateKey": "0x65df4ea787916f6ed9660f0b0fe36858a65735ad0dcd34527497f4ce32e53883"
|
||||
},
|
||||
"validator": {
|
||||
"address": "0xaaB52d66283F7A1D5978bcFcB55721ACB467384b",
|
||||
"privateKey": "0x8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
|
||||
@@ -50,8 +54,6 @@
|
||||
"home": "0x488Af810997eD1730cB3a3918cD83b3216E6eAda",
|
||||
"foreign": "0x488Af810997eD1730cB3a3918cD83b3216E6eAda",
|
||||
"foreignToken": "0x7cc4b1851c35959d34e635a470f6b5c43ba3c9c9",
|
||||
"halfDuplexToken": "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359",
|
||||
"saiTop": "0x9b0ccf7C8994E19F39b2B4CF708e0A7DF65fA8a3",
|
||||
"chaiToken": "0x06af07097c9eeb7fd685c692751d5c66db49c215",
|
||||
"ui": "http://localhost:3002",
|
||||
"monitor": "http://monitor-erc20-native:3012/bridge"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
---
|
||||
version: '3'
|
||||
version: '3.8'
|
||||
networks:
|
||||
ultimate:
|
||||
external: true
|
||||
@@ -27,6 +26,7 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
oracle:
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-oracle:${ORACLE_TAG:-local}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: oracle/Dockerfile
|
||||
@@ -37,9 +37,7 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
oracle-erc20:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: oracle/Dockerfile
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-oracle:${ORACLE_TAG:-local}
|
||||
env_file: ../e2e-commons/components-envs/oracle-erc20.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
@@ -47,19 +45,18 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
oracle-erc20-native:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: oracle/Dockerfile
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-oracle:${ORACLE_TAG:-local}
|
||||
env_file: ../e2e-commons/components-envs/oracle-erc20-native.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
command: "true"
|
||||
volumes:
|
||||
- '../e2e-commons/access-lists/block_list.txt:/mono/oracle/access-lists/block_list.txt'
|
||||
- '../e2e-commons/access-lists/allowance_list.txt:/mono/oracle/access-lists/allowance_list.txt'
|
||||
networks:
|
||||
- ultimate
|
||||
oracle-amb:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: oracle/Dockerfile
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-oracle:${ORACLE_TAG:-local}
|
||||
env_file: ../e2e-commons/components-envs/oracle-amb.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
@@ -67,6 +64,7 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
ui:
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-ui:${UI_TAG:-local}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ui/Dockerfile
|
||||
@@ -78,8 +76,10 @@ services:
|
||||
ui-erc20:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ui/Dockerfile
|
||||
dockerfile: e2e-commons/Dockerfile.ui
|
||||
args:
|
||||
DOCKER_IMAGE_BASE: ${DOCKER_IMAGE_BASE:-tokenbridge}
|
||||
UI_TAG: ${UI_TAG:-local}
|
||||
DOT_ENV_PATH: e2e-commons/components-envs/ui-erc20.env
|
||||
command: "true"
|
||||
networks:
|
||||
@@ -87,8 +87,10 @@ services:
|
||||
ui-erc20-native:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ui/Dockerfile
|
||||
dockerfile: e2e-commons/Dockerfile.ui
|
||||
args:
|
||||
DOCKER_IMAGE_BASE: ${DOCKER_IMAGE_BASE:-tokenbridge}
|
||||
UI_TAG: ${UI_TAG:-local}
|
||||
DOT_ENV_PATH: e2e-commons/components-envs/ui-erc20-native.env
|
||||
command: "true"
|
||||
networks:
|
||||
@@ -96,13 +98,26 @@ services:
|
||||
ui-amb-stake-erc20-erc20:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: ui/Dockerfile
|
||||
dockerfile: e2e-commons/Dockerfile.ui
|
||||
args:
|
||||
DOCKER_IMAGE_BASE: ${DOCKER_IMAGE_BASE:-tokenbridge}
|
||||
UI_TAG: ${UI_TAG:-local}
|
||||
DOT_ENV_PATH: e2e-commons/components-envs/ui-amb-stake-erc20-erc20.env
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
alm:
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-alm:${ALM_TAG:-local}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: alm/Dockerfile
|
||||
args:
|
||||
DOT_ENV_PATH: e2e-commons/components-envs/alm.env
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
monitor:
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-monitor:${MONITOR_TAG:-local}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: monitor/Dockerfile
|
||||
@@ -113,9 +128,7 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
monitor-erc20:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: monitor/Dockerfile
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-monitor:${MONITOR_TAG:-local}
|
||||
env_file: ../e2e-commons/components-envs/monitor-erc20.env
|
||||
entrypoint: yarn check-and-start
|
||||
ports:
|
||||
@@ -123,9 +136,7 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
monitor-erc20-native:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: monitor/Dockerfile
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-monitor:${MONITOR_TAG:-local}
|
||||
env_file: ../e2e-commons/components-envs/monitor-erc20-native.env
|
||||
entrypoint: yarn check-and-start
|
||||
ports:
|
||||
@@ -133,9 +144,7 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
monitor-amb:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: monitor/Dockerfile
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-monitor:${MONITOR_TAG:-local}
|
||||
env_file: ../e2e-commons/components-envs/monitor-amb.env
|
||||
entrypoint: yarn check-and-start
|
||||
ports:
|
||||
@@ -143,6 +152,7 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
e2e:
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-e2e:${E2E_TAG:-local}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.e2e
|
||||
@@ -150,9 +160,17 @@ services:
|
||||
networks:
|
||||
- ultimate
|
||||
blocks:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.e2e
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-e2e:${E2E_TAG:-local}
|
||||
entrypoint: node e2e-commons/scripts/blocks.js
|
||||
networks:
|
||||
- ultimate
|
||||
molecule_runner:
|
||||
image: ${DOCKER_IMAGE_BASE:-tokenbridge}/tokenbridge-e2e-molecule_runner:${MOLECULE_RUNNER_TAG:-local}
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: deployment-e2e/Dockerfile
|
||||
restart: 'no'
|
||||
privileged: true
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- ..:/mono
|
||||
|
||||
17
e2e-commons/pull.sh
Executable file
17
e2e-commons/pull.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
cd $(dirname $0)
|
||||
set -e # exit when any command fails
|
||||
|
||||
docker-compose pull e2e
|
||||
while [ "$1" != "" ]; do
|
||||
if [ "$1" == "oracle" ]; then
|
||||
docker-compose pull oracle
|
||||
elif [ "$1" == "monitor" ]; then
|
||||
docker-compose pull monitor
|
||||
elif [ "$1" == "ui" ]; then
|
||||
docker-compose pull ui
|
||||
elif [ "$1" == "alm" ]; then
|
||||
docker-compose pull alm
|
||||
fi
|
||||
shift
|
||||
done
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user