Compare commits

..

No commits in common. "master" and "1.0.0-rc0" have entirely different histories.

670 changed files with 17209 additions and 52667 deletions

88
.circleci/config.yml Normal file
View File

@ -0,0 +1,88 @@
version: 2
jobs:
build:
docker:
- image: circleci/node:10.15
steps:
- checkout
- run: git submodule update --init
- run: yarn
- run: yarn run build
lint:
docker:
- image: circleci/node:10.15
steps:
- checkout
- run: git submodule update --init
- run: yarn
- run: yarn compile:contracts
- run: yarn run lint
ansible-lint:
docker:
- image: particlekit/ansible-lint
steps:
- checkout
- run: ./deployment/lint.sh
test:
docker:
- image: circleci/node:10.15
environment:
HOME_RPC_URL: http://example.com
FOREIGN_RPC_URL: http://example.com
steps:
- checkout
- run: git submodule update --init
- run: yarn
- run: yarn compile:contracts
- run: yarn run test
oracle-e2e:
docker:
- image: circleci/node:10.15
steps:
- checkout
- run: git submodule update --init
- run: yarn
- setup_remote_docker
- run: yarn run oracle-e2e
ui-e2e:
machine:
image: circleci/classic:latest
steps:
- checkout
- run: |
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
echo ' [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
- run: nvm install 11.4.0 && nvm alias default 11.4.0
- run: curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
- run: echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
- run: sudo apt-get update && sudo apt-get install yarn
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
- run: sudo dpkg -i google-chrome-stable_current_amd64.deb
- run: git submodule update --init
- run: yarn
- run: yarn run ui-e2e
cover:
docker:
- image: circleci/node:10.15
steps:
- checkout
- run: git submodule update --init
- run: yarn
- run: yarn workspace ui run coverage
- run: yarn workspace ui run coveralls
workflows:
version: 2
coverage:
jobs:
- cover:
filters:
branches:
only: master
tokenbridge:
jobs:
- build
- lint
- ansible-lint
- test
- oracle-e2e
- ui-e2e

View File

@ -8,17 +8,7 @@
**/docs **/docs
**/*.md **/*.md
monitor/**/*.env*
oracle/**/*.env*
!**/.env.example
contracts/test contracts/test
contracts/build
oracle/test oracle/test
monitor/test
monitor/responses
monitor/cache/*
commons/test
oracle/**/*.png oracle/**/*.png
oracle/**/*.jpg oracle/**/*.jpg
audit

View File

@ -2,5 +2,3 @@ node_modules
submodules submodules
coverage coverage
lib lib
dist
build

View File

@ -1,227 +0,0 @@
name: tokenbridge
on: [push]
env:
DOCKER_REGISTRY: docker.pkg.github.com
DOCKER_REPO: poanetwork/tokenbridge
DOCKER_IMAGE_BASE: docker.pkg.github.com/poanetwork/tokenbridge
jobs:
initialize:
runs-on: ubuntu-latest
outputs:
cache_key: ${{ steps.get_cache_key.outputs.cache_key }}
steps:
- uses: actions/setup-node@v1
with:
node-version: 12
- uses: actions/checkout@v2
with:
submodules: true
- name: Set cache key
id: get_cache_key
run: |
git submodule status > submodule.status
echo "::set-output name=cache_key::cache-repo-${{ hashFiles('yarn.lock', 'package.json', 'submodule.status') }}"
- uses: actions/cache@v2
id: cache-repo
with:
path: |
**/node_modules
contracts/build
key: ${{ steps.get_cache_key.outputs.cache_key }}
- name: Install dependencies and compile contracts
if: ${{ !steps.cache-repo.outputs.cache-hit }}
run: |
yarn install --frozen-lockfile
yarn run install:deploy
yarn run compile:contracts
validate:
runs-on: ubuntu-latest
needs:
- initialize
strategy:
fail-fast: false
matrix:
task: [build, lint, test]
steps:
- uses: actions/setup-node@v1
with:
node-version: 12
- uses: actions/checkout@v2
with:
submodules: true
- uses: actions/cache@v2
id: cache-repo
with:
path: |
**/node_modules
contracts/build
key: ${{ needs.initialize.outputs.cache_key }}
- name: yarn run ${{ matrix.task }}
run: ${{ steps.cache-repo.outputs.cache-hit }} && yarn run ${{ matrix.task }}
build-e2e-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Evaluate e2e docker images tags
run: |
git submodule status > submodule.status
echo "E2E_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e', 'e2e-commons') }}" >> $GITHUB_ENV
echo "ORACLE_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}" >> $GITHUB_ENV
echo "MONITOR_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}" >> $GITHUB_ENV
echo "ALM_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}" >> $GITHUB_ENV
- name: Rebuild and push updated images
run: |
function check_if_image_exists() {
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
}
updated=()
if ! check_if_image_exists e2e ${E2E_TAG}; then updated+=("e2e"); fi
if ! check_if_image_exists oracle ${ORACLE_TAG}; then updated+=("oracle-amb"); fi
if ! check_if_image_exists monitor ${MONITOR_TAG}; then updated+=("monitor-amb"); fi
if ! check_if_image_exists alm ${ALM_TAG}; then updated+=("alm"); fi
if [ ${#updated[@]} -gt 0 ]; then
echo "Updated services: ${updated[@]}"
cd e2e-commons
docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
docker-compose build ${updated[@]}
docker-compose push ${updated[@]}
else
echo "Nothing relevant was changed in the source"
fi
build-molecule-runner:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Evaluate e2e molecule runner tag
run: echo "MOLECULE_RUNNER_TAG=${{ hashFiles('./deployment-e2e/Dockerfile') }}" >> $GITHUB_ENV
- name: Rebuild and push molecule runner e2e image
run: |
function check_if_image_exists() {
curl -fsSlL "https://${{ github.actor }}:${{ github.token }}@${DOCKER_REGISTRY}/v2/${DOCKER_REPO}/tokenbridge-e2e-$1/manifests/$2" > /dev/null
}
if check_if_image_exists molecule_runner ${MOLECULE_RUNNER_TAG}; then
echo "Image already exists"
else
cd e2e-commons
docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
docker-compose build molecule_runner
docker-compose push molecule_runner
fi
e2e:
runs-on: ubuntu-latest
needs:
- initialize
- build-e2e-images
strategy:
fail-fast: false
matrix:
task: [oracle-e2e, monitor-e2e, alm-e2e]
include:
- task: alm-e2e
use-cache: true
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Evaluate e2e docker images tags
run: |
git submodule status > submodule.status
echo "E2E_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e', 'e2e-commons') }}" >> $GITHUB_ENV
echo "ORACLE_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}" >> $GITHUB_ENV
echo "MONITOR_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}" >> $GITHUB_ENV
echo "ALM_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}" >> $GITHUB_ENV
- if: ${{ matrix.use-cache }}
uses: actions/cache@v2
id: cache-repo
with:
path: |
**/node_modules
contracts/build
key: ${{ needs.initialize.outputs.cache_key }}
- name: Login to docker registry
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
- name: yarn run ${{ matrix.task }}
run: ${{ !matrix.use-cache || steps.cache-repo.outputs.cache-hit }} && yarn run ${{ matrix.task }}
- name: Upload logs
if: always()
uses: actions/upload-artifact@v2
with:
name: logs-${{ matrix.task }}
path: e2e-commons/logs
deployment:
runs-on: ubuntu-latest
needs:
- build-e2e-images
- build-molecule-runner
strategy:
fail-fast: false
matrix:
task: [oracle, monitor, multiple, repo]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Evaluate e2e molecule runner tag
run: echo "MOLECULE_RUNNER_TAG=${{ hashFiles('./deployment-e2e/Dockerfile') }}" >> $GITHUB_ENV
- name: Login to docker registry
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
- run: deployment-e2e/molecule.sh ${{ matrix.task }}
ultimate:
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/tags') || contains(github.event.head_commit.message, 'ultimate')
runs-on: ubuntu-latest
needs:
- initialize
- build-e2e-images
strategy:
fail-fast: false
matrix:
task: [amb, erc-to-native]
steps:
- uses: actions/checkout@v2
with:
submodules: true
- name: Evaluate e2e docker images tags
run: |
git submodule status > submodule.status
echo "E2E_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'Dockerfile.e2e', 'commons', 'oracle-e2e', 'monitor-e2e', 'e2e-commons') }}" >> $GITHUB_ENV
echo "ORACLE_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'oracle') }}" >> $GITHUB_ENV
echo "MONITOR_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'monitor') }}" >> $GITHUB_ENV
echo "ALM_TAG=${{ hashFiles('yarn.lock', 'package.json', 'submodule.status', 'commons', 'alm') }}" >> $GITHUB_ENV
echo "MOLECULE_RUNNER_TAG=${{ hashFiles('./deployment-e2e/Dockerfile') }}" >> $GITHUB_ENV
- uses: actions/cache@v2
id: cache-repo
with:
path: |
**/node_modules
contracts/build
key: ${{ needs.initialize.outputs.cache_key }}
- name: Login to docker registry
run: docker login ${DOCKER_REGISTRY} -u ${{ github.actor }} -p ${{ github.token }}
- name: Deploy contracts
run: ${{ steps.cache-repo.outputs.cache-hit }} && e2e-commons/up.sh deploy generate-amb-tx blocks
- name: Pull e2e oracle image
run: |
docker-compose -f ./e2e-commons/docker-compose.yml pull oracle-amb
docker tag ${DOCKER_IMAGE_BASE}/tokenbridge-e2e-oracle:${ORACLE_TAG} poanetwork/tokenbridge-oracle:latest
- name: Deploy oracle
run: deployment-e2e/molecule.sh ultimate-${{ matrix.task }}
- name: Reset docker socket permissions
run: sudo chown -R $USER:docker /var/run/docker.sock
- name: Run oracle e2e tests
run: docker-compose -f ./e2e-commons/docker-compose.yml run -e ULTIMATE=true e2e yarn workspace oracle-e2e run ${{ matrix.task }}
- name: Save logs
if: always()
run: e2e-commons/down.sh
- name: Upload logs
if: always()
uses: actions/upload-artifact@v2
with:
name: logs-ultimate-${{ matrix.task }}
path: e2e-commons/logs

17
.gitignore vendored
View File

@ -6,12 +6,14 @@ coverage
# production # production
build build
dist
# misc # misc
.DS_Store .DS_Store
*.env* .env
!.env.example .env.local
.env.development.local
.env.test.local
.env.production.local
.idea .idea
.nyc_output .nyc_output
logs/ logs/
@ -41,14 +43,7 @@ hosts
Vagrantfile Vagrantfile
vagrant-hosts.yml vagrant-hosts.yml
.vagrant .vagrant
deployment/venv
__pycache__
#monitor #monitor
monitor/responses/* monitor/responses/*
monitor/cache/* !monitor/.gitkeep
!monitor/cache/.gitkeep
!monitor/.gitkeep
# Local Netlify folder
.netlify

2
.gitmodules vendored
View File

@ -1,3 +1,3 @@
[submodule "contracts"] [submodule "contracts"]
path = contracts path = contracts
url = https://github.com/poanetwork/tokenbridge-contracts.git url = https://github.com/poanetwork/poa-bridge-contracts.git

2
.nvmrc
View File

@ -1 +1 @@
12.22 10.15

View File

@ -1,6 +1,5 @@
{ {
"semi": false, "semi": false,
"singleQuote": true, "singleQuote": true,
"printWidth": 120, "printWidth": 100
"bracketSpacing": true
} }

View File

@ -1,82 +0,0 @@
# Configuration
## Common configuration
name | description | value
--- | --- | ---
COMMON_HOME_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Home network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
COMMON_FOREIGN_RPC_URL | The HTTPS URL(s) used to communicate to the RPC nodes in the Foreign network. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
COMMON_HOME_BRIDGE_ADDRESS | The address of the bridge contract address in the Home network. It is used to listen to events from and send validators' transactions to the Home network. | hexidecimal beginning with "0x"
COMMON_FOREIGN_BRIDGE_ADDRESS | The address of the bridge contract address in the Foreign network. It is used to listen to events from and send validators' transactions to the Foreign network. | hexidecimal beginning with "0x"
COMMON_HOME_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Home network. The gas price provided by the oracle is used to send the validator's transactions to the RPC node. Since it is assumed that the Home network has a predefined gas price (e.g. the gas price in the Core of POA.Network is `1 GWei`), the gas price oracle parameter can be omitted for such networks. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
COMMON_HOME_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_HOME_GAS_PRICE_SUPPLIER_URL` is not used. | `instant` / `fast` / `standard` / `slow`
COMMON_HOME_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Home Bridge contract are not available. | integer
COMMON_HOME_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL | The URL used to get a JSON response from the gas price prediction oracle for the Foreign network. The provided gas price is used to send the validator's transactions to the RPC node. If the Foreign network is Ethereum Foundation mainnet, the oracle URL can be: https://gasprice.poa.network. Otherwise this parameter can be omitted. Set to `gas-price-oracle` if you want to use npm `gas-price-oracle` package for retrieving gas price from multiple sources. Set to `eip1559-gas-estimation` if you want to use EIP1559 RPC-based gas estimation. | URL
COMMON_FOREIGN_GAS_PRICE_SPEED_TYPE | Assuming the gas price oracle responds with the following JSON structure: `{"fast": 20.0, "block_time": 12.834, "health": true, "standard": 6.0, "block_number": 6470469, "instant": 71.0, "slow": 1.889}`, this parameter specifies the desirable transaction speed. The speed type can be omitted when `COMMON_FOREIGN_GAS_PRICE_SUPPLIER_URL`is not used. | `instant` / `fast` / `standard` / `slow`
COMMON_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) used if both the oracle and fall back gas price specified in the Foreign Bridge contract are not available. | integer
COMMON_FOREIGN_GAS_PRICE_FACTOR | A value that will multiply the gas price of the oracle to convert it to gwei. If the oracle API returns gas prices in gwei then this can be set to `1`. Also, it could be used to intentionally pay more gas than suggested by the oracle to guarantee the transaction verification. E.g. `1.25` or `1.5`. | integer
## Oracle configuration
name | description | value
--- | --- | ---
ORACLE_BRIDGE_MODE | The bridge mode. The bridge starts listening to a different set of events based on this parameter. | ERC_TO_NATIVE / ARBITRARY_MESSAGE
ORACLE_ALLOW_HTTP_FOR_RPC | **Only use in test environments - must be omitted in production environments.**. If this parameter is specified and set to `yes`, RPC URLs can be specified in form of HTTP links. A warning that the connection is insecure will be written to the logs. | `yes` / `no`
ORACLE_HOME_RPC_POLLING_INTERVAL | The interval in milliseconds used to request the RPC node in the Home network for new blocks. The interval should match the average production time for a new block. | integer
ORACLE_FOREIGN_RPC_POLLING_INTERVAL | The interval in milliseconds used to request the RPC node in the Foreign network for new blocks. The interval should match the average production time for a new block. | integer
ORACLE_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
ORACLE_FOREIGN_GAS_PRICE_UPDATE_INTERVAL | The interval in milliseconds used to get the updated gas price value either from the oracle or from the Foreign Bridge contract. | integer
ORACLE_QUEUE_URL | RabbitMQ URL used by watchers and senders to communicate to the message queue. Typically set to: `amqp://127.0.0.1`. | local URL
ORACLE_REDIS_URL | Redis DB URL used by watchers and senders to communicate to the database. Typically set to: `redis://127.0.0.1:6379`. | local URL
ORACLE_HOME_START_BLOCK | The block number in the Home network used to start watching for events when the bridge instance is run for the first time. Usually this is the same block where the Home Bridge contract is deployed. If a new validator instance is being deployed for an existing set of validators, the block number could be the latest block in the chain. | integer
ORACLE_FOREIGN_START_BLOCK | The block number in the Foreign network used to start watching for events when the bridge instance runs for the first time. Usually this is the same block where the Foreign Bridge contract was deployed to. If a new validator instance is being deployed for an existing set of validators, the block number could be the latest block in the chain. | integer
ORACLE_LOG_LEVEL | Set the level of details in the logs. | `trace` / `debug` / `info` / `warn` / `error` / `fatal`
ORACLE_MAX_PROCESSING_TIME | The workers processes will be killed if this amount of time (in milliseconds) is elapsed before they finish processing. It is recommended to set this value to 4 times the value of the longest polling time (set with the `HOME_POLLING_INTERVAL` and `FOREIGN_POLLING_INTERVAL` variables). To disable this, set the time to 0. | integer
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY | The private key of the bridge validator used to sign confirmations before sending transactions to the bridge contracts. The validator account is calculated automatically from the private key. Every bridge instance (set of watchers and senders) must have its own unique private key. The specified private key is used to sign transactions on both sides of the bridge. | hexidecimal without "0x"
ORACLE_VALIDATOR_ADDRESS | The public address of the bridge validator | hexidecimal with "0x"
ORACLE_TX_REDUNDANCY | If set to `true`, instructs oracle to send `eth_sendRawTransaction` requests through all available RPC urls defined in `COMMON_HOME_RPC_URL` and `COMMON_FOREIGN_RPC_URL` variables instead of using first available one
ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST | Filename with a list of addresses, separated by newlines. If set, determines the privileged set of accounts whose requests will be automatically processed by the CollectedSignatures watcher. | string
ORACLE_HOME_TO_FOREIGN_BLOCK_LIST | Filename with a list of addresses, separated by newlines. If set, determines the blocked set of accounts whose requests will not be automatically processed by the CollectedSignatures watcher. Has a lower priority than the `ORACLE_HOME_TO_FOREIGN_ALLOWANCE_LIST` | string
ORACLE_HOME_TO_FOREIGN_CHECK_SENDER | If set to `true`, instructs the oracle to do an extra check for transaction origin in the block/allowance list. `false` by default. | `true` / `false`
ORACLE_ALWAYS_RELAY_SIGNATURES | If set to `true`, the oracle will always relay signatures even if it was not the last who finilized the signatures collecting process. The default is `false`. | `true` / `false`
ORACLE_RPC_REQUEST_TIMEOUT | Timeout in milliseconds for a single RPC request. Default value is `ORACLE_*_RPC_POLLING_INTERVAL * 2`. | integer
ORACLE_HOME_TX_RESEND_INTERVAL | Interval in milliseconds for automatic resending of stuck transactions for Home sender service. Defaults to 20 minutes. | integer
ORACLE_FOREIGN_TX_RESEND_INTERVAL | Interval in milliseconds for automatic resending of stuck transactions for Foreign sender service. Defaults to 20 minutes. | integer
ORACLE_SHUTDOWN_SERVICE_URL | Optional external URL to some other service/monitor/configuration manager that controls the remote shutdown process. GET request should return `application/json` message with the following schema: `{ shutdown: true/false }`. | URL
ORACLE_SHUTDOWN_SERVICE_POLLING_INTERVAL | Optional interval in milliseconds used to request the side RPC node or external shutdown service. Default is 120000. | integer
ORACLE_SIDE_RPC_URL | Optional HTTPS URL(s) for communication with the external shutdown service or side RPC nodes, used for shutdown manager activities. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
ORACLE_FOREIGN_ARCHIVE_RPC_URL | Optional HTTPS URL(s) for communication with the archive nodes on the foreign network. Only used in AMB bridge mode for async information request processing. Several URLs can be specified, delimited by spaces. If the connection to one of these nodes is lost the next URL is used for connection. | URL(s)
ORACLE_SHUTDOWN_CONTRACT_ADDRESS | Optional contract address in the side chain accessible through `ORACLE_SIDE_RPC_URL`, where the method passed in `ORACLE_SHUTDOWN_CONTRACT_METHOD` is implemented. | `address`
ORACLE_SHUTDOWN_CONTRACT_METHOD | Method signature to be used in the side chain to identify the current shutdown status. Method should return boolean. Default value is `isShutdown()`. | `function signature`
ORACLE_FOREIGN_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Foreign chain. Infinite, if not provided. | `integer`
ORACLE_HOME_RPC_BLOCK_POLLING_LIMIT | Max length for the block range used in `eth_getLogs` requests for polling contract events for the Home chain. Infinite, if not provided. | `integer`
ORACLE_JSONRPC_ERROR_CODES | Override default JSON rpc error codes that can trigger RPC fallback to the next URL from the list (or a retry in case of a single RPC URL). Default is `-32603,-32002,-32005`. Should be a comma-separated list of negative integers. | `string`
ORACLE_HOME_EVENTS_REPROCESSING | If set to `true`, home events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
ORACLE_HOME_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the home chain. Defaults to `1000` | `integer`
ORACLE_HOME_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the home chain. Defaults to `500` | `integer`
ORACLE_HOME_RPC_SYNC_STATE_CHECK_INTERVAL | Interval for checking JSON RPC sync state, by requesting the latest block number. Oracle will switch to the fallback JSON RPC in case sync process is stuck | `integer`
ORACLE_FOREIGN_EVENTS_REPROCESSING | If set to `true`, foreign events happened in the past will be refetched and processed once again, to ensure that nothing was missed on the first pass. | `bool`
ORACLE_FOREIGN_EVENTS_REPROCESSING_BATCH_SIZE | Batch size for one `eth_getLogs` request when reprocessing old logs in the foreign chain. Defaults to `500` | `integer`
ORACLE_FOREIGN_EVENTS_REPROCESSING_BLOCK_DELAY | Block confirmations number, after which old logs are being reprocessed in the foreign chain. Defaults to `250` | `integer`
ORACLE_FOREIGN_RPC_SYNC_STATE_CHECK_INTERVAL | Interval for checking JSON RPC sync state, by requesting the latest block number. Oracle will switch to the fallback JSON RPC in case sync process is stuck | `integer`
## Monitor configuration
name | description | value
--- | --- | ---
MONITOR_HOME_START_BLOCK | The app will monitor transactions starting from this block. | integer
MONITOR_FOREIGN_START_BLOCK | The app will monitor transactions starting from this block. | integer
MONITOR_VALIDATOR_HOME_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_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 | `true` / `false`
MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST | File with a list of addresses, separated by newlines. If set, determines the privileged set of accounts whose requests should be automatically processed by the CollectedSignatures watcher. | string
MONITOR_HOME_TO_FOREIGN_BLOCK_LIST | File with a list of addresses, separated by newlines. If set, determines the set of accounts whose requests should be marked as unclaimed. Has a lower priority than the `MONITOR_HOME_TO_FOREIGN_ALLOWANCE_LIST`. | string
MONITOR_HOME_TO_FOREIGN_CHECK_SENDER | If set to `true`, instructs the oracle to do an extra check for transaction origin in the block/allowance list. `false` by default. | `true` / `false`
MONITOR_HOME_VALIDATORS_BALANCE_ENABLE | If set, defines the list of home validator addresses for which balance should be checked. | `string`
MONITOR_FOREIGN_VALIDATORS_BALANCE_ENABLE | If set, defines the list of foreign validator addresses for which balance should be checked. | `string`

View File

@ -1,33 +1,20 @@
FROM node:12 as contracts FROM node:10
WORKDIR /mono
COPY contracts/package.json contracts/package-lock.json ./contracts/
WORKDIR /mono/contracts
RUN npm install --only=prod
COPY ./contracts/truffle-config.js ./
COPY ./contracts/contracts ./contracts
RUN npm run compile
FROM node:12
WORKDIR /mono WORKDIR /mono
COPY package.json . COPY package.json .
COPY --from=contracts /mono/contracts/build ./contracts/build COPY oracle/package.json ./oracle/
COPY commons/package.json ./commons/
COPY oracle-e2e/package.json ./oracle-e2e/ COPY oracle-e2e/package.json ./oracle-e2e/
COPY monitor-e2e/package.json ./monitor-e2e/ COPY ui/package.json ./ui/
COPY oracle/src/utils/constants.js ./oracle/src/utils/constants.js COPY ui/lib/web3-eth/index.js ./ui/lib/web3-eth/index.js
COPY ui-e2e/package.json ./ui-e2e/
COPY monitor/package.json ./monitor/
COPY contracts/package.json ./contracts/
COPY ui/lib/web3-eth/index.js ./ui/lib/web3-eth/index.js
COPY yarn.lock . COPY yarn.lock .
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production RUN yarn install
COPY ./contracts ./contracts
COPY ./contracts/deploy ./contracts/deploy
RUN yarn install:deploy RUN yarn install:deploy
RUN yarn compile:contracts
COPY commons/ ./commons/ COPY . .
COPY oracle-e2e/ ./oracle-e2e/
COPY monitor-e2e/ ./monitor-e2e/
COPY e2e-commons/ ./e2e-commons/

View File

@ -1,17 +1,17 @@
![tokenbridge](https://github.com/poanetwork/tokenbridge/workflows/tokenbridge/badge.svg?branch=master) [![CircleCI](https://circleci.com/gh/poanetwork/tokenbridge.svg?style=svg)](https://circleci.com/gh/poanetwork/tokenbridge)
[![Gitter](https://badges.gitter.im/poanetwork/poa-bridge.svg)](https://gitter.im/poanetwork/poa-bridge?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Gitter](https://badges.gitter.im/poanetwork/poa-bridge.svg)](https://gitter.im/poanetwork/poa-bridge?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![License: LGPL v3.0](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0) [![License: LGPL v3.0](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
# Tokenbridge # Tokenbridge
Welcome to the **POA TokenBridge** monorepository! Welcome to the **POA Token Bridge** monorepository!
Please note that this repository as a **work in progress**. Please note that this repository as a **work in progress**.
## Overview ## Overview
The POA TokenBridge allows users to transfer assets between two chains in the Ethereum ecosystem. It is composed of several elements which are contained within this monorepository. The POA Token Bridge allows users to transfer assets between two chains in the Ethereum ecosystem. It is composed of several elements which are contained within this monorepository.
For a complete picture of the POA TokenBridge functionality, it is useful to explore each subrepository. For a complete picture of the POA Token Bridge functionality, it is useful to explore each subrepository.
## Structure ## Structure
@ -19,28 +19,14 @@ Sub-repositories maintained within this monorepo are listed below.
| Sub-repository | Description | | Sub-repository | Description |
| --- | --- | | --- | --- |
| [Oracle](oracle/README.md) | Responsible for listening to bridge related events and authorizing asset transfers. | | [Oracle](oracle/README.md) | Oracle responsible for listening to bridge related events and authorizing asset transfers. |
| [UI](ui/README.md) | DApp interface to transfer tokens and coins between chains. |
| [Monitor](monitor/README.md) | Tool for checking balances and unprocessed events in bridged networks. | | [Monitor](monitor/README.md) | Tool for checking balances and unprocessed events in bridged networks. |
| [Deployment](deployment/README.md) | Ansible playbooks for deploying cross-chain bridges. | | [Deployment](deployment/README.md) | Ansible playbooks for deploying cross-chain bridges. |
| [Oracle-E2E](oracle-e2e/README.md) | End to end tests for the Oracle | | [Oracle-E2E](oracle-e2e/README.md) | End to end tests for the Oracle |
| [Monitor-E2E](monitor-e2e/README.md) | End to end tests for the Monitor | | [UI-E2E](ui-e2e/README.md) | End to end tests for the UI |
| [Deployment-E2E](deployment-e2e/README.md) | End to end tests for the Deployment |
| [Commons](commons/README.md) | Interfaces, constants and utilities shared between the sub-repositories |
| [E2E-Commons](e2e-commons/README.md) | Common utilities and configuration used in end to end tests |
| [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. Additionally there are [Smart Contracts](https://github.com/poanetwork/poa-bridge-contracts) used to manage bridge validators, collect signatures, and confirm asset relay and disposal.
## Available deployments
| **Launched by POA** | **Launched by 3rd parties** |
| ---------- | ---------- |
| [POA20 Bridge](https://bridge.poa.net/) | [Ocean TokenBridge](https://bridge.oceanprotocol.com/) |
| [xDai Bridge](https://dai-bridge.poa.network/) | [Thunder bridge](https://ui.stormdapps.com/) |
| [WETC Bridge](https://wetc.app/) | [Volta TokenBridge](https://vt.volta.bridge.eth.events/) & [DAI bridge to Volta Chain](https://dai.volta.bridge.eth.events/) |
| | [Artis Brige](https://bridge.artis.network/) |
| | [Tenda bridge](https://bridge-mainnet.tenda.network) & [xDai-to-Tenda bridge](https://bridge-xdai.tenda.network/) |
## Network Definitions ## Network Definitions
@ -52,24 +38,35 @@ Additionally there are [Smart Contracts](https://github.com/poanetwork/tokenbrid
## Operational Modes ## Operational Modes
The POA TokenBridge provides four operational modes: The POA TokenBridge provides three operational modes:
- [x] `Native-to-ERC20` **Coins** on a Home network can be converted to ERC20-compatible **tokens** on a Foreign network. Coins are locked on the Home side and the corresponding amount of ERC20 tokens are minted on the Foreign side. When the operation is reversed, tokens are burnt on the Foreign side and unlocked in the Home network. **More Information: [POA-to-POA20 Bridge](https://medium.com/poa-network/introducing-poa-bridge-and-poa20-55d8b78058ac)**
- [x] `ERC20-to-ERC20` ERC20-compatible tokens on the Foreign network are locked and minted as ERC20-compatible tokens (ERC677 tokens) on the Home network. When transferred from Home to Foreign, they are burnt on the Home side and unlocked in the Foreign network. This can be considered a form of atomic swap when a user swaps the token "X" in network "A" to the token "Y" in network "B". **More Information: [ERC20-to-ERC20](https://medium.com/poa-network/introducing-the-erc20-to-erc20-tokenbridge-ce266cc1a2d0)**
- [x] `ERC20-to-Native`: Pre-existing **tokens** in the Foreign network are locked and **coins** are minted in the `Home` network. In this mode, the Home network consensus engine invokes [Parity's Block Reward contract](https://wiki.parity.io/Block-Reward-Contract.html) to mint coins per the bridge contract request. **More Information: [xDai Chain](https://medium.com/poa-network/poa-network-partners-with-makerdao-on-xdai-chain-the-first-ever-usd-stable-blockchain-65a078c41e6a)** - [x] `ERC20-to-Native`: Pre-existing **tokens** in the Foreign network are locked and **coins** are minted in the `Home` network. In this mode, the Home network consensus engine invokes [Parity's Block Reward contract](https://wiki.parity.io/Block-Reward-Contract.html) to mint coins per the bridge contract request. **More Information: [xDai Chain](https://medium.com/poa-network/poa-network-partners-with-makerdao-on-xdai-chain-the-first-ever-usd-stable-blockchain-65a078c41e6a)**
- [x] `Arbitrary-Message`: Transfer arbitrary data between two networks as so the data could be interpreted as an arbitrary contract method invocation.
## Initializing the monorepository ## Initializing the monorepository
Clone the repository: Clone the repository with submodules:
```bash ```bash
git clone https://github.com/poanetwork/tokenbridge git clone --recursive https://github.com/poanetwork/tokenbridge
# or initialize submodules if already cloned without --recursive option:
git submodule update --init
``` ```
If there is no need to build docker images for the TokenBridge components (oracle, monitor), initialize submodules, install dependencies, compile the Smart Contracts: Install dependencies:
``` ```
yarn initialize yarn install && yarn install:deploy
``` ```
Then refer to the corresponding README files to get information about particular TokenBridge component. _**Note**: The installation should be performed with an unprivileged Linux account or with the following flag: `yarn install --unsafe-perm`. [More information](https://docs.npmjs.com/misc/scripts#user)_
Compile the Smart Contracts
```
yarn compile:contracts
```
## Linting ## Linting
@ -79,6 +76,14 @@ Running linter for all JS projects:
yarn lint yarn lint
``` ```
Running linter for all Ansible playbooks:
- [ansible-lint](https://github.com/ansible/ansible-lint) is required
```
yarn ansible-lint
```
## Tests ## Tests
Running tests for all projects: Running tests for all projects:
@ -87,15 +92,10 @@ Running tests for all projects:
yarn test yarn test
``` ```
Additionally there are end-to-end tests for [Oracle](oracle-e2e/README.md) and [Monitor](monitor-e2e/README.md). Additionaly there are end-to-end tests for [Oracle](oracle-e2e/README.md) and [UI](ui-e2e/README.md).
For details on building, running and developing please refer to respective READMEs in sub-repositories. For details on building, running and developing please refer to respective READMEs in sub-repositories.
## Building, running and deploying
Please refer to the instructions in sub-directories.
Configuration details are available [here](./CONFIGURATION.md).
## Contributing ## Contributing
See the [CONTRIBUTING](CONTRIBUTING.md) document for contribution, testing and pull request protocol. See the [CONTRIBUTING](CONTRIBUTING.md) document for contribution, testing and pull request protocol.
@ -106,4 +106,5 @@ This project is licensed under the GNU Lesser General Public License v3.0. See t
## References ## References
* [TokenBridge Documentation](https://docs.tokenbridge.net/) * [Additional Documentation](https://forum.poa.network/c/tokenbridge)
* [POA20 Bridge FAQ](https://forum.poa.network/c/tokenbridge/poa20-bridge)

View File

@ -1,15 +0,0 @@
{
"extends": [
"plugin:node/recommended",
"airbnb-base",
"../.eslintrc"
],
"plugins": ["node", "jest"],
"env": {
"jest/globals": true
},
"globals": {
"page": true,
"browser": true
}
}

View File

@ -1,24 +0,0 @@
{
"name": "alm-e2e",
"version": "0.0.1",
"private": true,
"scripts": {
"test": "jest --detectOpenHandles",
"lint": "eslint . --ignore-path ../.eslintignore",
"lint:fix": "eslint . --fix"
},
"dependencies": {
"jest": "24.7.1",
"jest-puppeteer": "^4.4.0",
"puppeteer": "^5.2.1"
},
"jest": {
"preset": "jest-puppeteer"
},
"devDependencies": {
"eslint-plugin-jest": "^23.18.0"
},
"engines": {
"node": ">= 12.22"
}
}

View File

@ -1,13 +0,0 @@
#!/usr/bin/env bash
cd $(dirname $0)
../e2e-commons/up.sh deploy generate-amb-tx blocks alm alm-e2e
# run oracle amb e2e tests to generate transactions for alm
docker-compose -f ../e2e-commons/docker-compose.yml run e2e yarn workspace oracle-e2e run alm
yarn test
rc=$?
../e2e-commons/down.sh
exit $rc

View File

@ -1,50 +0,0 @@
const puppeteer = require('puppeteer')
const { waitUntil } = require('./utils/utils')
jest.setTimeout(60000)
const statusText = 'Success'
const statusSelector = 'label[data-id="status"]'
const homeToForeignTxURL = 'http://localhost:3004/77/0x295efbe6ae98937ef35d939376c9bd752b4dc6f6899a9d5ddd6a57cea3d76c89'
const foreignToHomeTxURL = 'http://localhost:3004/42/0x7262f7dbe6c30599edded2137fbbe93c271b37f5c54dd27f713f0cf510e3b4dd'
describe('ALM', () => {
let browser
let page
beforeAll(async () => {
browser = await puppeteer.launch()
page = await browser.newPage()
})
afterAll(async () => {
await browser.close()
})
it('should be titled "AMB Live Monitoring"', async () => {
await page.goto(foreignToHomeTxURL)
await expect(page.title()).resolves.toMatch('AMB Live Monitoring')
})
it('should display information of foreign to home transaction', async () => {
await page.goto(foreignToHomeTxURL)
await page.waitForSelector(statusSelector)
await waitUntil(async () => {
const element = await page.$(statusSelector)
const text = await page.evaluate(element => element.textContent, element)
return text === statusText
})
})
it('should display information of home to foreign transaction', async () => {
await page.goto(homeToForeignTxURL)
await page.waitForSelector(statusSelector)
await waitUntil(async () => {
const element = await page.$(statusSelector)
const text = await page.evaluate(element => element.textContent, element)
return text === statusText
})
})
})

View File

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

View File

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

View File

@ -1,6 +0,0 @@
module.exports = {
extends: [
"react-app",
"../.eslintrc"
]
}

25
alm/.gitignore vendored
View File

@ -1,25 +0,0 @@
# 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*

View File

@ -1,38 +0,0 @@
FROM node:12 as contracts
WORKDIR /mono
COPY contracts/package.json contracts/package-lock.json ./contracts/
WORKDIR /mono/contracts
RUN npm install --only=prod
COPY ./contracts/truffle-config.js ./
COPY ./contracts/contracts ./contracts
RUN npm run compile
FROM node:12 as alm-builder
WORKDIR /mono
COPY package.json .
COPY --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
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 .

View File

@ -1,46 +0,0 @@
# 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 cant go back!**
If you arent 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 youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt 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/).

View File

@ -1,9 +0,0 @@
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())

View File

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

View File

@ -1,16 +0,0 @@
#!/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'
$*

View File

@ -1,64 +0,0 @@
{
"name": "alm",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ethersproject/bignumber": ">=5.0.0-beta.130",
"@react-hook/window-size": "^3.0.6",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/promise-retry": "^1.1.3",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.0",
"@use-it/interval": "^0.1.3",
"@web3-react/core": "^6.1.1",
"@web3-react/injected-connector": "^6.0.7",
"customize-cra": "^1.0.0",
"date-fns": "^2.14.0",
"dotenv": "^8.2.0",
"fast-memoize": "^2.5.2",
"promise-retry": "^2.0.1",
"react": "^16.13.1",
"react-app-rewired": "^2.1.6",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.0.1",
"styled-components": "^5.1.1",
"typescript": "^3.5.2",
"web3": "1.2.11",
"web3-eth-contract": "1.2.11",
"web3-utils": "1.2.11"
},
"scripts": {
"start": "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",
"node-fetch": "^2.6.1"
}
}

View File

@ -1 +0,0 @@
/* /index.html 200

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

View File

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

View File

@ -1,15 +0,0 @@
{
"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"
}

View File

@ -1,3 +0,0 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

View File

@ -1,155 +0,0 @@
const { BRIDGE_VALIDATORS_ABI, HOME_AMB_ABI } = require('commons')
const path = require('path')
require('dotenv').config()
const Web3 = require('web3')
const fetch = require('node-fetch')
const { URL } = require('url')
const fs = require('fs')
const {
COMMON_HOME_RPC_URL,
COMMON_HOME_BRIDGE_ADDRESS,
COMMON_FOREIGN_RPC_URL,
COMMON_FOREIGN_BRIDGE_ADDRESS,
ALM_FOREIGN_EXPLORER_API,
ALM_HOME_EXPLORER_API
} = process.env
const generateSnapshot = async (side, url, bridgeAddress) => {
const snapshotPath = `../src/snapshots/${side}.json`
const snapshotFullPath = path.join(__dirname, snapshotPath)
const snapshot = {}
const web3 = new Web3(new Web3.providers.HttpProvider(url))
const api = side === 'home' ? ALM_HOME_EXPLORER_API : ALM_FOREIGN_EXPLORER_API
const getPastEventsWithFallback = (contract, eventName, options) =>
contract.getPastEvents(eventName, options).catch(async e => {
if (e.message.includes('exceed maximum block range')) {
const abi = contract.options.jsonInterface.find(abi => abi.type === 'event' && abi.name === eventName)
const url = new URL(api)
url.searchParams.append('module', 'logs')
url.searchParams.append('action', 'getLogs')
url.searchParams.append('address', contract.options.address)
url.searchParams.append('fromBlock', options.fromBlock)
url.searchParams.append('toBlock', options.toBlock || 'latest')
url.searchParams.append('topic0', web3.eth.abi.encodeEventSignature(abi))
const logs = await fetch(url).then(res => res.json())
return logs.result.map(log => ({
transactionHash: log.transactionHash,
blockNumber: parseInt(log.blockNumber.slice(2), 16),
returnValues: web3.eth.abi.decodeLog(abi.inputs, log.data, log.topics.slice(1))
}))
}
throw e
})
const currentBlockNumber = await web3.eth.getBlockNumber()
snapshot.snapshotBlockNumber = currentBlockNumber
// Save chainId
snapshot.chainId = await web3.eth.getChainId()
const bridgeContract = new web3.eth.Contract(HOME_AMB_ABI, bridgeAddress)
// Save RequiredBlockConfirmationChanged events
let requiredBlockConfirmationChangedEvents = await getPastEventsWithFallback(
bridgeContract,
'RequiredBlockConfirmationChanged',
{
fromBlock: 0,
toBlock: currentBlockNumber
}
)
// In case RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB
// manually generate an event for this. Example Sokol - Kovan bridge
if (requiredBlockConfirmationChangedEvents.length === 0) {
const deployedAtBlock = await bridgeContract.methods.deployedAtBlock().call()
const blockConfirmations = await bridgeContract.methods.requiredBlockConfirmations().call()
requiredBlockConfirmationChangedEvents.push({
blockNumber: parseInt(deployedAtBlock),
returnValues: {
requiredBlockConfirmations: blockConfirmations
}
})
}
snapshot.RequiredBlockConfirmationChanged = requiredBlockConfirmationChangedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
requiredBlockConfirmations: e.returnValues.requiredBlockConfirmations
}
}))
const validatorAddress = await bridgeContract.methods.validatorContract().call()
const validatorContract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorAddress)
// Save RequiredSignaturesChanged events
const RequiredSignaturesChangedEvents = await getPastEventsWithFallback(
validatorContract,
'RequiredSignaturesChanged',
{
fromBlock: 0,
toBlock: currentBlockNumber
}
)
snapshot.RequiredSignaturesChanged = RequiredSignaturesChangedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
requiredSignatures: e.returnValues.requiredSignatures
}
}))
// Save ValidatorAdded events
const validatorAddedEvents = await getPastEventsWithFallback(validatorContract, 'ValidatorAdded', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.ValidatorAdded = validatorAddedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
validator: e.returnValues.validator
},
event: 'ValidatorAdded'
}))
// Save ValidatorRemoved events
const validatorRemovedEvents = await getPastEventsWithFallback(validatorContract, 'ValidatorRemoved', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.ValidatorRemoved = validatorRemovedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
validator: e.returnValues.validator
},
event: 'ValidatorRemoved'
}))
// Write snapshot
fs.writeFileSync(snapshotFullPath, JSON.stringify(snapshot, null, 2))
}
const main = async () => {
await Promise.all([
generateSnapshot('home', COMMON_HOME_RPC_URL, COMMON_HOME_BRIDGE_ADDRESS),
generateSnapshot('foreign', COMMON_FOREIGN_RPC_URL, COMMON_FOREIGN_BRIDGE_ADDRESS)
])
}
main()
.then(() => process.exit(0))
.catch(error => {
console.log('Error while creating snapshots')
console.error(error)
process.exit(0)
})

View File

@ -1,20 +0,0 @@
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import { Web3ReactProvider } from '@web3-react/core'
import Web3 from 'web3'
import { MainPage } from './components/MainPage'
import { StateProvider } from './state/StateProvider'
function App() {
return (
<BrowserRouter>
<Web3ReactProvider getLibrary={provider => new Web3(provider)}>
<StateProvider>
<MainPage />
</StateProvider>
</Web3ReactProvider>
</BrowserRouter>
)
}
export default App

View File

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

View File

@ -1,607 +0,0 @@
import { AbiItem } from 'web3-utils'
const abi: AbiItem[] = [
{
constant: true,
inputs: [],
name: 'transactionHash',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_txHash',
type: 'bytes32'
}
],
name: 'relayedMessages',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_sourceChainId',
type: 'uint256'
},
{
name: '_destinationChainId',
type: 'uint256'
},
{
name: '_validatorContract',
type: 'address'
},
{
name: '_maxGasPerTx',
type: 'uint256'
},
{
name: '_gasPrice',
type: 'uint256'
},
{
name: '_requiredBlockConfirmations',
type: 'uint256'
},
{
name: '_owner',
type: 'address'
}
],
name: 'initialize',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'isInitialized',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'requiredBlockConfirmations',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_data',
type: 'bytes'
},
{
name: '_signatures',
type: 'bytes'
}
],
name: 'executeSignatures',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_data',
type: 'bytes'
},
{
name: '_signatures',
type: 'bytes'
}
],
name: 'safeExecuteSignaturesWithAutoGasLimit',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_data',
type: 'bytes'
}
],
name: 'getMinimumGasUsage',
outputs: [
{
name: 'gas',
type: 'uint256'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'failedMessageReceiver',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getBridgeMode',
outputs: [
{
name: '_data',
type: 'bytes4'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_sourceChainId',
type: 'uint256'
},
{
name: '_destinationChainId',
type: 'uint256'
}
],
name: 'setChainIds',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'failedMessageSender',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'messageId',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_token',
type: 'address'
},
{
name: '_to',
type: 'address'
}
],
name: 'claimTokens',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_maxGasPerTx',
type: 'uint256'
}
],
name: 'setMaxGasPerTx',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'requiredSignatures',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'owner',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'validatorContract',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'deployedAtBlock',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getBridgeInterfacesVersion',
outputs: [
{
name: 'major',
type: 'uint64'
},
{
name: 'minor',
type: 'uint64'
},
{
name: 'patch',
type: 'uint64'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'messageSourceChainId',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_blockConfirmations',
type: 'uint256'
}
],
name: 'setRequiredBlockConfirmations',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_gasPrice',
type: 'uint256'
}
],
name: 'setGasPrice',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'messageCallStatus',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'messageSender',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_contract',
type: 'address'
},
{
name: '_data',
type: 'bytes'
},
{
name: '_gas',
type: 'uint256'
}
],
name: 'requireToPassMessage',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'failedMessageDataHash',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'maxGasPerTx',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'newOwner',
type: 'address'
}
],
name: 'transferOwnership',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'gasPrice',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'messageId',
type: 'bytes32'
},
{
indexed: false,
name: 'encodedData',
type: 'bytes'
}
],
name: 'UserRequestForAffirmation',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'sender',
type: 'address'
},
{
indexed: true,
name: 'executor',
type: 'address'
},
{
indexed: true,
name: 'messageId',
type: 'bytes32'
},
{
indexed: false,
name: 'status',
type: 'bool'
}
],
name: 'RelayedMessage',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'gasPrice',
type: 'uint256'
}
],
name: 'GasPriceChanged',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'requiredBlockConfirmations',
type: 'uint256'
}
],
name: 'RequiredBlockConfirmationChanged',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'previousOwner',
type: 'address'
},
{
indexed: false,
name: 'newOwner',
type: 'address'
}
],
name: 'OwnershipTransferred',
type: 'event'
}
]
export default abi

View File

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

View File

@ -1,3 +0,0 @@
export { default as HOME_AMB_ABI } from './HomeAMB'
export { default as FOREIGN_AMB_ABI } from './ForeignAMB'
export { default as BRIDGE_VALIDATORS_ABI } from './BridgeValidators'

View File

@ -1,150 +0,0 @@
import React, { useEffect, useState } from 'react'
import { TransactionReceipt } from 'web3-eth'
import { useMessageConfirmations } from '../hooks/useMessageConfirmations'
import { MessageObject } from '../utils/web3'
import styled from 'styled-components'
import { CONFIRMATIONS_STATUS, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { CONFIRMATIONS_STATUS_LABEL, CONFIRMATIONS_STATUS_LABEL_HOME } from '../config/descriptions'
import { SimpleLoading } from './commons/Loading'
import { ValidatorsConfirmations } from './ValidatorsConfirmations'
import { getConfirmationsStatusDescription } from '../utils/networks'
import { useStateProvider } from '../state/StateProvider'
import { ExecutionConfirmation } from './ExecutionConfirmation'
import { useValidatorContract } from '../hooks/useValidatorContract'
import { useBlockConfirmations } from '../hooks/useBlockConfirmations'
import { MultiLine } from './commons/MultiLine'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
const StatusLabel = styled.label`
font-weight: bold;
font-size: 18px;
`
const StatusResultLabel = styled.label`
font-size: 18px;
padding-left: 10px;
`
const StyledConfirmationContainer = styled.div`
background-color: var(--bg-color);
padding: 10px;
border-radius: 4px;
`
const StatusDescription = styled.div`
padding-top: 10px;
`
export interface ConfirmationsContainerParams {
message: MessageObject
receipt: Maybe<TransactionReceipt>
fromHome: boolean
homeStartBlock: Maybe<number>
foreignStartBlock: Maybe<number>
}
export const ConfirmationsContainer = ({
message,
receipt,
fromHome,
homeStartBlock,
foreignStartBlock
}: ConfirmationsContainerParams) => {
const {
home: { name: homeName },
foreign: { name: foreignName }
} = useStateProvider()
const src = useValidatorContract(fromHome, receipt ? receipt.blockNumber : 0)
const [executionBlockNumber, setExecutionBlockNumber] = useState(0)
const dst = useValidatorContract(!fromHome, executionBlockNumber || 'latest')
const { blockConfirmations } = useBlockConfirmations({ fromHome, receipt })
const {
confirmations,
status,
executionData,
signatureCollected,
waitingBlocksResolved,
setExecutionData,
executionEventsFetched,
setPendingExecution
} = useMessageConfirmations({
message,
receipt,
fromHome,
homeStartBlock,
foreignStartBlock,
requiredSignatures: src.requiredSignatures,
validatorList: src.validatorList,
targetValidatorList: dst.validatorList,
blockConfirmations
})
useEffect(
() => {
if (executionBlockNumber || executionData.status !== VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS) return
setExecutionBlockNumber(executionData.blockNumber)
},
[executionData.status, executionBlockNumber, executionData.blockNumber]
)
const statusLabel = fromHome ? CONFIRMATIONS_STATUS_LABEL_HOME : CONFIRMATIONS_STATUS_LABEL
const parseDescription = () => {
let description = getConfirmationsStatusDescription(status, homeName, foreignName, fromHome)
let link
const descArray = description.split('%link')
if (descArray.length > 1) {
description = descArray[0]
link = (
<ExplorerTxLink href={descArray[1]} target="_blank" rel="noopener noreferrer">
{descArray[1]}
</ExplorerTxLink>
)
}
return (
<div>
{description}
{link}
</div>
)
}
return (
<div className="row is-center">
<StyledConfirmationContainer className="col-9">
<div className="row is-center">
<StatusLabel>Status:</StatusLabel>
<StatusResultLabel data-id="status">
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? statusLabel[status] : <SimpleLoading />}
</StatusResultLabel>
</div>
<StatusDescription className="row is-center">
<MultiLine className="col-10">
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? parseDescription() : ''}
</MultiLine>
</StatusDescription>
<ValidatorsConfirmations
confirmations={fromHome ? confirmations.filter(c => dst.validatorList.includes(c.validator)) : confirmations}
requiredSignatures={dst.requiredSignatures}
validatorList={dst.validatorList}
waitingBlocksResolved={waitingBlocksResolved}
/>
{signatureCollected && (
<ExecutionConfirmation
message={message}
executionData={executionData}
isHome={!fromHome}
confirmations={confirmations}
setExecutionData={setExecutionData}
executionEventsFetched={executionEventsFetched}
setPendingExecution={setPendingExecution}
dstRequiredSignatures={dst.requiredSignatures}
dstValidatorList={dst.validatorList}
/>
)}
</StyledConfirmationContainer>
</div>
)
}

View File

@ -1,172 +0,0 @@
import React, { useEffect, useState } from 'react'
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
import { useWindowWidth } from '@react-hook/window-size'
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS, ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION } from '../config/constants'
import { SimpleLoading } from './commons/Loading'
import styled from 'styled-components'
import { ConfirmationParam, ExecutionData } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { Thead, AgeTd, StatusTd } from './commons/Table'
import { ManualExecutionButton } from './ManualExecutionButton'
import { useStateProvider } from '../state/StateProvider'
import { matchesRule, MessageObject, WarnRule } from '../utils/web3'
import { WarningAlert } from './commons/WarningAlert'
import { ErrorAlert } from './commons/ErrorAlert'
const StyledExecutionConfirmation = styled.div`
margin-top: 30px;
`
export interface ExecutionConfirmationParams {
message: MessageObject
executionData: ExecutionData
setExecutionData: Function
confirmations: ConfirmationParam[]
isHome: boolean
executionEventsFetched: boolean
setPendingExecution: Function
dstRequiredSignatures: number
dstValidatorList: string[]
}
export const ExecutionConfirmation = ({
message,
executionData,
setExecutionData,
confirmations,
isHome,
executionEventsFetched,
setPendingExecution,
dstRequiredSignatures,
dstValidatorList
}: ExecutionConfirmationParams) => {
const { foreign } = useStateProvider()
const [safeExecutionAvailable, setSafeExecutionAvailable] = useState(false)
const [error, setError] = useState('')
const [warning, setWarning] = useState('')
const availableManualExecution =
!isHome &&
(executionData.status === VALIDATOR_CONFIRMATION_STATUS.WAITING ||
executionData.status === VALIDATOR_CONFIRMATION_STATUS.FAILED ||
(executionData.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED &&
executionEventsFetched &&
!!executionData.validator))
const requiredManualExecution = availableManualExecution && ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION
const showAgeColumn = !requiredManualExecution || executionData.status === VALIDATOR_CONFIRMATION_STATUS.FAILED
const windowWidth = useWindowWidth()
const txExplorerLink = getExplorerTxUrl(executionData.txHash, isHome)
const formattedValidator =
windowWidth < 850 && executionData.validator ? formatTxHash(executionData.validator) : executionData.validator
useEffect(
() => {
if (!availableManualExecution || !foreign.bridgeContract) return
const p = foreign.bridgeContract.methods.getBridgeInterfacesVersion().call()
p.then(({ major, minor }: any) => {
major = parseInt(major, 10)
minor = parseInt(minor, 10)
if (major < 5 || (major === 5 && minor < 7)) return
setSafeExecutionAvailable(true)
})
},
[availableManualExecution, foreign.bridgeContract]
)
useEffect(
() => {
if (!message.data || !executionData || !availableManualExecution) return
try {
const fileName = 'warnRules'
const rules: WarnRule[] = require(`../snapshots/${fileName}.json`)
for (let rule of rules) {
if (matchesRule(rule, message)) {
setWarning(rule.message)
return
}
}
} catch (e) {}
},
[availableManualExecution, executionData, message, message.data, setWarning]
)
const getExecutionStatusElement = (validatorStatus = '') => {
switch (validatorStatus) {
case VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS:
return <SuccessLabel>{validatorStatus}</SuccessLabel>
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
return <RedLabel>{validatorStatus}</RedLabel>
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
return <GreyLabel>{validatorStatus}</GreyLabel>
default:
return executionData.validator ? (
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
) : (
<SimpleLoading />
)
}
}
return (
<StyledExecutionConfirmation>
{error && <ErrorAlert onClick={() => setError('')} error={error} />}
{warning && <WarningAlert onClick={() => setWarning('')} error={warning} />}
<table>
<Thead>
<tr>
<th>{requiredManualExecution ? 'Execution info' : 'Executed by'}</th>
<th className="text-center">Status</th>
{showAgeColumn && <th className="text-center">Age</th>}
{availableManualExecution && <th className="text-center">Actions</th>}
</tr>
</Thead>
<tbody>
<tr>
<td>
{requiredManualExecution ? (
'Manual user action is required to complete the operation'
) : formattedValidator ? (
formattedValidator
) : (
<SimpleLoading />
)}
</td>
<StatusTd className="text-center">{getExecutionStatusElement(executionData.status)}</StatusTd>
{showAgeColumn && (
<AgeTd className="text-center">
{executionData.timestamp > 0 ? (
<ExplorerTxLink href={txExplorerLink} target="_blank">
{formatTimestamp(executionData.timestamp)}
</ExplorerTxLink>
) : executionData.status === VALIDATOR_CONFIRMATION_STATUS.WAITING ? (
''
) : (
SEARCHING_TX
)}
</AgeTd>
)}
{availableManualExecution && (
<td>
<ManualExecutionButton
safeExecutionAvailable={safeExecutionAvailable}
messageData={message.data}
setExecutionData={setExecutionData}
confirmations={confirmations}
setPendingExecution={setPendingExecution}
setError={setError}
requiredSignatures={dstRequiredSignatures}
validatorList={dstValidatorList}
/>
</td>
)}
</tr>
</tbody>
</table>
</StyledExecutionConfirmation>
)
}

View File

@ -1,69 +0,0 @@
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>
)
}

View File

@ -1,148 +0,0 @@
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&nbsp;
<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>
)
}

View File

@ -1,231 +0,0 @@
import React, { useState, useEffect } from 'react'
import styled from 'styled-components'
import { InjectedConnector } from '@web3-react/injected-connector'
import { useWeb3React } from '@web3-react/core'
import {
DOUBLE_EXECUTION_ATTEMPT_ERROR,
EXECUTION_FAILED_ERROR,
EXECUTION_OUT_OF_GAS_ERROR,
FOREIGN_EXPLORER_API,
INCORRECT_CHAIN_ERROR,
VALIDATOR_CONFIRMATION_STATUS
} from '../config/constants'
import { useStateProvider } from '../state/StateProvider'
import { signatureToVRS, packSignatures } from '../utils/signatures'
import { getSuccessExecutionData } from '../utils/getFinalizationEvent'
import { TransactionReceipt } from 'web3-eth'
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
const ActionButton = styled.button`
color: var(--button-color);
border-color: var(--font-color);
margin-top: 10px;
min-width: 120px;
padding: 1rem;
&:focus {
outline: var(--button-color);
}
`
interface ManualExecutionButtonParams {
safeExecutionAvailable: boolean
messageData: string
setExecutionData: Function
confirmations: ConfirmationParam[]
setPendingExecution: Function
setError: Function
requiredSignatures: number
validatorList: string[]
}
export const ManualExecutionButton = ({
safeExecutionAvailable,
messageData,
setExecutionData,
confirmations,
setPendingExecution,
setError,
requiredSignatures,
validatorList
}: ManualExecutionButtonParams) => {
const { foreign } = useStateProvider()
const { library, activate, account, active } = useWeb3React()
const [manualExecution, setManualExecution] = useState(false)
const [allowFailures, setAllowFailures] = useState(false)
const [ready, setReady] = useState(false)
const [title, setTitle] = useState('Loading')
const [validSignatures, setValidSignatures] = useState<string[]>([])
useEffect(
() => {
if (
!foreign.bridgeContract ||
!foreign.web3 ||
!confirmations ||
!confirmations.length ||
!requiredSignatures ||
!validatorList ||
!validatorList.length
)
return
const signatures = []
for (let i = 0; i < confirmations.length && signatures.length < requiredSignatures; i++) {
const sig = confirmations[i].signature
if (!sig) {
continue
}
const { v, r, s } = signatureToVRS(sig)
const signer = foreign.web3.eth.accounts.recover(messageData, `0x${v}`, `0x${r}`, `0x${s}`)
if (validatorList.includes(signer)) {
signatures.push(sig)
}
}
if (signatures.length >= requiredSignatures) {
setValidSignatures(signatures.slice(0, requiredSignatures))
setTitle('Execute')
setReady(true)
} else {
setTitle('Unavailable')
}
},
[
foreign.bridgeContract,
foreign.web3,
validatorList,
requiredSignatures,
messageData,
setValidSignatures,
confirmations
]
)
useEffect(
() => {
if (!manualExecution || !foreign.chainId) return
if (!active) {
activate(new InjectedConnector({ supportedChainIds: [foreign.chainId] }), e => {
if (e.message.includes('Unsupported chain id')) {
setError(INCORRECT_CHAIN_ERROR)
const { ethereum } = window as any
// remove the error message after chain is correctly changed to the foreign one
const listener = (chainId: string) => {
if (parseInt(chainId.slice(2), 16) === foreign.chainId) {
ethereum.removeListener('chainChanged', listener)
setError((error: string) => (error === INCORRECT_CHAIN_ERROR ? '' : error))
}
}
ethereum.on('chainChanged', listener)
} else {
setError(e.message)
}
setManualExecution(false)
})
return
}
if (!library || !foreign.bridgeContract || !foreign.web3 || !validSignatures || !validSignatures.length) return
const signatures = packSignatures(validSignatures.map(signatureToVRS))
const messageId = messageData.slice(0, 66)
const bridge = foreign.bridgeContract
const executeMethod =
safeExecutionAvailable && !allowFailures
? bridge.methods.safeExecuteSignaturesWithAutoGasLimit
: bridge.methods.executeSignatures
const data = executeMethod(messageData, signatures).encodeABI()
setManualExecution(false)
library.eth
.sendTransaction({
from: account,
to: foreign.bridgeAddress,
data
})
.on('transactionHash', (txHash: string) => {
setExecutionData({
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
validator: account,
txHash,
timestamp: Math.floor(new Date().getTime() / 1000.0),
executionResult: false
})
setPendingExecution(true)
})
.on('error', async (e: Error, receipt: TransactionReceipt) => {
if (e.message.includes('Transaction has been reverted by the EVM')) {
const successExecutionData = await getSuccessExecutionData(
bridge,
'RelayedMessage',
library,
messageId,
FOREIGN_EXPLORER_API
)
if (successExecutionData) {
setExecutionData(successExecutionData)
setError(DOUBLE_EXECUTION_ATTEMPT_ERROR)
} else {
const { gas } = await library.eth.getTransaction(receipt.transactionHash)
setExecutionData({
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
validator: account,
txHash: receipt.transactionHash,
timestamp: Math.floor(new Date().getTime() / 1000.0),
executionResult: false
})
setError(gas === receipt.gasUsed ? EXECUTION_OUT_OF_GAS_ERROR : EXECUTION_FAILED_ERROR)
}
} else {
setError(e.message)
}
})
},
[
manualExecution,
library,
activate,
active,
account,
foreign.chainId,
foreign.bridgeAddress,
foreign.bridgeContract,
setError,
messageData,
setExecutionData,
setPendingExecution,
safeExecutionAvailable,
allowFailures,
foreign.web3,
validSignatures
]
)
return (
<div>
<div className="is-center">
<ActionButton disabled={!ready} className="button outline" onClick={() => setManualExecution(true)}>
{title}
</ActionButton>
</div>
{safeExecutionAvailable && (
<div
title="Allow executed message to fail and record its failure on-chain without reverting the whole transaction.
Use fixed gas limit for execution."
className="is-center"
style={{ paddingTop: 10 }}
>
<input
type="checkbox"
id="allow-failures"
checked={allowFailures}
onChange={e => setAllowFailures(e.target.checked)}
/>
<label htmlFor="allow-failures">Unsafe mode</label>
</div>
)}
</div>
)
}

View File

@ -1,47 +0,0 @@
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>
)
}

View File

@ -1,47 +0,0 @@
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>
)
}

View File

@ -1,118 +0,0 @@
import React, { useEffect } from 'react'
import { useHistory, useParams } from 'react-router-dom'
import { useTransactionStatus } from '../hooks/useTransactionStatus'
import { formatTxHash, getExplorerTxUrl, getTransactionStatusDescription, validTxHash } from '../utils/networks'
import { TRANSACTION_STATUS } from '../config/constants'
import { MessageSelector } from './MessageSelector'
import { Loading } from './commons/Loading'
import { useStateProvider } from '../state/StateProvider'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { ConfirmationsContainer } from './ConfirmationsContainer'
import { TransactionReceipt } from 'web3-eth'
import { BackButton } from './commons/BackButton'
import { useClosestBlock } from '../hooks/useClosestBlock'
export interface StatusContainerParam {
onBackToMain: () => void
setNetworkFromParams: (chainId: number) => void
receiptParam: Maybe<TransactionReceipt>
}
export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptParam }: StatusContainerParam) => {
const { home, foreign } = useStateProvider()
const history = useHistory()
const { chainId, txHash, messageIdParam } = useParams()
const validChainId = chainId === home.chainId.toString() || chainId === foreign.chainId.toString()
const validParameters = validChainId && validTxHash(txHash)
const isHome = chainId === home.chainId.toString()
const { messages, receipt, status, description, timestamp, loading } = useTransactionStatus({
txHash: validParameters ? txHash : '',
chainId: validParameters ? parseInt(chainId) : 0,
receiptParam
})
const homeStartBlock = useClosestBlock(true, isHome, receipt, timestamp)
const foreignStartBlock = useClosestBlock(false, isHome, receipt, timestamp)
const selectedMessageId = messageIdParam === undefined || messages[messageIdParam] === undefined ? -1 : messageIdParam
useEffect(
() => {
if (validChainId) {
setNetworkFromParams(parseInt(chainId))
}
},
[validChainId, chainId, setNetworkFromParams]
)
if (!validParameters && home.chainId && foreign.chainId) {
return (
<div>
<p>
Chain Id: {chainId} and/or Transaction Hash: {txHash} are not valid
</p>
</div>
)
}
if (loading) {
return <Loading />
}
const onMessageSelected = (messageId: number) => {
history.push(`/${chainId}/${txHash}/${messageId}`)
}
const displayMessageSelector = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId === -1
const multiMessageSelected = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId !== -1
const displayReference = multiMessageSelected ? messages[selectedMessageId].id : txHash
const formattedMessageId = formatTxHash(displayReference)
const txExplorerLink = getExplorerTxUrl(txHash, isHome)
const displayExplorerLink = status !== TRANSACTION_STATUS.NOT_FOUND
const displayConfirmations = status === TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE || multiMessageSelected
const messageToConfirm =
messages.length > 1 ? messages[selectedMessageId] : messages.length > 0 ? messages[0] : { id: '', data: '' }
let displayedDescription: string = multiMessageSelected
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
: description
let link
const descArray = displayedDescription.split('%link')
if (descArray.length > 1) {
displayedDescription = descArray[0]
link = (
<ExplorerTxLink href={descArray[1]} target="_blank" rel="noopener noreferrer">
{descArray[1]}
</ExplorerTxLink>
)
}
return (
<div>
{status && (
<p>
The transaction{' '}
{displayExplorerLink && (
<ExplorerTxLink href={txExplorerLink} target="_blank">
{formattedMessageId}
</ExplorerTxLink>
)}
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription} {link}
</p>
)}
{displayMessageSelector && <MessageSelector messages={messages} onMessageSelected={onMessageSelected} />}
{displayConfirmations && (
<ConfirmationsContainer
message={messageToConfirm}
receipt={receipt}
fromHome={isHome}
homeStartBlock={homeStartBlock}
foreignStartBlock={foreignStartBlock}
/>
)}
<BackButton onBackToMain={onBackToMain} />
</div>
)
}

View File

@ -1,67 +0,0 @@
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 />
}

View File

@ -1,105 +0,0 @@
import React from 'react'
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
import { useWindowWidth } from '@react-hook/window-size'
import { RECENT_AGE, SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { SimpleLoading } from './commons/Loading'
import styled from 'styled-components'
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { Thead, AgeTd, StatusTd } from './commons/Table'
const RequiredConfirmations = styled.label`
font-size: 14px;
`
export interface ValidatorsConfirmationsParams {
confirmations: Array<ConfirmationParam>
requiredSignatures: number
validatorList: string[]
waitingBlocksResolved: boolean
}
export const ValidatorsConfirmations = ({
confirmations,
requiredSignatures,
validatorList,
waitingBlocksResolved
}: ValidatorsConfirmationsParams) => {
const windowWidth = useWindowWidth()
const getValidatorStatusElement = (validatorStatus = '') => {
switch (validatorStatus) {
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
case VALIDATOR_CONFIRMATION_STATUS.MANUAL:
case VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID:
return <SuccessLabel>{VALIDATOR_CONFIRMATION_STATUS.SUCCESS}</SuccessLabel>
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
return <RedLabel>{validatorStatus}</RedLabel>
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
return <GreyLabel>{validatorStatus}</GreyLabel>
default:
return waitingBlocksResolved ? (
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
) : (
<SimpleLoading />
)
}
}
return (
<div>
<table>
<Thead>
<tr>
<th>Validator</th>
<th className="text-center">Status</th>
<th className="text-center">Age</th>
</tr>
</Thead>
<tbody>
{confirmations.map((confirmation, i) => {
const displayedStatus = confirmation.status
const explorerLink = getExplorerTxUrl(confirmation.txHash, true)
let elementIfNoTimestamp: any = <SimpleLoading />
switch (displayedStatus) {
case '':
case VALIDATOR_CONFIRMATION_STATUS.UNDEFINED:
if (waitingBlocksResolved) {
elementIfNoTimestamp = SEARCHING_TX
}
break
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
elementIfNoTimestamp = ''
break
case VALIDATOR_CONFIRMATION_STATUS.MANUAL:
elementIfNoTimestamp = RECENT_AGE
break
}
return (
<tr key={i}>
<td>{windowWidth < 850 ? formatTxHash(confirmation.validator) : confirmation.validator}</td>
<StatusTd className="text-center">{getValidatorStatusElement(displayedStatus)}</StatusTd>
<AgeTd className="text-center">
{confirmation && confirmation.timestamp > 0 ? (
<ExplorerTxLink href={explorerLink} target="_blank">
{formatTimestamp(confirmation.timestamp)}
</ExplorerTxLink>
) : (
elementIfNoTimestamp
)}
</AgeTd>
</tr>
)
})}
</tbody>
</table>
<RequiredConfirmations>
At least <strong>{requiredSignatures}</strong> of <strong>{validatorList.length}</strong> confirmations required
</RequiredConfirmations>
</div>
)
}

View File

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

View File

@ -1,10 +0,0 @@
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);
}
`

View File

@ -1,18 +0,0 @@
import React from 'react'
export const CloseIcon = ({ color }: { color?: string }) => (
<svg
aria-hidden="true"
focusable="false"
data-prefix="fa"
data-icon="times"
className="svg-inline--fa fa-times fa-w-11 fa-lg "
role="img"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 352 512"
fill={color || '#1890ff'}
height="1em"
>
<path d="M242.72 256l100.07-100.07c12.28-12.28 12.28-32.19 0-44.48l-22.24-22.24c-12.28-12.28-32.19-12.28-44.48 0L176 189.28 75.93 89.21c-12.28-12.28-32.19-12.28-44.48 0L9.21 111.45c-12.28 12.28-12.28 32.19 0 44.48L109.28 256 9.21 356.07c-12.28 12.28-12.28 32.19 0 44.48l22.24 22.24c12.28 12.28 32.2 12.28 44.48 0L176 322.72l100.07 100.07c12.28 12.28 32.2 12.28 44.48 0l22.24-22.24c12.28-12.28 12.28-32.19 0-44.48L242.72 256z" />
</svg>
)

View File

@ -1,48 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { InfoIcon } from './InfoIcon'
import { CloseIcon } from './CloseIcon'
import { ExplorerTxLink } from './ExplorerTxLink'
const StyledErrorAlert = styled.div`
border: 1px solid var(--failed-color);
border-radius: 4px;
margin-bottom: 20px;
padding-top: 10px;
`
const CloseIconContainer = styled.div`
cursor: pointer;
`
const TextContainer = styled.div`
white-space: pre-wrap;
flex-direction: column;
`
export const ErrorAlert = ({ onClick, error }: { onClick: () => void; error: string }) => {
const errorArray = error.split('%link')
const text = errorArray[0]
let link
if (errorArray.length > 1) {
link = (
<ExplorerTxLink href={errorArray[1]} target="_blank" rel="noopener noreferrer">
{errorArray[1]}
</ExplorerTxLink>
)
}
return (
<div className="row is-center">
<StyledErrorAlert className="col-12 is-vertical-align row">
<InfoIcon color="var(--failed-color)" />
<TextContainer className="col-10">
{text}
{link}
</TextContainer>
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
<CloseIcon color="var(--failed-color)" />
</CloseIconContainer>
</StyledErrorAlert>
</div>
)
}

View File

@ -1,7 +0,0 @@
import styled from 'styled-components'
export const ExplorerTxLink = styled.a`
color: var(--link-color);
text-decoration: underline;
font-weight: bold;
`

View File

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

View File

@ -1,16 +0,0 @@
import React from 'react'
export const InfoIcon = ({ color }: { color?: string }) => (
<svg
className="col-1 is-left"
viewBox="64 64 896 896"
focusable="false"
data-icon="info-circle"
width="1em"
height="1em"
fill={color || '#1890ff'}
aria-hidden="true"
>
<path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64zm32 664c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V456c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v272zm-32-344a48.01 48.01 0 010-96 48.01 48.01 0 010 96z" />
</svg>
)

View File

@ -1,22 +0,0 @@
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;
`

View File

@ -1,20 +0,0 @@
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>
)
}

View File

@ -1,165 +0,0 @@
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} />

View File

@ -1,5 +0,0 @@
import styled from 'styled-components'
export const MultiLine = styled.div`
white-space: pre-wrap;
`

View File

@ -1,9 +0,0 @@
import styled from 'styled-components'
export const RadioButtonLabel = styled.label`
padding-left: 5px;
`
export const RadioButtonContainer = styled.div`
padding: 10px;
`

View File

@ -1,13 +0,0 @@
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;
`

View File

@ -1,34 +0,0 @@
import React from 'react'
import styled from 'styled-components'
import { InfoIcon } from './InfoIcon'
import { CloseIcon } from './CloseIcon'
const StyledErrorAlert = styled.div`
border: 1px solid var(--warning-color);
border-radius: 4px;
margin-bottom: 20px;
padding-top: 10px;
`
const CloseIconContainer = styled.div`
cursor: pointer;
`
const TextContainer = styled.div`
white-space: pre-wrap;
flex-direction: column;
`
export const WarningAlert = ({ onClick, error }: { onClick: () => void; error: string }) => {
return (
<div className="row is-center">
<StyledErrorAlert className="col-12 is-vertical-align row">
<InfoIcon color="var(--warning-color)" />
<TextContainer className="col-10">{error}</TextContainer>
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
<CloseIcon color="var(--warning-color)" />
</CloseIconContainer>
</StyledErrorAlert>
</div>
)
}

View File

@ -1,81 +0,0 @@
export const HOME_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_HOME_BRIDGE_ADDRESS || ''
export const FOREIGN_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_FOREIGN_BRIDGE_ADDRESS || ''
export const HOME_RPC_URL: string = process.env.REACT_APP_COMMON_HOME_RPC_URL || ''
export const FOREIGN_RPC_URL: string = process.env.REACT_APP_COMMON_FOREIGN_RPC_URL || ''
export const HOME_NETWORK_NAME: string = process.env.REACT_APP_ALM_HOME_NETWORK_NAME || ''
export const FOREIGN_NETWORK_NAME: string = process.env.REACT_APP_ALM_FOREIGN_NETWORK_NAME || ''
export const HOME_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_HOME_EXPLORER_TX_TEMPLATE || ''
export const FOREIGN_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_TX_TEMPLATE || ''
export const HOME_EXPLORER_API: string = process.env.REACT_APP_ALM_HOME_EXPLORER_API || ''
export const FOREIGN_EXPLORER_API: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_API || ''
export const ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION: boolean =
(process.env.REACT_APP_ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION || '').toLowerCase() === 'true'
export const HOME_RPC_POLLING_INTERVAL: number = 5000
export const FOREIGN_RPC_POLLING_INTERVAL: number = 5000
export const BLOCK_RANGE: number = 500
export const MAX_TX_SEARCH_BLOCK_RANGE: number = 10000
export const EXECUTE_AFFIRMATION_HASH = 'e7a2c01f'
export const SUBMIT_SIGNATURE_HASH = '630cea8e'
export const EXECUTE_SIGNATURES_HASH = '3f7658fd'
export const CACHE_KEY_SUCCESS = 'success-confirmation-validator-'
export const CACHE_KEY_FAILED = 'failed-confirmation-validator-'
export const CACHE_KEY_EXECUTION_FAILED = 'failed-execution-validator-'
export const TRANSACTION_STATUS = {
SUCCESS_MULTIPLE_MESSAGES: 'SUCCESS_MULTIPLE_MESSAGES',
SUCCESS_ONE_MESSAGE: 'SUCCESS_ONE_MESSAGE',
SUCCESS_NO_MESSAGES: 'SUCCESS_NO_MESSAGES',
FAILED: 'FAILED',
FOUND: 'FOUND',
NOT_FOUND: 'NOT_FOUND',
UNDEFINED: 'UNDEFINED'
}
export const CONFIRMATIONS_STATUS = {
SUCCESS: 'SUCCESS',
SUCCESS_MESSAGE_FAILED: 'SUCCESS_MESSAGE_FAILED',
EXECUTION_FAILED: 'EXECUTION_FAILED',
EXECUTION_PENDING: 'EXECUTION_PENDING',
EXECUTION_WAITING: 'EXECUTION_WAITING',
FAILED: 'FAILED',
PENDING: 'PENDING',
SEARCHING: 'SEARCHING',
WAITING_VALIDATORS: 'WAITING_VALIDATORS',
WAITING_CHAIN: 'WAITING_CHAIN',
UNDEFINED: 'UNDEFINED'
}
export const VALIDATOR_CONFIRMATION_STATUS = {
SUCCESS: 'Confirmed',
MANUAL: 'Manual',
EXECUTION_SUCCESS: 'Executed',
FAILED: 'Failed',
FAILED_VALID: 'Failed valid',
PENDING: 'Pending',
WAITING: 'Waiting',
NOT_REQUIRED: 'Not required',
UNDEFINED: 'UNDEFINED'
}
export const RECENT_AGE = 'Recent'
export const SEARCHING_TX = 'Searching Transaction...'
export const INCORRECT_CHAIN_ERROR = `Incorrect chain chosen. Switch to ${FOREIGN_NETWORK_NAME} in the wallet.`
export const DOUBLE_EXECUTION_ATTEMPT_ERROR = `Your execution transaction has been reverted.
However, the execution completed successfully in the transaction sent by a different party.`
export const EXECUTION_FAILED_ERROR = `Your execution transaction has been reverted.
Please, contact the support by messaging on %linkhttps://forum.poa.network/c/support`
export const EXECUTION_OUT_OF_GAS_ERROR = `Your execution transaction has been reverted due to Out-of-Gas error.
Please, resend the transaction and provide more gas to it.`

View File

@ -1,76 +0,0 @@
// %t will be replaced by the time -> x minutes/hours/days ago
import { ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION } from './constants'
export const TRANSACTION_STATUS_DESCRIPTION: { [key: string]: string } = {
SUCCESS_MULTIPLE_MESSAGES: 'was initiated %t and contains several bridge messages. Specify one of them:',
SUCCESS_ONE_MESSAGE: 'was initiated %t',
SUCCESS_NO_MESSAGES:
'successfully mined %t but it does not seem to contain any request to the bridge, \nso nothing needs to be confirmed by the validators. \nIf you are sure that the transaction should contain a request to the bridge,\ncontact to the validators by \nmessaging on %linkhttps://forum.poa.network/c/support',
FAILED: 'failed %t',
NOT_FOUND:
'Transaction not found. \n1. Check that the transaction hash is correct. \n2. Wait several blocks for the transaction to be\nmined, gas price affects mining speed.'
}
export const CONFIRMATIONS_STATUS_LABEL: { [key: string]: string } = {
SUCCESS: 'Success',
SUCCESS_MESSAGE_FAILED: 'Success',
FAILED: 'Failed',
PENDING: 'Pending',
WAITING_VALIDATORS: 'Waiting',
SEARCHING: 'Waiting',
WAITING_CHAIN: 'Waiting'
}
export const CONFIRMATIONS_STATUS_LABEL_HOME: { [key: string]: string } = {
SUCCESS: 'Success',
SUCCESS_MESSAGE_FAILED: 'Success',
EXECUTION_FAILED: 'Execution failed',
EXECUTION_PENDING: 'Execution pending',
EXECUTION_WAITING: ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION ? 'Manual execution waiting' : 'Execution waiting',
FAILED: 'Confirmation Failed',
PENDING: 'Confirmation Pending',
WAITING_VALIDATORS: 'Confirmation Waiting',
SEARCHING: 'Confirmation Waiting',
WAITING_CHAIN: 'Confirmation Waiting'
}
// use %link to identify a link
export const CONFIRMATIONS_STATUS_DESCRIPTION: { [key: string]: string } = {
SUCCESS: '',
SUCCESS_MESSAGE_FAILED:
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
FAILED:
'The specified transaction was included in a block,\nbut confirmations sent by a majority of validators\nfailed. The cross-chain relay request will not be\nprocessed. Contact to the validators by\nmessaging on %linkhttps://forum.poa.network/c/support',
PENDING:
'The specified transaction was included in a block. A\nmajority of validators sent confirmations which have\nnot yet been added to a block.',
WAITING_VALIDATORS:
'The specified transaction was included in a block.\nSome validators have sent confirmations, others are\nwaiting for chain finalization.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
SEARCHING:
'The specified transaction was included in a block. The app is looking for confirmations. Either\n1. Validators are waiting for chain finalization before sending their signatures.\n2. Validators are not active.\n3. The bridge was stopped.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
WAITING_CHAIN:
'The specified transaction was included in a block.\nValidators are waiting for chain finalization before\nsending their confirmations.'
}
// use %link to identify a link
export const CONFIRMATIONS_STATUS_DESCRIPTION_HOME: { [key: string]: string } = {
SUCCESS: '',
SUCCESS_MESSAGE_FAILED:
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
EXECUTION_FAILED:
'The specified transaction was included in a block\nand the validators collected signatures. The\n transaction with collected signatures was\nsent but did not succeed. Contact to the validators by messaging\non %linkhttps://forum.poa.network/c/support',
EXECUTION_PENDING:
'The specified transaction was included in a block\nand the validators collected signatures. The\n transaction with collected signatures was\nsent but is not yet added to a block.',
EXECUTION_WAITING: ALM_HOME_TO_FOREIGN_MANUAL_EXECUTION
? 'The specified transaction was included in a block\nand the validators collected signatures.\nNow the manual user action is required to complete message execution.\n Please, press the "Execute" button.'
: 'The specified transaction was included in a block\nand the validators collected signatures. Either\n1. One of the validators is waiting for chain finalization.\n2. A validator skipped its duty to relay signatures.\n3. The execution transaction is still pending (e.g. due to the gas price spike).\nCheck status again after a few blocks or force execution by pressing the "Execute" button.\nIf the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
FAILED:
'The specified transaction was included in a block,\nbut transactions with signatures sent by a majority of\nvalidators failed. The cross-chain relay request will\nnot be processed. Contact to the validators by\nmessaging on %linkhttps://forum.poa.network/c/support',
PENDING:
'The specified transaction was included in a block.\nA majority of validators sent signatures which have not\nyet been added to a block.',
WAITING_VALIDATORS:
'The specified transaction was included in a block.\nSome validators have sent signatures, others are\nwaiting for chain finalization.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
SEARCHING:
'The specified transaction was included in a block. The app is looking for confirmations. Either\n1. Validators are waiting for chain finalization before sending their signatures.\n2. Validators are not active.\n3. The bridge was stopped.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
WAITING_CHAIN:
'The specified transaction was included in a block.\nValidators are waiting for chain finalization\nbefore sending their signatures.'
}

1
alm/src/global.d.ts vendored
View File

@ -1 +0,0 @@
declare type Maybe<T> = T | null

View File

@ -1,47 +0,0 @@
import { useEffect, useState } from 'react'
import { TransactionReceipt } from 'web3-eth'
import { useStateProvider } from '../state/StateProvider'
import { Contract } from 'web3-eth-contract'
import { getRequiredBlockConfirmations } from '../utils/contract'
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
import Web3 from 'web3'
import { FOREIGN_EXPLORER_API, HOME_EXPLORER_API } from '../config/constants'
export interface UseBlockConfirmationsParams {
fromHome: boolean
receipt: Maybe<TransactionReceipt>
}
export const useBlockConfirmations = ({ receipt, fromHome }: UseBlockConfirmationsParams) => {
const [blockConfirmations, setBlockConfirmations] = useState(0)
const { home, foreign } = useStateProvider()
const callRequireBlockConfirmations = async (
contract: Contract,
receipt: TransactionReceipt,
setResult: Function,
snapshotProvider: SnapshotProvider,
web3: Web3,
api: string
) => {
const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber, snapshotProvider, web3, api)
setResult(result)
}
useEffect(
() => {
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
const web3 = fromHome ? home.web3 : foreign.web3
const api = fromHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
if (!bridgeContract || !receipt || !web3) return
callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations, snapshotProvider, web3, api)
},
[home.bridgeContract, foreign.bridgeContract, receipt, fromHome, home.web3, foreign.web3]
)
return {
blockConfirmations
}
}

View File

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

View File

@ -1,68 +0,0 @@
import { useEffect, useState } from 'react'
import { TransactionReceipt } from 'web3-eth'
import { useStateProvider } from '../state/StateProvider'
import { FOREIGN_EXPLORER_API, HOME_EXPLORER_API } from '../config/constants'
import { getClosestBlockByTimestamp } from '../utils/explorer'
export function useClosestBlock(
searchHome: boolean,
fromHome: boolean,
receipt: Maybe<TransactionReceipt>,
timestamp: number
) {
const { home, foreign } = useStateProvider()
const [blockNumber, setBlockNumber] = useState<number | null>(null)
useEffect(
() => {
if (!receipt || blockNumber || !timestamp) return
if (fromHome === searchHome) {
setBlockNumber(receipt.blockNumber)
return
}
const web3 = searchHome ? home.web3 : foreign.web3
if (!web3) return
const getBlock = async () => {
// try to fast-fetch closest block number from the chain explorer
try {
const api = searchHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
setBlockNumber(await getClosestBlockByTimestamp(api, timestamp))
return
} catch {}
const lastBlock = await web3.eth.getBlock('latest')
if (lastBlock.timestamp <= timestamp) {
setBlockNumber(lastBlock.number)
return
}
const oldBlock = await web3.eth.getBlock(Math.max(lastBlock.number - 10000, 1))
const blockDiff = lastBlock.number - oldBlock.number
const timeDiff = (lastBlock.timestamp as number) - (oldBlock.timestamp as number)
const averageBlockTime = timeDiff / blockDiff
let currentBlock = lastBlock
let prevBlockDiff = Infinity
while (true) {
const timeDiff = (currentBlock.timestamp as number) - timestamp
const blockDiff = Math.ceil(timeDiff / averageBlockTime)
if (Math.abs(blockDiff) < 5 || Math.abs(blockDiff) >= Math.abs(prevBlockDiff)) {
setBlockNumber(currentBlock.number - blockDiff - 5)
break
}
prevBlockDiff = blockDiff
currentBlock = await web3.eth.getBlock(currentBlock.number - blockDiff)
}
}
getBlock()
},
[blockNumber, foreign.web3, fromHome, home.web3, receipt, searchHome, timestamp]
)
return blockNumber
}

View File

@ -1,444 +0,0 @@
import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth'
import { MessageObject } from '../utils/web3'
import { useEffect, useState } from 'react'
import { EventData } from 'web3-eth-contract'
import {
BLOCK_RANGE,
CONFIRMATIONS_STATUS,
FOREIGN_RPC_POLLING_INTERVAL,
HOME_RPC_POLLING_INTERVAL,
VALIDATOR_CONFIRMATION_STATUS
} from '../config/constants'
import { homeBlockNumberProvider, foreignBlockNumberProvider } from '../services/BlockNumberProvider'
import { getConfirmationsForTx } from '../utils/getConfirmationsForTx'
import { getFinalizationEvent } from '../utils/getFinalizationEvent'
import {
getValidatorFailedTransactionsForMessage,
getExecutionFailedTransactionForMessage,
getValidatorPendingTransactionsForMessage,
getExecutionPendingTransactionsForMessage,
getValidatorSuccessTransactionsForMessage
} from '../utils/explorer'
export interface useMessageConfirmationsParams {
message: MessageObject
receipt: Maybe<TransactionReceipt>
fromHome: boolean
homeStartBlock: Maybe<number>
foreignStartBlock: Maybe<number>
requiredSignatures: number
validatorList: string[]
targetValidatorList: string[]
blockConfirmations: number
}
export interface ConfirmationParam {
validator: string
status: string
txHash: string
timestamp: number
signature?: string
}
export interface ExecutionData {
status: string
validator: string
txHash: string
timestamp: number
executionResult: boolean
blockNumber: number
}
export const useMessageConfirmations = ({
message,
receipt,
fromHome,
homeStartBlock,
foreignStartBlock,
requiredSignatures,
validatorList,
targetValidatorList,
blockConfirmations
}: useMessageConfirmationsParams) => {
const { home, foreign } = useStateProvider()
const [confirmations, setConfirmations] = useState<ConfirmationParam[]>([])
const [status, setStatus] = useState(CONFIRMATIONS_STATUS.UNDEFINED)
const [waitingBlocks, setWaitingBlocks] = useState(false)
const [waitingBlocksResolved, setWaitingBlocksResolved] = useState(false)
const [signatureCollected, setSignatureCollected] = useState(false)
const [executionEventsFetched, setExecutionEventsFetched] = useState(false)
const [collectedSignaturesEvent, setCollectedSignaturesEvent] = useState<Maybe<EventData>>(null)
const [executionData, setExecutionData] = useState<ExecutionData>({
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
validator: '',
txHash: '',
timestamp: 0,
executionResult: false,
blockNumber: 0
})
const [waitingBlocksForExecution, setWaitingBlocksForExecution] = useState(false)
const [waitingBlocksForExecutionResolved, setWaitingBlocksForExecutionResolved] = useState(false)
const [failedConfirmations, setFailedConfirmations] = useState(false)
const [failedExecution, setFailedExecution] = useState(false)
const [pendingConfirmations, setPendingConfirmations] = useState(false)
const [pendingExecution, setPendingExecution] = useState(false)
const existsConfirmation = (confirmationArray: ConfirmationParam[]) =>
confirmationArray.some(
c => c.status !== VALIDATOR_CONFIRMATION_STATUS.UNDEFINED && c.status !== VALIDATOR_CONFIRMATION_STATUS.WAITING
)
// start watching blocks at the start
useEffect(
() => {
if (!home.web3 || !foreign.web3) return
homeBlockNumberProvider.start(home.web3)
foreignBlockNumberProvider.start(foreign.web3)
},
[foreign.web3, home.web3]
)
// Check if the validators are waiting for block confirmations to verify the message
useEffect(
() => {
if (!receipt || !blockConfirmations || waitingBlocksResolved) return
let timeoutId: number
const blockProvider = fromHome ? homeBlockNumberProvider : foreignBlockNumberProvider
const interval = fromHome ? HOME_RPC_POLLING_INTERVAL : FOREIGN_RPC_POLLING_INTERVAL
const targetBlock = receipt.blockNumber + blockConfirmations
const validatorsWaiting = validatorList.map(validator => ({
validator,
status: VALIDATOR_CONFIRMATION_STATUS.WAITING,
txHash: '',
timestamp: 0
}))
const checkSignaturesWaitingForBLocks = () => {
const currentBlock = blockProvider.get()
if (currentBlock && currentBlock >= targetBlock) {
setWaitingBlocksResolved(true)
setWaitingBlocks(false)
} else if (currentBlock) {
setWaitingBlocks(true)
setConfirmations(validatorsWaiting)
timeoutId = setTimeout(checkSignaturesWaitingForBLocks, interval)
} else {
timeoutId = setTimeout(checkSignaturesWaitingForBLocks, 500)
}
}
checkSignaturesWaitingForBLocks()
return () => clearTimeout(timeoutId)
},
[blockConfirmations, fromHome, receipt, validatorList, waitingBlocksResolved]
)
// The collected signature event is only fetched once the signatures are collected on tx from home to foreign, to calculate if
// the execution tx on the foreign network is waiting for block confirmations
// This is executed if the message is in Home to Foreign direction only
useEffect(
() => {
if (!fromHome || !receipt || !home.web3 || !home.bridgeContract || !signatureCollected) return
let timeoutId: number
let isCancelled = false
const messageHash = home.web3.utils.soliditySha3Raw(message.data)
const contract = home.bridgeContract
const getCollectedSignaturesEvent = async (fromBlock: number, toBlock: number) => {
const currentBlock = homeBlockNumberProvider.get()
if (currentBlock) {
// prevent errors if the toBlock parameter is bigger than the latest
const securedToBlock = toBlock >= currentBlock ? currentBlock : toBlock
const events = await contract.getPastEvents('CollectedSignatures', {
fromBlock,
toBlock: securedToBlock
})
const event = events.find(e => e.returnValues.messageHash === messageHash)
if (event) {
setCollectedSignaturesEvent(event)
} else if (!isCancelled) {
timeoutId = setTimeout(() => getCollectedSignaturesEvent(securedToBlock, securedToBlock + BLOCK_RANGE), 500)
}
} else if (!isCancelled) {
timeoutId = setTimeout(() => getCollectedSignaturesEvent(fromBlock, toBlock), 500)
}
}
getCollectedSignaturesEvent(receipt.blockNumber, receipt.blockNumber + BLOCK_RANGE)
return () => {
clearTimeout(timeoutId)
isCancelled = true
}
},
[fromHome, home.bridgeContract, home.web3, message.data, receipt, signatureCollected]
)
// Check if the responsible validator is waiting for block confirmations to execute the message on foreign network
// This is executed if the message is in Home to Foreign direction only
useEffect(
() => {
if (!fromHome || !home.web3 || !collectedSignaturesEvent || !blockConfirmations) return
if (waitingBlocksForExecutionResolved) return
let timeoutId: number
const targetBlock = collectedSignaturesEvent.blockNumber + blockConfirmations
const checkWaitingBlocksForExecution = () => {
const currentBlock = homeBlockNumberProvider.get()
if (currentBlock && currentBlock >= targetBlock) {
const undefinedExecutionState = {
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
txHash: '',
timestamp: 0,
executionResult: false
}
setExecutionData(
(data: any) =>
data.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED ||
data.status === VALIDATOR_CONFIRMATION_STATUS.WAITING
? undefinedExecutionState
: data
)
setWaitingBlocksForExecutionResolved(true)
setWaitingBlocksForExecution(false)
} else if (currentBlock) {
setWaitingBlocksForExecution(true)
const waitingExecutionState = {
status: VALIDATOR_CONFIRMATION_STATUS.WAITING,
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
txHash: '',
timestamp: 0,
executionResult: false
}
setExecutionData(
(data: any) =>
data.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED ||
data.status === VALIDATOR_CONFIRMATION_STATUS.WAITING
? waitingExecutionState
: data
)
timeoutId = setTimeout(() => checkWaitingBlocksForExecution(), HOME_RPC_POLLING_INTERVAL)
} else {
timeoutId = setTimeout(() => checkWaitingBlocksForExecution(), 500)
}
}
checkWaitingBlocksForExecution()
return () => clearTimeout(timeoutId)
},
[collectedSignaturesEvent, fromHome, blockConfirmations, home.web3, waitingBlocksForExecutionResolved]
)
// Checks if validators verified the message
// To avoid making extra requests, this is only executed when validators finished waiting for blocks confirmations
useEffect(
() => {
if (!waitingBlocksResolved || !homeStartBlock || !requiredSignatures || !home.web3 || !home.bridgeContract) return
if (!validatorList || !validatorList.length) return
let timeoutId: number
let isCancelled = false
if (fromHome) {
if (!targetValidatorList || !targetValidatorList.length) return
const msgHash = home.web3.utils.sha3(message.data)!
const allValidators = [...validatorList, ...targetValidatorList].filter((v, i, s) => s.indexOf(v) === i)
const manualConfirmations = []
for (let i = 0; i < allValidators.length; i++) {
try {
const overrideSignatures: {
[key: string]: string
} = require(`../snapshots/signatures_${allValidators[i]}.json`)
if (overrideSignatures[msgHash]) {
console.log(`Adding manual signature from ${allValidators[i]}`)
manualConfirmations.push({
status: VALIDATOR_CONFIRMATION_STATUS.MANUAL,
validator: allValidators[i],
timestamp: 0,
txHash: '',
signature: overrideSignatures[msgHash]
})
} else {
console.log(`No manual signature from ${allValidators[i]} was found`)
}
} catch (e) {
console.log(`Signatures overrides are not present for ${allValidators[i]}`)
}
}
setConfirmations(manualConfirmations)
}
getConfirmationsForTx(
message.data,
home.web3,
validatorList,
home.bridgeContract,
fromHome,
setConfirmations,
requiredSignatures,
setSignatureCollected,
id => (timeoutId = id),
() => isCancelled,
homeStartBlock,
getValidatorFailedTransactionsForMessage,
setFailedConfirmations,
getValidatorPendingTransactionsForMessage,
setPendingConfirmations,
getValidatorSuccessTransactionsForMessage
)
return () => {
clearTimeout(timeoutId)
isCancelled = true
}
},
[
fromHome,
message.data,
home.web3,
validatorList,
home.bridgeContract,
requiredSignatures,
waitingBlocksResolved,
homeStartBlock,
targetValidatorList
]
)
// Gets finalization event to display the information about the execution of the message
// In a message from Home to Foreign it will be executed after finishing waiting for block confirmations for the execution transaction on Foreign
// In a message from Foreign to Home it will be executed after finishing waiting for block confirmations of the message request
useEffect(
() => {
if ((fromHome && !waitingBlocksForExecutionResolved) || (!fromHome && !waitingBlocksResolved)) return
const bridgeContract = fromHome ? foreign.bridgeContract : home.bridgeContract
const web3 = fromHome ? foreign.web3 : home.web3
const startBlock = fromHome ? foreignStartBlock : homeStartBlock
if (!startBlock || !bridgeContract || !web3) return
let timeoutId: number
let isCancelled = false
getFinalizationEvent(
fromHome,
bridgeContract,
web3,
setExecutionData,
message,
id => (timeoutId = id),
() => isCancelled,
startBlock,
collectedSignaturesEvent,
getExecutionFailedTransactionForMessage,
setFailedExecution,
getExecutionPendingTransactionsForMessage,
setPendingExecution,
setExecutionEventsFetched
)
return () => {
clearTimeout(timeoutId)
isCancelled = true
}
},
[
fromHome,
foreign.bridgeContract,
home.bridgeContract,
message,
foreign.web3,
home.web3,
waitingBlocksResolved,
waitingBlocksForExecutionResolved,
collectedSignaturesEvent,
foreignStartBlock,
homeStartBlock
]
)
// Sets the message status based in the collected information
useEffect(
() => {
if (
executionData.status === VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS &&
existsConfirmation(confirmations)
) {
const newStatus = executionData.executionResult
? CONFIRMATIONS_STATUS.SUCCESS
: CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED
setStatus(newStatus)
foreignBlockNumberProvider.stop()
homeBlockNumberProvider.stop()
} else if (signatureCollected) {
if (fromHome) {
if (waitingBlocksForExecution) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
} else if (failedExecution) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_FAILED)
} else if (pendingExecution) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_PENDING)
} else if (waitingBlocksForExecutionResolved) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
} else {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
}
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
}
} else if (waitingBlocks) {
setStatus(CONFIRMATIONS_STATUS.WAITING_CHAIN)
} else if (failedConfirmations) {
setStatus(CONFIRMATIONS_STATUS.FAILED)
} else if (pendingConfirmations) {
setStatus(CONFIRMATIONS_STATUS.PENDING)
} else if (waitingBlocksResolved && existsConfirmation(confirmations)) {
setStatus(CONFIRMATIONS_STATUS.WAITING_VALIDATORS)
} else if (waitingBlocksResolved) {
setStatus(CONFIRMATIONS_STATUS.SEARCHING)
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
}
},
[
executionData,
fromHome,
signatureCollected,
waitingBlocks,
waitingBlocksForExecution,
failedConfirmations,
failedExecution,
pendingConfirmations,
pendingExecution,
waitingBlocksResolved,
confirmations,
waitingBlocksForExecutionResolved
]
)
return {
confirmations,
status,
signatureCollected,
executionData,
setExecutionData,
waitingBlocksResolved,
executionEventsFetched,
setPendingExecution
}
}

View File

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

View File

@ -1,39 +0,0 @@
import { useEffect, useState } from 'react'
import { TransactionReceipt } from 'web3-eth'
import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constants'
import Web3 from 'web3'
export const useTransactionFinder = ({ txHash, web3 }: { txHash: string; web3: Maybe<Web3> }) => {
const [status, setStatus] = useState(TRANSACTION_STATUS.UNDEFINED)
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
useEffect(
() => {
if (!txHash || !web3) return
let timeoutId: number
const getReceipt = async () => {
const txReceipt = await web3.eth.getTransactionReceipt(txHash)
setReceipt(txReceipt)
if (!txReceipt) {
setStatus(TRANSACTION_STATUS.NOT_FOUND)
timeoutId = setTimeout(getReceipt, HOME_RPC_POLLING_INTERVAL)
} else {
setStatus(TRANSACTION_STATUS.FOUND)
}
}
getReceipt()
return () => clearTimeout(timeoutId)
},
[txHash, web3]
)
return {
status,
receipt
}
}

View File

@ -1,120 +0,0 @@
import { useEffect, useState } from 'react'
import { TransactionReceipt } from 'web3-eth'
import { HOME_RPC_POLLING_INTERVAL, TRANSACTION_STATUS } from '../config/constants'
import { getTransactionStatusDescription } from '../utils/networks'
import { useStateProvider } from '../state/StateProvider'
import { getHomeMessagesFromReceipt, getForeignMessagesFromReceipt, MessageObject, getBlock } from '../utils/web3'
import useInterval from '@use-it/interval'
export const useTransactionStatus = ({
txHash,
chainId,
receiptParam
}: {
txHash: string
chainId: number
receiptParam: Maybe<TransactionReceipt>
}) => {
const { home, foreign } = useStateProvider()
const [messages, setMessages] = useState<Array<MessageObject>>([])
const [status, setStatus] = useState('')
const [description, setDescription] = useState('')
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
const [timestamp, setTimestamp] = useState(0)
const [loading, setLoading] = useState(true)
// Update description so the time displayed is accurate
useInterval(() => {
if (!status || !timestamp || !description) return
setDescription(getTransactionStatusDescription(status, timestamp))
}, 30000)
useEffect(
() => {
if (!chainId || !txHash || !home.chainId || !foreign.chainId || !home.web3 || !foreign.web3) return
const isHome = chainId === home.chainId
const web3 = isHome ? home.web3 : foreign.web3
let timeoutId: number
const getReceipt = async () => {
setLoading(true)
let txReceipt
if (receiptParam) {
txReceipt = receiptParam
} else {
txReceipt = await web3.eth.getTransactionReceipt(txHash)
}
setReceipt(txReceipt)
if (!txReceipt) {
setStatus(TRANSACTION_STATUS.NOT_FOUND)
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.NOT_FOUND))
setMessages([{ id: txHash, data: '' }])
timeoutId = setTimeout(() => getReceipt(), HOME_RPC_POLLING_INTERVAL)
} else {
const blockNumber = txReceipt.blockNumber
const block = await getBlock(web3, blockNumber)
const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp
setTimestamp(blockTimestamp)
if (txReceipt.status) {
let bridgeMessages: Array<MessageObject>
if (isHome) {
bridgeMessages = getHomeMessagesFromReceipt(txReceipt, web3, home.bridgeAddress)
} else {
bridgeMessages = getForeignMessagesFromReceipt(txReceipt, web3, foreign.bridgeAddress)
}
if (bridgeMessages.length === 0) {
setMessages([{ id: txHash, data: '' }])
setStatus(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES)
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_NO_MESSAGES, blockTimestamp))
} else if (bridgeMessages.length === 1) {
setMessages(bridgeMessages)
setStatus(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE)
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, blockTimestamp))
} else {
setMessages(bridgeMessages)
setStatus(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES)
setDescription(
getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES, blockTimestamp)
)
}
} else {
setStatus(TRANSACTION_STATUS.FAILED)
setDescription(getTransactionStatusDescription(TRANSACTION_STATUS.FAILED, blockTimestamp))
}
}
setLoading(false)
}
getReceipt()
return () => clearTimeout(timeoutId)
},
[
txHash,
chainId,
home.chainId,
foreign.chainId,
home.web3,
foreign.web3,
home.bridgeAddress,
foreign.bridgeAddress,
receiptParam
]
)
return {
messages,
status,
description,
receipt,
timestamp,
loading
}
}

View File

@ -1,76 +0,0 @@
import { useEffect, useState } from 'react'
import { Contract } from 'web3-eth-contract'
import Web3 from 'web3'
import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '../utils/contract'
import { BRIDGE_VALIDATORS_ABI } from '../abis'
import { useStateProvider } from '../state/StateProvider'
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
import { FOREIGN_EXPLORER_API, HOME_EXPLORER_API } from '../config/constants'
export const useValidatorContract = (isHome: boolean, blockNumber: number | 'latest') => {
const [validatorContract, setValidatorContract] = useState<Maybe<Contract>>(null)
const [requiredSignatures, setRequiredSignatures] = useState(0)
const [validatorList, setValidatorList] = useState<string[]>([])
const { home, foreign } = useStateProvider()
const callValidatorContract = async (bridgeContract: Maybe<Contract>, web3: Web3, setValidatorContract: Function) => {
if (!web3 || !bridgeContract) return
const address = await getValidatorAddress(bridgeContract)
const contract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, address)
setValidatorContract(contract)
}
const callRequiredSignatures = async (
contract: Maybe<Contract>,
blockNumber: number | 'latest',
setResult: Function,
snapshotProvider: SnapshotProvider,
web3: Web3,
api: string
) => {
if (!contract) return
const result = await getRequiredSignatures(contract, blockNumber, snapshotProvider, web3, api)
setResult(result)
}
const callValidatorList = async (
contract: Maybe<Contract>,
blockNumber: number | 'latest',
setResult: Function,
snapshotProvider: SnapshotProvider,
web3: Web3,
api: string
) => {
if (!contract) return
const result = await getValidatorList(contract, blockNumber, snapshotProvider, web3, api)
setResult(result)
}
const web3 = isHome ? home.web3 : foreign.web3
const api = isHome ? HOME_EXPLORER_API : FOREIGN_EXPLORER_API
const bridgeContract = isHome ? home.bridgeContract : foreign.bridgeContract
const snapshotProvider = isHome ? homeSnapshotProvider : foreignSnapshotProvider
useEffect(
() => {
if (!web3 || !bridgeContract) return
callValidatorContract(bridgeContract, web3, setValidatorContract)
},
[web3, bridgeContract]
)
useEffect(
() => {
if (!web3 || !blockNumber) return
callRequiredSignatures(validatorContract, blockNumber, setRequiredSignatures, snapshotProvider, web3, api)
callValidatorList(validatorContract, blockNumber, setValidatorList, snapshotProvider, web3, api)
},
[validatorContract, blockNumber, web3, snapshotProvider, api]
)
return {
requiredSignatures,
validatorList
}
}

View File

@ -1,16 +0,0 @@
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')
)

View File

@ -1 +0,0 @@
/// <reference types="react-scripts" />

View File

@ -1,64 +0,0 @@
import Web3 from 'web3'
import differenceInMilliseconds from 'date-fns/differenceInMilliseconds'
import { FOREIGN_RPC_POLLING_INTERVAL, HOME_RPC_POLLING_INTERVAL } from '../config/constants'
export class BlockNumberProvider {
private running: number
private web3: Maybe<Web3>
private ref: number | undefined
private value: Maybe<number>
private lastValueTimestamp: Maybe<Date>
private readonly interval: number
constructor(interval = 5000) {
this.running = 0
this.web3 = null
this.ref = undefined
this.value = null
this.lastValueTimestamp = null
this.interval = interval
return this
}
start(web3: Maybe<Web3>) {
if (!this.running) {
clearTimeout(this.ref)
this.web3 = web3
this.running = this.running + 1
this.fetchLastBlock()
} else {
this.running = this.running + 1
}
}
stop() {
this.running = this.running > 0 ? this.running - 1 : 0
if (!this.running) {
clearTimeout(this.ref)
this.ref = undefined
this.web3 = null
}
}
get() {
return this.value
}
private async fetchLastBlock() {
if (!this.web3) return
const now = new Date()
const distance = differenceInMilliseconds(now, this.lastValueTimestamp || 0)
if (distance >= this.interval) {
this.value = await this.web3.eth.getBlockNumber()
this.lastValueTimestamp = now
}
this.ref = setTimeout(() => this.fetchLastBlock(), this.interval)
}
}
export const homeBlockNumberProvider = new BlockNumberProvider(HOME_RPC_POLLING_INTERVAL)
export const foreignBlockNumberProvider = new BlockNumberProvider(FOREIGN_RPC_POLLING_INTERVAL)

View File

@ -1,69 +0,0 @@
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')

View File

@ -1,29 +0,0 @@
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()

View File

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

View File

@ -1,79 +0,0 @@
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)
}

View File

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

View File

@ -1,34 +0,0 @@
import { createGlobalStyle } from 'styled-components'
import theme from './Light'
type ThemeType = typeof theme
export const GlobalStyle = createGlobalStyle<{ theme: ThemeType }>`
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
:root {
--bg-color: ${props => props.theme.backgroundColor};
--font-color: ${props => props.theme.fontColor};
--button-color: ${props => props.theme.buttonColor};
--color-primary: ${props => props.theme.colorPrimary};
--color-grey: ${props => props.theme.colorGrey};
--color-lightGrey: ${props => props.theme.colorLightGrey};
--link-color: ${props => props.theme.linkColor};
--success-color: ${props => props.theme.success.textColor};
--success-bg-color: ${props => props.theme.success.backgroundColor};
--not-required-color: ${props => props.theme.notRequired.textColor};
--not-required-bg-color: ${props => props.theme.notRequired.backgroundColor};
--failed-color: ${props => props.theme.failed.textColor};
--failed-bg-color: ${props => props.theme.failed.backgroundColor};
--warning-color: ${props => props.theme.warning.textColor};
--warning-bg-color: ${props => props.theme.warning.backgroundColor};
}
`

View File

@ -1,26 +0,0 @@
const theme = {
backgroundColor: '#FFFFFF',
fontColor: 'rgba(0, 0, 0, 0.65)',
buttonColor: '#1890ff',
colorPrimary: '#BDBDBD',
colorGrey: '#1890ff',
colorLightGrey: '#1890ff',
linkColor: '#1890ff',
success: {
textColor: '#388E3C',
backgroundColor: 'rgba(0,201,167,.1)'
},
notRequired: {
textColor: '#77838f',
backgroundColor: 'rgba(119,131,143,.1)'
},
failed: {
textColor: '#de4437',
backgroundColor: 'rgba(222,68,55,.1)'
},
warning: {
textColor: '#ffa758',
backgroundColor: 'rgba(222,68,55,.1)'
}
}
export default theme

View File

@ -1,469 +0,0 @@
import 'jest'
import { getRequiredBlockConfirmations, getRequiredSignatures, getValidatorList } from '../contract'
import { Contract } from 'web3-eth-contract'
import { SnapshotProvider } from '../../services/SnapshotProvider'
describe('getRequiredBlockConfirmations', () => {
const methodsBuilder = (value: string) => ({
requiredBlockConfirmations: () => {
return {
call: () => {
return value
}
}
}
})
test('Should call requiredBlockConfirmations method if no events present', async () => {
const contract = ({
getPastEvents: async () => {
return []
},
methods: methodsBuilder('1')
} as unknown) as Contract
const snapshotProvider = ({
requiredBlockConfirmationEvents: () => {
return []
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const result = await getRequiredBlockConfirmations(contract, 10, snapshotProvider)
expect(result).toEqual(1)
})
test('Should not call to get events if block number was included in the snapshot', async () => {
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => []),
methods: methodsBuilder('3')
} as unknown) as Contract
const snapshotProvider = ({
requiredBlockConfirmationEvents: () => {
return [
{
blockNumber: 8,
returnValues: {
requiredBlockConfirmations: '1'
}
}
]
},
snapshotBlockNumber: () => {
return 15
}
} as unknown) as SnapshotProvider
const result = await getRequiredBlockConfirmations(contract, 10, snapshotProvider)
expect(result).toEqual(1)
expect(contract.getPastEvents).toBeCalledTimes(0)
})
test('Should call to get events if block number was not included in the snapshot', async () => {
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => [
{
blockNumber: 9,
returnValues: {
requiredBlockConfirmations: '2'
}
}
]),
methods: methodsBuilder('3')
} as unknown) as Contract
const snapshotProvider = ({
requiredBlockConfirmationEvents: () => {
return [
{
blockNumber: 8,
returnValues: {
requiredBlockConfirmations: '1'
}
}
]
},
snapshotBlockNumber: () => {
return 8
}
} as unknown) as SnapshotProvider
const result = await getRequiredBlockConfirmations(contract, 10, snapshotProvider)
expect(result).toEqual(2)
expect(contract.getPastEvents).toBeCalledTimes(1)
expect(contract.getPastEvents).toHaveBeenCalledWith('RequiredBlockConfirmationChanged', {
fromBlock: 9,
toBlock: 10
})
})
test('Should use the most updated event', async () => {
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => [
{
blockNumber: 9,
returnValues: {
requiredBlockConfirmations: '2'
}
},
{
blockNumber: 11,
returnValues: {
requiredBlockConfirmations: '3'
}
}
]),
methods: methodsBuilder('3')
} as unknown) as Contract
const snapshotProvider = ({
requiredBlockConfirmationEvents: () => {
return []
},
snapshotBlockNumber: () => {
return 11
}
} as unknown) as SnapshotProvider
const result = await getRequiredBlockConfirmations(contract, 15, snapshotProvider)
expect(result).toEqual(3)
expect(contract.getPastEvents).toBeCalledTimes(1)
expect(contract.getPastEvents).toHaveBeenCalledWith('RequiredBlockConfirmationChanged', {
fromBlock: 12,
toBlock: 15
})
})
})
describe('getRequiredSignatures', () => {
test('Should not call to get events if block number was included in the snapshot', async () => {
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => [])
} as unknown) as Contract
const snapshotProvider = ({
requiredSignaturesEvents: () => {
return [
{
blockNumber: 7,
returnValues: {
requiredSignatures: '1'
}
},
{
blockNumber: 8,
returnValues: {
requiredSignatures: '2'
}
}
]
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const result = await getRequiredSignatures(contract, 10, snapshotProvider)
expect(result).toEqual(2)
expect(contract.getPastEvents).toBeCalledTimes(0)
})
test('Should call to get events if block number is higher than the snapshot block number', async () => {
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => [
{
blockNumber: 15,
returnValues: {
requiredSignatures: '3'
}
}
])
} as unknown) as Contract
const snapshotProvider = ({
requiredSignaturesEvents: () => {
return [
{
blockNumber: 7,
returnValues: {
requiredSignatures: '1'
}
},
{
blockNumber: 8,
returnValues: {
requiredSignatures: '2'
}
}
]
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const result = await getRequiredSignatures(contract, 20, snapshotProvider)
expect(result).toEqual(3)
expect(contract.getPastEvents).toBeCalledTimes(1)
expect(contract.getPastEvents).toHaveBeenCalledWith('RequiredSignaturesChanged', {
fromBlock: 11,
toBlock: 20
})
})
test('Should use the most updated event before the block number', async () => {
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => [
{
blockNumber: 15,
returnValues: {
requiredSignatures: '4'
}
}
])
} as unknown) as Contract
const snapshotProvider = ({
requiredSignaturesEvents: () => {
return [
{
blockNumber: 5,
returnValues: {
requiredSignatures: '1'
}
},
{
blockNumber: 6,
returnValues: {
requiredSignatures: '2'
}
}
]
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const result = await getRequiredSignatures(contract, 7, snapshotProvider)
expect(result).toEqual(2)
expect(contract.getPastEvents).toBeCalledTimes(0)
})
})
describe('getValidatorList', () => {
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
const methodsBuilder = (value: string[]) => ({
validatorList: () => {
return {
call: () => {
return value
}
}
}
})
test('Should return the current validator list if no events found', async () => {
const currentValidators = [validator1, validator2, validator3]
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => []),
methods: methodsBuilder(currentValidators)
} as unknown) as Contract
const snapshotProvider = ({
validatorAddedEvents: () => {
return []
},
validatorRemovedEvents: () => {
return []
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const list = await getValidatorList(contract, 20, snapshotProvider)
expect(list.length).toEqual(3)
expect(list).toEqual(expect.arrayContaining(currentValidators))
expect(contract.getPastEvents).toBeCalledTimes(2)
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
fromBlock: 20
})
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
fromBlock: 20
})
})
test('If validator was added later from snapshot it should not include it', async () => {
const currentValidators = [validator1, validator2, validator3]
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => []),
methods: methodsBuilder(currentValidators)
} as unknown) as Contract
const snapshotProvider = ({
validatorAddedEvents: () => {
return [
{
blockNumber: 9,
returnValues: {
validator: validator3
},
event: 'ValidatorAdded'
}
]
},
validatorRemovedEvents: () => {
return []
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const list = await getValidatorList(contract, 5, snapshotProvider)
expect(list.length).toEqual(2)
expect(list).toEqual(expect.arrayContaining([validator1, validator2]))
expect(contract.getPastEvents).toBeCalledTimes(2)
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
fromBlock: 11
})
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
fromBlock: 11
})
})
test('If validator was added later from chain it should not include it', async () => {
const currentValidators = [validator1, validator2, validator3]
const contract = ({
getPastEvents: jest.fn().mockImplementation(async event => {
if (event === 'ValidatorAdded') {
return [
{
blockNumber: 9,
returnValues: {
validator: validator3
},
event: 'ValidatorAdded'
}
]
} else {
return []
}
}),
methods: methodsBuilder(currentValidators)
} as unknown) as Contract
const snapshotProvider = ({
validatorAddedEvents: () => {
return []
},
validatorRemovedEvents: () => {
return []
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const list = await getValidatorList(contract, 15, snapshotProvider)
expect(list.length).toEqual(2)
expect(list).toEqual(expect.arrayContaining([validator1, validator2]))
expect(contract.getPastEvents).toBeCalledTimes(2)
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
fromBlock: 15
})
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
fromBlock: 15
})
})
test('If validator was removed later from snapshot it should include it', async () => {
const currentValidators = [validator1, validator2]
const contract = ({
getPastEvents: jest.fn().mockImplementation(async () => []),
methods: methodsBuilder(currentValidators)
} as unknown) as Contract
const snapshotProvider = ({
validatorAddedEvents: () => {
return []
},
validatorRemovedEvents: () => {
return [
{
blockNumber: 9,
returnValues: {
validator: validator3
},
event: 'ValidatorRemoved'
}
]
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const list = await getValidatorList(contract, 5, snapshotProvider)
expect(list.length).toEqual(3)
expect(list).toEqual(expect.arrayContaining([validator1, validator2, validator3]))
expect(contract.getPastEvents).toBeCalledTimes(2)
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
fromBlock: 11
})
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
fromBlock: 11
})
})
test('If validator was removed later from chain it should include it', async () => {
const currentValidators = [validator1, validator2]
const contract = ({
getPastEvents: jest.fn().mockImplementation(async event => {
if (event === 'ValidatorRemoved') {
return [
{
blockNumber: 9,
returnValues: {
validator: validator3
},
event: 'ValidatorRemoved'
}
]
} else {
return []
}
}),
methods: methodsBuilder(currentValidators)
} as unknown) as Contract
const snapshotProvider = ({
validatorAddedEvents: () => {
return []
},
validatorRemovedEvents: () => {
return []
},
snapshotBlockNumber: () => {
return 10
}
} as unknown) as SnapshotProvider
const list = await getValidatorList(contract, 15, snapshotProvider)
expect(list.length).toEqual(3)
expect(list).toEqual(expect.arrayContaining([validator1, validator2, validator3]))
expect(contract.getPastEvents).toBeCalledTimes(2)
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorAdded', {
fromBlock: 15
})
expect(contract.getPastEvents).toHaveBeenCalledWith('ValidatorRemoved', {
fromBlock: 15
})
})
})

View File

@ -1,172 +0,0 @@
import 'jest'
import {
getFailedTransactions,
getSuccessTransactions,
filterValidatorSignatureTransaction,
getExecutionFailedTransactionForMessage,
APITransaction,
getValidatorPendingTransactionsForMessage,
getExecutionPendingTransactionsForMessage
} from '../explorer'
import { EXECUTE_AFFIRMATION_HASH, EXECUTE_SIGNATURES_HASH, SUBMIT_SIGNATURE_HASH } from '../../config/constants'
const messageData = '0x123456'
const OTHER_HASH = 'aabbccdd'
const bridgeAddress = '0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560'
const otherAddress = '0xD4075FB57fCf038bFc702c915Ef9592534bED5c1'
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
describe('getFailedTransactions', () => {
test('should only return failed transactions', async () => {
const to = otherAddress
const transactions = [
{ isError: '0', to, from: validator1 },
{ isError: '1', to, from: validator1 },
{ isError: '0', to, from: validator2 },
{ isError: '1', to, from: validator2 },
{ isError: '1', to, from: validator3 }
]
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
const result = await getFailedTransactions(validator1, to, 0, 1, '', fetchAccountTransactions)
expect(result.length).toEqual(1)
})
})
describe('getSuccessTransactions', () => {
test('should only return success transactions', async () => {
const to = otherAddress
const transactions = [
{ isError: '0', to, from: validator1 },
{ isError: '1', to, from: validator1 },
{ isError: '0', to, from: validator2 },
{ isError: '1', to, from: validator2 },
{ isError: '1', to, from: validator3 }
]
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
const result = await getSuccessTransactions(validator1, to, 0, 1, '', fetchAccountTransactions)
expect(result.length).toEqual(1)
})
})
describe('filterValidatorSignatureTransaction', () => {
test('should return submit signatures related transaction', () => {
const transactions = [
{ input: `0x${SUBMIT_SIGNATURE_HASH}112233` },
{ input: `0x${SUBMIT_SIGNATURE_HASH}123456` },
{ input: `0x${OTHER_HASH}123456` },
{ input: `0x${OTHER_HASH}112233` }
] as APITransaction[]
const result = filterValidatorSignatureTransaction(transactions, messageData)
expect(result.length).toEqual(1)
expect(result[0]).toEqual({ input: `0x${SUBMIT_SIGNATURE_HASH}123456` })
})
test('should return execute affirmation related transaction', () => {
const transactions = [
{ input: `0x${EXECUTE_AFFIRMATION_HASH}112233` },
{ input: `0x${EXECUTE_AFFIRMATION_HASH}123456` },
{ input: `0x${OTHER_HASH}123456` },
{ input: `0x${OTHER_HASH}112233` }
] as APITransaction[]
const result = filterValidatorSignatureTransaction(transactions, messageData)
expect(result.length).toEqual(1)
expect(result[0]).toEqual({ input: `0x${EXECUTE_AFFIRMATION_HASH}123456` })
})
})
describe('getExecutionFailedTransactionForMessage', () => {
test('should return failed transaction related to signatures execution', async () => {
const transactions = [
{ input: `0x${EXECUTE_SIGNATURES_HASH}112233` },
{ input: `0x${EXECUTE_SIGNATURES_HASH}123456` },
{ input: `0x${OTHER_HASH}123456` },
{ input: `0x${OTHER_HASH}112233` }
] as APITransaction[]
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
const result = await getExecutionFailedTransactionForMessage(
{
account: '',
to: '',
messageData,
startBlock: 0,
endBlock: 1
},
fetchAccountTransactions
)
expect(result.length).toEqual(1)
expect(result[0]).toEqual({ input: `0x${EXECUTE_SIGNATURES_HASH}123456` })
})
})
describe('getValidatorPendingTransactionsForMessage', () => {
test('should return pending transaction for submit signature transaction', async () => {
const transactions = [
{ input: `0x${SUBMIT_SIGNATURE_HASH}112233`, to: bridgeAddress },
{ input: `0x${SUBMIT_SIGNATURE_HASH}123456`, to: bridgeAddress },
{ input: `0x${SUBMIT_SIGNATURE_HASH}123456`, to: otherAddress },
{ input: `0x${OTHER_HASH}123456`, to: bridgeAddress },
{ input: `0x${OTHER_HASH}112233`, to: bridgeAddress }
] as APITransaction[]
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
const result = await getValidatorPendingTransactionsForMessage(
{
account: '',
to: bridgeAddress,
messageData
},
fetchAccountTransactions
)
expect(result.length).toEqual(1)
expect(result[0]).toEqual({ input: `0x${SUBMIT_SIGNATURE_HASH}123456`, to: bridgeAddress })
})
test('should return pending transaction for execute affirmation transaction', async () => {
const transactions = [
{ input: `0x${EXECUTE_AFFIRMATION_HASH}112233`, to: bridgeAddress },
{ input: `0x${EXECUTE_AFFIRMATION_HASH}123456`, to: bridgeAddress },
{ input: `0x${EXECUTE_AFFIRMATION_HASH}123456`, to: otherAddress },
{ input: `0x${OTHER_HASH}123456`, to: bridgeAddress },
{ input: `0x${OTHER_HASH}112233`, to: bridgeAddress }
] as APITransaction[]
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
const result = await getValidatorPendingTransactionsForMessage(
{
account: '',
to: bridgeAddress,
messageData
},
fetchAccountTransactions
)
expect(result.length).toEqual(1)
expect(result[0]).toEqual({ input: `0x${EXECUTE_AFFIRMATION_HASH}123456`, to: bridgeAddress })
})
})
describe('getExecutionPendingTransactionsForMessage', () => {
test('should return pending transaction for signatures execution transaction', async () => {
const transactions = [
{ input: `0x${EXECUTE_SIGNATURES_HASH}112233`, to: bridgeAddress },
{ input: `0x${EXECUTE_SIGNATURES_HASH}123456`, to: bridgeAddress },
{ input: `0x${EXECUTE_SIGNATURES_HASH}123456`, to: otherAddress },
{ input: `0x${OTHER_HASH}123456`, to: bridgeAddress },
{ input: `0x${OTHER_HASH}112233`, to: bridgeAddress }
] as APITransaction[]
const fetchAccountTransactions = jest.fn().mockImplementation(() => transactions)
const result = await getExecutionPendingTransactionsForMessage(
{
account: '',
to: bridgeAddress,
messageData
},
fetchAccountTransactions
)
expect(result.length).toEqual(1)
expect(result[0]).toEqual({ input: `0x${EXECUTE_SIGNATURES_HASH}123456`, to: bridgeAddress })
})
})

View File

@ -1,830 +0,0 @@
import 'jest'
import { getConfirmationsForTx } from '../getConfirmationsForTx'
import * as helpers from '../validatorConfirmationHelpers'
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
import { APIPendingTransaction, APITransaction } from '../explorer'
import { VALIDATOR_CONFIRMATION_STATUS } from '../../config/constants'
import { ConfirmationParam } from '../../hooks/useMessageConfirmations'
jest.mock('../validatorConfirmationHelpers')
const getSuccessExecutionTransaction = helpers.getSuccessExecutionTransaction as jest.Mock<any>
const getValidatorConfirmation = helpers.getValidatorConfirmation as jest.Mock<any>
const getValidatorFailedTransaction = helpers.getValidatorFailedTransaction as jest.Mock<any>
const getValidatorPendingTransaction = helpers.getValidatorPendingTransaction as jest.Mock<any>
const messageData = '0x111111111'
const web3 = {
utils: {
soliditySha3Raw: (data: string) => `0xaaaa${data.replace('0x', '')}`
},
eth: {
accounts: new Web3().eth.accounts
}
} as Web3
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
const validatorList = [validator1, validator2, validator3]
const signature =
'0x6f5b74905669999f1abdb52e1e215506907e1849aac7b31854da458b33a5954e15b165007c3703cfd16e61ca46a96a56727ed11fa47be359d3834515accd016e1b'
const bridgeContract = {
methods: {
signature: () => ({
call: () => signature
})
}
} as Contract
const requiredSignatures = 2
const isCancelled = () => false
let subscriptions: Array<number> = []
const timestamp = 1594045859
const getFailedTransactions = (): Promise<APITransaction[]> => Promise.resolve([])
const getPendingTransactions = (): Promise<APIPendingTransaction[]> => Promise.resolve([])
const getSuccessTransactions = (): Promise<APITransaction[]> => Promise.resolve([])
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
getSuccessExecutionTransaction.mockClear()
getValidatorConfirmation.mockClear()
getValidatorFailedTransaction.mockClear()
getValidatorPendingTransaction.mockClear()
subscriptions = []
})
describe('getConfirmationsForTx', () => {
test('should set validator confirmations status when signatures collected even if validator transactions not found yet and set remaining validator as not required', async () => {
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: '',
timestamp: 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(1)
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
const res1 = setResult.mock.calls[0][0]()
const res2 = setResult.mock.calls[1][0](res1)
expect(res1).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res2).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
])
)
})
test('should set validator confirmations status when signatures not collected even if validator transactions not found yet', async () => {
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator === validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: '',
timestamp: 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(1)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
})
test('should set validator confirmations status, validator transactions and not retry', async () => {
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator !== validator3 ? '0x123' : '',
timestamp: validatorData.validator !== validator3 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(0)
expect(setResult).toBeCalledTimes(3)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
const res1 = setResult.mock.calls[0][0]()
const res2 = setResult.mock.calls[1][0](res1)
const res3 = setResult.mock.calls[2][0](res2)
expect(res1).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res2).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res3).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
])
)
})
test('should set validator confirmations status, validator transactions, keep failed found transaction and not retry', async () => {
const validator4 = '0x9d2dC11C342F4eF3C5491A048D0f0eBCd2D8f7C3'
const validatorList = [validator1, validator2, validator3, validator4]
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status:
validator !== validator3 && validator !== validator4
? VALIDATOR_CONFIRMATION_STATUS.SUCCESS
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? '0x123' : '',
timestamp: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator3
? VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator3 ? '0x123' : '',
timestamp: validatorData.validator === validator3 ? 123 : 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(0)
expect(setResult).toBeCalledTimes(4)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
const res1 = setResult.mock.calls[0][0]()
const res2 = setResult.mock.calls[1][0](res1)
const res3 = setResult.mock.calls[2][0](res2)
const res4 = setResult.mock.calls[3][0](res3)
expect(res1).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res2).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res3).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res4).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED, txHash: '', timestamp: 0 }
])
)
})
test('should look for failed and pending transactions for not confirmed validators', async () => {
// Validator1 success
// Validator2 failed
// Validator3 Pending
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator === validator1 ? '0x123' : '',
timestamp: validatorData.validator === validator1 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator2
? VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator2 ? '0x123' : '',
timestamp: validatorData.validator === validator2 ? 123 : 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator3
? VALIDATOR_CONFIRMATION_STATUS.PENDING
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator3 ? '0x123' : '',
timestamp: validatorData.validator === validator3 ? 123 : 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(4)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
const res1 = setResult.mock.calls[0][0]()
const res2 = setResult.mock.calls[1][0](res1)
const res3 = setResult.mock.calls[2][0](res2)
const res4 = setResult.mock.calls[3][0](res3)
expect(res1).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res2).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res3).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
])
)
expect(res4).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
])
)
})
test('should set as failed if enough signatures failed', async () => {
// Validator1 success
// Validator2 failed
// Validator3 failed
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getSuccessExecutionTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator === validator1 ? '0x123' : '',
timestamp: validatorData.validator === validator1 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator !== validator1
? VALIDATOR_CONFIRMATION_STATUS.FAILED
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator !== validator1 ? '0x123' : '',
timestamp: validatorData.validator !== validator1 ? 123 : 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(0)
expect(setResult).toBeCalledTimes(3)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
const res1 = setResult.mock.calls[0][0]()
const res2 = setResult.mock.calls[1][0](res1)
const res3 = setResult.mock.calls[2][0](res2)
expect(res1).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res2).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res3).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 }
])
)
})
test('should remove pending state after transaction mined', async () => {
const validator4 = '0x9d2dC11C342F4eF3C5491A048D0f0eBCd2D8f7C3'
const validatorList = [validator1, validator2, validator3, validator4]
// Validator1 success (ts=100)
// Validator2 failed (ts=200)
// Validator3 Pending (ts=300)
// Validator4 Excess confirmation (Failed) (ts=400)
getValidatorConfirmation
.mockImplementationOnce(() => async (validator: string) => ({
validator,
status:
validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
.mockImplementation(() => async (validator: string) => ({
validator,
status:
validator === validator1 || validator === validator3
? VALIDATOR_CONFIRMATION_STATUS.SUCCESS
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getSuccessExecutionTransaction
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator === validator1 ? '0x100' : '',
timestamp: validatorData.validator === validator1 ? 100 : 0
}))
.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash:
validatorData.validator === validator1 ? '0x100' : validatorData.validator === validator3 ? '0x300' : '',
timestamp: validatorData.validator === validator1 ? 100 : validatorData.validator === validator3 ? 300 : ''
}))
getValidatorFailedTransaction
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator2
? VALIDATOR_CONFIRMATION_STATUS.FAILED
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator2 ? '0x200' : '',
timestamp: validatorData.validator === validator2 ? 200 : 0
}))
.mockImplementation(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator2 || validatorData.validator === validator4
? validatorData.validator === validator2
? VALIDATOR_CONFIRMATION_STATUS.FAILED
: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash:
validatorData.validator === validator2 ? '0x200' : validatorData.validator === validator4 ? '0x400' : '',
timestamp: validatorData.validator === validator2 ? 200 : validatorData.validator === validator4 ? 400 : ''
}))
getValidatorPendingTransaction
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator3
? VALIDATOR_CONFIRMATION_STATUS.PENDING
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator3 ? '0x300' : '',
timestamp: validatorData.validator === validator3 ? 300 : 0
}))
.mockImplementationOnce(() => async (validatorData: ConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(4)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getSuccessExecutionTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
const res1 = setResult.mock.calls[0][0]()
const res2 = setResult.mock.calls[1][0](res1)
const res3 = setResult.mock.calls[2][0](res2)
const res4 = setResult.mock.calls[3][0](res3)
expect(res1).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res2).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res3).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x300', timestamp: 300 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res4).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x300', timestamp: 300 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
true,
setResult,
requiredSignatures,
setSignatureCollected,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(7)
expect(getValidatorConfirmation).toBeCalledTimes(2)
expect(getSuccessExecutionTransaction).toBeCalledTimes(2)
expect(setSignatureCollected).toBeCalledTimes(2)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(setSignatureCollected.mock.calls[1][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(2)
expect(setFailedConfirmations).toBeCalledTimes(2)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
expect(setFailedConfirmations.mock.calls[1][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(2)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
expect(setPendingConfirmations.mock.calls[1][0]).toEqual(false)
const res5 = setResult.mock.calls[4][0](res4)
const res6 = setResult.mock.calls[5][0](res5)
const res7 = setResult.mock.calls[6][0](res6)
expect(res5).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res6).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(res7).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x100', timestamp: 100 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x200', timestamp: 200 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x300', timestamp: 300 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID, txHash: '0x400', timestamp: 400 }
])
)
})
})

View File

@ -1,309 +0,0 @@
import 'jest'
import { Contract, EventData } from 'web3-eth-contract'
import Web3 from 'web3'
import { getFinalizationEvent } from '../getFinalizationEvent'
import { VALIDATOR_CONFIRMATION_STATUS } from '../../config/constants'
const timestamp = 1594045859
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
const txHash = '0xdab36c9210e7e45fb82af10ffe4960461e41661dce0c9cd36b2843adaa1df156'
const web3 = ({
eth: {
getTransactionReceipt: () => ({
from: validator1
}),
getBlock: () => ({ timestamp })
},
utils: {
toChecksumAddress: (a: string) => a
}
} as unknown) as Web3
const message = {
id: '0x123',
data: '0x123456789'
}
const isCancelled = () => false
let subscriptions: Array<number> = []
const event = {
transactionHash: txHash,
blockNumber: 5523145,
returnValues: {
status: true
}
}
const bridgeAddress = '0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560'
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
beforeEach(() => {
subscriptions = []
})
describe('getFinalizationEvent', () => {
test('should get finalization event and not try to get failed or pending transactions', async () => {
const contract = ({
getPastEvents: async () => {
return [event]
}
} as unknown) as Contract
const collectedSignaturesEvent = null
const setResult = jest.fn()
const getFailedExecution = jest.fn()
const setFailedExecution = jest.fn()
const getPendingExecution = jest.fn()
const setPendingExecution = jest.fn()
const setExecutionEventsFetched = jest.fn()
await getFinalizationEvent(
true,
contract,
web3,
setResult,
message,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
collectedSignaturesEvent,
getFailedExecution,
setFailedExecution,
getPendingExecution,
setPendingExecution,
setExecutionEventsFetched
)
unsubscribe()
expect(subscriptions.length).toEqual(0)
expect(setResult).toBeCalledTimes(1)
expect(setResult.mock.calls[0][0]).toEqual({
validator: validator1,
status: VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS,
txHash,
timestamp,
executionResult: true,
blockNumber: 5523145
})
expect(getFailedExecution).toBeCalledTimes(0)
expect(setFailedExecution).toBeCalledTimes(0)
expect(getPendingExecution).toBeCalledTimes(0)
expect(setPendingExecution).toBeCalledTimes(0)
})
test('should retry to get finalization event and not try to get failed or pending transactions if foreign to home transaction', async () => {
const contract = ({
getPastEvents: async () => {
return []
}
} as unknown) as Contract
const collectedSignaturesEvent = null
const setResult = jest.fn()
const getFailedExecution = jest.fn()
const setFailedExecution = jest.fn()
const getPendingExecution = jest.fn()
const setPendingExecution = jest.fn()
const setExecutionEventsFetched = jest.fn()
await getFinalizationEvent(
true,
contract,
web3,
setResult,
message,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
collectedSignaturesEvent,
getFailedExecution,
setFailedExecution,
getPendingExecution,
setPendingExecution,
setExecutionEventsFetched
)
unsubscribe()
expect(subscriptions.length).toEqual(1)
expect(setResult).toBeCalledTimes(0)
expect(getFailedExecution).toBeCalledTimes(0)
expect(setFailedExecution).toBeCalledTimes(0)
expect(getPendingExecution).toBeCalledTimes(0)
expect(setPendingExecution).toBeCalledTimes(0)
})
test('should retry to get finalization event and try to get failed and pending transactions if home to foreign transaction', async () => {
const contract = ({
getPastEvents: async () => {
return []
},
options: {
address: bridgeAddress
}
} as unknown) as Contract
const collectedSignaturesEvent = ({
returnValues: {
authorityResponsibleForRelay: validator1
}
} as unknown) as EventData
const setResult = jest.fn()
const getFailedExecution = jest.fn().mockResolvedValue([])
const setFailedExecution = jest.fn()
const getPendingExecution = jest.fn().mockResolvedValue([])
const setPendingExecution = jest.fn()
const setExecutionEventsFetched = jest.fn()
await getFinalizationEvent(
true,
contract,
web3,
setResult,
message,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
collectedSignaturesEvent,
getFailedExecution,
setFailedExecution,
getPendingExecution,
setPendingExecution,
setExecutionEventsFetched
)
unsubscribe()
expect(subscriptions.length).toEqual(1)
expect(setResult).toBeCalledTimes(0)
expect(getFailedExecution).toBeCalledTimes(1)
expect(setFailedExecution).toBeCalledTimes(0)
expect(getPendingExecution).toBeCalledTimes(1)
expect(setPendingExecution).toBeCalledTimes(0)
})
test('should retry to get finalization event and not to try to get failed transaction if pending transactions found if home to foreign transaction', async () => {
const contract = ({
getPastEvents: async () => {
return []
},
options: {
address: bridgeAddress
}
} as unknown) as Contract
const collectedSignaturesEvent = ({
returnValues: {
authorityResponsibleForRelay: validator1
}
} as unknown) as EventData
const setResult = jest.fn()
const getFailedExecution = jest.fn().mockResolvedValue([])
const setFailedExecution = jest.fn()
const getPendingExecution = jest.fn().mockResolvedValue([{ hash: txHash }])
const setPendingExecution = jest.fn()
const setExecutionEventsFetched = jest.fn()
await getFinalizationEvent(
true,
contract,
web3,
setResult,
message,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
collectedSignaturesEvent,
getFailedExecution,
setFailedExecution,
getPendingExecution,
setPendingExecution,
setExecutionEventsFetched
)
unsubscribe()
expect(subscriptions.length).toEqual(1)
expect(setResult).toBeCalledTimes(1)
expect(setResult.mock.calls[0][0]).toEqual({
validator: validator1,
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
txHash,
timestamp: expect.any(Number),
executionResult: false,
blockNumber: 0
})
expect(getFailedExecution).toBeCalledTimes(0)
expect(setFailedExecution).toBeCalledTimes(0)
expect(getPendingExecution).toBeCalledTimes(1)
expect(setPendingExecution).toBeCalledTimes(1)
})
test('should retry to get finalization event even if failed transaction found if home to foreign transaction', async () => {
const contract = ({
getPastEvents: async () => {
return []
},
options: {
address: bridgeAddress
}
} as unknown) as Contract
const collectedSignaturesEvent = ({
returnValues: {
authorityResponsibleForRelay: validator1
}
} as unknown) as EventData
const setResult = jest.fn()
const getFailedExecution = jest.fn().mockResolvedValue([{ timeStamp: timestamp, hash: txHash }])
const setFailedExecution = jest.fn()
const getPendingExecution = jest.fn().mockResolvedValue([])
const setPendingExecution = jest.fn()
const setExecutionEventsFetched = jest.fn()
await getFinalizationEvent(
true,
contract,
web3,
setResult,
message,
subscriptions.push.bind(subscriptions),
isCancelled,
timestamp,
collectedSignaturesEvent,
getFailedExecution,
setFailedExecution,
getPendingExecution,
setPendingExecution,
setExecutionEventsFetched
)
unsubscribe()
expect(subscriptions.length).toEqual(1)
expect(setResult).toBeCalledTimes(1)
expect(setResult.mock.calls[0][0]).toEqual({
validator: validator1,
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
txHash,
timestamp: expect.any(Number),
executionResult: false,
blockNumber: expect.any(Number)
})
expect(getFailedExecution).toBeCalledTimes(1)
expect(setFailedExecution).toBeCalledTimes(1)
expect(getPendingExecution).toBeCalledTimes(1)
expect(setPendingExecution).toBeCalledTimes(0)
})
})

View File

@ -1,150 +0,0 @@
import { Contract } from 'web3-eth-contract'
import { EventData } from 'web3-eth-contract'
import { SnapshotProvider } from '../services/SnapshotProvider'
import { getLogs } from './explorer'
import Web3 from 'web3'
const getPastEventsWithFallback = (
api: string,
web3: Web3 | null,
contract: Contract,
eventName: string,
options: any
) =>
contract
.getPastEvents(eventName, options)
.catch(() => (api && web3 ? getLogs(api, web3, contract, eventName, options) : []))
export const getRequiredBlockConfirmations = async (
contract: Contract,
blockNumber: number,
snapshotProvider: SnapshotProvider,
web3: Web3 | null = null,
api: string = ''
) => {
let blockConfirmations
try {
blockConfirmations = await contract.methods.requiredBlockConfirmations().call()
} catch {}
if (blockConfirmations) {
return parseInt(blockConfirmations)
}
const eventsFromSnapshot = snapshotProvider.requiredBlockConfirmationEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
let contractEvents: EventData[] = []
if (blockNumber > snapshotBlockNumber) {
contractEvents = await getPastEventsWithFallback(api, web3, contract, 'RequiredBlockConfirmationChanged', {
fromBlock: snapshotBlockNumber + 1,
toBlock: blockNumber
})
}
const events = [...eventsFromSnapshot, ...contractEvents]
// Use the value from last event before the transaction
const event = events[events.length - 1]
blockConfirmations = event.returnValues.requiredBlockConfirmations
return parseInt(blockConfirmations)
}
export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call()
export const getRequiredSignatures = async (
contract: Contract,
blockNumber: number | 'latest',
snapshotProvider: SnapshotProvider,
web3: Web3 | null = null,
api: string = ''
) => {
let requiredSignatures
try {
requiredSignatures = await contract.methods.requiredSignatures().call()
} catch {}
if (requiredSignatures) {
return parseInt(requiredSignatures)
}
if (blockNumber === 'latest') {
return contract.methods.requiredSignatures().call()
}
const eventsFromSnapshot = snapshotProvider.requiredSignaturesEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
let contractEvents: EventData[] = []
if (blockNumber > snapshotBlockNumber) {
contractEvents = await getPastEventsWithFallback(api, web3, contract, 'RequiredSignaturesChanged', {
fromBlock: snapshotBlockNumber + 1,
toBlock: blockNumber
})
}
const events = [...eventsFromSnapshot, ...contractEvents]
// Use the value form last event before the transaction
const event = events[events.length - 1]
;({ requiredSignatures } = event.returnValues)
return parseInt(requiredSignatures)
}
export const getValidatorList = async (
contract: Contract,
blockNumber: number | 'latest',
snapshotProvider: SnapshotProvider,
web3: Web3 | null = null,
api: string = ''
) => {
try {
const currentList = await contract.methods.validatorList().call()
if (currentList) {
return currentList
}
} catch {}
const addedEventsFromSnapshot = snapshotProvider.validatorAddedEvents(blockNumber)
const removedEventsFromSnapshot = snapshotProvider.validatorRemovedEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
const fromBlock = snapshotBlockNumber > blockNumber ? snapshotBlockNumber + 1 : blockNumber
const [currentList, added, removed] = await Promise.all([
contract.methods.validatorList().call(),
getPastEventsWithFallback(api, web3, contract, 'ValidatorAdded', {
fromBlock
}),
getPastEventsWithFallback(api, web3, contract, 'ValidatorRemoved', {
fromBlock
})
])
// Ordered desc
const orderedEvents = [...addedEventsFromSnapshot, ...added, ...removedEventsFromSnapshot, ...removed].sort(
({ blockNumber: prev }, { blockNumber: next }) => next - prev
)
// Stored as a Set to avoid duplicates
const validatorList = new Set(currentList)
orderedEvents.forEach(e => {
const { validator } = e.returnValues
if (e.event === 'ValidatorRemoved') {
validatorList.add(validator)
} else if (e.event === 'ValidatorAdded') {
validatorList.delete(validator)
}
})
return Array.from(validatorList)
}
export const getMessagesSigned = (contract: Contract, hash: string) => contract.methods.messagesSigned(hash).call()
export const getAffirmationsSigned = (contract: Contract, hash: string) =>
contract.methods.affirmationsSigned(hash).call()

View File

@ -1,312 +0,0 @@
import {
BLOCK_RANGE,
EXECUTE_AFFIRMATION_HASH,
EXECUTE_SIGNATURES_HASH,
FOREIGN_EXPLORER_API,
HOME_EXPLORER_API,
MAX_TX_SEARCH_BLOCK_RANGE,
SUBMIT_SIGNATURE_HASH
} from '../config/constants'
import { AbiItem } from 'web3-utils'
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
export interface APITransaction {
from: string
timeStamp: string
isError: string
input: string
to: string
hash: string
blockNumber: string
}
export interface APIPendingTransaction {
input: string
to: string
hash: string
}
export interface PendingTransactionsParams {
account: string
api: string
}
export interface AccountTransactionsParams {
account: string
startBlock: number
endBlock: number
api: string
}
export interface GetPendingTransactionParams {
account: string
to: string
messageData: string
}
export interface GetTransactionParams extends GetPendingTransactionParams {
startBlock: number
endBlock: number
}
export const fetchAccountTransactions = async ({ account, startBlock, endBlock, api }: AccountTransactionsParams) => {
const url = new URL(api)
url.searchParams.append('module', 'account')
url.searchParams.append('action', 'txlist')
url.searchParams.append('address', account)
url.searchParams.append('filterby', 'to')
url.searchParams.append('startblock', startBlock.toString())
url.searchParams.append('endblock', endBlock.toString())
const result = await fetch(url.toString()).then(res => res.json())
if (result.message === 'No transactions found') {
return []
}
return result.result || []
}
export const fetchPendingTransactions = async ({
account,
api
}: PendingTransactionsParams): Promise<APIPendingTransaction[]> => {
if (!api.includes('blockscout')) {
return []
}
const url = new URL(api)
url.searchParams.append('module', 'account')
url.searchParams.append('action', 'pendingtxlist')
url.searchParams.append('address', account)
try {
const result = await fetch(url.toString()).then(res => res.json())
if (result.status === '0') {
return []
}
return result.result
} catch (e) {
return []
}
}
export const getClosestBlockByTimestamp = async (api: string, timestamp: number): Promise<number> => {
if (api.includes('blockscout')) {
throw new Error('Blockscout does not support getblocknobytime')
}
const url = new URL(api)
url.searchParams.append('module', 'block')
url.searchParams.append('action', 'getblocknobytime')
url.searchParams.append('timestamp', timestamp.toString())
url.searchParams.append('closest', 'before')
const blockNumber = await fetch(url.toString()).then(res => res.json())
return parseInt(blockNumber.result)
}
// fast version of fetchAccountTransactions
// sequentially fetches transactions in small batches
// caches the result
const transactionsCache: { [key: string]: { lastBlock: number; transactions: APITransaction[] } } = {}
export const getAccountTransactions = async ({
account,
startBlock,
endBlock,
api
}: AccountTransactionsParams): Promise<APITransaction[]> => {
const key = `${account}-${startBlock}-${api}`
// initialize empty cache if it doesn't exist yet
if (!transactionsCache[key]) {
transactionsCache[key] = { lastBlock: startBlock - 1, transactions: [] }
}
// if cache contains events up to block X,
// new batch is fetched for range [X + 1, X + 1 + BLOCK_RANGE]
const newStartBlock = transactionsCache[key].lastBlock + 1
const newEndBlock = newStartBlock + BLOCK_RANGE
// search for new transactions only if max allowed block range is not yet exceeded
if (newEndBlock <= startBlock + MAX_TX_SEARCH_BLOCK_RANGE) {
const newTransactions = await fetchAccountTransactions({
account,
startBlock: newStartBlock,
endBlock: newEndBlock,
api
})
const transactions = transactionsCache[key].transactions.concat(...newTransactions)
// cache updated transactions list
transactionsCache[key].transactions = transactions
// enbBlock is assumed to be the current block number of the chain
// if the whole range is finalized, last block can be safely updated to the end of the range
// this works even if there are no transactions in the list
if (newEndBlock < endBlock) {
transactionsCache[key].lastBlock = newEndBlock
} else if (transactions.length > 0) {
transactionsCache[key].lastBlock = parseInt(transactions[transactions.length - 1].blockNumber, 10)
}
return transactions
}
console.warn(`Reached max transaction searching range, returning previously cached transactions for ${account}`)
return transactionsCache[key].transactions
}
export const getLogs = async (
api: string,
web3: Web3,
contract: Contract,
event: string,
options: { fromBlock: number; toBlock: number | 'latest'; topics: (string | null)[] }
) => {
const abi = contract.options.jsonInterface.find((abi: AbiItem) => abi.type === 'event' && abi.name === event)!
const url = new URL(api)
url.searchParams.append('module', 'logs')
url.searchParams.append('action', 'getLogs')
url.searchParams.append('address', contract.options.address)
url.searchParams.append('fromBlock', options.fromBlock.toString())
url.searchParams.append('toBlock', (options.toBlock || 'latest').toString())
const topics = [web3.eth.abi.encodeEventSignature(abi), ...(options.topics || [])]
for (let i = 0; i < topics.length; i++) {
if (topics[i] !== null) {
url.searchParams.append(`topic${i}`, topics[i] as string)
for (let j = 0; j < i; j++) {
if (topics[j] !== null) {
url.searchParams.append(`topic${j}_${i}_opr`, 'and')
}
}
}
}
const logs = await fetch(url.toString()).then(res => res.json())
return logs.result.map((log: any) => ({
transactionHash: log.transactionHash,
blockNumber: parseInt(log.blockNumber.slice(2), 16),
returnValues: web3.eth.abi.decodeLog(abi.inputs!, log.data, log.topics.slice(1))
}))
}
const filterSender = (from: string) => (tx: APITransaction) => tx.from.toLowerCase() === from.toLowerCase()
export const getFailedTransactions = async (
account: string,
to: string,
startBlock: number,
endBlock: number,
api: string,
getAccountTransactionsMethod = getAccountTransactions
): Promise<APITransaction[]> => {
const transactions = await getAccountTransactionsMethod({ account: to, startBlock, endBlock, api })
return transactions.filter(t => t.isError !== '0').filter(filterSender(account))
}
export const getSuccessTransactions = async (
account: string,
to: string,
startBlock: number,
endBlock: number,
api: string,
getAccountTransactionsMethod = getAccountTransactions
): Promise<APITransaction[]> => {
const transactions = await getAccountTransactionsMethod({ account: to, startBlock, endBlock, api })
return transactions.filter(t => t.isError === '0').filter(filterSender(account))
}
export const filterValidatorSignatureTransaction = (
transactions: APITransaction[],
messageData: string
): APITransaction[] => {
const messageDataValue = messageData.replace('0x', '')
return transactions.filter(
t =>
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
t.input.includes(messageDataValue)
)
}
export const getValidatorFailedTransactionsForMessage = async ({
account,
to,
messageData,
startBlock,
endBlock
}: GetTransactionParams): Promise<APITransaction[]> => {
const failedTransactions = await getFailedTransactions(account, to, startBlock, endBlock, HOME_EXPLORER_API)
return filterValidatorSignatureTransaction(failedTransactions, messageData)
}
export const getValidatorSuccessTransactionsForMessage = async ({
account,
to,
messageData,
startBlock,
endBlock
}: GetTransactionParams): Promise<APITransaction[]> => {
const transactions = await getSuccessTransactions(account, to, startBlock, endBlock, HOME_EXPLORER_API)
return filterValidatorSignatureTransaction(transactions, messageData)
}
export const getExecutionFailedTransactionForMessage = async (
{ account, to, messageData, startBlock, endBlock }: GetTransactionParams,
getFailedTransactionsMethod = getFailedTransactions
): Promise<APITransaction[]> => {
const failedTransactions = await getFailedTransactionsMethod(account, to, startBlock, endBlock, FOREIGN_EXPLORER_API)
const messageDataValue = messageData.replace('0x', '')
return failedTransactions.filter(t => t.input.includes(EXECUTE_SIGNATURES_HASH) && t.input.includes(messageDataValue))
}
export const getValidatorPendingTransactionsForMessage = async (
{ account, to, messageData }: GetPendingTransactionParams,
fetchPendingTransactionsMethod = fetchPendingTransactions
): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactionsMethod({
account,
api: HOME_EXPLORER_API
})
const toAddressLowerCase = to.toLowerCase()
const messageDataValue = messageData.replace('0x', '')
return pendingTransactions.filter(
t =>
t.to.toLowerCase() === toAddressLowerCase &&
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
t.input.includes(messageDataValue)
)
}
export const getExecutionPendingTransactionsForMessage = async (
{ account, to, messageData }: GetPendingTransactionParams,
fetchPendingTransactionsMethod = fetchPendingTransactions
): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactionsMethod({
account,
api: FOREIGN_EXPLORER_API
})
const toAddressLowerCase = to.toLowerCase()
const messageDataValue = messageData.replace('0x', '')
return pendingTransactions.filter(
t =>
t.to.toLowerCase() === toAddressLowerCase &&
t.input.includes(EXECUTE_SIGNATURES_HASH) &&
t.input.includes(messageDataValue)
)
}

View File

@ -1,201 +0,0 @@
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
import { HOME_RPC_POLLING_INTERVAL, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { GetTransactionParams, APITransaction, APIPendingTransaction, GetPendingTransactionParams } from './explorer'
import {
getValidatorConfirmation,
getValidatorFailedTransaction,
getValidatorPendingTransaction,
getSuccessExecutionTransaction
} from './validatorConfirmationHelpers'
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
import { signatureToVRS } from './signatures'
const mergeConfirmations = (oldConfirmations: ConfirmationParam[], newConfirmations: ConfirmationParam[]) => {
const confirmations = [...oldConfirmations]
newConfirmations.forEach(validatorData => {
const index = confirmations.findIndex(e => e.validator === validatorData.validator)
if (index === -1) {
confirmations.push(validatorData)
return
}
const currentStatus = confirmations[index].status
const newStatus = validatorData.status
if (
validatorData.txHash ||
!!validatorData.signature ||
(newStatus !== currentStatus && newStatus !== VALIDATOR_CONFIRMATION_STATUS.UNDEFINED)
) {
confirmations[index] = {
status: validatorData.status,
validator: validatorData.validator,
timestamp: confirmations[index].timestamp || validatorData.timestamp,
txHash: confirmations[index].txHash || validatorData.txHash,
signature: confirmations[index].signature || validatorData.signature
}
}
})
return confirmations
}
export const getConfirmationsForTx = async (
messageData: string,
web3: Web3,
validatorList: string[],
bridgeContract: Contract,
fromHome: boolean,
setResult: Function,
requiredSignatures: number,
setSignatureCollected: Function,
setTimeoutId: (timeoutId: number) => void,
isCancelled: () => boolean,
startBlock: number,
getFailedTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>,
setFailedConfirmations: Function,
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>,
setPendingConfirmations: Function,
getSuccessTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
) => {
const hashMsg = web3.utils.soliditySha3Raw(messageData)
let validatorConfirmations = await Promise.all(
validatorList.map(getValidatorConfirmation(web3, hashMsg, bridgeContract, fromHome))
)
const updateConfirmations = (confirmations: ConfirmationParam[]) => {
if (confirmations.length === 0) {
return
}
validatorConfirmations = mergeConfirmations(validatorConfirmations, confirmations)
setResult((currentConfirmations: ConfirmationParam[]) => {
if (currentConfirmations && currentConfirmations.length) {
return mergeConfirmations(currentConfirmations, confirmations)
}
return confirmations
})
}
const successConfirmations = validatorConfirmations.filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
const notSuccessConfirmations = validatorConfirmations.filter(c => c.status !== VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
const hasEnoughSignatures = successConfirmations.length >= requiredSignatures
updateConfirmations(validatorConfirmations)
setSignatureCollected(hasEnoughSignatures)
if (hasEnoughSignatures) {
setPendingConfirmations(false)
if (fromHome) {
// fetch collected signatures for possible manual processing
const signatures = await Promise.all(
Array.from(Array(requiredSignatures).keys()).map(i => bridgeContract.methods.signature(hashMsg, i).call())
)
const confirmations = signatures.flatMap(sig => {
const { v, r, s } = signatureToVRS(sig)
const address = web3.eth.accounts.recover(messageData, `0x${v}`, `0x${r}`, `0x${s}`)
return successConfirmations.filter(c => c.validator === address).map(c => ({ ...c, signature: sig }))
})
updateConfirmations(confirmations)
}
}
// get transactions from success signatures
const successConfirmationWithData = await Promise.all(
successConfirmations.map(
getSuccessExecutionTransaction(web3, bridgeContract, fromHome, messageData, startBlock, getSuccessTransactions)
)
)
const successConfirmationWithTxFound = successConfirmationWithData.filter(v => v.txHash !== '')
updateConfirmations(successConfirmationWithTxFound)
// If signatures not collected, look for pending transactions
if (!hasEnoughSignatures) {
// Check if confirmation is pending
const validatorPendingConfirmationsChecks = await Promise.all(
notSuccessConfirmations.map(getValidatorPendingTransaction(bridgeContract, messageData, getPendingTransactions))
)
const validatorPendingConfirmations = validatorPendingConfirmationsChecks.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
)
updateConfirmations(validatorPendingConfirmations)
setPendingConfirmations(validatorPendingConfirmations.length > 0)
}
const undefinedConfirmations = validatorConfirmations.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
)
// Check if confirmation failed
const validatorFailedConfirmationsChecks = await Promise.all(
undefinedConfirmations.map(
getValidatorFailedTransaction(web3, bridgeContract, messageData, startBlock, getFailedTransactions)
)
)
let validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED || c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
)
if (hasEnoughSignatures && !fromHome) {
const lastTS = Math.max(...successConfirmationWithTxFound.map(c => c.timestamp || 0))
validatorFailedConfirmations = validatorFailedConfirmations.map(
c =>
c.timestamp < lastTS
? c
: {
...c,
status: VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
}
)
}
setFailedConfirmations(
!hasEnoughSignatures && validatorFailedConfirmations.some(c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED)
)
updateConfirmations(validatorFailedConfirmations)
const missingConfirmations = validatorConfirmations.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
)
if (hasEnoughSignatures) {
// If signatures collected, it should set other signatures not found as not required
const notRequiredConfirmations = missingConfirmations.map(c => ({
validator: c.validator,
status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED,
timestamp: 0,
txHash: ''
}))
updateConfirmations(notRequiredConfirmations)
}
// retry if not all signatures are collected and some confirmations are still missing
// or some success transactions were not fetched successfully
if (
(!hasEnoughSignatures && missingConfirmations.length > 0) ||
successConfirmationWithTxFound.length < successConfirmationWithData.length
) {
if (!isCancelled()) {
const timeoutId = setTimeout(
() =>
getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
fromHome,
setResult,
requiredSignatures,
setSignatureCollected,
setTimeoutId,
isCancelled,
startBlock,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
),
HOME_RPC_POLLING_INTERVAL
)
setTimeoutId(timeoutId)
}
}
}

View File

@ -1,182 +0,0 @@
import { Contract, EventData } from 'web3-eth-contract'
import Web3 from 'web3'
import {
CACHE_KEY_EXECUTION_FAILED,
FOREIGN_EXPLORER_API,
FOREIGN_RPC_POLLING_INTERVAL,
HOME_EXPLORER_API,
HOME_RPC_POLLING_INTERVAL,
VALIDATOR_CONFIRMATION_STATUS
} from '../config/constants'
import { ExecutionData } from '../hooks/useMessageConfirmations'
import {
APIPendingTransaction,
APITransaction,
GetTransactionParams,
GetPendingTransactionParams,
getLogs
} from './explorer'
import { getBlock, MessageObject } from './web3'
import validatorsCache from '../services/ValidatorsCache'
import { foreignBlockNumberProvider, homeBlockNumberProvider } from '../services/BlockNumberProvider'
const getPastEventsWithFallback = (api: string, web3: Web3, contract: Contract, eventName: string, options: any) =>
contract.getPastEvents(eventName, options).catch(
() =>
api
? getLogs(api, web3, contract, eventName, {
fromBlock: options.fromBlock,
toBlock: options.toBlock,
topics: [null, null, options.filter.messageId]
})
: []
)
export const getSuccessExecutionData = async (
contract: Contract,
eventName: string,
web3: Web3,
messageId: string,
api: string = ''
) => {
// Since it filters by the message id, only one event will be fetched
// so there is no need to limit the range of the block to reduce the network traffic
const events: EventData[] = await getPastEventsWithFallback(api, web3, contract, eventName, {
fromBlock: 0,
toBlock: 'latest',
filter: {
messageId
}
})
if (events.length > 0) {
const event = events[0]
const [txReceipt, block] = await Promise.all([
web3.eth.getTransactionReceipt(event.transactionHash),
getBlock(web3, event.blockNumber)
])
const blockTimestamp = typeof block.timestamp === 'string' ? parseInt(block.timestamp) : block.timestamp
const validatorAddress = web3.utils.toChecksumAddress(txReceipt.from)
return {
status: VALIDATOR_CONFIRMATION_STATUS.EXECUTION_SUCCESS,
validator: validatorAddress,
txHash: event.transactionHash,
timestamp: blockTimestamp,
executionResult: event.returnValues.status,
blockNumber: event.blockNumber
}
}
return null
}
export const getFinalizationEvent = async (
fromHome: boolean,
contract: Contract,
web3: Web3,
setResult: React.Dispatch<React.SetStateAction<ExecutionData>>,
message: MessageObject,
setTimeoutId: (timeoutId: number) => void,
isCancelled: () => boolean,
startBlock: number,
collectedSignaturesEvent: Maybe<EventData>,
getFailedExecution: (args: GetTransactionParams) => Promise<APITransaction[]>,
setFailedExecution: Function,
getPendingExecution: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>,
setPendingExecution: Function,
setExecutionEventsFetched: Function
) => {
const eventName = fromHome ? 'RelayedMessage' : 'AffirmationCompleted'
const api = fromHome ? FOREIGN_EXPLORER_API : HOME_EXPLORER_API
const successExecutionData = await getSuccessExecutionData(contract, eventName, web3, message.id, api)
if (successExecutionData) {
setResult(successExecutionData)
} else {
setExecutionEventsFetched(true)
// If event is defined, it means it is a message from Home to Foreign
if (collectedSignaturesEvent) {
const validator = collectedSignaturesEvent.returnValues.authorityResponsibleForRelay
const pendingTransactions = await getPendingExecution({
account: validator,
messageData: message.data,
to: contract.options.address
})
// If the transaction is pending it sets the status and avoid making the request for failed transactions
if (pendingTransactions.length > 0) {
const pendingTx = pendingTransactions[0]
const nowTimestamp = Math.floor(new Date().getTime() / 1000.0)
setResult({
status: VALIDATOR_CONFIRMATION_STATUS.PENDING,
validator: validator,
txHash: pendingTx.hash,
timestamp: nowTimestamp,
executionResult: false,
blockNumber: 0
})
setPendingExecution(true)
} else {
const validatorExecutionCacheKey = `${CACHE_KEY_EXECUTION_FAILED}${validator}-${message.id}`
const failedFromCache = validatorsCache.get(validatorExecutionCacheKey)
const blockProvider = fromHome ? foreignBlockNumberProvider : homeBlockNumberProvider
if (!failedFromCache) {
const failedTransactions = await getFailedExecution({
account: validator,
to: contract.options.address,
messageData: message.data,
startBlock,
endBlock: blockProvider.get() || 0
})
if (failedTransactions.length > 0) {
const failedTx = failedTransactions[0]
// If validator execution failed, we cache the result to avoid doing future requests for a result that won't change
validatorsCache.set(validatorExecutionCacheKey, true)
const timestamp = parseInt(failedTx.timeStamp)
setResult({
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
validator: validator,
txHash: failedTx.hash,
timestamp,
executionResult: false,
blockNumber: parseInt(failedTx.blockNumber)
})
setFailedExecution(true)
}
}
}
}
if (!isCancelled()) {
const timeoutId = setTimeout(
() =>
getFinalizationEvent(
fromHome,
contract,
web3,
setResult,
message,
setTimeoutId,
isCancelled,
startBlock,
collectedSignaturesEvent,
getFailedExecution,
setFailedExecution,
getPendingExecution,
setPendingExecution,
setExecutionEventsFetched
),
fromHome ? FOREIGN_RPC_POLLING_INTERVAL : HOME_RPC_POLLING_INTERVAL
)
setTimeoutId(timeoutId)
}
}
}

View File

@ -1,42 +0,0 @@
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]
}

View File

@ -1,26 +0,0 @@
import Web3 from 'web3'
function strip0x(s: string) {
return Web3.utils.isHexStrict(s) ? s.substr(2) : s
}
export interface Signature {
v: string
r: string
s: string
}
export function signatureToVRS(rawSignature: string): Signature {
const signature = strip0x(rawSignature)
const v = signature.substr(64 * 2)
const r = signature.substr(0, 32 * 2)
const s = signature.substr(32 * 2, 32 * 2)
return { v, r, s }
}
export function packSignatures(array: Array<Signature>): string {
const length = strip0x(Web3.utils.toHex(array.length))
const msgLength = length.length === 1 ? `0${length}` : length
const [v, r, s] = array.reduce(([vs, rs, ss], { v, r, s }) => [vs + v, rs + r, ss + s], ['', '', ''])
return `0x${msgLength}${v}${r}${s}`
}

View File

@ -1,176 +0,0 @@
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
import validatorsCache from '../services/ValidatorsCache'
import { CACHE_KEY_FAILED, CACHE_KEY_SUCCESS, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { APIPendingTransaction, APITransaction, GetTransactionParams, GetPendingTransactionParams } from './explorer'
import { homeBlockNumberProvider } from '../services/BlockNumberProvider'
import { getAffirmationsSigned, getMessagesSigned } from './contract'
export const getValidatorConfirmation = (
web3: Web3,
hashMsg: string,
bridgeContract: Contract,
fromHome: boolean
) => async (validator: string): Promise<ConfirmationParam> => {
const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg)
const fromCache = validatorsCache.getData(hashSenderMsg)
if (fromCache) {
return fromCache
}
const confirmationContractMethod = fromHome ? getMessagesSigned : getAffirmationsSigned
const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg)
// If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change
if (confirmed) {
const confirmation: ConfirmationParam = {
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
validator,
timestamp: 0,
txHash: ''
}
validatorsCache.setData(hashSenderMsg, confirmation)
return confirmation
}
return {
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
validator,
timestamp: 0,
txHash: ''
}
}
export const getSuccessExecutionTransaction = (
web3: Web3,
bridgeContract: Contract,
fromHome: boolean,
messageData: string,
startBlock: number,
getSuccessTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
const { validator } = validatorData
const validatorCacheKey = `${CACHE_KEY_SUCCESS}${validatorData.validator}-${messageData}`
const fromCache = validatorsCache.getData(validatorCacheKey)
if (fromCache && fromCache.txHash) {
return fromCache
}
const transactions = await getSuccessTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData,
startBlock,
endBlock: homeBlockNumberProvider.get() || 0
})
let txHashTimestamp = 0
let txHash = ''
const status = VALIDATOR_CONFIRMATION_STATUS.SUCCESS
if (transactions.length > 0) {
const tx = transactions[0]
txHashTimestamp = parseInt(tx.timeStamp)
txHash = tx.hash
// cache the result
validatorsCache.setData(validatorCacheKey, {
validator,
status,
txHash,
timestamp: txHashTimestamp
})
}
return {
validator,
status,
txHash,
timestamp: txHashTimestamp
}
}
export const getValidatorFailedTransaction = (
web3: Web3,
bridgeContract: Contract,
messageData: string,
startBlock: number,
getFailedTransactions: (args: GetTransactionParams) => Promise<APITransaction[]>
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
const validatorCacheKey = `${CACHE_KEY_FAILED}${validatorData.validator}-${messageData}`
const failedFromCache = validatorsCache.getData(validatorCacheKey)
if (failedFromCache && failedFromCache.txHash) {
return failedFromCache
}
const failedTransactions = await getFailedTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData,
startBlock,
endBlock: homeBlockNumberProvider.get() || 0
})
// If validator signature failed, we cache the result to avoid doing future requests for a result that won't change
if (failedTransactions.length > 0) {
const failedTx = failedTransactions[0]
const confirmation: ConfirmationParam = {
status: VALIDATOR_CONFIRMATION_STATUS.FAILED,
validator: validatorData.validator,
txHash: failedTx.hash,
timestamp: parseInt(failedTx.timeStamp)
}
if (failedTx.input && failedTx.input.length > 10) {
try {
const res = web3.eth.abi.decodeParameters(['bytes', 'bytes'], `0x${failedTx.input.slice(10)}`)
confirmation.signature = res[0]
confirmation.status = VALIDATOR_CONFIRMATION_STATUS.FAILED_VALID
console.log(`Adding manual signature from failed message from ${validatorData.validator}`)
} catch {}
}
validatorsCache.setData(validatorCacheKey, confirmation)
return confirmation
}
return {
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
validator: validatorData.validator,
txHash: '',
timestamp: 0
}
}
export const getValidatorPendingTransaction = (
bridgeContract: Contract,
messageData: string,
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>
) => async (validatorData: ConfirmationParam): Promise<ConfirmationParam> => {
const failedTransactions = await getPendingTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData
})
const newStatus =
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.PENDING : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
let timestamp = 0
let txHash = ''
if (failedTransactions.length > 0) {
const failedTx = failedTransactions[0]
timestamp = Math.floor(new Date().getTime() / 1000.0)
txHash = failedTx.hash
}
return {
validator: validatorData.validator,
status: newStatus,
txHash,
timestamp
}
}

View File

@ -1,121 +0,0 @@
import Web3 from 'web3'
import { BlockTransactionString } from 'web3-eth'
import { TransactionReceipt } from 'web3-eth'
import { AbiItem } from 'web3-utils'
import memoize from 'fast-memoize'
import promiseRetry from 'promise-retry'
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
import { SnapshotProvider } from '../services/SnapshotProvider'
export interface MessageObject {
id: string
data: string
sender?: string
executor?: string
obToken?: string
obReceiver?: string
}
export interface WarnRule {
message: string
sender?: string
executor?: string
obToken?: string
obReceiver?: string
}
export const matchesRule = (rule: WarnRule, msg: MessageObject) => {
if (!msg.executor || !msg.sender) {
return false
}
if (!!rule.executor && rule.executor.toLowerCase() !== msg.executor.toLowerCase()) {
return false
}
if (!!rule.sender && rule.sender.toLowerCase() !== msg.sender.toLowerCase()) {
return false
}
if (!!rule.obToken && (!msg.obToken || rule.obToken.toLowerCase() !== msg.obToken.toLowerCase())) {
return false
}
if (!!rule.obReceiver && (!msg.obReceiver || rule.obReceiver.toLowerCase() !== msg.obReceiver.toLowerCase())) {
return false
}
return true
}
const rawGetWeb3 = (url: string) => new Web3(new Web3.providers.HttpProvider(url))
const memoized = memoize(rawGetWeb3)
export const getWeb3 = (url: string) => memoized(url)
export const filterEventsByAbi = (
txReceipt: TransactionReceipt,
web3: Web3,
bridgeAddress: string,
eventAbi: AbiItem
): MessageObject[] => {
const eventHash = web3.eth.abi.encodeEventSignature(eventAbi)
const events = txReceipt.logs.filter(e => e.address === bridgeAddress && e.topics[0] === eventHash)
if (!eventAbi || !eventAbi.inputs || !eventAbi.inputs.length) {
return []
}
const inputs = eventAbi.inputs
return events.map(e => {
const { messageId, encodedData } = web3.eth.abi.decodeLog(inputs, e.data, [e.topics[1]])
let sender, executor, obToken, obReceiver
if (encodedData.length >= 160) {
sender = `0x${encodedData.slice(66, 106)}`
executor = `0x${encodedData.slice(106, 146)}`
const dataOffset =
160 + (parseInt(encodedData.slice(154, 156), 16) + parseInt(encodedData.slice(156, 158), 16)) * 2 + 8
if (encodedData.length >= dataOffset + 64) {
obToken = `0x${encodedData.slice(dataOffset + 24, dataOffset + 64)}`
}
if (encodedData.length >= dataOffset + 128) {
obReceiver = `0x${encodedData.slice(dataOffset + 88, dataOffset + 128)}`
}
}
return {
id: messageId || '',
data: encodedData || '',
sender,
executor,
obToken,
obReceiver
}
})
}
export const getHomeMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
const UserRequestForSignatureAbi: AbiItem = HOME_AMB_ABI.filter(
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForSignature'
)[0]
return filterEventsByAbi(txReceipt, web3, bridgeAddress, UserRequestForSignatureAbi)
}
export const getForeignMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
const userRequestForAffirmationAbi: AbiItem = FOREIGN_AMB_ABI.filter(
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForAffirmation'
)[0]
return filterEventsByAbi(txReceipt, web3, bridgeAddress, userRequestForAffirmationAbi)
}
// In some rare cases the block data is not available yet for the block of a new event detected
// so this logic retry to get the block in case it fails
export const getBlock = async (web3: Web3, blockNumber: number): Promise<BlockTransactionString> =>
promiseRetry(async retry => {
const result = await web3.eth.getBlock(blockNumber)
if (!result) {
return retry('Error getting block data')
}
return result
})
export const getChainId = async (web3: Web3, snapshotProvider: SnapshotProvider) => {
let id = snapshotProvider.chainId()
if (id === 0) {
id = await web3.eth.getChainId()
}
return id
}

View File

@ -1,25 +0,0 @@
{
"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"
]
}

View File

@ -1,31 +0,0 @@
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",
}
}
};

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