Compare commits
91 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2f6b5e8ba | ||
|
|
f3f226afdf | ||
|
|
1eb8a8b1dc | ||
|
|
8a42cfbe2b | ||
|
|
ab406bc1db | ||
|
|
8cf73d572c | ||
|
|
fa6b37db1c | ||
|
|
64cd258354 | ||
|
|
7a48495118 | ||
|
|
fdfa5cd7af | ||
|
|
77bc6c662a | ||
|
|
dc060387bc | ||
|
|
efc433e9e0 | ||
|
|
ebd97dce5c | ||
|
|
42953ffe30 | ||
|
|
4f6d53964f | ||
|
|
9e6833eb40 | ||
|
|
4c44aa5fcd | ||
|
|
2edd8f2783 | ||
|
|
861c755b09 | ||
|
|
8c268d6f06 | ||
|
|
ffd88f6cd0 | ||
|
|
caf2e2b4d3 | ||
|
|
d5d0c8f56a | ||
|
|
dc27bd6caa | ||
|
|
ab814f831c | ||
|
|
691e4294ae | ||
|
|
4a727dc159 | ||
|
|
2ca07e998a | ||
|
|
0eb7c41278 | ||
|
|
9e9e891db8 | ||
|
|
d228bb7ea9 | ||
|
|
d2606997a3 | ||
|
|
3c956ab9ec | ||
|
|
9b3e6a51a9 | ||
|
|
d512b9850f | ||
|
|
bcdf691000 | ||
|
|
8e10a5d609 | ||
|
|
da60edae4b | ||
|
|
e7eb8ec758 | ||
|
|
571dec8022 | ||
|
|
4db62d721d | ||
|
|
8d6acd0339 | ||
|
|
c013cc7378 | ||
|
|
d6e39f34af | ||
|
|
3b368ce644 | ||
|
|
c1d58c2908 | ||
|
|
d17e9e0eea | ||
|
|
a2c678d0a2 | ||
|
|
4bd3576691 | ||
|
|
d3576f5a79 | ||
|
|
62f9a080c9 | ||
|
|
10f67168a7 | ||
|
|
f90f888ae4 | ||
|
|
84508e2b84 | ||
|
|
2369e876aa | ||
|
|
4117f29a74 | ||
|
|
1921087ad1 | ||
|
|
b4abbc4910 | ||
|
|
bcbe34a839 | ||
|
|
738442e4cf | ||
|
|
64c5a5670f | ||
|
|
c8c589536b | ||
|
|
7b0edff624 | ||
|
|
b02b879b02 | ||
|
|
5b4b01a79b | ||
|
|
881fafe9a8 | ||
|
|
c1ed6f21e6 | ||
|
|
8ae0fa82d5 | ||
|
|
dbf3d3d90d | ||
|
|
8977ed6d3b | ||
|
|
df0dc1c313 | ||
|
|
7b2bebbcf0 | ||
|
|
85104d67c0 | ||
|
|
2a3d7c8e08 | ||
|
|
e6052f162a | ||
|
|
52ed4e85e2 | ||
|
|
bc6dd13193 | ||
|
|
bce1e6509e | ||
|
|
52358d477b | ||
|
|
232f807e9d | ||
|
|
fe4a569e34 | ||
|
|
c408d57716 | ||
|
|
7fcb118c8c | ||
|
|
1eaf774e33 | ||
|
|
8650ba4d21 | ||
|
|
edc51c78e2 | ||
|
|
612f130544 | ||
|
|
73d5002105 | ||
|
|
65dd131107 | ||
|
|
4de24efc01 |
@@ -11,7 +11,7 @@ orbs:
|
||||
sudo apt-get clean
|
||||
sudo apt-get update
|
||||
sudo apt-get install dpkg
|
||||
- run:
|
||||
- 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
|
||||
@@ -36,20 +36,6 @@ orbs:
|
||||
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:
|
||||
@@ -73,45 +59,54 @@ orbs:
|
||||
echo "Sleeping..."
|
||||
sleep 3
|
||||
done
|
||||
init-repo:
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
executors:
|
||||
docker-node:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
machine-with-docker-caching:
|
||||
machine-with-dlc:
|
||||
machine:
|
||||
image: ubuntu-1604:202007-01
|
||||
docker_layer_caching: true
|
||||
classic-machine-without-dlc:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
machine-without-dlc:
|
||||
machine:
|
||||
image: ubuntu-1604:202007-01
|
||||
|
||||
jobs:
|
||||
initialize:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/init-repo
|
||||
- restore_cache:
|
||||
name: Restore Yarn Package Cache
|
||||
keys:
|
||||
- yarn-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
- run: git submodule status > submodule.status
|
||||
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" }}
|
||||
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
|
||||
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
|
||||
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 }}
|
||||
@@ -140,30 +135,43 @@ jobs:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run test
|
||||
build-e2e-images:
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- tokenbridge-orb/init-repo
|
||||
- run:
|
||||
command: |
|
||||
docker login -u ${DOCKER_LOGIN} -p ${DOCKER_PASSWORD}
|
||||
cd e2e-commons
|
||||
docker-compose build oracle monitor ui alm e2e molecule_runner
|
||||
docker-compose push oracle monitor ui alm e2e molecule_runner
|
||||
oracle-e2e:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- setup_remote_docker:
|
||||
docker_layer_caching: true
|
||||
- run: yarn run oracle-e2e
|
||||
- tokenbridge-orb/init-repo
|
||||
- run: ./oracle-e2e/run-tests.sh
|
||||
ui-e2e:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
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
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run ui-e2e
|
||||
monitor-e2e:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run: ./monitor-e2e/run-tests.sh
|
||||
- tokenbridge-orb/init-repo
|
||||
- run: ./monitor-e2e/run-tests.sh
|
||||
alm-e2e:
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- tokenbridge-orb/install-node
|
||||
- tokenbridge-orb/install-yarn
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run alm-e2e
|
||||
cover:
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
@@ -172,47 +180,47 @@ jobs:
|
||||
- run: yarn workspace ui run coverage
|
||||
- run: yarn workspace ui run coveralls
|
||||
deployment-oracle:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/init-repo
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh oracle
|
||||
no_output_timeout: 40m
|
||||
deployment-ui:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/init-repo
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh ui
|
||||
no_output_timeout: 40m
|
||||
deployment-monitor:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run:
|
||||
- tokenbridge-orb/init-repo
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh monitor
|
||||
no_output_timeout: 40m
|
||||
deployment-repo:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/install-node
|
||||
- tokenbridge-orb/install-yarn
|
||||
- tokenbridge-orb/yarn-install-cached-on-machine
|
||||
- tokenbridge-orb/init-repo
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh repo
|
||||
no_output_timeout: 40m
|
||||
|
||||
deployment-multiple:
|
||||
executor: tokenbridge-orb/machine-without-dlc
|
||||
steps:
|
||||
- tokenbridge-orb/init-repo
|
||||
- run:
|
||||
name: Run the scenario
|
||||
command: deployment-e2e/molecule.sh multiple
|
||||
no_output_timeout: 40m
|
||||
ultimate:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
executor: tokenbridge-orb/classic-machine-without-dlc
|
||||
parameters:
|
||||
scenario-name:
|
||||
description: "Molecule scenario name used to create the infrastructure"
|
||||
@@ -222,26 +230,38 @@ jobs:
|
||||
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
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run:
|
||||
name: Prepare the infrastructure
|
||||
command: e2e-commons/up.sh deploy << parameters.scenario-name >>
|
||||
command: e2e-commons/up.sh deploy << parameters.scenario-name >> blocks
|
||||
no_output_timeout: 50m
|
||||
- tokenbridge-orb/wait-for-oracle:
|
||||
redis-key: << parameters.redis-key >>
|
||||
- run:
|
||||
name: Run the ui-e2e tests
|
||||
command: |
|
||||
nvm use default;
|
||||
node ./e2e-commons/scripts/blocks.js &
|
||||
cd ui-e2e; yarn mocha -g "<< parameters.ui-e2e-grep >>" -b ./test.js
|
||||
- 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:
|
||||
@@ -265,25 +285,98 @@ workflows:
|
||||
filters:
|
||||
branches:
|
||||
only: master
|
||||
- oracle-e2e
|
||||
- ui-e2e
|
||||
- monitor-e2e
|
||||
- deployment-oracle
|
||||
- deployment-ui
|
||||
- deployment-monitor
|
||||
- deployment-repo
|
||||
- build-e2e-images
|
||||
- oracle-e2e:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- ui-e2e:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- initialize
|
||||
- monitor-e2e:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- alm-e2e:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- initialize
|
||||
- deployment-oracle:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- deployment-ui:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- deployment-monitor:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- deployment-repo:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- deployment-multiple:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- ultimate:
|
||||
name: "ultimate: native to erc"
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- initialize
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
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"
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- initialize
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
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"
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- initialize
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
name: "ultimate: erc to erc"
|
||||
scenario-name: erc-to-erc
|
||||
redis-key: erc-erc-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "ERC TO ERC"
|
||||
- ultimate:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- initialize
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
name: "ultimate: amb"
|
||||
scenario-name: amb
|
||||
redis-key: amb-collected-signatures:lastProcessedBlock
|
||||
oracle-e2e-script: "amb"
|
||||
- ultimate:
|
||||
requires:
|
||||
- build-e2e-images
|
||||
- initialize
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
- develop
|
||||
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"
|
||||
|
||||
@@ -9,6 +9,8 @@
|
||||
**/*.md
|
||||
|
||||
contracts/test
|
||||
contracts/build
|
||||
oracle/test
|
||||
oracle/**/*.png
|
||||
oracle/**/*.jpg
|
||||
audit
|
||||
|
||||
@@ -2,3 +2,5 @@ node_modules
|
||||
submodules
|
||||
coverage
|
||||
lib
|
||||
dist
|
||||
build
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -6,6 +6,7 @@ coverage
|
||||
|
||||
# production
|
||||
build
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
@@ -48,4 +49,5 @@ __pycache__
|
||||
|
||||
#monitor
|
||||
monitor/responses/*
|
||||
!monitor/.gitkeep
|
||||
monitor/configs/*.env
|
||||
!monitor/.gitkeep
|
||||
|
||||
@@ -36,6 +36,7 @@ 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
|
||||
|
||||
|
||||
## UI configuration
|
||||
@@ -43,6 +44,7 @@ ORACLE_VALIDATOR_ADDRESS | The public address of the bridge validator | hexideci
|
||||
name | description | value
|
||||
--- | --- | ---
|
||||
UI_TITLE | The title for the bridge UI page. `%c` will be replaced by the name of the network. | string
|
||||
UI_OG_TITLE | The meta title for the deployed bridge page. | string
|
||||
UI_DESCRIPTION | The meta description for the deployed bridge page. | string
|
||||
UI_NATIVE_TOKEN_DISPLAY_NAME | name of the home native coin | string
|
||||
UI_HOME_NETWORK_DISPLAY_NAME | name to be displayed for home network | string
|
||||
@@ -56,7 +58,8 @@ UI_FOREIGN_EXPLORER_ADDRESS_TEMPLATE | template link to address on foreign explo
|
||||
UI_HOME_GAS_PRICE_UPDATE_INTERVAL | An interval in milliseconds used to get the updated gas price value either from the oracle or from the Home Bridge contract. | integer
|
||||
UI_FOREIGN_GAS_PRICE_UPDATE_INTERVAL | An interval in milliseconds used to get the updated gas price value either from the oracle or from the Foreign Bridge contract. | integer
|
||||
UI_PORT | The port for the UI app. | integer
|
||||
UI_STYLES | The set of styles to render the bridge UI page. Currently only `classic` is implemented | classic
|
||||
UI_STYLES | The set of styles to render the bridge UI page. | core/classic/stake
|
||||
UI_PUBLIC_URL | The public url for the deployed bridge page | string
|
||||
|
||||
|
||||
## Monitor configuration
|
||||
@@ -69,3 +72,5 @@ MONITOR_VALIDATOR_HOME_TX_LIMIT | Average gas usage of a transaction sent by a v
|
||||
MONITOR_VALIDATOR_FOREIGN_TX_LIMIT | Average gas usage of a transaction sent by a validator, it is used to estimate the number of transaction that can be paid by the validator. | integer
|
||||
MONITOR_TX_NUMBER_THRESHOLD | If estimated number of transaction is equal to or below this value, the monitor will report that the validator has less funds than it is required. | integer
|
||||
MONITOR_PORT | The port for the Monitor. | integer
|
||||
MONITOR_BRIDGE_NAME | The name to be used in the url path for the bridge | string
|
||||
MONITOR_CACHE_EVENTS | If set to true, monitor will cache obtained events for other workers runs
|
||||
|
||||
@@ -1,15 +1,31 @@
|
||||
FROM node:10
|
||||
FROM node:8 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:8
|
||||
|
||||
WORKDIR /mono
|
||||
COPY package.json .
|
||||
COPY --from=contracts /mono/contracts/build ./contracts/build
|
||||
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/
|
||||
|
||||
@@ -29,6 +29,8 @@ Sub-repositories maintained within this monorepo are listed below.
|
||||
| [Deployment-E2E](deployment-e2e/README.md) | End to end tests for the Deployment |
|
||||
| [Commons](commons/README.md) | Interfaces, constants and utilities shared between the sub-repositories |
|
||||
| [E2E-Commons](e2e-commons/README.md) | Common utilities and configuration used in end to end tests |
|
||||
| [ALM](alm/README.md) | DApp interface tool for AMB Live Monitoring |
|
||||
| [Burner-wallet-plugin](burner-wallet-plugin/README.md) | TokenBridge Burner Wallet 2 Plugin |
|
||||
|
||||
Additionally there are [Smart Contracts](https://github.com/poanetwork/tokenbridge-contracts) used to manage bridge validators, collect signatures, and confirm asset relay and disposal.
|
||||
|
||||
@@ -66,11 +68,13 @@ Clone the repository:
|
||||
git clone https://github.com/poanetwork/tokenbridge
|
||||
```
|
||||
|
||||
Initialize submodules, install dependencies, compile the Smart Contracts:
|
||||
If there is no need to build docker images for the TokenBridge components (oracle, monitor, UI), initialize submodules, install dependencies, compile the Smart Contracts:
|
||||
```
|
||||
yarn initialize
|
||||
```
|
||||
|
||||
Then refer to the corresponding README files to get information about particular TokenBridge component.
|
||||
|
||||
## Linting
|
||||
|
||||
Running linter for all JS projects:
|
||||
|
||||
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": ">= 8.9"
|
||||
}
|
||||
}
|
||||
13
alm-e2e/run-tests.sh
Normal file
13
alm-e2e/run-tests.sh
Normal 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
|
||||
}
|
||||
15
alm/.env.example
Normal file
15
alm/.env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
COMMON_HOME_BRIDGE_ADDRESS=0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560
|
||||
COMMON_FOREIGN_BRIDGE_ADDRESS=0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560
|
||||
|
||||
COMMON_HOME_RPC_URL=https://sokol.poa.network
|
||||
COMMON_FOREIGN_RPC_URL=https://kovan.infura.io/v3/
|
||||
|
||||
ALM_HOME_NETWORK_NAME=Sokol Testnet
|
||||
ALM_FOREIGN_NETWORK_NAME=Kovan Testnet
|
||||
|
||||
ALM_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx/%s
|
||||
ALM_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
|
||||
|
||||
ALM_HOME_EXPLORER_API=https://blockscout.com/poa/sokol/api
|
||||
ALM_FOREIGN_EXPLORER_API=https://kovan.etherscan.io/api?apikey=YourApiKeyToken
|
||||
PORT=8080
|
||||
6
alm/.eslintrc.js
Normal file
6
alm/.eslintrc.js
Normal file
@@ -0,0 +1,6 @@
|
||||
module.exports = {
|
||||
extends: [
|
||||
"react-app",
|
||||
"../.eslintrc"
|
||||
]
|
||||
}
|
||||
25
alm/.gitignore
vendored
Normal file
25
alm/.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
src/snapshots/*.json
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
38
alm/Dockerfile
Normal file
38
alm/Dockerfile
Normal file
@@ -0,0 +1,38 @@
|
||||
FROM node:8 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 --from=contracts /mono/contracts/build ./contracts/build
|
||||
COPY commons/package.json ./commons/
|
||||
COPY alm/package.json ./alm/
|
||||
COPY yarn.lock .
|
||||
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
|
||||
|
||||
WORKDIR /mono/alm
|
||||
RUN yarn run build
|
||||
|
||||
|
||||
FROM node:12 as alm-production
|
||||
RUN yarn global add serve
|
||||
WORKDIR /app
|
||||
COPY --from=alm-builder /mono/alm/build .
|
||||
CMD serve -p $PORT -s .
|
||||
46
alm/README.md
Normal file
46
alm/README.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# AMB Live Monitoring
|
||||
|
||||
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
|
||||
|
||||
## Available Scripts
|
||||
|
||||
In the project directory, you can run:
|
||||
|
||||
### `yarn start`
|
||||
|
||||
Runs the app in the development mode.<br />
|
||||
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
|
||||
|
||||
The page will reload if you make edits.<br />
|
||||
You will also see any lint errors in the console.
|
||||
|
||||
### `yarn test`
|
||||
|
||||
Launches the test runner in the interactive watch mode.<br />
|
||||
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
|
||||
|
||||
### `yarn build`
|
||||
|
||||
Builds the app for production to the `build` folder.<br />
|
||||
It correctly bundles React in production mode and optimizes the build for the best performance.
|
||||
|
||||
The build is minified and the filenames include the hashes.<br />
|
||||
Your app is ready to be deployed!
|
||||
|
||||
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
|
||||
|
||||
### `yarn eject`
|
||||
|
||||
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
|
||||
|
||||
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
|
||||
|
||||
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
|
||||
|
||||
## Learn More
|
||||
|
||||
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
|
||||
|
||||
To learn React, check out the [React documentation](https://reactjs.org/).
|
||||
9
alm/config-overrides.js
Normal file
9
alm/config-overrides.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const { override, disableEsLint } = require('customize-cra')
|
||||
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
|
||||
|
||||
const disableModuleScopePlugin = () => config => {
|
||||
config.resolve.plugins = config.resolve.plugins.filter(plugin => !(plugin instanceof ModuleScopePlugin))
|
||||
return config
|
||||
}
|
||||
|
||||
module.exports = override(disableEsLint(), disableModuleScopePlugin())
|
||||
14
alm/docker-compose.yml
Normal file
14
alm/docker-compose.yml
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
version: '2.4'
|
||||
services:
|
||||
alm:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: alm/Dockerfile
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
env_file: ./.env
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
restart: unless-stopped
|
||||
entrypoint: serve -p ${PORT} -s .
|
||||
16
alm/load-env.sh
Executable file
16
alm/load-env.sh
Executable file
@@ -0,0 +1,16 @@
|
||||
#!/bin/bash
|
||||
|
||||
while read line; do
|
||||
if [ "$line" = "" ]; then
|
||||
: # Skip empty lines
|
||||
elif [[ "$line" =~ \#.* ]]; then
|
||||
: # Skip comment lines
|
||||
elif [[ "$line" =~ "UI_PORT"* ]]; then
|
||||
eval $line
|
||||
export PORT="$UI_PORT"
|
||||
else
|
||||
export "REACT_APP_$line"
|
||||
fi
|
||||
done < '.env'
|
||||
|
||||
$*
|
||||
59
alm/package.json
Normal file
59
alm/package.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "alm",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@react-hook/window-size": "^3.0.6",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.3.2",
|
||||
"@testing-library/user-event": "^7.1.2",
|
||||
"@types/jest": "^24.0.0",
|
||||
"@types/node": "^12.0.0",
|
||||
"@types/promise-retry": "^1.1.3",
|
||||
"@types/react": "^16.9.0",
|
||||
"@types/react-dom": "^16.9.0",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"@types/styled-components": "^5.1.0",
|
||||
"@use-it/interval": "^0.1.3",
|
||||
"customize-cra": "^1.0.0",
|
||||
"date-fns": "^2.14.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"fast-memoize": "^2.5.2",
|
||||
"promise-retry": "^2.0.1",
|
||||
"react": "^16.13.1",
|
||||
"react-app-rewired": "^2.1.6",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.0.1",
|
||||
"styled-components": "^5.1.1",
|
||||
"typescript": "^3.5.2",
|
||||
"web3": "1.2.7",
|
||||
"web3-eth-contract": "1.2.7"
|
||||
},
|
||||
"scripts": {
|
||||
"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",
|
||||
"createSnapshots": "node scripts/createSnapshots.js"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "react-app"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-plugin-prettier": "^3.1.3"
|
||||
}
|
||||
}
|
||||
BIN
alm/public/favicon.ico
Normal file
BIN
alm/public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.1 KiB |
45
alm/public/index.html
Normal file
45
alm/public/index.html
Normal file
@@ -0,0 +1,45 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta
|
||||
name="description"
|
||||
content="AMB Live Monitoring"
|
||||
/>
|
||||
<link rel="stylesheet" href="https://unpkg.com/chota@latest">
|
||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
||||
<!--
|
||||
manifest.json provides metadata used when your web app is installed on a
|
||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
||||
-->
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
<!--
|
||||
Notice the use of %PUBLIC_URL% in the tags above.
|
||||
It will be replaced with the URL of the `public` folder during the build.
|
||||
Only files inside the `public` folder can be referenced from the HTML.
|
||||
|
||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
||||
work correctly both with client-side routing and a non-root public URL.
|
||||
Learn how to configure a non-root public URL by running `npm run build`.
|
||||
-->
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,700" rel="stylesheet">
|
||||
<title>AMB Live Monitoring</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<!--
|
||||
This HTML file is a template.
|
||||
If you open it directly in the browser, you will see an empty page.
|
||||
|
||||
You can add webfonts, meta tags, or analytics to this file.
|
||||
The build step will place the bundled scripts into the <body> tag.
|
||||
|
||||
To begin the development, run `npm start` or `yarn start`.
|
||||
To create a production bundle, use `npm run build` or `yarn build`.
|
||||
-->
|
||||
</body>
|
||||
</html>
|
||||
15
alm/public/manifest.json
Normal file
15
alm/public/manifest.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"short_name": "ALM",
|
||||
"name": "AMB Live Monitoring",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#000000",
|
||||
"background_color": "#ffffff"
|
||||
}
|
||||
3
alm/public/robots.txt
Normal file
3
alm/public/robots.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
||||
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)
|
||||
})
|
||||
16
alm/src/App.tsx
Normal file
16
alm/src/App.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import { MainPage } from './components/MainPage'
|
||||
import { StateProvider } from './state/StateProvider'
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
<StateProvider>
|
||||
<MainPage />
|
||||
</StateProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
319
alm/src/abis/BridgeValidators.ts
Normal file
319
alm/src/abis/BridgeValidators.ts
Normal file
@@ -0,0 +1,319 @@
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
const abi: AbiItem[] = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorCount',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeValidatorsInterfacesVersion',
|
||||
outputs: [
|
||||
{
|
||||
name: 'major',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'minor',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'patch',
|
||||
type: 'uint64'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'isInitialized',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorList',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address[]'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_requiredSignatures',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setRequiredSignatures',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredSignatures',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_address',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'getNextValidator',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'isValidatorDuty',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'deployedAtBlock',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'F_ADDR',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'isValidator',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'ValidatorAdded',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'ValidatorRemoved',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'requiredSignatures',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'RequiredSignaturesChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'previousOwner',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_requiredSignatures',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_initialValidators',
|
||||
type: 'address[]'
|
||||
},
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'addValidator',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_validator',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'removeValidator',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
export default abi
|
||||
589
alm/src/abis/ForeignAMB.ts
Normal file
589
alm/src/abis/ForeignAMB.ts
Normal file
@@ -0,0 +1,589 @@
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
const abi: AbiItem[] = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'transactionHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_txHash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'relayedMessages',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_validatorContract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'isInitialized',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredBlockConfirmations',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: '_signatures',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'executeSignatures',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'getMinimumGasUsage',
|
||||
outputs: [
|
||||
{
|
||||
name: 'gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageReceiver',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeMode',
|
||||
outputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes4'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setChainIds',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_token',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_to',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'claimTokens',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setMaxGasPerTx',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredSignatures',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorContract',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'deployedAtBlock',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeInterfacesVersion',
|
||||
outputs: [
|
||||
{
|
||||
name: 'major',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'minor',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'patch',
|
||||
type: 'uint64'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSourceChainId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_blockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setRequiredBlockConfirmations',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setGasPrice',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'messageCallStatus',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_contract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: '_gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'requireToPassMessage',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageDataHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'maxGasPerTx',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'gasPrice',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'encodedData',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'UserRequestForAffirmation',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'sender',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'executor',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'status',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
name: 'RelayedMessage',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'GasPriceChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'RequiredBlockConfirmationChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'previousOwner',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event'
|
||||
}
|
||||
]
|
||||
|
||||
export default abi
|
||||
777
alm/src/abis/HomeAMB.ts
Normal file
777
alm/src/abis/HomeAMB.ts
Normal file
@@ -0,0 +1,777 @@
|
||||
import { AbiItem } from 'web3-utils'
|
||||
|
||||
const abi: AbiItem[] = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'transactionHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_message',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'numMessagesSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
name: '_index',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'signature',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_validatorContract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_owner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'initialize',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'isInitialized',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredBlockConfirmations',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'getMinimumGasUsage',
|
||||
outputs: [
|
||||
{
|
||||
name: 'gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageReceiver',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeMode',
|
||||
outputs: [
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes4'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_sourceChainId',
|
||||
type: 'uint256'
|
||||
},
|
||||
{
|
||||
name: '_destinationChainId',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setChainIds',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'message',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'signature',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: 'message',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'submitSignature',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_token',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_to',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'claimTokens',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'numAffirmationsSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_hash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'affirmationsSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_maxGasPerTx',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setMaxGasPerTx',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'requiredSignatures',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'owner',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_message',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'messagesSigned',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'validatorContract',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'deployedAtBlock',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'getBridgeInterfacesVersion',
|
||||
outputs: [
|
||||
{
|
||||
name: 'major',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'minor',
|
||||
type: 'uint64'
|
||||
},
|
||||
{
|
||||
name: 'patch',
|
||||
type: 'uint64'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSourceChainId',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_blockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setRequiredBlockConfirmations',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'setGasPrice',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'messageCallStatus',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'messageSender',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: '_contract',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
name: '_data',
|
||||
type: 'bytes'
|
||||
},
|
||||
{
|
||||
name: '_gas',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'requireToPassMessage',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_messageId',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'failedMessageDataHash',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'maxGasPerTx',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'message',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'executeAffirmation',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: false,
|
||||
inputs: [
|
||||
{
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'transferOwnership',
|
||||
outputs: [],
|
||||
payable: false,
|
||||
stateMutability: 'nonpayable',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'gasPrice',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [
|
||||
{
|
||||
name: '_number',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'isAlreadyProcessed',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'pure',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'encodedData',
|
||||
type: 'bytes'
|
||||
}
|
||||
],
|
||||
name: 'UserRequestForSignature',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'sender',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'executor',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: true,
|
||||
name: 'messageId',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'status',
|
||||
type: 'bool'
|
||||
}
|
||||
],
|
||||
name: 'AffirmationCompleted',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'signer',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'messageHash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'SignedForUserRequest',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: true,
|
||||
name: 'signer',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'messageHash',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
name: 'SignedForAffirmation',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'authorityResponsibleForRelay',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'messageHash',
|
||||
type: 'bytes32'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'NumberOfCollectedSignatures',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'CollectedSignatures',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'gasPrice',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'GasPriceChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'requiredBlockConfirmations',
|
||||
type: 'uint256'
|
||||
}
|
||||
],
|
||||
name: 'RequiredBlockConfirmationChanged',
|
||||
type: 'event'
|
||||
},
|
||||
{
|
||||
anonymous: false,
|
||||
inputs: [
|
||||
{
|
||||
indexed: false,
|
||||
name: 'previousOwner',
|
||||
type: 'address'
|
||||
},
|
||||
{
|
||||
indexed: false,
|
||||
name: 'newOwner',
|
||||
type: 'address'
|
||||
}
|
||||
],
|
||||
name: 'OwnershipTransferred',
|
||||
type: 'event'
|
||||
}
|
||||
]
|
||||
|
||||
export default abi
|
||||
3
alm/src/abis/index.ts
Normal file
3
alm/src/abis/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export { default as HOME_AMB_ABI } from './HomeAMB'
|
||||
export { default as FOREIGN_AMB_ABI } from './ForeignAMB'
|
||||
export { default as BRIDGE_VALIDATORS_ABI } from './BridgeValidators'
|
||||
109
alm/src/components/ConfirmationsContainer.tsx
Normal file
109
alm/src/components/ConfirmationsContainer.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { useMessageConfirmations } from '../hooks/useMessageConfirmations'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
import styled from 'styled-components'
|
||||
import { CONFIRMATIONS_STATUS } from '../config/constants'
|
||||
import { CONFIRMATIONS_STATUS_LABEL, CONFIRMATIONS_STATUS_LABEL_HOME } from '../config/descriptions'
|
||||
import { SimpleLoading } from './commons/Loading'
|
||||
import { ValidatorsConfirmations } from './ValidatorsConfirmations'
|
||||
import { getConfirmationsStatusDescription } from '../utils/networks'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { ExecutionConfirmation } from './ExecutionConfirmation'
|
||||
import { useValidatorContract } from '../hooks/useValidatorContract'
|
||||
import { useBlockConfirmations } from '../hooks/useBlockConfirmations'
|
||||
import { MultiLine } from './commons/MultiLine'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
|
||||
const StatusLabel = styled.label`
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
`
|
||||
|
||||
const StatusResultLabel = styled.label`
|
||||
font-size: 18px;
|
||||
padding-left: 10px;
|
||||
`
|
||||
|
||||
const StyledConfirmationContainer = styled.div`
|
||||
background-color: var(--bg-color);
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
const StatusDescription = styled.div`
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
export interface ConfirmationsContainerParams {
|
||||
message: MessageObject
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
fromHome: boolean
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export const ConfirmationsContainer = ({ message, receipt, fromHome, timestamp }: ConfirmationsContainerParams) => {
|
||||
const {
|
||||
home: { name: homeName },
|
||||
foreign: { name: foreignName }
|
||||
} = useStateProvider()
|
||||
const { requiredSignatures, validatorList } = useValidatorContract({ fromHome, receipt })
|
||||
const { blockConfirmations } = useBlockConfirmations({ fromHome, receipt })
|
||||
const { confirmations, status, executionData, signatureCollected, waitingBlocksResolved } = useMessageConfirmations({
|
||||
message,
|
||||
receipt,
|
||||
fromHome,
|
||||
timestamp,
|
||||
requiredSignatures,
|
||||
validatorList,
|
||||
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 data-id="status">
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? statusLabel[status] : <SimpleLoading />}
|
||||
</StatusResultLabel>
|
||||
</div>
|
||||
<StatusDescription className="row is-center">
|
||||
<MultiLine className="col-10">
|
||||
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? parseDescription() : ''}
|
||||
</MultiLine>
|
||||
</StatusDescription>
|
||||
<ValidatorsConfirmations
|
||||
confirmations={confirmations}
|
||||
requiredSignatures={requiredSignatures}
|
||||
validatorList={validatorList}
|
||||
waitingBlocksResolved={waitingBlocksResolved}
|
||||
/>
|
||||
{signatureCollected && <ExecutionConfirmation executionData={executionData} isHome={!fromHome} />}
|
||||
</StyledConfirmationContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
76
alm/src/components/ExecutionConfirmation.tsx
Normal file
76
alm/src/components/ExecutionConfirmation.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import React from 'react'
|
||||
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
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'
|
||||
import { Thead, AgeTd, StatusTd } from './commons/Table'
|
||||
|
||||
const StyledExecutionConfirmation = styled.div`
|
||||
margin-top: 30px;
|
||||
`
|
||||
|
||||
export interface ExecutionConfirmationParams {
|
||||
executionData: ExecutionData
|
||||
isHome: boolean
|
||||
}
|
||||
|
||||
export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfirmationParams) => {
|
||||
const windowWidth = useWindowWidth()
|
||||
|
||||
const txExplorerLink = getExplorerTxUrl(executionData.txHash, isHome)
|
||||
const formattedValidator =
|
||||
windowWidth < 850 && executionData.validator ? formatTxHash(executionData.validator) : executionData.validator
|
||||
|
||||
const getExecutionStatusElement = (validatorStatus = '') => {
|
||||
switch (validatorStatus) {
|
||||
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
|
||||
return <SuccessLabel>{validatorStatus}</SuccessLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
|
||||
return <RedLabel>{validatorStatus}</RedLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
|
||||
return <GreyLabel>{validatorStatus}</GreyLabel>
|
||||
default:
|
||||
return executionData.validator ? (
|
||||
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StyledExecutionConfirmation>
|
||||
<table>
|
||||
<Thead>
|
||||
<tr>
|
||||
<th>Executed by</th>
|
||||
<th className="text-center">Status</th>
|
||||
<th className="text-center">Age</th>
|
||||
</tr>
|
||||
</Thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{formattedValidator ? formattedValidator : <SimpleLoading />}</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>
|
||||
</StyledExecutionConfirmation>
|
||||
)
|
||||
}
|
||||
69
alm/src/components/Form.tsx
Normal file
69
alm/src/components/Form.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useState, FormEvent } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { FormSubmitParams } from './MainPage'
|
||||
import { Button } from './commons/Button'
|
||||
import { TransactionSelector } from './TransactionSelector'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
|
||||
const LabelText = styled.label`
|
||||
line-height: 36px;
|
||||
max-width: 140px;
|
||||
`
|
||||
|
||||
const Input = styled.input`
|
||||
background-color: var(--bg-color);
|
||||
color: var(--font-color);
|
||||
max-width: 100%;
|
||||
border-color: var(--color-primary) !important;
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
border-color: var(--button-color) !important;
|
||||
}
|
||||
`
|
||||
|
||||
export const Form = ({ onSubmit }: { onSubmit: ({ chainId, txHash, receipt }: FormSubmitParams) => void }) => {
|
||||
const [txHash, setTxHash] = useState('')
|
||||
const [searchTx, setSearchTx] = useState(false)
|
||||
|
||||
const formSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
setSearchTx(true)
|
||||
}
|
||||
|
||||
const onSelected = (chainId: number, receipt: TransactionReceipt) => {
|
||||
onSubmit({ chainId, txHash, receipt })
|
||||
}
|
||||
|
||||
const onBack = () => {
|
||||
setTxHash('')
|
||||
setSearchTx(false)
|
||||
}
|
||||
|
||||
if (searchTx) {
|
||||
return <TransactionSelector txHash={txHash} onSelected={onSelected} onBack={onBack} />
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={formSubmit}>
|
||||
<div className="row is-center">
|
||||
<LabelText className="col-2">Bridgeable tx hash:</LabelText>
|
||||
<div className="col-7">
|
||||
<Input
|
||||
placeholder="Enter transaction hash"
|
||||
type="text"
|
||||
onChange={e => setTxHash(e.target.value)}
|
||||
required
|
||||
pattern="^0x[a-fA-F0-9]{64}$"
|
||||
value={txHash}
|
||||
/>
|
||||
</div>
|
||||
<div className="col-1">
|
||||
<Button className="button outline" type="submit">
|
||||
Check
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
148
alm/src/components/MainPage.tsx
Normal file
148
alm/src/components/MainPage.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { Route, useHistory } from 'react-router-dom'
|
||||
import { Form } from './Form'
|
||||
import { StatusContainer } from './StatusContainer'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { InfoAlert } from './commons/InfoAlert'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { FOREIGN_NETWORK_NAME, HOME_NETWORK_NAME } from '../config/constants'
|
||||
|
||||
const StyledMainPage = styled.div`
|
||||
text-align: center;
|
||||
min-height: 100vh;
|
||||
`
|
||||
|
||||
const Header = styled.header`
|
||||
background-color: #001529;
|
||||
color: #ffffff;
|
||||
margin-bottom: 50px;
|
||||
`
|
||||
|
||||
const HeaderContainer = styled.header`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
font-size: 16px;
|
||||
height: 64px;
|
||||
line-height: 64px;
|
||||
padding: 0 50px;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding: 0 20px;
|
||||
}
|
||||
`
|
||||
|
||||
const AlertP = styled.p`
|
||||
align-items: start;
|
||||
margin-bottom: 0;
|
||||
@media (max-width: 600px) {
|
||||
flex-direction: column;
|
||||
}
|
||||
`
|
||||
|
||||
export interface FormSubmitParams {
|
||||
chainId: number
|
||||
txHash: string
|
||||
receipt: TransactionReceipt
|
||||
}
|
||||
|
||||
export const MainPage = () => {
|
||||
const history = useHistory()
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [networkName, setNetworkName] = useState('')
|
||||
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||
const [showInfoAlert, setShowInfoAlert] = useState(false)
|
||||
|
||||
const loadFromStorage = useCallback(() => {
|
||||
const hideAlert = window.localStorage.getItem('hideInfoAlert')
|
||||
setShowInfoAlert(!hideAlert)
|
||||
}, [])
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
loadFromStorage()
|
||||
},
|
||||
[loadFromStorage]
|
||||
)
|
||||
|
||||
const onAlertClose = useCallback(
|
||||
() => {
|
||||
window.localStorage.setItem('hideInfoAlert', 'true')
|
||||
loadFromStorage()
|
||||
},
|
||||
[loadFromStorage]
|
||||
)
|
||||
|
||||
const setNetworkData = (chainId: number) => {
|
||||
const network = chainId === home.chainId ? home.name : foreign.name
|
||||
|
||||
setNetworkName(network)
|
||||
}
|
||||
|
||||
const onFormSubmit = ({ chainId, txHash, receipt }: FormSubmitParams) => {
|
||||
setNetworkData(chainId)
|
||||
setReceipt(receipt)
|
||||
history.push(`/${chainId}/${txHash}`)
|
||||
}
|
||||
|
||||
const resetNetworkHeader = () => {
|
||||
setNetworkName('')
|
||||
}
|
||||
|
||||
const setNetworkFromParams = (chainId: number) => {
|
||||
setNetworkData(chainId)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const w = window as any
|
||||
if (w.ethereum) {
|
||||
w.ethereum.autoRefreshOnNetworkChange = false
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<StyledMainPage>
|
||||
<Header>
|
||||
<HeaderContainer>
|
||||
<span>AMB Live Monitoring</span>
|
||||
<span>{networkName}</span>
|
||||
</HeaderContainer>
|
||||
</Header>
|
||||
<div className="container">
|
||||
{showInfoAlert && (
|
||||
<InfoAlert onClick={onAlertClose}>
|
||||
<p className="is-left text-left">
|
||||
The Arbitrary Message Bridge Live Monitoring application provides real-time status updates for messages
|
||||
bridged between {HOME_NETWORK_NAME} and {FOREIGN_NETWORK_NAME}. You can check current tx status, view
|
||||
validator info, and troubleshoot potential issues with bridge transfers.
|
||||
</p>
|
||||
<AlertP className="is-left text-left">
|
||||
For more information refer to
|
||||
<ExplorerTxLink
|
||||
href="https://docs.tokenbridge.net/about-tokenbridge/components/amb-live-monitoring-application"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
the ALM documentation
|
||||
</ExplorerTxLink>
|
||||
</AlertP>
|
||||
</InfoAlert>
|
||||
)}
|
||||
<Route exact path={['/']} children={<Form onSubmit={onFormSubmit} />} />
|
||||
<Route
|
||||
path={['/:chainId/:txHash/:messageIdParam', '/:chainId/:txHash']}
|
||||
children={
|
||||
<StatusContainer
|
||||
onBackToMain={resetNetworkHeader}
|
||||
setNetworkFromParams={setNetworkFromParams}
|
||||
receiptParam={receipt}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</StyledMainPage>
|
||||
)
|
||||
}
|
||||
47
alm/src/components/MessageSelector.tsx
Normal file
47
alm/src/components/MessageSelector.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from './commons/Button'
|
||||
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
import { formatTxHashExtended } from '../utils/networks'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
|
||||
export interface MessageSelectorParams {
|
||||
messages: Array<MessageObject>
|
||||
onMessageSelected: (index: number) => void
|
||||
}
|
||||
|
||||
export const MessageSelector = ({ messages, onMessageSelected }: MessageSelectorParams) => {
|
||||
const [messageIndex, setMessageIndex] = useState(0)
|
||||
const windowWidth = useWindowWidth()
|
||||
|
||||
const onSelect = () => {
|
||||
onMessageSelected(messageIndex)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<div className="col-7-lg col-12 is-marginless">
|
||||
{messages.map((message, i) => (
|
||||
<RadioButtonContainer className="row is-center is-vertical-align" key={i} onClick={() => setMessageIndex(i)}>
|
||||
<input
|
||||
className="is-marginless"
|
||||
type="radio"
|
||||
name="message"
|
||||
value={i}
|
||||
checked={i === messageIndex}
|
||||
onChange={() => setMessageIndex(i)}
|
||||
/>
|
||||
<RadioButtonLabel htmlFor={i.toString()}>
|
||||
{windowWidth < 700 ? formatTxHashExtended(message.id) : message.id}
|
||||
</RadioButtonLabel>
|
||||
</RadioButtonContainer>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-1-lg col-12 is-marginless">
|
||||
<Button className="button outline" onClick={onSelect}>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
47
alm/src/components/NetworkTransactionSelector.tsx
Normal file
47
alm/src/components/NetworkTransactionSelector.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React, { useState } from 'react'
|
||||
import { Button } from './commons/Button'
|
||||
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
|
||||
export const NetworkTransactionSelector = ({ onNetworkSelected }: { onNetworkSelected: (chainId: number) => void }) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [chainId, setChainId] = useState(home.chainId)
|
||||
|
||||
const networks = [home, foreign]
|
||||
|
||||
const onSelect = () => {
|
||||
onNetworkSelected(chainId)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>The transaction was found in both networks, please select one:</p>
|
||||
<div className="row is-center">
|
||||
<div className="col-3-lg col-12 is-marginless">
|
||||
{networks.map((network, i) => (
|
||||
<RadioButtonContainer
|
||||
className="row is-center is-vertical-align"
|
||||
key={i}
|
||||
onClick={() => setChainId(network.chainId)}
|
||||
>
|
||||
<input
|
||||
className="is-marginless"
|
||||
type="radio"
|
||||
name="message"
|
||||
value={network.chainId}
|
||||
checked={network.chainId === chainId}
|
||||
onChange={() => setChainId(network.chainId)}
|
||||
/>
|
||||
<RadioButtonLabel htmlFor={i.toString()}>{network.name}</RadioButtonLabel>
|
||||
</RadioButtonContainer>
|
||||
))}
|
||||
</div>
|
||||
<div className="col-3-lg col-12 is-marginless">
|
||||
<Button className="button outline" onClick={onSelect}>
|
||||
Select
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
109
alm/src/components/StatusContainer.tsx
Normal file
109
alm/src/components/StatusContainer.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useHistory, useParams } from 'react-router-dom'
|
||||
import { useTransactionStatus } from '../hooks/useTransactionStatus'
|
||||
import { formatTxHash, getExplorerTxUrl, getTransactionStatusDescription, validTxHash } from '../utils/networks'
|
||||
import { TRANSACTION_STATUS } from '../config/constants'
|
||||
import { MessageSelector } from './MessageSelector'
|
||||
import { Loading } from './commons/Loading'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { ExplorerTxLink } from './commons/ExplorerTxLink'
|
||||
import { ConfirmationsContainer } from './ConfirmationsContainer'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { BackButton } from './commons/BackButton'
|
||||
|
||||
export interface StatusContainerParam {
|
||||
onBackToMain: () => void
|
||||
setNetworkFromParams: (chainId: number) => void
|
||||
receiptParam: Maybe<TransactionReceipt>
|
||||
}
|
||||
|
||||
export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptParam }: StatusContainerParam) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const history = useHistory()
|
||||
const { chainId, txHash, messageIdParam } = useParams()
|
||||
const validChainId = chainId === home.chainId.toString() || chainId === foreign.chainId.toString()
|
||||
const validParameters = validChainId && validTxHash(txHash)
|
||||
|
||||
const { messages, receipt, status, description, timestamp, loading } = useTransactionStatus({
|
||||
txHash: validParameters ? txHash : '',
|
||||
chainId: validParameters ? parseInt(chainId) : 0,
|
||||
receiptParam
|
||||
})
|
||||
|
||||
const selectedMessageId = messageIdParam === undefined || messages[messageIdParam] === undefined ? -1 : messageIdParam
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (validChainId) {
|
||||
setNetworkFromParams(parseInt(chainId))
|
||||
}
|
||||
},
|
||||
[validChainId, chainId, setNetworkFromParams]
|
||||
)
|
||||
|
||||
if (!validParameters && home.chainId && foreign.chainId) {
|
||||
return (
|
||||
<div>
|
||||
<p>
|
||||
Chain Id: {chainId} and/or Transaction Hash: {txHash} are not valid
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <Loading />
|
||||
}
|
||||
|
||||
const onMessageSelected = (messageId: number) => {
|
||||
history.push(`/${chainId}/${txHash}/${messageId}`)
|
||||
}
|
||||
|
||||
const displayMessageSelector = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId === -1
|
||||
const multiMessageSelected = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId !== -1
|
||||
const displayReference = multiMessageSelected ? messages[selectedMessageId].id : txHash
|
||||
const formattedMessageId = formatTxHash(displayReference)
|
||||
|
||||
const isHome = chainId === home.chainId.toString()
|
||||
const txExplorerLink = getExplorerTxUrl(txHash, isHome)
|
||||
const displayExplorerLink = status !== TRANSACTION_STATUS.NOT_FOUND
|
||||
|
||||
const displayConfirmations = status === TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE || multiMessageSelected
|
||||
const messageToConfirm =
|
||||
messages.length > 1 ? messages[selectedMessageId] : messages.length > 0 ? messages[0] : { id: '', data: '' }
|
||||
|
||||
let displayedDescription: string = multiMessageSelected
|
||||
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
|
||||
: description
|
||||
let link
|
||||
const descArray = displayedDescription.split('%link')
|
||||
if (descArray.length > 1) {
|
||||
displayedDescription = descArray[0]
|
||||
link = (
|
||||
<ExplorerTxLink href={descArray[1]} target="_blank" rel="noopener noreferrer">
|
||||
{descArray[1]}
|
||||
</ExplorerTxLink>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{status && (
|
||||
<p>
|
||||
The transaction{' '}
|
||||
{displayExplorerLink && (
|
||||
<ExplorerTxLink href={txExplorerLink} target="_blank">
|
||||
{formattedMessageId}
|
||||
</ExplorerTxLink>
|
||||
)}
|
||||
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription} {link}
|
||||
</p>
|
||||
)}
|
||||
{displayMessageSelector && <MessageSelector messages={messages} onMessageSelected={onMessageSelected} />}
|
||||
{displayConfirmations && (
|
||||
<ConfirmationsContainer message={messageToConfirm} receipt={receipt} fromHome={isHome} timestamp={timestamp} />
|
||||
)}
|
||||
<BackButton onBackToMain={onBackToMain} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
67
alm/src/components/TransactionSelector.tsx
Normal file
67
alm/src/components/TransactionSelector.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useEffect } from 'react'
|
||||
import { useTransactionFinder } from '../hooks/useTransactionFinder'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TRANSACTION_STATUS } from '../config/constants'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { Loading } from './commons/Loading'
|
||||
import { NetworkTransactionSelector } from './NetworkTransactionSelector'
|
||||
import { BackButton } from './commons/BackButton'
|
||||
import { TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
|
||||
import { MultiLine } from './commons/MultiLine'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledMultiLine = styled(MultiLine)`
|
||||
margin-bottom: 40px;
|
||||
`
|
||||
|
||||
export const TransactionSelector = ({
|
||||
txHash,
|
||||
onSelected,
|
||||
onBack
|
||||
}: {
|
||||
txHash: string
|
||||
onSelected: (chainId: number, receipt: TransactionReceipt) => void
|
||||
onBack: () => void
|
||||
}) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const { receipt: homeReceipt, status: homeStatus } = useTransactionFinder({ txHash, web3: home.web3 })
|
||||
const { receipt: foreignReceipt, status: foreignStatus } = useTransactionFinder({ txHash, web3: foreign.web3 })
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!home.chainId || !foreign.chainId) return
|
||||
if (homeStatus === TRANSACTION_STATUS.FOUND && foreignStatus === TRANSACTION_STATUS.NOT_FOUND) {
|
||||
if (!homeReceipt) return
|
||||
onSelected(home.chainId, homeReceipt)
|
||||
} else if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
|
||||
if (!foreignReceipt) return
|
||||
onSelected(foreign.chainId, foreignReceipt)
|
||||
}
|
||||
},
|
||||
[homeReceipt, homeStatus, foreignReceipt, foreignStatus, home.chainId, foreign.chainId, onSelected]
|
||||
)
|
||||
|
||||
const onSelectedNetwork = (chainId: number) => {
|
||||
const chain = chainId === home.chainId ? home.chainId : foreign.chainId
|
||||
const receipt = chainId === home.chainId ? homeReceipt : foreignReceipt
|
||||
|
||||
if (!receipt) return
|
||||
onSelected(chain, receipt)
|
||||
}
|
||||
|
||||
if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.FOUND) {
|
||||
return <NetworkTransactionSelector onNetworkSelected={onSelectedNetwork} />
|
||||
}
|
||||
|
||||
if (foreignStatus === TRANSACTION_STATUS.NOT_FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
|
||||
const message = TRANSACTION_STATUS_DESCRIPTION[TRANSACTION_STATUS.NOT_FOUND]
|
||||
return (
|
||||
<div>
|
||||
<StyledMultiLine>{message}</StyledMultiLine>
|
||||
<BackButton onBackToMain={onBack} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <Loading />
|
||||
}
|
||||
101
alm/src/components/ValidatorsConfirmations.tsx
Normal file
101
alm/src/components/ValidatorsConfirmations.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
import React from 'react'
|
||||
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
|
||||
import { useWindowWidth } from '@react-hook/window-size'
|
||||
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'
|
||||
import { Thead, AgeTd, StatusTd } from './commons/Table'
|
||||
|
||||
const RequiredConfirmations = styled.label`
|
||||
font-size: 14px;
|
||||
`
|
||||
|
||||
export interface ValidatorsConfirmationsParams {
|
||||
confirmations: Array<ConfirmationParam>
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
waitingBlocksResolved: boolean
|
||||
}
|
||||
|
||||
export const ValidatorsConfirmations = ({
|
||||
confirmations,
|
||||
requiredSignatures,
|
||||
validatorList,
|
||||
waitingBlocksResolved
|
||||
}: ValidatorsConfirmationsParams) => {
|
||||
const windowWidth = useWindowWidth()
|
||||
|
||||
const getValidatorStatusElement = (validatorStatus = '') => {
|
||||
switch (validatorStatus) {
|
||||
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
|
||||
return <SuccessLabel>{validatorStatus}</SuccessLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
|
||||
return <RedLabel>{validatorStatus}</RedLabel>
|
||||
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
|
||||
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
|
||||
return <GreyLabel>{validatorStatus}</GreyLabel>
|
||||
default:
|
||||
return waitingBlocksResolved ? (
|
||||
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<table>
|
||||
<Thead>
|
||||
<tr>
|
||||
<th>Validator</th>
|
||||
<th className="text-center">Status</th>
|
||||
<th className="text-center">Age</th>
|
||||
</tr>
|
||||
</Thead>
|
||||
<tbody>
|
||||
{validatorList.map((validator, i) => {
|
||||
const filteredConfirmation = confirmations.filter(c => c.validator === validator)
|
||||
const confirmation = filteredConfirmation.length > 0 ? filteredConfirmation[0] : null
|
||||
const displayedStatus = confirmation && confirmation.status ? confirmation.status : ''
|
||||
const explorerLink = confirmation && confirmation.txHash ? getExplorerTxUrl(confirmation.txHash, true) : ''
|
||||
const elementIfNoTimestamp =
|
||||
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.WAITING &&
|
||||
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED ? (
|
||||
(displayedStatus === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || displayedStatus === '') &&
|
||||
waitingBlocksResolved ? (
|
||||
SEARCHING_TX
|
||||
) : (
|
||||
<SimpleLoading />
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)
|
||||
return (
|
||||
<tr key={i}>
|
||||
<td>{windowWidth < 850 ? formatTxHash(validator) : validator}</td>
|
||||
<StatusTd className="text-center">{getValidatorStatusElement(displayedStatus)}</StatusTd>
|
||||
<AgeTd className="text-center">
|
||||
{confirmation && confirmation.timestamp > 0 ? (
|
||||
<ExplorerTxLink href={explorerLink} target="_blank">
|
||||
{formatTimestamp(confirmation.timestamp)}
|
||||
</ExplorerTxLink>
|
||||
) : (
|
||||
elementIfNoTimestamp
|
||||
)}
|
||||
</AgeTd>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
<RequiredConfirmations>
|
||||
{requiredSignatures} of {validatorList.length} confirmations required
|
||||
</RequiredConfirmations>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
35
alm/src/components/commons/BackButton.tsx
Normal file
35
alm/src/components/commons/BackButton.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Link } from 'react-router-dom'
|
||||
import { LeftArrow } from './LeftArrow'
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
|
||||
const StyledButton = styled.button`
|
||||
color: var(--button-color);
|
||||
border-color: var(--font-color);
|
||||
margin-top: 10px;
|
||||
&:focus {
|
||||
outline: var(--button-color);
|
||||
}
|
||||
`
|
||||
|
||||
const BackLabel = styled.label`
|
||||
margin-left: 5px;
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
export interface BackButtonParam {
|
||||
onBackToMain: () => void
|
||||
}
|
||||
|
||||
export const BackButton = ({ onBackToMain }: BackButtonParam) => (
|
||||
<div className="row is-center">
|
||||
<div className="col-9">
|
||||
<Link to="/" onClick={onBackToMain}>
|
||||
<StyledButton className="button outline is-left">
|
||||
<LeftArrow />
|
||||
<BackLabel>Search another transaction</BackLabel>
|
||||
</StyledButton>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
10
alm/src/components/commons/Button.tsx
Normal file
10
alm/src/components/commons/Button.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Button = styled.button`
|
||||
height: 36px;
|
||||
color: var(--button-color);
|
||||
border-color: var(--button-color);
|
||||
&:focus {
|
||||
outline: var(--button-color);
|
||||
}
|
||||
`
|
||||
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>
|
||||
)
|
||||
7
alm/src/components/commons/ExplorerTxLink.tsx
Normal file
7
alm/src/components/commons/ExplorerTxLink.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const ExplorerTxLink = styled.a`
|
||||
color: var(--link-color);
|
||||
text-decoration: underline;
|
||||
font-weight: bold;
|
||||
`
|
||||
31
alm/src/components/commons/InfoAlert.tsx
Normal file
31
alm/src/components/commons/InfoAlert.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
import React from 'react'
|
||||
import styled from 'styled-components'
|
||||
import { InfoIcon } from './InfoIcon'
|
||||
import { CloseIcon } from './CloseIcon'
|
||||
|
||||
const StyledInfoAlert = styled.div`
|
||||
border: 1px solid var(--button-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 20px;
|
||||
padding-top: 10px;
|
||||
`
|
||||
|
||||
const CloseIconContainer = styled.div`
|
||||
cursor: pointer;
|
||||
`
|
||||
|
||||
const TextContainer = styled.div`
|
||||
flex-direction: column;
|
||||
`
|
||||
|
||||
export const InfoAlert = ({ onClick, children }: { onClick: () => void; children: React.ReactChild[] }) => (
|
||||
<div className="row is-center">
|
||||
<StyledInfoAlert className="col-10 is-vertical-align row">
|
||||
<InfoIcon />
|
||||
<TextContainer className="col-10">{children}</TextContainer>
|
||||
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
|
||||
<CloseIcon />
|
||||
</CloseIconContainer>
|
||||
</StyledInfoAlert>
|
||||
</div>
|
||||
)
|
||||
16
alm/src/components/commons/InfoIcon.tsx
Normal file
16
alm/src/components/commons/InfoIcon.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
|
||||
export const InfoIcon = () => (
|
||||
<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>
|
||||
)
|
||||
22
alm/src/components/commons/Labels.tsx
Normal file
22
alm/src/components/commons/Labels.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const SuccessLabel = styled.label`
|
||||
color: var(--success-color);
|
||||
background-color: var(--success-bg-color);
|
||||
padding: 0.4rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export const GreyLabel = styled.label`
|
||||
color: var(--not-required-color);
|
||||
background-color: var(--not-required-bg-color);
|
||||
padding: 0.4rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
`
|
||||
|
||||
export const RedLabel = styled.label`
|
||||
color: var(--failed-color);
|
||||
background-color: var(--failed-bg-color);
|
||||
padding: 0.4rem 0.7rem;
|
||||
border-radius: 4px;
|
||||
`
|
||||
20
alm/src/components/commons/LeftArrow.tsx
Normal file
20
alm/src/components/commons/LeftArrow.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import React from 'react'
|
||||
import { useContext } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
export const LeftArrow = () => {
|
||||
const themeContext = useContext(ThemeContext)
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
version="1.1"
|
||||
id="mdi-arrow-left"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill={themeContext.buttonColor}
|
||||
>
|
||||
<path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
165
alm/src/components/commons/Loading.tsx
Normal file
165
alm/src/components/commons/Loading.tsx
Normal file
@@ -0,0 +1,165 @@
|
||||
import React, { useContext } from 'react'
|
||||
import { ThemeContext } from 'styled-components'
|
||||
|
||||
export interface LoadingParams {
|
||||
width?: string
|
||||
height?: string
|
||||
displayMessage?: boolean
|
||||
}
|
||||
|
||||
export const Loading = ({ width = '50px', height = '50px', displayMessage = true }: LoadingParams) => {
|
||||
const themeContext = useContext(ThemeContext)
|
||||
return (
|
||||
<div className="row is-center">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{ background: 'none', display: 'block', shapeRendering: 'auto' }}
|
||||
width={width}
|
||||
height={height}
|
||||
viewBox="0 0 100 100"
|
||||
preserveAspectRatio="xMidYMid"
|
||||
>
|
||||
<g transform="rotate(0 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.9166666666666666s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(30 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.8333333333333334s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(60 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.75s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(90 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.6666666666666666s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(120 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.5833333333333334s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(150 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.5s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(180 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.4166666666666667s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(210 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.3333333333333333s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(240 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.25s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(270 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.16666666666666666s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(300 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate
|
||||
attributeName="opacity"
|
||||
values="1;0"
|
||||
keyTimes="0;1"
|
||||
dur="1s"
|
||||
begin="-0.08333333333333333s"
|
||||
repeatCount="indefinite"
|
||||
/>
|
||||
</rect>
|
||||
</g>
|
||||
<g transform="rotate(330 50 50)">
|
||||
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
|
||||
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite" />
|
||||
</rect>
|
||||
</g>
|
||||
</svg>
|
||||
{displayMessage && <label style={{ color: themeContext.buttonColor }}>Loading...</label>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const SimpleLoading = () => <Loading width="30px" height="30px" displayMessage={false} />
|
||||
5
alm/src/components/commons/MultiLine.tsx
Normal file
5
alm/src/components/commons/MultiLine.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const MultiLine = styled.div`
|
||||
white-space: pre-wrap;
|
||||
`
|
||||
9
alm/src/components/commons/RadioButton.tsx
Normal file
9
alm/src/components/commons/RadioButton.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const RadioButtonLabel = styled.label`
|
||||
padding-left: 5px;
|
||||
`
|
||||
|
||||
export const RadioButtonContainer = styled.div`
|
||||
padding: 10px;
|
||||
`
|
||||
13
alm/src/components/commons/Table.tsx
Normal file
13
alm/src/components/commons/Table.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import styled from 'styled-components'
|
||||
|
||||
export const Thead = styled.thead`
|
||||
border-bottom: 2px solid #9e9e9e;
|
||||
`
|
||||
|
||||
export const StatusTd = styled.td`
|
||||
width: 150px;
|
||||
`
|
||||
|
||||
export const AgeTd = styled.td`
|
||||
width: 180px;
|
||||
`
|
||||
63
alm/src/config/constants.ts
Normal file
63
alm/src/config/constants.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
export const HOME_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_HOME_BRIDGE_ADDRESS || ''
|
||||
export const FOREIGN_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_FOREIGN_BRIDGE_ADDRESS || ''
|
||||
|
||||
export const HOME_RPC_URL: string = process.env.REACT_APP_COMMON_HOME_RPC_URL || ''
|
||||
export const FOREIGN_RPC_URL: string = process.env.REACT_APP_COMMON_FOREIGN_RPC_URL || ''
|
||||
|
||||
export const HOME_NETWORK_NAME: string = process.env.REACT_APP_ALM_HOME_NETWORK_NAME || ''
|
||||
export const FOREIGN_NETWORK_NAME: string = process.env.REACT_APP_ALM_FOREIGN_NETWORK_NAME || ''
|
||||
|
||||
export const HOME_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_HOME_EXPLORER_TX_TEMPLATE || ''
|
||||
export const FOREIGN_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_TX_TEMPLATE || ''
|
||||
|
||||
export const HOME_EXPLORER_API: string = process.env.REACT_APP_ALM_HOME_EXPLORER_API || ''
|
||||
export const FOREIGN_EXPLORER_API: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_API || ''
|
||||
|
||||
export const HOME_RPC_POLLING_INTERVAL: number = 5000
|
||||
export const FOREIGN_RPC_POLLING_INTERVAL: number = 5000
|
||||
export const BLOCK_RANGE: number = 50
|
||||
export const ONE_DAY_TIMESTAMP: number = 86400
|
||||
export const THREE_DAYS_TIMESTAMP: number = 259200
|
||||
|
||||
export const EXECUTE_AFFIRMATION_HASH = 'e7a2c01f'
|
||||
export const SUBMIT_SIGNATURE_HASH = '630cea8e'
|
||||
export const EXECUTE_SIGNATURES_HASH = '3f7658fd'
|
||||
|
||||
export const CACHE_KEY_SUCCESS = 'success-confirmation-validator-'
|
||||
export const CACHE_KEY_FAILED = 'failed-confirmation-validator-'
|
||||
export const CACHE_KEY_EXECUTION_FAILED = 'failed-execution-validator-'
|
||||
|
||||
export const TRANSACTION_STATUS = {
|
||||
SUCCESS_MULTIPLE_MESSAGES: 'SUCCESS_MULTIPLE_MESSAGES',
|
||||
SUCCESS_ONE_MESSAGE: 'SUCCESS_ONE_MESSAGE',
|
||||
SUCCESS_NO_MESSAGES: 'SUCCESS_NO_MESSAGES',
|
||||
FAILED: 'FAILED',
|
||||
FOUND: 'FOUND',
|
||||
NOT_FOUND: 'NOT_FOUND',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const CONFIRMATIONS_STATUS = {
|
||||
SUCCESS: 'SUCCESS',
|
||||
SUCCESS_MESSAGE_FAILED: 'SUCCESS_MESSAGE_FAILED',
|
||||
EXECUTION_FAILED: 'EXECUTION_FAILED',
|
||||
EXECUTION_PENDING: 'EXECUTION_PENDING',
|
||||
EXECUTION_WAITING: 'EXECUTION_WAITING',
|
||||
FAILED: 'FAILED',
|
||||
PENDING: 'PENDING',
|
||||
SEARCHING: 'SEARCHING',
|
||||
WAITING_VALIDATORS: 'WAITING_VALIDATORS',
|
||||
WAITING_CHAIN: 'WAITING_CHAIN',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const VALIDATOR_CONFIRMATION_STATUS = {
|
||||
SUCCESS: 'Success',
|
||||
FAILED: 'Failed',
|
||||
PENDING: 'Pending',
|
||||
WAITING: 'Waiting',
|
||||
NOT_REQUIRED: 'Not required',
|
||||
UNDEFINED: 'UNDEFINED'
|
||||
}
|
||||
|
||||
export const SEARCHING_TX = 'Searching Transaction...'
|
||||
73
alm/src/config/descriptions.ts
Normal file
73
alm/src/config/descriptions.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
// %t will be replaced by the time -> x minutes/hours/days ago
|
||||
export const TRANSACTION_STATUS_DESCRIPTION: { [key: string]: string } = {
|
||||
SUCCESS_MULTIPLE_MESSAGES: 'was initiated %t and contains several bridge messages. Specify one of them:',
|
||||
SUCCESS_ONE_MESSAGE: 'was initiated %t',
|
||||
SUCCESS_NO_MESSAGES:
|
||||
'successfully mined %t but it does not seem to contain any request to the bridge, \nso nothing needs to be confirmed by the validators. \nIf you are sure that the transaction should contain a request to the bridge,\ncontact to the validators by \nmessaging on %linkhttps://forum.poa.network/c/support',
|
||||
FAILED: 'failed %t',
|
||||
NOT_FOUND:
|
||||
'Transaction not found. \n1. Check that the transaction hash is correct. \n2. Wait several blocks for the transaction to be\nmined, gas price affects mining speed.'
|
||||
}
|
||||
|
||||
export const CONFIRMATIONS_STATUS_LABEL: { [key: string]: string } = {
|
||||
SUCCESS: 'Success',
|
||||
SUCCESS_MESSAGE_FAILED: 'Success',
|
||||
FAILED: 'Failed',
|
||||
PENDING: 'Pending',
|
||||
WAITING_VALIDATORS: 'Waiting',
|
||||
SEARCHING: 'Waiting',
|
||||
WAITING_CHAIN: 'Waiting'
|
||||
}
|
||||
|
||||
export const CONFIRMATIONS_STATUS_LABEL_HOME: { [key: string]: string } = {
|
||||
SUCCESS: 'Success',
|
||||
SUCCESS_MESSAGE_FAILED: 'Success',
|
||||
EXECUTION_FAILED: 'Execution failed',
|
||||
EXECUTION_PENDING: 'Execution pending',
|
||||
EXECUTION_WAITING: 'Execution waiting',
|
||||
FAILED: 'Confirmation Failed',
|
||||
PENDING: 'Confirmation Pending',
|
||||
WAITING_VALIDATORS: 'Confirmation Waiting',
|
||||
SEARCHING: 'Confirmation Waiting',
|
||||
WAITING_CHAIN: 'Confirmation Waiting'
|
||||
}
|
||||
|
||||
// use %link to identify a link
|
||||
export const CONFIRMATIONS_STATUS_DESCRIPTION: { [key: string]: string } = {
|
||||
SUCCESS: '',
|
||||
SUCCESS_MESSAGE_FAILED:
|
||||
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
|
||||
FAILED:
|
||||
'The specified transaction was included in a block,\nbut confirmations sent by a majority of validators\nfailed. The cross-chain relay request will not be\nprocessed. Contact to the validators by\nmessaging on %linkhttps://forum.poa.network/c/support',
|
||||
PENDING:
|
||||
'The specified transaction was included in a block. A\nmajority of validators sent confirmations which have\nnot yet been added to a block.',
|
||||
WAITING_VALIDATORS:
|
||||
'The specified transaction was included in a block.\nSome validators have sent confirmations, others are\nwaiting for chain finalization.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
|
||||
SEARCHING:
|
||||
'The specified transaction was included in a block. The app is looking for confirmations. Either\n1. Validators are waiting for chain finalization before sending their signatures.\n2. Validators are not active.\n3. The bridge was stopped.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
|
||||
WAITING_CHAIN:
|
||||
'The specified transaction was included in a block.\nValidators are waiting for chain finalization before\nsending their confirmations.'
|
||||
}
|
||||
|
||||
// use %link to identify a link
|
||||
export const CONFIRMATIONS_STATUS_DESCRIPTION_HOME: { [key: string]: string } = {
|
||||
SUCCESS: '',
|
||||
SUCCESS_MESSAGE_FAILED:
|
||||
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
|
||||
EXECUTION_FAILED:
|
||||
'The specified transaction was included in a block\nand the validators collected signatures. The\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.\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.'
|
||||
}
|
||||
1
alm/src/global.d.ts
vendored
Normal file
1
alm/src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare type Maybe<T> = T | null
|
||||
41
alm/src/hooks/useBlockConfirmations.ts
Normal file
41
alm/src/hooks/useBlockConfirmations.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { getRequiredBlockConfirmations } from '../utils/contract'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export interface UseBlockConfirmationsParams {
|
||||
fromHome: boolean
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
}
|
||||
|
||||
export const useBlockConfirmations = ({ receipt, fromHome }: UseBlockConfirmationsParams) => {
|
||||
const [blockConfirmations, setBlockConfirmations] = useState(0)
|
||||
|
||||
const { home, foreign } = useStateProvider()
|
||||
|
||||
const callRequireBlockConfirmations = async (
|
||||
contract: Contract,
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
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, snapshotProvider)
|
||||
},
|
||||
[home.bridgeContract, foreign.bridgeContract, receipt, fromHome]
|
||||
)
|
||||
|
||||
return {
|
||||
blockConfirmations
|
||||
}
|
||||
}
|
||||
38
alm/src/hooks/useBridgeContracts.ts
Normal file
38
alm/src/hooks/useBridgeContracts.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
|
||||
import { FOREIGN_BRIDGE_ADDRESS, HOME_BRIDGE_ADDRESS } from '../config/constants'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export interface useBridgeContractsParams {
|
||||
homeWeb3: Web3
|
||||
foreignWeb3: Web3
|
||||
}
|
||||
|
||||
export const useBridgeContracts = ({ homeWeb3, foreignWeb3 }: useBridgeContractsParams) => {
|
||||
const [homeBridge, setHomeBridge] = useState<Maybe<Contract>>(null)
|
||||
const [foreignBridge, setForeignBridge] = useState<Maybe<Contract>>(null)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!homeWeb3) return
|
||||
const homeContract = new homeWeb3.eth.Contract(HOME_AMB_ABI, HOME_BRIDGE_ADDRESS)
|
||||
setHomeBridge(homeContract)
|
||||
},
|
||||
[homeWeb3]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!foreignWeb3) return
|
||||
const foreignContract = new foreignWeb3.eth.Contract(FOREIGN_AMB_ABI, FOREIGN_BRIDGE_ADDRESS)
|
||||
setForeignBridge(foreignContract)
|
||||
},
|
||||
[foreignWeb3]
|
||||
)
|
||||
|
||||
return {
|
||||
homeBridge,
|
||||
foreignBridge
|
||||
}
|
||||
}
|
||||
374
alm/src/hooks/useMessageConfirmations.ts
Normal file
374
alm/src/hooks/useMessageConfirmations.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { MessageObject } from '../utils/web3'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { EventData } from 'web3-eth-contract'
|
||||
import { getAffirmationsSigned, getMessagesSigned } from '../utils/contract'
|
||||
import {
|
||||
BLOCK_RANGE,
|
||||
CONFIRMATIONS_STATUS,
|
||||
FOREIGN_RPC_POLLING_INTERVAL,
|
||||
HOME_RPC_POLLING_INTERVAL,
|
||||
VALIDATOR_CONFIRMATION_STATUS
|
||||
} from '../config/constants'
|
||||
import { homeBlockNumberProvider, foreignBlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
import { checkSignaturesWaitingForBLocks } from '../utils/signatureWaitingForBlocks'
|
||||
import { getCollectedSignaturesEvent } from '../utils/getCollectedSignaturesEvent'
|
||||
import { checkWaitingBlocksForExecution } from '../utils/executionWaitingForBlocks'
|
||||
import { getConfirmationsForTx } from '../utils/getConfirmationsForTx'
|
||||
import { getFinalizationEvent } from '../utils/getFinalizationEvent'
|
||||
import {
|
||||
getValidatorFailedTransactionsForMessage,
|
||||
getExecutionFailedTransactionForMessage,
|
||||
getValidatorPendingTransactionsForMessage,
|
||||
getExecutionPendingTransactionsForMessage,
|
||||
getValidatorSuccessTransactionsForMessage
|
||||
} from '../utils/explorer'
|
||||
|
||||
export interface useMessageConfirmationsParams {
|
||||
message: MessageObject
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
fromHome: boolean
|
||||
timestamp: number
|
||||
requiredSignatures: number
|
||||
validatorList: string[]
|
||||
blockConfirmations: number
|
||||
}
|
||||
|
||||
export interface BasicConfirmationParam {
|
||||
validator: string
|
||||
status: string
|
||||
}
|
||||
|
||||
export interface ConfirmationParam extends BasicConfirmationParam {
|
||||
txHash: string
|
||||
timestamp: number
|
||||
}
|
||||
|
||||
export interface ExecutionData {
|
||||
status: string
|
||||
validator: string
|
||||
txHash: string
|
||||
timestamp: number
|
||||
executionResult: boolean
|
||||
}
|
||||
|
||||
export const useMessageConfirmations = ({
|
||||
message,
|
||||
receipt,
|
||||
fromHome,
|
||||
timestamp,
|
||||
requiredSignatures,
|
||||
validatorList,
|
||||
blockConfirmations
|
||||
}: useMessageConfirmationsParams) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [confirmations, setConfirmations] = useState<Array<ConfirmationParam>>([])
|
||||
const [status, setStatus] = useState(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
const [waitingBlocks, setWaitingBlocks] = useState(false)
|
||||
const [waitingBlocksResolved, setWaitingBlocksResolved] = useState(false)
|
||||
const [signatureCollected, setSignatureCollected] = useState(false)
|
||||
const [collectedSignaturesEvent, setCollectedSignaturesEvent] = useState<Maybe<EventData>>(null)
|
||||
const [executionData, setExecutionData] = useState<ExecutionData>({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator: '',
|
||||
txHash: '',
|
||||
timestamp: 0,
|
||||
executionResult: false
|
||||
})
|
||||
const [waitingBlocksForExecution, setWaitingBlocksForExecution] = useState(false)
|
||||
const [waitingBlocksForExecutionResolved, setWaitingBlocksForExecutionResolved] = useState(false)
|
||||
const [failedConfirmations, setFailedConfirmations] = useState(false)
|
||||
const [failedExecution, setFailedExecution] = useState(false)
|
||||
const [pendingConfirmations, setPendingConfirmations] = useState(false)
|
||||
const [pendingExecution, setPendingExecution] = useState(false)
|
||||
|
||||
const existsConfirmation = (confirmationArray: ConfirmationParam[]) => {
|
||||
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(
|
||||
() => {
|
||||
if (!receipt || !blockConfirmations) return
|
||||
|
||||
const subscriptions: Array<number> = []
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
const blockProvider = fromHome ? homeBlockNumberProvider : foreignBlockNumberProvider
|
||||
const interval = fromHome ? HOME_RPC_POLLING_INTERVAL : FOREIGN_RPC_POLLING_INTERVAL
|
||||
const web3 = fromHome ? home.web3 : foreign.web3
|
||||
blockProvider.start(web3)
|
||||
|
||||
const targetBlock = receipt.blockNumber + blockConfirmations
|
||||
|
||||
checkSignaturesWaitingForBLocks(
|
||||
targetBlock,
|
||||
setWaitingBlocks,
|
||||
setWaitingBlocksResolved,
|
||||
validatorList,
|
||||
setConfirmations,
|
||||
blockProvider,
|
||||
interval,
|
||||
subscriptions
|
||||
)
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
blockProvider.stop()
|
||||
}
|
||||
},
|
||||
[blockConfirmations, foreign.web3, fromHome, validatorList, home.web3, receipt]
|
||||
)
|
||||
|
||||
// The collected signature event is only fetched once the signatures are collected on tx from home to foreign, to calculate if
|
||||
// the execution tx on the foreign network is waiting for block confirmations
|
||||
// This is executed if the message is in Home to Foreign direction only
|
||||
useEffect(
|
||||
() => {
|
||||
if (!fromHome || !receipt || !home.web3 || !signatureCollected) return
|
||||
|
||||
const subscriptions: Array<number> = []
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
homeBlockNumberProvider.start(home.web3)
|
||||
|
||||
const fromBlock = receipt.blockNumber
|
||||
const toBlock = fromBlock + BLOCK_RANGE
|
||||
const messageHash = home.web3.utils.soliditySha3Raw(message.data)
|
||||
|
||||
getCollectedSignaturesEvent(
|
||||
home.web3,
|
||||
home.bridgeContract,
|
||||
fromBlock,
|
||||
toBlock,
|
||||
messageHash,
|
||||
setCollectedSignaturesEvent,
|
||||
subscriptions
|
||||
)
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
homeBlockNumberProvider.stop()
|
||||
}
|
||||
},
|
||||
[fromHome, home.bridgeContract, home.web3, message.data, receipt, signatureCollected]
|
||||
)
|
||||
|
||||
// Check if the responsible validator is waiting for block confirmations to execute the message on foreign network
|
||||
// This is executed if the message is in Home to Foreign direction only
|
||||
useEffect(
|
||||
() => {
|
||||
if (!fromHome || !home.web3 || !receipt || !collectedSignaturesEvent || !blockConfirmations) return
|
||||
|
||||
const subscriptions: Array<number> = []
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
homeBlockNumberProvider.start(home.web3)
|
||||
const targetBlock = collectedSignaturesEvent.blockNumber + blockConfirmations
|
||||
|
||||
checkWaitingBlocksForExecution(
|
||||
homeBlockNumberProvider,
|
||||
HOME_RPC_POLLING_INTERVAL,
|
||||
targetBlock,
|
||||
collectedSignaturesEvent,
|
||||
setWaitingBlocksForExecution,
|
||||
setWaitingBlocksForExecutionResolved,
|
||||
setExecutionData,
|
||||
subscriptions
|
||||
)
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
homeBlockNumberProvider.stop()
|
||||
}
|
||||
},
|
||||
[collectedSignaturesEvent, fromHome, blockConfirmations, home.web3, receipt]
|
||||
)
|
||||
|
||||
// Checks if validators verified the message
|
||||
// To avoid making extra requests, this is only executed when validators finished waiting for blocks confirmations
|
||||
useEffect(
|
||||
() => {
|
||||
if (!waitingBlocksResolved || !timestamp || !requiredSignatures) return
|
||||
|
||||
const subscriptions: Array<number> = []
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
const confirmationContractMethod = fromHome ? getMessagesSigned : getAffirmationsSigned
|
||||
|
||||
getConfirmationsForTx(
|
||||
message.data,
|
||||
home.web3,
|
||||
validatorList,
|
||||
home.bridgeContract,
|
||||
confirmationContractMethod,
|
||||
setConfirmations,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getValidatorFailedTransactionsForMessage,
|
||||
setFailedConfirmations,
|
||||
getValidatorPendingTransactionsForMessage,
|
||||
setPendingConfirmations,
|
||||
getValidatorSuccessTransactionsForMessage
|
||||
)
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
},
|
||||
[
|
||||
fromHome,
|
||||
message.data,
|
||||
home.web3,
|
||||
validatorList,
|
||||
home.bridgeContract,
|
||||
requiredSignatures,
|
||||
waitingBlocksResolved,
|
||||
timestamp
|
||||
]
|
||||
)
|
||||
|
||||
// Gets finalization event to display the information about the execution of the message
|
||||
// In a message from Home to Foreign it will be executed after finishing waiting for block confirmations for the execution transaction on Foreign
|
||||
// In a message from Foreign to Home it will be executed after finishing waiting for block confirmations of the message request
|
||||
useEffect(
|
||||
() => {
|
||||
if ((fromHome && !waitingBlocksForExecutionResolved) || (!fromHome && !waitingBlocksResolved)) return
|
||||
|
||||
const subscriptions: Array<number> = []
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
const contractEvent = fromHome ? 'RelayedMessage' : 'AffirmationCompleted'
|
||||
const bridgeContract = fromHome ? foreign.bridgeContract : home.bridgeContract
|
||||
const providedWeb3 = fromHome ? foreign.web3 : home.web3
|
||||
const interval = fromHome ? FOREIGN_RPC_POLLING_INTERVAL : HOME_RPC_POLLING_INTERVAL
|
||||
|
||||
getFinalizationEvent(
|
||||
bridgeContract,
|
||||
contractEvent,
|
||||
providedWeb3,
|
||||
setExecutionData,
|
||||
waitingBlocksResolved,
|
||||
message,
|
||||
interval,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getExecutionFailedTransactionForMessage,
|
||||
setFailedExecution,
|
||||
getExecutionPendingTransactionsForMessage,
|
||||
setPendingExecution
|
||||
)
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
},
|
||||
[
|
||||
fromHome,
|
||||
foreign.bridgeContract,
|
||||
home.bridgeContract,
|
||||
message,
|
||||
foreign.web3,
|
||||
home.web3,
|
||||
waitingBlocksResolved,
|
||||
waitingBlocksForExecutionResolved,
|
||||
timestamp,
|
||||
collectedSignaturesEvent
|
||||
]
|
||||
)
|
||||
|
||||
// Sets the message status based in the collected information
|
||||
useEffect(
|
||||
() => {
|
||||
if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS && existsConfirmation(confirmations)) {
|
||||
const newStatus = executionData.executionResult
|
||||
? CONFIRMATIONS_STATUS.SUCCESS
|
||||
: CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED
|
||||
setStatus(newStatus)
|
||||
} else if (signatureCollected) {
|
||||
if (fromHome) {
|
||||
if (waitingBlocksForExecution) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
|
||||
} else if (failedExecution) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_FAILED)
|
||||
} else if (pendingExecution) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_PENDING)
|
||||
} else if (waitingBlocksForExecutionResolved) {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
|
||||
} else {
|
||||
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
|
||||
}
|
||||
} else {
|
||||
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
}
|
||||
} else if (waitingBlocks) {
|
||||
setStatus(CONFIRMATIONS_STATUS.WAITING_CHAIN)
|
||||
} else if (failedConfirmations) {
|
||||
setStatus(CONFIRMATIONS_STATUS.FAILED)
|
||||
} else if (pendingConfirmations) {
|
||||
setStatus(CONFIRMATIONS_STATUS.PENDING)
|
||||
} else if (waitingBlocksResolved && existsConfirmation(confirmations)) {
|
||||
setStatus(CONFIRMATIONS_STATUS.WAITING_VALIDATORS)
|
||||
} else if (waitingBlocksResolved) {
|
||||
setStatus(CONFIRMATIONS_STATUS.SEARCHING)
|
||||
} else {
|
||||
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
|
||||
}
|
||||
},
|
||||
[
|
||||
executionData,
|
||||
fromHome,
|
||||
signatureCollected,
|
||||
waitingBlocks,
|
||||
waitingBlocksForExecution,
|
||||
failedConfirmations,
|
||||
failedExecution,
|
||||
pendingConfirmations,
|
||||
pendingExecution,
|
||||
waitingBlocksResolved,
|
||||
confirmations,
|
||||
waitingBlocksForExecutionResolved
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
confirmations,
|
||||
status,
|
||||
signatureCollected,
|
||||
executionData,
|
||||
waitingBlocksResolved
|
||||
}
|
||||
}
|
||||
28
alm/src/hooks/useNetwork.ts
Normal file
28
alm/src/hooks/useNetwork.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getChainId, getWeb3 } from '../utils/web3'
|
||||
import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export const useNetwork = (url: string, snapshotProvider: SnapshotProvider) => {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [chainId, setChainId] = useState(0)
|
||||
const web3 = getWeb3(url)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
setLoading(true)
|
||||
const getWeb3ChainId = async () => {
|
||||
const id = await getChainId(web3, snapshotProvider)
|
||||
setChainId(id)
|
||||
setLoading(false)
|
||||
}
|
||||
getWeb3ChainId()
|
||||
},
|
||||
[web3, snapshotProvider]
|
||||
)
|
||||
|
||||
return {
|
||||
web3,
|
||||
chainId,
|
||||
loading
|
||||
}
|
||||
}
|
||||
56
alm/src/hooks/useTransactionFinder.ts
Normal file
56
alm/src/hooks/useTransactionFinder.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constants'
|
||||
import Web3 from 'web3'
|
||||
|
||||
export const useTransactionFinder = ({ txHash, web3 }: { txHash: string; web3: Maybe<Web3> }) => {
|
||||
const [status, setStatus] = useState(TRANSACTION_STATUS.UNDEFINED)
|
||||
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!txHash || !web3) return
|
||||
|
||||
const subscriptions: number[] = []
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
const getReceipt = async (
|
||||
web3: Web3,
|
||||
txHash: string,
|
||||
setReceipt: Function,
|
||||
setStatus: Function,
|
||||
subscriptions: number[]
|
||||
) => {
|
||||
const txReceipt = await web3.eth.getTransactionReceipt(txHash)
|
||||
setReceipt(txReceipt)
|
||||
|
||||
if (!txReceipt) {
|
||||
setStatus(TRANSACTION_STATUS.NOT_FOUND)
|
||||
const timeoutId = setTimeout(
|
||||
() => getReceipt(web3, txHash, setReceipt, setStatus, subscriptions),
|
||||
HOME_RPC_POLLING_INTERVAL
|
||||
)
|
||||
subscriptions.push(timeoutId)
|
||||
} else {
|
||||
setStatus(TRANSACTION_STATUS.FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
getReceipt(web3, txHash, setReceipt, setStatus, subscriptions)
|
||||
return () => {
|
||||
unsubscribe()
|
||||
}
|
||||
},
|
||||
[txHash, web3]
|
||||
)
|
||||
|
||||
return {
|
||||
status,
|
||||
receipt
|
||||
}
|
||||
}
|
||||
131
alm/src/hooks/useTransactionStatus.ts
Normal file
131
alm/src/hooks/useTransactionStatus.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constants'
|
||||
import { getTransactionStatusDescription } from '../utils/networks'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { getHomeMessagesFromReceipt, getForeignMessagesFromReceipt, MessageObject, getBlock } from '../utils/web3'
|
||||
import useInterval from '@use-it/interval'
|
||||
|
||||
export const useTransactionStatus = ({
|
||||
txHash,
|
||||
chainId,
|
||||
receiptParam
|
||||
}: {
|
||||
txHash: string
|
||||
chainId: number
|
||||
receiptParam: Maybe<TransactionReceipt>
|
||||
}) => {
|
||||
const { home, foreign } = useStateProvider()
|
||||
const [messages, setMessages] = useState<Array<MessageObject>>([])
|
||||
const [status, setStatus] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
|
||||
const [timestamp, setTimestamp] = useState(0)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
// Update description so the time displayed is accurate
|
||||
useInterval(() => {
|
||||
if (!status || !timestamp || !description) return
|
||||
setDescription(getTransactionStatusDescription(status, timestamp))
|
||||
}, 30000)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const subscriptions: Array<number> = []
|
||||
|
||||
const unsubscribe = () => {
|
||||
subscriptions.forEach(s => {
|
||||
clearTimeout(s)
|
||||
})
|
||||
}
|
||||
|
||||
const getReceipt = async () => {
|
||||
if (!chainId || !txHash || !home.chainId || !foreign.chainId || !home.web3 || !foreign.web3) return
|
||||
setLoading(true)
|
||||
const isHome = chainId === home.chainId
|
||||
const web3 = isHome ? home.web3 : foreign.web3
|
||||
|
||||
let txReceipt
|
||||
|
||||
if (receiptParam) {
|
||||
txReceipt = receiptParam
|
||||
} else {
|
||||
txReceipt = await web3.eth.getTransactionReceipt(txHash)
|
||||
}
|
||||
|
||||
setReceipt(txReceipt)
|
||||
|
||||
if (!txReceipt) {
|
||||
setStatus(TRANSACTION_STATUS.NOT_FOUND)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.NOT_FOUND))
|
||||
setMessages([{ id: txHash, data: '' }])
|
||||
const timeoutId = setTimeout(() => getReceipt(), HOME_RPC_POLLING_INTERVAL)
|
||||
subscriptions.push(timeoutId)
|
||||
} else {
|
||||
const blockNumber = txReceipt.blockNumber
|
||||
const block = await getBlock(web3, blockNumber)
|
||||
const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp
|
||||
setTimestamp(blockTimestamp)
|
||||
|
||||
if (txReceipt.status) {
|
||||
let bridgeMessages: Array<MessageObject>
|
||||
if (isHome) {
|
||||
bridgeMessages = getHomeMessagesFromReceipt(txReceipt, home.web3, home.bridgeAddress)
|
||||
} else {
|
||||
bridgeMessages = getForeignMessagesFromReceipt(txReceipt, foreign.web3, foreign.bridgeAddress)
|
||||
}
|
||||
|
||||
if (bridgeMessages.length === 0) {
|
||||
setMessages([{ id: txHash, data: '' }])
|
||||
setStatus(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES, blockTimestamp))
|
||||
} else if (bridgeMessages.length === 1) {
|
||||
setMessages(bridgeMessages)
|
||||
setStatus(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, blockTimestamp))
|
||||
} else {
|
||||
setMessages(bridgeMessages)
|
||||
setStatus(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES)
|
||||
setDescription(
|
||||
getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES, blockTimestamp)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
setStatus(TRANSACTION_STATUS.FAILED)
|
||||
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.FAILED, blockTimestamp))
|
||||
}
|
||||
}
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
// unsubscribe from previous txHash
|
||||
unsubscribe()
|
||||
|
||||
getReceipt()
|
||||
return () => {
|
||||
// unsubscribe when unmount component
|
||||
unsubscribe()
|
||||
}
|
||||
},
|
||||
[
|
||||
txHash,
|
||||
chainId,
|
||||
home.chainId,
|
||||
foreign.chainId,
|
||||
home.web3,
|
||||
foreign.web3,
|
||||
home.bridgeAddress,
|
||||
foreign.bridgeAddress,
|
||||
receiptParam
|
||||
]
|
||||
)
|
||||
|
||||
return {
|
||||
messages,
|
||||
status,
|
||||
description,
|
||||
receipt,
|
||||
timestamp,
|
||||
loading
|
||||
}
|
||||
}
|
||||
76
alm/src/hooks/useValidatorContract.ts
Normal file
76
alm/src/hooks/useValidatorContract.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '../utils/contract'
|
||||
import { BRIDGE_VALIDATORS_ABI } from '../abis'
|
||||
import { useStateProvider } from '../state/StateProvider'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export interface useValidatorContractParams {
|
||||
fromHome: boolean
|
||||
receipt: Maybe<TransactionReceipt>
|
||||
}
|
||||
|
||||
export const useValidatorContract = ({ receipt, fromHome }: useValidatorContractParams) => {
|
||||
const [validatorContract, setValidatorContract] = useState<Maybe<Contract>>(null)
|
||||
const [requiredSignatures, setRequiredSignatures] = useState(0)
|
||||
const [validatorList, setValidatorList] = useState([])
|
||||
|
||||
const { home, foreign } = useStateProvider()
|
||||
|
||||
const callValidatorContract = async (bridgeContract: Maybe<Contract>, web3: Web3, setValidatorContract: Function) => {
|
||||
if (!web3 || !bridgeContract) return
|
||||
const address = await getValidatorAddress(bridgeContract)
|
||||
const contract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, address)
|
||||
setValidatorContract(contract)
|
||||
}
|
||||
|
||||
const callRequiredSignatures = async (
|
||||
contract: Maybe<Contract>,
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getRequiredSignatures(contract, receipt.blockNumber, snapshotProvider)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
const callValidatorList = async (
|
||||
contract: Maybe<Contract>,
|
||||
receipt: TransactionReceipt,
|
||||
setResult: Function,
|
||||
snapshotProvider: SnapshotProvider
|
||||
) => {
|
||||
if (!contract) return
|
||||
const result = await getValidatorList(contract, receipt.blockNumber, snapshotProvider)
|
||||
setResult(result)
|
||||
}
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
const web3 = fromHome ? home.web3 : foreign.web3
|
||||
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
|
||||
|
||||
if (!web3 || !bridgeContract) return
|
||||
callValidatorContract(bridgeContract, web3, setValidatorContract)
|
||||
},
|
||||
[home.web3, foreign.web3, home.bridgeContract, foreign.bridgeContract, fromHome]
|
||||
)
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
if (!receipt) return
|
||||
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
|
||||
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures, snapshotProvider)
|
||||
callValidatorList(validatorContract, receipt, setValidatorList, snapshotProvider)
|
||||
},
|
||||
[validatorContract, receipt, fromHome]
|
||||
)
|
||||
|
||||
return {
|
||||
requiredSignatures,
|
||||
validatorList
|
||||
}
|
||||
}
|
||||
16
alm/src/index.tsx
Normal file
16
alm/src/index.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
import { ThemeProvider } from 'styled-components'
|
||||
import { GlobalStyle } from './themes/GlobalStyle'
|
||||
import App from './App'
|
||||
import Light from './themes/Light'
|
||||
|
||||
ReactDOM.render(
|
||||
<React.StrictMode>
|
||||
<ThemeProvider theme={Light}>
|
||||
<GlobalStyle />
|
||||
<App />
|
||||
</ThemeProvider>
|
||||
</React.StrictMode>,
|
||||
document.getElementById('root')
|
||||
)
|
||||
1
alm/src/react-app-env.d.ts
vendored
Normal file
1
alm/src/react-app-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="react-scripts" />
|
||||
64
alm/src/services/BlockNumberProvider.ts
Normal file
64
alm/src/services/BlockNumberProvider.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import Web3 from 'web3'
|
||||
import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'
|
||||
import { HOME_RPC_POLLING_INTERVAL } from '../config/constants'
|
||||
|
||||
export class BlockNumberProvider {
|
||||
private running: number
|
||||
private web3: Maybe<Web3>
|
||||
private ref: number | undefined
|
||||
private value: Maybe<number>
|
||||
private lastValueTimestamp: Maybe<Date>
|
||||
private readonly interval: number
|
||||
|
||||
constructor(interval = 5000) {
|
||||
this.running = 0
|
||||
this.web3 = null
|
||||
this.ref = undefined
|
||||
this.value = null
|
||||
this.lastValueTimestamp = null
|
||||
this.interval = interval
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
start(web3: Maybe<Web3>) {
|
||||
if (!this.running) {
|
||||
clearTimeout(this.ref)
|
||||
this.web3 = web3
|
||||
this.running = this.running + 1
|
||||
this.fetchLastBlock()
|
||||
} else {
|
||||
this.running = this.running + 1
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.running = this.running > 0 ? this.running - 1 : 0
|
||||
|
||||
if (!this.running) {
|
||||
clearTimeout(this.ref)
|
||||
this.ref = undefined
|
||||
this.web3 = null
|
||||
}
|
||||
}
|
||||
|
||||
get() {
|
||||
return this.value
|
||||
}
|
||||
|
||||
private async fetchLastBlock() {
|
||||
if (!this.web3) return
|
||||
const now = new Date()
|
||||
const distance = differenceInMilliseconds(now, this.lastValueTimestamp || 0)
|
||||
|
||||
if (distance >= this.interval) {
|
||||
this.value = await this.web3.eth.getBlockNumber()
|
||||
this.lastValueTimestamp = now
|
||||
}
|
||||
|
||||
this.ref = setTimeout(() => this.fetchLastBlock(), this.interval)
|
||||
}
|
||||
}
|
||||
|
||||
export const homeBlockNumberProvider = new BlockNumberProvider(HOME_RPC_POLLING_INTERVAL)
|
||||
export const foreignBlockNumberProvider = new BlockNumberProvider(HOME_RPC_POLLING_INTERVAL)
|
||||
69
alm/src/services/SnapshotProvider.ts
Normal file
69
alm/src/services/SnapshotProvider.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
const initialValue = {
|
||||
chainId: 0,
|
||||
RequiredBlockConfirmationChanged: [],
|
||||
RequiredSignaturesChanged: [],
|
||||
ValidatorAdded: [],
|
||||
ValidatorRemoved: [],
|
||||
snapshotBlockNumber: 0
|
||||
}
|
||||
|
||||
export interface SnapshotEvent {
|
||||
blockNumber: number
|
||||
returnValues: any
|
||||
}
|
||||
|
||||
export interface SnapshotValidatorEvent {
|
||||
blockNumber: number
|
||||
returnValues: any
|
||||
event: string
|
||||
}
|
||||
|
||||
export interface Snapshot {
|
||||
chainId: number
|
||||
RequiredBlockConfirmationChanged: SnapshotEvent[]
|
||||
RequiredSignaturesChanged: SnapshotEvent[]
|
||||
ValidatorAdded: SnapshotValidatorEvent[]
|
||||
ValidatorRemoved: SnapshotValidatorEvent[]
|
||||
snapshotBlockNumber: number
|
||||
}
|
||||
|
||||
export class SnapshotProvider {
|
||||
private data: Snapshot
|
||||
|
||||
constructor(side: string) {
|
||||
let data = initialValue
|
||||
try {
|
||||
data = require(`../snapshots/${side}.json`)
|
||||
} catch (e) {
|
||||
console.log('Snapshot not found')
|
||||
}
|
||||
this.data = data
|
||||
}
|
||||
|
||||
chainId() {
|
||||
return this.data.chainId
|
||||
}
|
||||
|
||||
snapshotBlockNumber() {
|
||||
return this.data.snapshotBlockNumber
|
||||
}
|
||||
|
||||
requiredBlockConfirmationEvents(toBlock: number) {
|
||||
return this.data.RequiredBlockConfirmationChanged.filter(e => e.blockNumber <= toBlock)
|
||||
}
|
||||
|
||||
requiredSignaturesEvents(toBlock: number) {
|
||||
return this.data.RequiredSignaturesChanged.filter(e => e.blockNumber <= toBlock)
|
||||
}
|
||||
|
||||
validatorAddedEvents(fromBlock: number) {
|
||||
return this.data.ValidatorAdded.filter(e => e.blockNumber >= fromBlock)
|
||||
}
|
||||
|
||||
validatorRemovedEvents(fromBlock: number) {
|
||||
return this.data.ValidatorRemoved.filter(e => e.blockNumber >= fromBlock)
|
||||
}
|
||||
}
|
||||
|
||||
export const homeSnapshotProvider = new SnapshotProvider('home')
|
||||
export const foreignSnapshotProvider = new SnapshotProvider('foreign')
|
||||
29
alm/src/services/ValidatorsCache.ts
Normal file
29
alm/src/services/ValidatorsCache.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
class ValidatorsCache {
|
||||
private readonly store: { [key: string]: boolean }
|
||||
private readonly dataStore: { [key: string]: ConfirmationParam }
|
||||
|
||||
constructor() {
|
||||
this.store = {}
|
||||
this.dataStore = {}
|
||||
}
|
||||
|
||||
get(key: string) {
|
||||
return this.store[key]
|
||||
}
|
||||
|
||||
set(key: string, value: boolean) {
|
||||
this.store[key] = value
|
||||
}
|
||||
|
||||
getData(key: string) {
|
||||
return this.dataStore[key]
|
||||
}
|
||||
|
||||
setData(key: string, value: ConfirmationParam) {
|
||||
this.dataStore[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
export default new ValidatorsCache()
|
||||
5
alm/src/setupTests.ts
Normal file
5
alm/src/setupTests.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom/extend-expect'
|
||||
0
alm/src/snapshots/.gitkeep
Normal file
0
alm/src/snapshots/.gitkeep
Normal file
79
alm/src/state/StateProvider.tsx
Normal file
79
alm/src/state/StateProvider.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
import React, { createContext, ReactNode } from 'react'
|
||||
import { useNetwork } from '../hooks/useNetwork'
|
||||
import {
|
||||
HOME_RPC_URL,
|
||||
FOREIGN_RPC_URL,
|
||||
HOME_BRIDGE_ADDRESS,
|
||||
FOREIGN_BRIDGE_ADDRESS,
|
||||
HOME_NETWORK_NAME,
|
||||
FOREIGN_NETWORK_NAME
|
||||
} from '../config/constants'
|
||||
import Web3 from 'web3'
|
||||
import { useBridgeContracts } from '../hooks/useBridgeContracts'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { foreignSnapshotProvider, homeSnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export interface BaseNetworkParams {
|
||||
chainId: number
|
||||
name: string
|
||||
web3: Maybe<Web3>
|
||||
bridgeAddress: string
|
||||
bridgeContract: Maybe<Contract>
|
||||
}
|
||||
|
||||
export interface StateContext {
|
||||
home: BaseNetworkParams
|
||||
foreign: BaseNetworkParams
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
const initialState = {
|
||||
home: {
|
||||
chainId: 0,
|
||||
name: '',
|
||||
web3: null,
|
||||
bridgeAddress: HOME_BRIDGE_ADDRESS,
|
||||
bridgeContract: null
|
||||
},
|
||||
foreign: {
|
||||
chainId: 0,
|
||||
name: '',
|
||||
web3: null,
|
||||
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
|
||||
bridgeContract: null
|
||||
},
|
||||
loading: true
|
||||
}
|
||||
|
||||
const StateContext = createContext<StateContext>(initialState)
|
||||
|
||||
export const StateProvider = ({ children }: { children: ReactNode }) => {
|
||||
const homeNetwork = useNetwork(HOME_RPC_URL, homeSnapshotProvider)
|
||||
const foreignNetwork = useNetwork(FOREIGN_RPC_URL, foreignSnapshotProvider)
|
||||
const { homeBridge, foreignBridge } = useBridgeContracts({
|
||||
homeWeb3: homeNetwork.web3,
|
||||
foreignWeb3: foreignNetwork.web3
|
||||
})
|
||||
|
||||
const value = {
|
||||
home: {
|
||||
bridgeAddress: HOME_BRIDGE_ADDRESS,
|
||||
name: HOME_NETWORK_NAME,
|
||||
bridgeContract: homeBridge,
|
||||
...homeNetwork
|
||||
},
|
||||
foreign: {
|
||||
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
|
||||
name: FOREIGN_NETWORK_NAME,
|
||||
bridgeContract: foreignBridge,
|
||||
...foreignNetwork
|
||||
},
|
||||
loading: homeNetwork.loading || foreignNetwork.loading
|
||||
}
|
||||
|
||||
return <StateContext.Provider value={value}>{children}</StateContext.Provider>
|
||||
}
|
||||
|
||||
export const useStateProvider = (): StateContext => {
|
||||
return React.useContext(StateContext)
|
||||
}
|
||||
22
alm/src/themes/Dark.tsx
Normal file
22
alm/src/themes/Dark.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
const theme = {
|
||||
backgroundColor: '#121212',
|
||||
fontColor: '#f5f5f5',
|
||||
buttonColor: '#f5f5f5',
|
||||
colorPrimary: '#272727',
|
||||
colorGrey: '#272727',
|
||||
colorLightGrey: '#272727',
|
||||
linkColor: '#ffffff',
|
||||
success: {
|
||||
textColor: '#00c9a7',
|
||||
backgroundColor: '#004d40'
|
||||
},
|
||||
notRequired: {
|
||||
textColor: '#bdbdbd',
|
||||
backgroundColor: '#424242'
|
||||
},
|
||||
failed: {
|
||||
textColor: '#EF5350',
|
||||
backgroundColor: '#4E342E'
|
||||
}
|
||||
}
|
||||
export default theme
|
||||
32
alm/src/themes/GlobalStyle.tsx
Normal file
32
alm/src/themes/GlobalStyle.tsx
Normal file
@@ -0,0 +1,32 @@
|
||||
import { createGlobalStyle } from 'styled-components'
|
||||
|
||||
import theme from './Light'
|
||||
|
||||
type ThemeType = typeof theme
|
||||
|
||||
export const GlobalStyle = createGlobalStyle<{ theme: ThemeType }>`
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg-color: ${props => props.theme.backgroundColor};
|
||||
--font-color: ${props => props.theme.fontColor};
|
||||
--button-color: ${props => props.theme.buttonColor};
|
||||
--color-primary: ${props => props.theme.colorPrimary};
|
||||
--color-grey: ${props => props.theme.colorGrey};
|
||||
--color-lightGrey: ${props => props.theme.colorLightGrey};
|
||||
--link-color: ${props => props.theme.linkColor};
|
||||
--success-color: ${props => props.theme.success.textColor};
|
||||
--success-bg-color: ${props => props.theme.success.backgroundColor};
|
||||
--not-required-color: ${props => props.theme.notRequired.textColor};
|
||||
--not-required-bg-color: ${props => props.theme.notRequired.backgroundColor};
|
||||
--failed-color: ${props => props.theme.failed.textColor};
|
||||
--failed-bg-color: ${props => props.theme.failed.backgroundColor};
|
||||
}
|
||||
`
|
||||
22
alm/src/themes/Light.ts
Normal file
22
alm/src/themes/Light.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
const theme = {
|
||||
backgroundColor: '#FFFFFF',
|
||||
fontColor: 'rgba(0, 0, 0, 0.65)',
|
||||
buttonColor: '#1890ff',
|
||||
colorPrimary: '#BDBDBD',
|
||||
colorGrey: '#1890ff',
|
||||
colorLightGrey: '#1890ff',
|
||||
linkColor: '#1890ff',
|
||||
success: {
|
||||
textColor: '#388E3C',
|
||||
backgroundColor: 'rgba(0,201,167,.1)'
|
||||
},
|
||||
notRequired: {
|
||||
textColor: '#77838f',
|
||||
backgroundColor: 'rgba(119,131,143,.1)'
|
||||
},
|
||||
failed: {
|
||||
textColor: '#de4437',
|
||||
backgroundColor: 'rgba(222,68,55,.1)'
|
||||
}
|
||||
}
|
||||
export default theme
|
||||
469
alm/src/utils/__tests__/contracts.test.ts
Normal file
469
alm/src/utils/__tests__/contracts.test.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
import 'jest'
|
||||
import { getRequiredBlockConfirmations, getRequiredSignatures, getValidatorList } from '../contract'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { SnapshotProvider } from '../../services/SnapshotProvider'
|
||||
|
||||
describe('getRequiredBlockConfirmations', () => {
|
||||
const methodsBuilder = (value: string) => ({
|
||||
requiredBlockConfirmations: () => {
|
||||
return {
|
||||
call: () => {
|
||||
return value
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
test('Should call requiredBlockConfirmations method if no events present', async () => {
|
||||
const contract = ({
|
||||
getPastEvents: () => {
|
||||
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.UNDEFINED }
|
||||
])
|
||||
)
|
||||
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)
|
||||
})
|
||||
})
|
||||
101
alm/src/utils/contract.ts
Normal file
101
alm/src/utils/contract.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
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,
|
||||
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) {
|
||||
// Use the value from last event before the transaction
|
||||
const event = events[events.length - 1]
|
||||
blockConfirmations = event.returnValues.requiredBlockConfirmations
|
||||
} else {
|
||||
// This is a special case where RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB
|
||||
// of Sokol - Kovan. In this case the current value is used.
|
||||
blockConfirmations = await contract.methods.requiredBlockConfirmations().call()
|
||||
}
|
||||
return parseInt(blockConfirmations)
|
||||
}
|
||||
|
||||
export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call()
|
||||
|
||||
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]
|
||||
const { requiredSignatures } = event.returnValues
|
||||
return parseInt(requiredSignatures)
|
||||
}
|
||||
|
||||
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
|
||||
}),
|
||||
contract.getPastEvents('ValidatorRemoved', {
|
||||
fromBlock
|
||||
})
|
||||
])
|
||||
|
||||
// Ordered desc
|
||||
const orderedEvents = [...addedEventsFromSnapshot, ...added, ...removedEventsFromSnapshot, ...removed].sort(
|
||||
({ blockNumber: prev }, { blockNumber: next }) => next - prev
|
||||
)
|
||||
|
||||
// Stored as a Set to avoid duplicates
|
||||
const validatorList = new Set(currentList)
|
||||
|
||||
orderedEvents.forEach(e => {
|
||||
const { validator } = e.returnValues
|
||||
if (e.event === 'ValidatorRemoved') {
|
||||
validatorList.add(validator)
|
||||
} else if (e.event === 'ValidatorAdded') {
|
||||
validatorList.delete(validator)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(validatorList)
|
||||
}
|
||||
|
||||
export const getMessagesSigned = (contract: Contract, hash: string) => contract.methods.messagesSigned(hash).call()
|
||||
|
||||
export const getAffirmationsSigned = (contract: Contract, hash: string) =>
|
||||
contract.methods.affirmationsSigned(hash).call()
|
||||
58
alm/src/utils/executionWaitingForBlocks.ts
Normal file
58
alm/src/utils/executionWaitingForBlocks.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { BlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { EventData } from 'web3-eth-contract'
|
||||
|
||||
export const checkWaitingBlocksForExecution = async (
|
||||
blockProvider: BlockNumberProvider,
|
||||
interval: number,
|
||||
targetBlock: number,
|
||||
collectedSignaturesEvent: EventData,
|
||||
setWaitingBlocksForExecution: Function,
|
||||
setWaitingBlocksForExecutionResolved: Function,
|
||||
setExecutionData: Function,
|
||||
subscriptions: number[]
|
||||
) => {
|
||||
const currentBlock = blockProvider.get()
|
||||
|
||||
if (currentBlock && currentBlock >= targetBlock) {
|
||||
setExecutionData({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
|
||||
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
|
||||
txHash: '',
|
||||
timestamp: 0,
|
||||
executionResult: false
|
||||
})
|
||||
setWaitingBlocksForExecutionResolved(true)
|
||||
setWaitingBlocksForExecution(false)
|
||||
blockProvider.stop()
|
||||
} else {
|
||||
let nextInterval = interval
|
||||
if (!currentBlock) {
|
||||
nextInterval = 500
|
||||
} else {
|
||||
setWaitingBlocksForExecution(true)
|
||||
setExecutionData({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.WAITING,
|
||||
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
|
||||
txHash: '',
|
||||
timestamp: 0,
|
||||
executionResult: false
|
||||
})
|
||||
}
|
||||
const timeoutId = setTimeout(
|
||||
() =>
|
||||
checkWaitingBlocksForExecution(
|
||||
blockProvider,
|
||||
interval,
|
||||
targetBlock,
|
||||
collectedSignaturesEvent,
|
||||
setWaitingBlocksForExecution,
|
||||
setWaitingBlocksForExecutionResolved,
|
||||
setExecutionData,
|
||||
subscriptions
|
||||
),
|
||||
nextInterval
|
||||
)
|
||||
subscriptions.push(timeoutId)
|
||||
}
|
||||
}
|
||||
275
alm/src/utils/explorer.ts
Normal file
275
alm/src/utils/explorer.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
EXECUTE_AFFIRMATION_HASH,
|
||||
EXECUTE_SIGNATURES_HASH,
|
||||
FOREIGN_EXPLORER_API,
|
||||
HOME_EXPLORER_API,
|
||||
SUBMIT_SIGNATURE_HASH
|
||||
} from '../config/constants'
|
||||
|
||||
export interface APITransaction {
|
||||
timeStamp: string
|
||||
isError: string
|
||||
input: string
|
||||
to: string
|
||||
hash: string
|
||||
}
|
||||
|
||||
export interface APIPendingTransaction {
|
||||
input: string
|
||||
to: string
|
||||
hash: string
|
||||
}
|
||||
|
||||
export interface PendingTransactionsParams {
|
||||
account: string
|
||||
api: string
|
||||
}
|
||||
|
||||
export interface AccountTransactionsParams {
|
||||
account: string
|
||||
to: string
|
||||
startTimestamp: number
|
||||
endTimestamp: number
|
||||
api: string
|
||||
}
|
||||
|
||||
export interface GetFailedTransactionParams {
|
||||
account: string
|
||||
to: string
|
||||
messageData: string
|
||||
startTimestamp: number
|
||||
endTimestamp: number
|
||||
}
|
||||
|
||||
export interface GetPendingTransactionParams {
|
||||
account: string
|
||||
to: string
|
||||
messageData: string
|
||||
}
|
||||
|
||||
export const fetchAccountTransactionsFromBlockscout = async ({
|
||||
account,
|
||||
to,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
api
|
||||
}: AccountTransactionsParams): Promise<APITransaction[]> => {
|
||||
const url = `${api}?module=account&action=txlist&address=${account}&filterby=from=${account}&to=${to}&starttimestamp=${startTimestamp}&endtimestamp=${endTimestamp}`
|
||||
|
||||
try {
|
||||
const result = await fetch(url).then(res => res.json())
|
||||
if (result.status === '0') {
|
||||
return []
|
||||
}
|
||||
|
||||
return result.result
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const getBlockByTimestampUrl = (api: string, timestamp: number) =>
|
||||
`${api}&module=block&action=getblocknobytime×tamp=${timestamp}&closest=before`
|
||||
|
||||
export const fetchAccountTransactionsFromEtherscan = async ({
|
||||
account,
|
||||
to,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
api
|
||||
}: AccountTransactionsParams): Promise<APITransaction[]> => {
|
||||
const startBlockUrl = getBlockByTimestampUrl(api, startTimestamp)
|
||||
const endBlockUrl = getBlockByTimestampUrl(api, endTimestamp)
|
||||
let fromBlock = 0
|
||||
let toBlock = 9999999999999
|
||||
try {
|
||||
const [fromBlockResult, toBlockResult] = await Promise.all([
|
||||
fetch(startBlockUrl).then(res => res.json()),
|
||||
fetch(endBlockUrl).then(res => res.json())
|
||||
])
|
||||
|
||||
if (fromBlockResult.status !== '0') {
|
||||
fromBlock = parseInt(fromBlockResult.result)
|
||||
}
|
||||
|
||||
if (toBlockResult.status !== '0') {
|
||||
toBlock = parseInt(toBlockResult.result)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return []
|
||||
}
|
||||
|
||||
const url = `${api}&module=account&action=txlist&address=${account}&startblock=${fromBlock}&endblock=${toBlock}`
|
||||
|
||||
try {
|
||||
const result = await fetch(url).then(res => res.json())
|
||||
|
||||
if (result.status === '0') {
|
||||
return []
|
||||
}
|
||||
|
||||
const toAddressLowerCase = to.toLowerCase()
|
||||
const transactions: APITransaction[] = result.result
|
||||
return transactions.filter(t => t.to.toLowerCase() === toAddressLowerCase)
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const fetchAccountTransactions = (api: string) => {
|
||||
return api.includes('blockscout') ? fetchAccountTransactionsFromBlockscout : fetchAccountTransactionsFromEtherscan
|
||||
}
|
||||
|
||||
export const fetchPendingTransactions = async ({
|
||||
account,
|
||||
api
|
||||
}: PendingTransactionsParams): Promise<APIPendingTransaction[]> => {
|
||||
const url = `${api}?module=account&action=pendingtxlist&address=${account}`
|
||||
|
||||
try {
|
||||
const result = await fetch(url).then(res => res.json())
|
||||
if (result.status === '0') {
|
||||
return []
|
||||
}
|
||||
|
||||
return result.result
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
export const getFailedTransactions = async (
|
||||
account: string,
|
||||
to: string,
|
||||
startTimestamp: number,
|
||||
endTimestamp: number,
|
||||
api: string,
|
||||
fetchAccountTransactions: (args: AccountTransactionsParams) => Promise<APITransaction[]>
|
||||
): Promise<APITransaction[]> => {
|
||||
const transactions = await fetchAccountTransactions({ account, to, startTimestamp, endTimestamp, api })
|
||||
|
||||
return transactions.filter(t => t.isError !== '0')
|
||||
}
|
||||
|
||||
export const getSuccessTransactions = async (
|
||||
account: string,
|
||||
to: string,
|
||||
startTimestamp: number,
|
||||
endTimestamp: number,
|
||||
api: string,
|
||||
fetchAccountTransactions: (args: AccountTransactionsParams) => Promise<APITransaction[]>
|
||||
): Promise<APITransaction[]> => {
|
||||
const transactions = await fetchAccountTransactions({ account, to, startTimestamp, endTimestamp, api })
|
||||
|
||||
return transactions.filter(t => t.isError === '0')
|
||||
}
|
||||
|
||||
export const filterValidatorSignatureTransaction = (
|
||||
transactions: APITransaction[],
|
||||
messageData: string
|
||||
): APITransaction[] => {
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
return transactions.filter(
|
||||
t =>
|
||||
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
|
||||
t.input.includes(messageDataValue)
|
||||
)
|
||||
}
|
||||
|
||||
export const getValidatorFailedTransactionsForMessage = async ({
|
||||
account,
|
||||
to,
|
||||
messageData,
|
||||
startTimestamp,
|
||||
endTimestamp
|
||||
}: GetFailedTransactionParams): Promise<APITransaction[]> => {
|
||||
const failedTransactions = await getFailedTransactions(
|
||||
account,
|
||||
to,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
HOME_EXPLORER_API,
|
||||
fetchAccountTransactionsFromBlockscout
|
||||
)
|
||||
|
||||
return filterValidatorSignatureTransaction(failedTransactions, messageData)
|
||||
}
|
||||
|
||||
export const getValidatorSuccessTransactionsForMessage = async ({
|
||||
account,
|
||||
to,
|
||||
messageData,
|
||||
startTimestamp,
|
||||
endTimestamp
|
||||
}: GetFailedTransactionParams): Promise<APITransaction[]> => {
|
||||
const transactions = await getSuccessTransactions(
|
||||
account,
|
||||
to,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
HOME_EXPLORER_API,
|
||||
fetchAccountTransactionsFromBlockscout
|
||||
)
|
||||
|
||||
return filterValidatorSignatureTransaction(transactions, messageData)
|
||||
}
|
||||
|
||||
export const getExecutionFailedTransactionForMessage = async (
|
||||
{ account, to, messageData, startTimestamp, endTimestamp }: GetFailedTransactionParams,
|
||||
getFailedTransactionsMethod = getFailedTransactions
|
||||
): Promise<APITransaction[]> => {
|
||||
const failedTransactions = await getFailedTransactionsMethod(
|
||||
account,
|
||||
to,
|
||||
startTimestamp,
|
||||
endTimestamp,
|
||||
FOREIGN_EXPLORER_API,
|
||||
fetchAccountTransactions(FOREIGN_EXPLORER_API)
|
||||
)
|
||||
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
return failedTransactions.filter(t => t.input.includes(EXECUTE_SIGNATURES_HASH) && t.input.includes(messageDataValue))
|
||||
}
|
||||
|
||||
export const getValidatorPendingTransactionsForMessage = async (
|
||||
{ account, to, messageData }: GetPendingTransactionParams,
|
||||
fetchPendingTransactionsMethod = fetchPendingTransactions
|
||||
): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactionsMethod({
|
||||
account,
|
||||
api: HOME_EXPLORER_API
|
||||
})
|
||||
|
||||
const toAddressLowerCase = to.toLowerCase()
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
|
||||
return pendingTransactions.filter(
|
||||
t =>
|
||||
t.to.toLowerCase() === toAddressLowerCase &&
|
||||
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
|
||||
t.input.includes(messageDataValue)
|
||||
)
|
||||
}
|
||||
|
||||
export const getExecutionPendingTransactionsForMessage = async (
|
||||
{ account, to, messageData }: GetPendingTransactionParams,
|
||||
fetchPendingTransactionsMethod = fetchPendingTransactions
|
||||
): Promise<APIPendingTransaction[]> => {
|
||||
const pendingTransactions = await fetchPendingTransactionsMethod({
|
||||
account,
|
||||
api: FOREIGN_EXPLORER_API
|
||||
})
|
||||
|
||||
const toAddressLowerCase = to.toLowerCase()
|
||||
const messageDataValue = messageData.replace('0x', '')
|
||||
|
||||
return pendingTransactions.filter(
|
||||
t =>
|
||||
t.to.toLowerCase() === toAddressLowerCase &&
|
||||
t.input.includes(EXECUTE_SIGNATURES_HASH) &&
|
||||
t.input.includes(messageDataValue)
|
||||
)
|
||||
}
|
||||
53
alm/src/utils/getCollectedSignaturesEvent.ts
Normal file
53
alm/src/utils/getCollectedSignaturesEvent.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import Web3 from 'web3'
|
||||
import { Contract, EventData } from 'web3-eth-contract'
|
||||
import { homeBlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
import { BLOCK_RANGE } from '../config/constants'
|
||||
|
||||
export const getCollectedSignaturesEvent = async (
|
||||
web3: Maybe<Web3>,
|
||||
contract: Maybe<Contract>,
|
||||
fromBlock: number,
|
||||
toBlock: number,
|
||||
messageHash: string,
|
||||
setCollectedSignaturesEvent: Function,
|
||||
subscriptions: number[]
|
||||
) => {
|
||||
if (!web3 || !contract) return
|
||||
const currentBlock = homeBlockNumberProvider.get()
|
||||
|
||||
let events: EventData[] = []
|
||||
let securedToBlock = toBlock
|
||||
if (currentBlock) {
|
||||
// prevent errors if the toBlock parameter is bigger than the latest
|
||||
securedToBlock = toBlock >= currentBlock ? currentBlock : toBlock
|
||||
events = await contract.getPastEvents('CollectedSignatures', {
|
||||
fromBlock,
|
||||
toBlock: securedToBlock
|
||||
})
|
||||
}
|
||||
|
||||
const filteredEvents = events.filter(e => e.returnValues.messageHash === messageHash)
|
||||
|
||||
if (filteredEvents.length) {
|
||||
const event = filteredEvents[0]
|
||||
setCollectedSignaturesEvent(event)
|
||||
homeBlockNumberProvider.stop()
|
||||
} else {
|
||||
const newFromBlock = currentBlock ? securedToBlock : fromBlock
|
||||
const newToBlock = currentBlock ? toBlock + BLOCK_RANGE : toBlock
|
||||
const timeoutId = setTimeout(
|
||||
() =>
|
||||
getCollectedSignaturesEvent(
|
||||
web3,
|
||||
contract,
|
||||
newFromBlock,
|
||||
newToBlock,
|
||||
messageHash,
|
||||
setCollectedSignaturesEvent,
|
||||
subscriptions
|
||||
),
|
||||
500
|
||||
)
|
||||
subscriptions.push(timeoutId)
|
||||
}
|
||||
}
|
||||
182
alm/src/utils/getConfirmationsForTx.ts
Normal file
182
alm/src/utils/getConfirmationsForTx.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import Web3 from 'web3'
|
||||
import { Contract } from 'web3-eth-contract'
|
||||
import { HOME_RPC_POLLING_INTERVAL, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import {
|
||||
GetFailedTransactionParams,
|
||||
APITransaction,
|
||||
APIPendingTransaction,
|
||||
GetPendingTransactionParams
|
||||
} from './explorer'
|
||||
import {
|
||||
getValidatorConfirmation,
|
||||
getValidatorFailedTransaction,
|
||||
getValidatorPendingTransaction,
|
||||
getValidatorSuccessTransaction
|
||||
} from './validatorConfirmationHelpers'
|
||||
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
|
||||
|
||||
export const getConfirmationsForTx = async (
|
||||
messageData: string,
|
||||
web3: Maybe<Web3>,
|
||||
validatorList: string[],
|
||||
bridgeContract: Maybe<Contract>,
|
||||
confirmationContractMethod: Function,
|
||||
setResult: Function,
|
||||
requiredSignatures: number,
|
||||
setSignatureCollected: Function,
|
||||
waitingBlocksResolved: boolean,
|
||||
subscriptions: number[],
|
||||
timestamp: number,
|
||||
getFailedTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>,
|
||||
setFailedConfirmations: Function,
|
||||
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>,
|
||||
setPendingConfirmations: Function,
|
||||
getSuccessTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
|
||||
) => {
|
||||
if (!web3 || !validatorList || !validatorList.length || !bridgeContract || !waitingBlocksResolved) return
|
||||
|
||||
// If all the information was not collected, then it should retry
|
||||
let shouldRetry = false
|
||||
const hashMsg = web3.utils.soliditySha3Raw(messageData)
|
||||
let validatorConfirmations = await Promise.all(
|
||||
validatorList.map(getValidatorConfirmation(web3, hashMsg, bridgeContract, confirmationContractMethod))
|
||||
)
|
||||
|
||||
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, look for pending transactions
|
||||
let pendingConfirmationsResult = false
|
||||
if (successConfirmations.length !== requiredSignatures) {
|
||||
// Check if confirmation is pending
|
||||
const validatorPendingConfirmationsChecks = await Promise.all(
|
||||
notSuccessConfirmations.map(getValidatorPendingTransaction(bridgeContract, messageData, getPendingTransactions))
|
||||
)
|
||||
const validatorPendingConfirmations = validatorPendingConfirmationsChecks.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
|
||||
)
|
||||
|
||||
validatorPendingConfirmations.forEach(validatorData => {
|
||||
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
validatorConfirmations[index] = validatorData
|
||||
})
|
||||
|
||||
if (validatorPendingConfirmations.length > 0) {
|
||||
pendingConfirmationsResult = true
|
||||
}
|
||||
}
|
||||
|
||||
const undefinedConfirmations = validatorConfirmations.filter(
|
||||
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
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
|
||||
}))
|
||||
|
||||
notRequiredConfirmations.forEach(validatorData => {
|
||||
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
validatorConfirmations[index] = validatorData
|
||||
})
|
||||
signatureCollectedResult = true
|
||||
}
|
||||
|
||||
// get transactions from success signatures
|
||||
const successConfirmationWithData = await Promise.all(
|
||||
validatorConfirmations
|
||||
.filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
|
||||
.map(getValidatorSuccessTransaction(bridgeContract, messageData, timestamp, getSuccessTransactions))
|
||||
)
|
||||
|
||||
const successConfirmationWithTxFound = successConfirmationWithData.filter(v => v.txHash !== '')
|
||||
|
||||
const updatedValidatorConfirmations = [...validatorConfirmations]
|
||||
|
||||
if (successConfirmationWithTxFound.length > 0) {
|
||||
successConfirmationWithTxFound.forEach(validatorData => {
|
||||
const index = updatedValidatorConfirmations.findIndex(e => e.validator === validatorData.validator)
|
||||
updatedValidatorConfirmations[index] = validatorData
|
||||
})
|
||||
}
|
||||
|
||||
// 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) {
|
||||
shouldRetry = true
|
||||
}
|
||||
|
||||
if (shouldRetry) {
|
||||
const timeoutId = setTimeout(
|
||||
() =>
|
||||
getConfirmationsForTx(
|
||||
messageData,
|
||||
web3,
|
||||
validatorList,
|
||||
bridgeContract,
|
||||
confirmationContractMethod,
|
||||
setResult,
|
||||
requiredSignatures,
|
||||
setSignatureCollected,
|
||||
waitingBlocksResolved,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
getFailedTransactions,
|
||||
setFailedConfirmations,
|
||||
getPendingTransactions,
|
||||
setPendingConfirmations,
|
||||
getSuccessTransactions
|
||||
),
|
||||
HOME_RPC_POLLING_INTERVAL
|
||||
)
|
||||
subscriptions.push(timeoutId)
|
||||
}
|
||||
}
|
||||
137
alm/src/utils/getFinalizationEvent.ts
Normal file
137
alm/src/utils/getFinalizationEvent.ts
Normal file
@@ -0,0 +1,137 @@
|
||||
import { Contract, EventData } from 'web3-eth-contract'
|
||||
import Web3 from 'web3'
|
||||
import { CACHE_KEY_EXECUTION_FAILED, THREE_DAYS_TIMESTAMP, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { ExecutionData } from '../hooks/useMessageConfirmations'
|
||||
import {
|
||||
APIPendingTransaction,
|
||||
APITransaction,
|
||||
GetFailedTransactionParams,
|
||||
GetPendingTransactionParams
|
||||
} from './explorer'
|
||||
import { getBlock, MessageObject } from './web3'
|
||||
import validatorsCache from '../services/ValidatorsCache'
|
||||
|
||||
export const getFinalizationEvent = async (
|
||||
contract: Maybe<Contract>,
|
||||
eventName: string,
|
||||
web3: Maybe<Web3>,
|
||||
setResult: React.Dispatch<React.SetStateAction<ExecutionData>>,
|
||||
waitingBlocksResolved: boolean,
|
||||
message: MessageObject,
|
||||
interval: number,
|
||||
subscriptions: number[],
|
||||
timestamp: number,
|
||||
collectedSignaturesEvent: Maybe<EventData>,
|
||||
getFailedExecution: (args: GetFailedTransactionParams) => Promise<APITransaction[]>,
|
||||
setFailedExecution: Function,
|
||||
getPendingExecution: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>,
|
||||
setPendingExecution: Function
|
||||
) => {
|
||||
if (!contract || !web3 || !waitingBlocksResolved) return
|
||||
// Since it filters by the message id, only one event will be fetched
|
||||
// so there is no need to limit the range of the block to reduce the network traffic
|
||||
const events: EventData[] = await contract.getPastEvents(eventName, {
|
||||
fromBlock: 0,
|
||||
toBlock: 'latest',
|
||||
filter: {
|
||||
messageId: message.id
|
||||
}
|
||||
})
|
||||
if (events.length > 0) {
|
||||
const event = events[0]
|
||||
const [txReceipt, block] = await Promise.all([
|
||||
web3.eth.getTransactionReceipt(event.transactionHash),
|
||||
getBlock(web3, event.blockNumber)
|
||||
])
|
||||
|
||||
const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp
|
||||
const validatorAddress = web3.utils.toChecksumAddress(txReceipt.from)
|
||||
|
||||
setResult({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
|
||||
validator: validatorAddress,
|
||||
txHash: event.transactionHash,
|
||||
timestamp: blockTimestamp,
|
||||
executionResult: event.returnValues.status
|
||||
})
|
||||
} else {
|
||||
// If event is defined, it means it is a message from Home to Foreign
|
||||
if (collectedSignaturesEvent) {
|
||||
const validator = collectedSignaturesEvent.returnValues.authorityResponsibleForRelay
|
||||
|
||||
const pendingTransactions = await getPendingExecution({
|
||||
account: validator,
|
||||
messageData: message.data,
|
||||
to: contract.options.address
|
||||
})
|
||||
|
||||
// If the transaction is pending it sets the status and avoid making the request for failed transactions
|
||||
if (pendingTransactions.length > 0) {
|
||||
const pendingTx = pendingTransactions[0]
|
||||
|
||||
const nowTimestamp = Math.floor(new Date().getTime() / 1000.0)
|
||||
|
||||
setResult({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
|
||||
validator: validator,
|
||||
txHash: pendingTx.hash,
|
||||
timestamp: nowTimestamp,
|
||||
executionResult: false
|
||||
})
|
||||
setPendingExecution(true)
|
||||
} else {
|
||||
const validatorExecutionCacheKey = `${CACHE_KEY_EXECUTION_FAILED}${validator}-${message.id}`
|
||||
const failedFromCache = validatorsCache.get(validatorExecutionCacheKey)
|
||||
|
||||
if (!failedFromCache) {
|
||||
const failedTransactions = await getFailedExecution({
|
||||
account: validator,
|
||||
to: contract.options.address,
|
||||
messageData: message.data,
|
||||
startTimestamp: timestamp,
|
||||
endTimestamp: timestamp + THREE_DAYS_TIMESTAMP
|
||||
})
|
||||
|
||||
if (failedTransactions.length > 0) {
|
||||
const failedTx = failedTransactions[0]
|
||||
|
||||
// If validator execution failed, we cache the result to avoid doing future requests for a result that won't change
|
||||
validatorsCache.set(validatorExecutionCacheKey, true)
|
||||
|
||||
const timestamp = parseInt(failedTx.timeStamp)
|
||||
setResult({
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
|
||||
validator: validator,
|
||||
txHash: failedTx.hash,
|
||||
timestamp,
|
||||
executionResult: false
|
||||
})
|
||||
setFailedExecution(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const timeoutId = setTimeout(
|
||||
() =>
|
||||
getFinalizationEvent(
|
||||
contract,
|
||||
eventName,
|
||||
web3,
|
||||
setResult,
|
||||
waitingBlocksResolved,
|
||||
message,
|
||||
interval,
|
||||
subscriptions,
|
||||
timestamp,
|
||||
collectedSignaturesEvent,
|
||||
getFailedExecution,
|
||||
setFailedExecution,
|
||||
getPendingExecution,
|
||||
setPendingExecution
|
||||
),
|
||||
interval
|
||||
)
|
||||
subscriptions.push(timeoutId)
|
||||
}
|
||||
}
|
||||
42
alm/src/utils/networks.ts
Normal file
42
alm/src/utils/networks.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { formatDistance } from 'date-fns'
|
||||
import {
|
||||
CONFIRMATIONS_STATUS_DESCRIPTION,
|
||||
CONFIRMATIONS_STATUS_DESCRIPTION_HOME,
|
||||
TRANSACTION_STATUS_DESCRIPTION
|
||||
} from '../config/descriptions'
|
||||
import { FOREIGN_EXPLORER_TX_TEMPLATE, HOME_EXPLORER_TX_TEMPLATE } from '../config/constants'
|
||||
|
||||
export const validTxHash = (txHash: string) => /^0x[a-fA-F0-9]{64}$/.test(txHash)
|
||||
|
||||
export const formatTxHash = (txHash: string) => `${txHash.substring(0, 6)}...${txHash.substring(txHash.length - 4)}`
|
||||
|
||||
export const getExplorerTxUrl = (txHash: string, isHome: boolean) => {
|
||||
const template = isHome ? HOME_EXPLORER_TX_TEMPLATE : FOREIGN_EXPLORER_TX_TEMPLATE
|
||||
return template.replace('%s', txHash)
|
||||
}
|
||||
|
||||
export const formatTxHashExtended = (txHash: string) =>
|
||||
`${txHash.substring(0, 10)}...${txHash.substring(txHash.length - 8)}`
|
||||
|
||||
export const formatTimestamp = (timestamp: number): string => {
|
||||
const txDate = new Date(0).setUTCSeconds(timestamp)
|
||||
return formatDistance(txDate, new Date(), {
|
||||
addSuffix: true
|
||||
})
|
||||
}
|
||||
|
||||
export const getTransactionStatusDescription = (status: string, timestamp: Maybe<number> = null) => {
|
||||
let description = TRANSACTION_STATUS_DESCRIPTION[status]
|
||||
|
||||
if (timestamp) {
|
||||
description = description.replace('%t', formatTimestamp(timestamp))
|
||||
}
|
||||
|
||||
return description
|
||||
}
|
||||
|
||||
export const getConfirmationsStatusDescription = (status: string, home: string, foreign: string, fromHome: boolean) => {
|
||||
const statusDescription = fromHome ? CONFIRMATIONS_STATUS_DESCRIPTION_HOME : CONFIRMATIONS_STATUS_DESCRIPTION
|
||||
|
||||
return statusDescription[status]
|
||||
}
|
||||
50
alm/src/utils/signatureWaitingForBlocks.ts
Normal file
50
alm/src/utils/signatureWaitingForBlocks.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
|
||||
import { BlockNumberProvider } from '../services/BlockNumberProvider'
|
||||
|
||||
export const checkSignaturesWaitingForBLocks = async (
|
||||
targetBlock: number,
|
||||
setWaitingStatus: Function,
|
||||
setWaitingBlocksResolved: Function,
|
||||
validatorList: string[],
|
||||
setConfirmations: Function,
|
||||
blockProvider: BlockNumberProvider,
|
||||
interval: number,
|
||||
subscriptions: number[]
|
||||
) => {
|
||||
const currentBlock = blockProvider.get()
|
||||
|
||||
if (currentBlock && currentBlock >= targetBlock) {
|
||||
setWaitingBlocksResolved(true)
|
||||
setWaitingStatus(false)
|
||||
blockProvider.stop()
|
||||
} else {
|
||||
let nextInterval = interval
|
||||
if (!currentBlock) {
|
||||
nextInterval = 500
|
||||
} else {
|
||||
const validatorsWaiting = validatorList.map(validator => {
|
||||
return {
|
||||
validator,
|
||||
status: VALIDATOR_CONFIRMATION_STATUS.WAITING
|
||||
}
|
||||
})
|
||||
setWaitingStatus(true)
|
||||
setConfirmations(validatorsWaiting)
|
||||
}
|
||||
const timeoutId = setTimeout(
|
||||
() =>
|
||||
checkSignaturesWaitingForBLocks(
|
||||
targetBlock,
|
||||
setWaitingStatus,
|
||||
setWaitingBlocksResolved,
|
||||
validatorList,
|
||||
setConfirmations,
|
||||
blockProvider,
|
||||
interval,
|
||||
subscriptions
|
||||
),
|
||||
nextInterval
|
||||
)
|
||||
subscriptions.push(timeoutId)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
72
alm/src/utils/web3.ts
Normal file
72
alm/src/utils/web3.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import Web3 from 'web3'
|
||||
import { BlockTransactionString } from 'web3-eth'
|
||||
import { TransactionReceipt } from 'web3-eth'
|
||||
import { AbiItem } from 'web3-utils'
|
||||
import memoize from 'fast-memoize'
|
||||
import promiseRetry from 'promise-retry'
|
||||
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
|
||||
import { SnapshotProvider } from '../services/SnapshotProvider'
|
||||
|
||||
export interface MessageObject {
|
||||
id: string
|
||||
data: string
|
||||
}
|
||||
|
||||
const rawGetWeb3 = (url: string) => new Web3(new Web3.providers.HttpProvider(url))
|
||||
const memoized = memoize(rawGetWeb3)
|
||||
|
||||
export const getWeb3 = (url: string) => memoized(url)
|
||||
|
||||
export const filterEventsByAbi = (
|
||||
txReceipt: TransactionReceipt,
|
||||
web3: Web3,
|
||||
bridgeAddress: string,
|
||||
eventAbi: AbiItem
|
||||
): MessageObject[] => {
|
||||
const eventHash = web3.eth.abi.encodeEventSignature(eventAbi)
|
||||
const events = txReceipt.logs.filter(e => e.address === bridgeAddress && e.topics[0] === eventHash)
|
||||
|
||||
return events.map(e => {
|
||||
let decodedLogs: { [p: string]: string } = {
|
||||
messageId: '',
|
||||
encodedData: ''
|
||||
}
|
||||
if (eventAbi && eventAbi.inputs && eventAbi.inputs.length) {
|
||||
decodedLogs = web3.eth.abi.decodeLog(eventAbi.inputs, e.data, [e.topics[1]])
|
||||
}
|
||||
return { id: decodedLogs.messageId, data: decodedLogs.encodedData }
|
||||
})
|
||||
}
|
||||
|
||||
export const getHomeMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
|
||||
const UserRequestForSignatureAbi: AbiItem = HOME_AMB_ABI.filter(
|
||||
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForSignature'
|
||||
)[0]
|
||||
return filterEventsByAbi(txReceipt, web3, bridgeAddress, UserRequestForSignatureAbi)
|
||||
}
|
||||
|
||||
export const getForeignMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
|
||||
const userRequestForAffirmationAbi: AbiItem = FOREIGN_AMB_ABI.filter(
|
||||
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForAffirmation'
|
||||
)[0]
|
||||
return filterEventsByAbi(txReceipt, web3, bridgeAddress, userRequestForAffirmationAbi)
|
||||
}
|
||||
|
||||
// In some rare cases the block data is not available yet for the block of a new event detected
|
||||
// so this logic retry to get the block in case it fails
|
||||
export const getBlock = async (web3: Web3, blockNumber: number): Promise<BlockTransactionString> =>
|
||||
promiseRetry(async retry => {
|
||||
const result = await web3.eth.getBlock(blockNumber)
|
||||
if (!result) {
|
||||
return retry('Error getting block data')
|
||||
}
|
||||
return result
|
||||
})
|
||||
|
||||
export const getChainId = async (web3: Web3, snapshotProvider: SnapshotProvider) => {
|
||||
let id = snapshotProvider.chainId()
|
||||
if (id === 0) {
|
||||
id = await web3.eth.getChainId()
|
||||
}
|
||||
return id
|
||||
}
|
||||
25
alm/tsconfig.json
Normal file
25
alm/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "preserve"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
]
|
||||
}
|
||||
Binary file not shown.
31
burner-wallet-plugin/.eslintrc.js
Normal file
31
burner-wallet-plugin/.eslintrc.js
Normal file
@@ -0,0 +1,31 @@
|
||||
module.exports = {
|
||||
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
|
||||
extends: [
|
||||
"plugin:react/recommended",
|
||||
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from @typescript-eslint/eslint-plugin
|
||||
"../.eslintrc"
|
||||
],
|
||||
parserOptions: {
|
||||
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
|
||||
sourceType: "module", // Allows for the use of imports
|
||||
ecmaFeatures: {
|
||||
jsx: true // Allows for the parsing of JSX
|
||||
}
|
||||
},
|
||||
rules: {
|
||||
"@typescript-eslint/explicit-function-return-type": "off",
|
||||
"@typescript-eslint/no-explicit-any": "off", // Reduce the use of 'any'
|
||||
"@typescript-eslint/no-non-null-assertion": "off",
|
||||
"@typescript-eslint/no-var-requires": "off",
|
||||
"react/prop-types": "off",
|
||||
"@typescript-eslint/ban-ts-ignore": "off",
|
||||
"@typescript-eslint/member-delimiter-style": "off",
|
||||
"@typescript-eslint/indent": "off",
|
||||
"@typescript-eslint/explicit-member-accessibility": "off"
|
||||
},
|
||||
settings: {
|
||||
react: {
|
||||
version: "detect",
|
||||
}
|
||||
}
|
||||
};
|
||||
30
burner-wallet-plugin/Dockerfile
Normal file
30
burner-wallet-plugin/Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
||||
FROM node:12 as plugin-base
|
||||
|
||||
WORKDIR /mono
|
||||
COPY package.json .
|
||||
RUN mkdir -p contracts/node_modules
|
||||
|
||||
COPY burner-wallet-plugin/package.json ./burner-wallet-plugin/
|
||||
COPY burner-wallet-plugin/lerna.json ./burner-wallet-plugin/
|
||||
COPY burner-wallet-plugin/yarn.lock ./burner-wallet-plugin/
|
||||
COPY burner-wallet-plugin/tsconfig.json ./burner-wallet-plugin/
|
||||
COPY burner-wallet-plugin/tokenbridge-bw-exchange/package.json ./burner-wallet-plugin/tokenbridge-bw-exchange/
|
||||
COPY burner-wallet-plugin/staging/package.json ./burner-wallet-plugin/staging/
|
||||
COPY burner-wallet-plugin/testing/package.json ./burner-wallet-plugin/testing/
|
||||
COPY yarn.lock .
|
||||
RUN yarn install --production --frozen-lockfile
|
||||
|
||||
COPY ./burner-wallet-plugin/tokenbridge-bw-exchange ./burner-wallet-plugin/tokenbridge-bw-exchange
|
||||
RUN yarn build:plugin
|
||||
|
||||
|
||||
FROM plugin-base as testing
|
||||
COPY ./burner-wallet-plugin/testing ./burner-wallet-plugin/testing
|
||||
WORKDIR /mono/burner-wallet-plugin
|
||||
CMD ["yarn", "start-testing"]
|
||||
|
||||
|
||||
FROM plugin-base as staging
|
||||
COPY ./burner-wallet-plugin/staging ./burner-wallet-plugin/staging
|
||||
WORKDIR /mono/burner-wallet-plugin
|
||||
CMD ["yarn", "start-staging"]
|
||||
41
burner-wallet-plugin/README.md
Normal file
41
burner-wallet-plugin/README.md
Normal file
@@ -0,0 +1,41 @@
|
||||
# TokenBridge Burner Wallet 2 Plugin
|
||||
|
||||
Please refer to the [Plugin README](./tokenrbdige-bw-exchange/README.md) for resources provided, instructions to install and use the plugin.
|
||||
|
||||
### Setup
|
||||
1. [Initialize](../README.md#initializing-the-monorepository) the monorepository.
|
||||
2. Run `yarn build` or from the monorepository root `yarn build:plugin`
|
||||
|
||||
### Run Burner Wallet with the plugin in Mainnet & Classic
|
||||
1. Create `.env` file in `staging` folder and set `REACT_APP_INFURA_KEY=<your key from infura.com>`
|
||||
2. Run `yarn start-staging` to start the wallet connected to Mainnet & Classic and interact with the ETH - WETC Bridge.
|
||||
|
||||
### Run Burner Wallet with the plugin in Sokol & Kovan
|
||||
1. Create `.env` file in `testing` folder and set `REACT_APP_INFURA_KEY=<your key from infura.com>`.
|
||||
Also, a private key can be set to start the wallet with the specified account `REACT_APP_PK=0x...`
|
||||
2. Run `yarn start-testing` to start the wallet connected to Sokol & Kovan and interact with a test bridge
|
||||
that works on top of the AMB bridge.
|
||||
|
||||
### Docker Setup
|
||||
Docker can be used to build the services and run the testing and staging wallets.
|
||||
|
||||
First you may want to create the `.env` files for testing and staging as mentioned before. This is optional before building the containers, variables can be passes later using `--env-file` or `--env` parameters in `docker run`.
|
||||
|
||||
Build the services with docker-compose:
|
||||
```bash
|
||||
docker-compose build
|
||||
```
|
||||
|
||||
### Run Burner Wallet with the plugin in Mainnet & Classic using Docker
|
||||
```bash
|
||||
docker run -ti -p 8080:8080 -e PORT=8080 --rm burner-wallet-plugin_staging
|
||||
```
|
||||
|
||||
### Run Burner Wallet with the plugin in Sokol & Kovan using Docker
|
||||
```bash
|
||||
docker run -ti -p 8080:8080 -e PORT=8080 --rm burner-wallet-plugin_testing
|
||||
```
|
||||
### Publish to npm
|
||||
In order to make this plugin accessible, it should be available as a npm package. Follow the [instructions](publish.md) to publish
|
||||
the package to npm registry.
|
||||
|
||||
17
burner-wallet-plugin/docker-compose.yml
Normal file
17
burner-wallet-plugin/docker-compose.yml
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
version: '2.4'
|
||||
services:
|
||||
staging:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: burner-wallet-plugin/Dockerfile
|
||||
target: staging
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
testing:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: burner-wallet-plugin/Dockerfile
|
||||
target: testing
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
10
burner-wallet-plugin/lerna.json
Normal file
10
burner-wallet-plugin/lerna.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"packages": [
|
||||
"basic-wallet",
|
||||
"local-wallet",
|
||||
"tokenbridge-bw-exchange"
|
||||
],
|
||||
"npmClient": "yarn",
|
||||
"useWorkspaces": true,
|
||||
"version": "independent"
|
||||
}
|
||||
28
burner-wallet-plugin/package.json
Normal file
28
burner-wallet-plugin/package.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"name": "burner-wallet-plugin",
|
||||
"description": "Burner Wallet 2 plugin",
|
||||
"version": "1.0.0",
|
||||
"license": "GPL-3.0-only",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"install": "lerna bootstrap",
|
||||
"build": "lerna run --ignore testing --ignore staging build --stream",
|
||||
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore",
|
||||
"start-staging": "lerna run --scope staging start --stream",
|
||||
"start-testing": "lerna run --scope testing start --stream",
|
||||
"test": "lerna run --ignore testing --ignore staging test --stream"
|
||||
},
|
||||
"workspaces": [
|
||||
"staging",
|
||||
"testing",
|
||||
"tokenbridge-bw-exchange"
|
||||
],
|
||||
"dependencies": {
|
||||
"@types/color": "3.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "1.13.0",
|
||||
"@typescript-eslint/parser": "1.13.0",
|
||||
"eslint-plugin-react": "7.19.0",
|
||||
"lerna": "3.16.4",
|
||||
"typescript": "3.5.3"
|
||||
}
|
||||
}
|
||||
36
burner-wallet-plugin/publish.md
Normal file
36
burner-wallet-plugin/publish.md
Normal file
@@ -0,0 +1,36 @@
|
||||
## Plugin Package Information
|
||||
|
||||
The package to be published gets its configuration from `tokenbridge/burner-wallet-plugin/tokenbridge-bw-exchange/package.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "tokenbridge-bw-exchange",
|
||||
"version": "1.0.0",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"/dist"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- `name` is the name of how package will be available in npm.
|
||||
- `main` is entry point for the package
|
||||
- `types` is the entry point for typescript types
|
||||
- `files` is the list of files included when publishing the package. So we have to run `yarn build` first to
|
||||
generate the `dist` folder.
|
||||
|
||||
## Steps to publish to npm
|
||||
|
||||
1. Create account in https://www.npmjs.com/
|
||||
|
||||
2. Go to `tokenbridge/burner-wallet-plugin/tokenbridge-bw-exchange/`
|
||||
|
||||
3. Run `yarn build`. Make sure it generates the `dist` folder
|
||||
|
||||
4. Update `version` in `tokenbridge/burner-wallet-plugin/tokenbridge-bw-exchange/package.json`
|
||||
5. Run `yarn login` and fill login information if required.
|
||||
6. Run `yarn publish --access public`.
|
||||
The prompt will ask for the new version, complete it with the version from `package.json`
|
||||
|
||||
More information in https://classic.yarnpkg.com/en/docs/publishing-a-package/
|
||||
1
burner-wallet-plugin/staging/.env.example
Normal file
1
burner-wallet-plugin/staging/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
REACT_APP_INFURA_KEY=
|
||||
40
burner-wallet-plugin/staging/package.json
Normal file
40
burner-wallet-plugin/staging/package.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "staging",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@burner-wallet/assets": "^1.1.10",
|
||||
"@burner-wallet/core": "^1.1.0",
|
||||
"@burner-wallet/exchange": "^1.1.4",
|
||||
"@burner-wallet/metamask-plugin": "^1.0.0",
|
||||
"@burner-wallet/modern-ui": "^1.0.7",
|
||||
"@poanet/tokenbridge-bw-exchange": "^1.0.0",
|
||||
"@types/node": "12.0.4",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "16.8.4",
|
||||
"@types/react-router-dom": "^4.3.3",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-scripts": "3.0.1",
|
||||
"typescript": "3.5.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 3.8 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user