Compare commits

...

21 Commits

Author SHA1 Message Date
Alexander Kolotov
c2f6b5e8ba Merge the develop branch to the master branch, preparation to v2.3.1 2020-08-11 20:13:58 +03:00
Alexander Kolotov
f3f226afdf Correction of the message for a transaction without the bridge requests (#416) 2020-08-06 23:43:13 +03:00
Kirill Fedoseev
1eb8a8b1dc Use requestGasLimit parameter in AMB oracle in estimateGas (#415) 2020-08-06 16:15:51 +03:00
Alexander Kolotov
8a42cfbe2b Merge the develop branch to the master branch, preparation to v2.3.0 2020-07-29 16:13:24 +03:00
Gerardo Nardelli
ab406bc1db Add alm basic e2e tests (#407) 2020-07-29 13:30:36 +03:00
Gerardo Nardelli
8cf73d572c Correctly set a validator status as not required (#410) 2020-07-27 22:23:08 +03:00
Jongchan J
fa6b37db1c Change the remote RPC url for Ethereum Classic (#408) 2020-07-26 00:16:10 +03:00
Alexander Kolotov
64cd258354 Merge the develop branch to the master branch, preparation to v2.3.0-rc1 2020-07-22 23:34:22 +03:00
Alexander Kolotov
7a48495118 Update the contract's submodule to the release 5.2.0 (#406) 2020-07-22 23:24:47 +03:00
Gerardo Nardelli
fdfa5cd7af Add new status for when there is no registered activity by validators (#401) 2020-07-21 00:44:29 +03:00
Gerardo Nardelli
77bc6c662a Add info alert in ALM (#402) 2020-07-20 18:29:16 +03:00
Gerardo Nardelli
dc060387bc Update ALM logic to continue displaying failed signatures from validators (#399) 2020-07-17 23:59:25 +03:00
Gerardo Nardelli
efc433e9e0 Set descriptions for every state in ALM (#398) 2020-07-14 22:55:22 +03:00
Kirill Fedoseev
ebd97dce5c Speed up e2e building process (#395) 2020-07-14 17:53:05 +03:00
Gerardo Nardelli
42953ffe30 Properly handle not found transaction and disable auto refresh (#397) 2020-07-13 21:11:43 +03:00
Kirill Fedoseev
4f6d53964f Possibility to send a tx through all provided RPC endpoints (#394) 2020-07-13 15:09:07 +03:00
Kirill Fedoseev
9e6833eb40 Update bw plugin README and version (#389) 2020-07-10 11:28:03 +03:00
Gerardo Nardelli
4c44aa5fcd Add Alm unit tests (#388) 2020-07-09 13:20:49 +03:00
Kirill Fedoseev
2edd8f2783 Add bw plugin assets (#387) 2020-07-07 17:26:14 +03:00
Alexander Kolotov
861c755b09 Merge branch 'master' into develop 2020-07-06 21:34:40 +03:00
Gerardo Nardelli
8c268d6f06 Add ALM snapshots (#382) 2020-07-06 21:33:23 +03:00
107 changed files with 3803 additions and 618 deletions

View File

@@ -36,20 +36,6 @@ orbs:
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get -y install yarn
yarn-install-cached-on-machine:
steps:
- restore_cache:
name: Restore Machine Yarn Package Cache
keys:
- yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- run:
name: Install npm dependencies using Yarn
command: nvm use default; yarn install --frozen-lockfile
- save_cache:
name: Save Machine Yarn Package Cache
key: yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
wait-for-oracle:
parameters:
redis-key:
@@ -73,21 +59,30 @@ orbs:
echo "Sleeping..."
sleep 3
done
init-repo:
steps:
- checkout
- run: git submodule update --init
executors:
docker-node:
docker:
- image: circleci/node:10.15
machine-with-docker-caching:
machine-with-dlc:
machine:
image: ubuntu-1604:202007-01
docker_layer_caching: true
classic-machine-without-dlc:
machine:
image: circleci/classic:latest
docker_layer_caching: true
machine-without-dlc:
machine:
image: ubuntu-1604:202007-01
jobs:
initialize:
executor: tokenbridge-orb/docker-node
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/init-repo
- restore_cache:
name: Restore Yarn Package Cache
keys:
@@ -140,30 +135,43 @@ jobs:
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn run test
build-e2e-images:
executor: tokenbridge-orb/machine-without-dlc
steps:
- tokenbridge-orb/init-repo
- run:
command: |
docker login -u ${DOCKER_LOGIN} -p ${DOCKER_PASSWORD}
cd e2e-commons
docker-compose build oracle monitor ui alm e2e molecule_runner
docker-compose push oracle monitor ui alm e2e molecule_runner
oracle-e2e:
executor: tokenbridge-orb/docker-node
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- run: git submodule update --init
- setup_remote_docker:
docker_layer_caching: true
- run: yarn run oracle-e2e
- tokenbridge-orb/init-repo
- run: ./oracle-e2e/run-tests.sh
ui-e2e:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- tokenbridge-orb/install-node
- tokenbridge-orb/install-yarn
- tokenbridge-orb/install-chrome
- run: git submodule update --init
- tokenbridge-orb/yarn-install-cached-on-machine
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn run ui-e2e
monitor-e2e:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/init-repo
- run: ./monitor-e2e/run-tests.sh
alm-e2e:
executor: tokenbridge-orb/machine-without-dlc
steps:
- tokenbridge-orb/install-node
- tokenbridge-orb/install-yarn
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn run alm-e2e
cover:
executor: tokenbridge-orb/docker-node
steps:
@@ -172,55 +180,47 @@ jobs:
- run: yarn workspace ui run coverage
- run: yarn workspace ui run coveralls
deployment-oracle:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/init-repo
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh oracle
no_output_timeout: 40m
deployment-ui:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/init-repo
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh ui
no_output_timeout: 40m
deployment-monitor:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/init-repo
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh monitor
no_output_timeout: 40m
deployment-repo:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/install-node
- tokenbridge-orb/install-yarn
- tokenbridge-orb/yarn-install-cached-on-machine
- tokenbridge-orb/init-repo
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh repo
no_output_timeout: 40m
deployment-multiple:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/machine-without-dlc
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/init-repo
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh multiple
no_output_timeout: 40m
ultimate:
executor: tokenbridge-orb/machine-with-docker-caching
executor: tokenbridge-orb/classic-machine-without-dlc
parameters:
scenario-name:
description: "Molecule scenario name used to create the infrastructure"
@@ -237,12 +237,11 @@ jobs:
default: ''
type: string
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/install-node
- tokenbridge-orb/install-chrome
- tokenbridge-orb/install-yarn
- tokenbridge-orb/yarn-install-cached-on-machine
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run:
name: Prepare the infrastructure
command: e2e-commons/up.sh deploy << parameters.scenario-name >> blocks
@@ -286,35 +285,97 @@ workflows:
filters:
branches:
only: master
- oracle-e2e
- ui-e2e
- monitor-e2e
- deployment-oracle
- deployment-ui
- deployment-monitor
- deployment-repo
- deployment-multiple
- build-e2e-images
- oracle-e2e:
requires:
- build-e2e-images
- ui-e2e:
requires:
- build-e2e-images
- initialize
- monitor-e2e:
requires:
- build-e2e-images
- alm-e2e:
requires:
- build-e2e-images
- initialize
- deployment-oracle:
requires:
- build-e2e-images
- deployment-ui:
requires:
- build-e2e-images
- deployment-monitor:
requires:
- build-e2e-images
- deployment-repo:
requires:
- build-e2e-images
- deployment-multiple:
requires:
- build-e2e-images
- ultimate:
requires:
- build-e2e-images
- initialize
filters:
branches:
only:
- master
- develop
name: "ultimate: native to erc"
scenario-name: native-to-erc
redis-key: native-erc-collected-signatures:lastProcessedBlock
ui-e2e-grep: "NATIVE TO ERC"
- ultimate:
requires:
- build-e2e-images
- initialize
filters:
branches:
only:
- master
- develop
name: "ultimate: erc to native"
scenario-name: erc-to-native
redis-key: erc-native-collected-signatures:lastProcessedBlock
ui-e2e-grep: "ERC TO NATIVE"
- ultimate:
requires:
- build-e2e-images
- initialize
filters:
branches:
only:
- master
- develop
name: "ultimate: erc to erc"
scenario-name: erc-to-erc
redis-key: erc-erc-collected-signatures:lastProcessedBlock
ui-e2e-grep: "ERC TO ERC"
- ultimate:
requires:
- build-e2e-images
- initialize
filters:
branches:
only:
- master
- develop
name: "ultimate: amb"
scenario-name: amb
redis-key: amb-collected-signatures:lastProcessedBlock
oracle-e2e-script: "amb"
- ultimate:
requires:
- build-e2e-images
- initialize
filters:
branches:
only:
- master
- develop
name: "ultimate: amb stake erc to erc"
scenario-name: ultimate-amb-stake-erc-to-erc
redis-key: amb-collected-signatures:lastProcessedBlock

2
.nvmrc
View File

@@ -1 +1 @@
10.16
10.22

View File

@@ -36,6 +36,7 @@ ORACLE_LOG_LEVEL | Set the level of details in the logs. | `trace` / `debug` / `
ORACLE_MAX_PROCESSING_TIME | The workers processes will be killed if this amount of time (in milliseconds) is elapsed before they finish processing. It is recommended to set this value to 4 times the value of the longest polling time (set with the `HOME_POLLING_INTERVAL` and `FOREIGN_POLLING_INTERVAL` variables). To disable this, set the time to 0. | integer
ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY | The private key of the bridge validator used to sign confirmations before sending transactions to the bridge contracts. The validator account is calculated automatically from the private key. Every bridge instance (set of watchers and senders) must have its own unique private key. The specified private key is used to sign transactions on both sides of the bridge. | hexidecimal without "0x"
ORACLE_VALIDATOR_ADDRESS | The public address of the bridge validator | hexidecimal with "0x"
ORACLE_TX_REDUNDANCY | If set to `true`, instructs oracle to send `eth_sendRawTransaction` requests through all available RPC urls defined in `COMMON_HOME_RPC_URL` and `COMMON_FOREIGN_RPC_URL` variables instead of using first available one
## UI configuration

View File

@@ -1,15 +1,31 @@
FROM node:10
FROM node:8 as contracts
WORKDIR /mono
COPY contracts/package.json contracts/package-lock.json ./contracts/
WORKDIR /mono/contracts
RUN npm install --only=prod
COPY ./contracts/truffle-config.js ./
COPY ./contracts/contracts ./contracts
RUN npm run compile
FROM node:8
WORKDIR /mono
COPY package.json .
COPY --from=contracts /mono/contracts/build ./contracts/build
COPY oracle-e2e/package.json ./oracle-e2e/
COPY monitor-e2e/package.json ./monitor-e2e/
COPY contracts/package.json ./contracts/
COPY yarn.lock .
RUN yarn install --frozen-lockfile
COPY ./contracts ./contracts
RUN yarn install:deploy
RUN yarn compile:contracts
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production
COPY . .
COPY ./contracts/deploy ./contracts/deploy
RUN yarn install:deploy
COPY commons/ ./commons/
COPY oracle-e2e/ ./oracle-e2e/
COPY monitor-e2e/ ./monitor-e2e/
COPY e2e-commons/ ./e2e-commons/

15
alm-e2e/.eslintrc Normal file
View File

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

24
alm-e2e/package.json Normal file
View File

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

13
alm-e2e/run-tests.sh Normal file
View File

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

50
alm-e2e/src/test.js Normal file
View File

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

View File

@@ -0,0 +1,15 @@
const waitUntil = async (predicate, step = 100, timeout = 20000) => {
const stopTime = Date.now() + timeout
while (Date.now() <= stopTime) {
const result = await predicate()
if (result) {
return result
}
await new Promise(resolve => setTimeout(resolve, step)) // sleep
}
throw new Error(`waitUntil timed out after ${timeout} ms`)
}
module.exports = {
waitUntil
}

2
alm/.gitignore vendored
View File

@@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
src/snapshots/*.json
# dependencies
/node_modules
/.pnp

View File

@@ -1,20 +1,29 @@
FROM node:8 as contracts
WORKDIR /mono
COPY contracts/package.json contracts/package-lock.json ./contracts/
WORKDIR /mono/contracts
RUN npm install --only=prod
COPY ./contracts/truffle-config.js ./
COPY ./contracts/contracts ./contracts
RUN npm run compile
FROM node:12 as alm-builder
WORKDIR /mono
COPY package.json .
COPY contracts/package.json ./contracts/
COPY --from=contracts /mono/contracts/build ./contracts/build
COPY commons/package.json ./commons/
COPY alm/package.json ./alm/
COPY yarn.lock .
RUN yarn install --production --frozen-lockfile
COPY ./contracts ./contracts
RUN yarn run compile:contracts
RUN mv ./contracts/build ./ && rm -rf ./contracts/* ./contracts/.[!.]* && mv ./build ./contracts/
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production
COPY ./commons ./commons
COPY ./alm ./alm
ARG DOT_ENV_PATH=./alm/.env
COPY ${DOT_ENV_PATH} ./alm/.env

View File

@@ -17,6 +17,7 @@
"@use-it/interval": "^0.1.3",
"customize-cra": "^1.0.0",
"date-fns": "^2.14.0",
"dotenv": "^8.2.0",
"fast-memoize": "^2.5.2",
"promise-retry": "^2.0.1",
"react": "^16.13.1",
@@ -30,11 +31,12 @@
"web3-eth-contract": "1.2.7"
},
"scripts": {
"start": "./load-env.sh react-app-rewired start",
"build": "./load-env.sh react-app-rewired build",
"start": "yarn createSnapshots && ./load-env.sh react-app-rewired start",
"build": "yarn createSnapshots && ./load-env.sh react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject",
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore"
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore",
"createSnapshots": "node scripts/createSnapshots.js"
},
"eslintConfig": {
"extends": "react-app"

View File

@@ -0,0 +1,118 @@
const { BRIDGE_VALIDATORS_ABI, HOME_AMB_ABI } = require('commons')
const path = require('path')
require('dotenv').config()
const Web3 = require('web3')
const fs = require('fs')
const {
COMMON_HOME_RPC_URL,
COMMON_HOME_BRIDGE_ADDRESS,
COMMON_FOREIGN_RPC_URL,
COMMON_FOREIGN_BRIDGE_ADDRESS
} = process.env
const generateSnapshot = async (side, url, bridgeAddress) => {
const snapshotPath = `../src/snapshots/${side}.json`
const snapshotFullPath = path.join(__dirname, snapshotPath)
const snapshot = {}
const web3 = new Web3(new Web3.providers.HttpProvider(url))
const currentBlockNumber = await web3.eth.getBlockNumber()
snapshot.snapshotBlockNumber = currentBlockNumber
// Save chainId
snapshot.chainId = await web3.eth.getChainId()
const bridgeContract = new web3.eth.Contract(HOME_AMB_ABI, bridgeAddress)
// Save RequiredBlockConfirmationChanged events
let requiredBlockConfirmationChangedEvents = await bridgeContract.getPastEvents('RequiredBlockConfirmationChanged', {
fromBlock: 0,
toBlock: currentBlockNumber
})
// In case RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB
// manually generate an event for this. Example Sokol - Kovan bridge
if (requiredBlockConfirmationChangedEvents.length === 0) {
const deployedAtBlock = await bridgeContract.methods.deployedAtBlock().call()
const blockConfirmations = await bridgeContract.methods.requiredBlockConfirmations().call()
requiredBlockConfirmationChangedEvents.push({
blockNumber: parseInt(deployedAtBlock),
returnValues: {
requiredBlockConfirmations: blockConfirmations
}
})
}
snapshot.RequiredBlockConfirmationChanged = requiredBlockConfirmationChangedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
requiredBlockConfirmations: e.returnValues.requiredBlockConfirmations
}
}))
const validatorAddress = await bridgeContract.methods.validatorContract().call()
const validatorContract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorAddress)
// Save RequiredSignaturesChanged events
const RequiredSignaturesChangedEvents = await validatorContract.getPastEvents('RequiredSignaturesChanged', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.RequiredSignaturesChanged = RequiredSignaturesChangedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
requiredSignatures: e.returnValues.requiredSignatures
}
}))
// Save ValidatorAdded events
const validatorAddedEvents = await validatorContract.getPastEvents('ValidatorAdded', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.ValidatorAdded = validatorAddedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
validator: e.returnValues.validator
},
event: 'ValidatorAdded'
}))
// Save ValidatorRemoved events
const validatorRemovedEvents = await validatorContract.getPastEvents('ValidatorRemoved', {
fromBlock: 0,
toBlock: currentBlockNumber
})
snapshot.ValidatorRemoved = validatorRemovedEvents.map(e => ({
blockNumber: e.blockNumber,
returnValues: {
validator: e.returnValues.validator
},
event: 'ValidatorRemoved'
}))
// Write snapshot
fs.writeFileSync(snapshotFullPath, JSON.stringify(snapshot, null, 2))
}
const main = async () => {
await Promise.all([
generateSnapshot('home', COMMON_HOME_RPC_URL, COMMON_HOME_BRIDGE_ADDRESS),
generateSnapshot('foreign', COMMON_FOREIGN_RPC_URL, COMMON_FOREIGN_BRIDGE_ADDRESS)
])
}
main()
.then(() => process.exit(0))
.catch(error => {
console.log('Error while creating snapshots')
console.error(error)
process.exit(0)
})

View File

@@ -1,5 +0,0 @@
import React from 'react'
test('renders learn react link', () => {
// Removed basic test from setup. Keeping this so CI passes until we add unit tests.
})

View File

@@ -4,7 +4,7 @@ import { useMessageConfirmations } from '../hooks/useMessageConfirmations'
import { MessageObject } from '../utils/web3'
import styled from 'styled-components'
import { CONFIRMATIONS_STATUS } from '../config/constants'
import { CONFIRMATIONS_STATUS_LABEL } from '../config/descriptions'
import { CONFIRMATIONS_STATUS_LABEL, CONFIRMATIONS_STATUS_LABEL_HOME } from '../config/descriptions'
import { SimpleLoading } from './commons/Loading'
import { ValidatorsConfirmations } from './ValidatorsConfirmations'
import { getConfirmationsStatusDescription } from '../utils/networks'
@@ -12,6 +12,8 @@ import { useStateProvider } from '../state/StateProvider'
import { ExecutionConfirmation } from './ExecutionConfirmation'
import { useValidatorContract } from '../hooks/useValidatorContract'
import { useBlockConfirmations } from '../hooks/useBlockConfirmations'
import { MultiLine } from './commons/MultiLine'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
const StatusLabel = styled.label`
font-weight: bold;
@@ -47,7 +49,7 @@ export const ConfirmationsContainer = ({ message, receipt, fromHome, timestamp }
} = useStateProvider()
const { requiredSignatures, validatorList } = useValidatorContract({ fromHome, receipt })
const { blockConfirmations } = useBlockConfirmations({ fromHome, receipt })
const { confirmations, status, executionData, signatureCollected } = useMessageConfirmations({
const { confirmations, status, executionData, signatureCollected, waitingBlocksResolved } = useMessageConfirmations({
message,
receipt,
fromHome,
@@ -57,26 +59,48 @@ export const ConfirmationsContainer = ({ message, receipt, fromHome, timestamp }
blockConfirmations
})
const statusLabel = fromHome ? CONFIRMATIONS_STATUS_LABEL_HOME : CONFIRMATIONS_STATUS_LABEL
const parseDescription = () => {
let description = getConfirmationsStatusDescription(status, homeName, foreignName, fromHome)
let link
const descArray = description.split('%link')
if (descArray.length > 1) {
description = descArray[0]
link = (
<ExplorerTxLink href={descArray[1]} target="_blank" rel="noopener noreferrer">
{descArray[1]}
</ExplorerTxLink>
)
}
return (
<div>
{description}
{link}
</div>
)
}
return (
<div className="row is-center">
<StyledConfirmationContainer className="col-9">
<div className="row is-center">
<StatusLabel>Status:</StatusLabel>
<StatusResultLabel>
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? CONFIRMATIONS_STATUS_LABEL[status] : <SimpleLoading />}
<StatusResultLabel data-id="status">
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? statusLabel[status] : <SimpleLoading />}
</StatusResultLabel>
</div>
<StatusDescription className="row is-center">
<p className="col-10">
{status !== CONFIRMATIONS_STATUS.UNDEFINED
? getConfirmationsStatusDescription(status, homeName, foreignName)
: ''}
</p>
<MultiLine className="col-10">
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? parseDescription() : ''}
</MultiLine>
</StatusDescription>
<ValidatorsConfirmations
confirmations={confirmations}
requiredSignatures={requiredSignatures}
validatorList={validatorList}
waitingBlocksResolved={waitingBlocksResolved}
/>
{signatureCollected && <ExecutionConfirmation executionData={executionData} isHome={!fromHome} />}
</StyledConfirmationContainer>

View File

@@ -1,16 +1,13 @@
import React from 'react'
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
import { useWindowWidth } from '@react-hook/window-size'
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { SimpleLoading } from './commons/Loading'
import styled from 'styled-components'
import { ExecutionData } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
import { Thead, AgeTd, StatusTd } from './commons/Table'
const StyledExecutionConfirmation = styled.div`
margin-top: 30px;
@@ -38,7 +35,11 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
return <GreyLabel>{validatorStatus}</GreyLabel>
default:
return <SimpleLoading />
return executionData.validator ? (
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
) : (
<SimpleLoading />
)
}
}
@@ -55,12 +56,18 @@ export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfir
<tbody>
<tr>
<td>{formattedValidator ? formattedValidator : <SimpleLoading />}</td>
<td className="text-center">{getExecutionStatusElement(executionData.status)}</td>
<td className="text-center">
<ExplorerTxLink href={txExplorerLink} target="_blank">
{executionData.timestamp > 0 ? formatTimestamp(executionData.timestamp) : ''}
</ExplorerTxLink>
</td>
<StatusTd className="text-center">{getExecutionStatusElement(executionData.status)}</StatusTd>
<AgeTd className="text-center">
{executionData.timestamp > 0 ? (
<ExplorerTxLink href={txExplorerLink} target="_blank">
{formatTimestamp(executionData.timestamp)}
</ExplorerTxLink>
) : executionData.status === VALIDATOR_CONFIRMATION_STATUS.WAITING ? (
''
) : (
SEARCHING_TX
)}
</AgeTd>
</tr>
</tbody>
</table>

View File

@@ -35,8 +35,13 @@ export const Form = ({ onSubmit }: { onSubmit: ({ chainId, txHash, receipt }: Fo
onSubmit({ chainId, txHash, receipt })
}
const onBack = () => {
setTxHash('')
setSearchTx(false)
}
if (searchTx) {
return <TransactionSelector txHash={txHash} onSelected={onSelected} />
return <TransactionSelector txHash={txHash} onSelected={onSelected} onBack={onBack} />
}
return (

View File

@@ -1,10 +1,13 @@
import React, { useState } from 'react'
import React, { useEffect, useState, useCallback } from 'react'
import styled from 'styled-components'
import { Route, useHistory } from 'react-router-dom'
import { Form } from './Form'
import { StatusContainer } from './StatusContainer'
import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth'
import { InfoAlert } from './commons/InfoAlert'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { FOREIGN_NETWORK_NAME, HOME_NETWORK_NAME } from '../config/constants'
const StyledMainPage = styled.div`
text-align: center;
@@ -32,6 +35,14 @@ const HeaderContainer = styled.header`
}
`
const AlertP = styled.p`
align-items: start;
margin-bottom: 0;
@media (max-width: 600px) {
flex-direction: column;
}
`
export interface FormSubmitParams {
chainId: number
txHash: string
@@ -43,6 +54,27 @@ export const MainPage = () => {
const { home, foreign } = useStateProvider()
const [networkName, setNetworkName] = useState('')
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
const [showInfoAlert, setShowInfoAlert] = useState(false)
const loadFromStorage = useCallback(() => {
const hideAlert = window.localStorage.getItem('hideInfoAlert')
setShowInfoAlert(!hideAlert)
}, [])
useEffect(
() => {
loadFromStorage()
},
[loadFromStorage]
)
const onAlertClose = useCallback(
() => {
window.localStorage.setItem('hideInfoAlert', 'true')
loadFromStorage()
},
[loadFromStorage]
)
const setNetworkData = (chainId: number) => {
const network = chainId === home.chainId ? home.name : foreign.name
@@ -64,6 +96,13 @@ export const MainPage = () => {
setNetworkData(chainId)
}
useEffect(() => {
const w = window as any
if (w.ethereum) {
w.ethereum.autoRefreshOnNetworkChange = false
}
}, [])
return (
<StyledMainPage>
<Header>
@@ -73,6 +112,25 @@ export const MainPage = () => {
</HeaderContainer>
</Header>
<div className="container">
{showInfoAlert && (
<InfoAlert onClick={onAlertClose}>
<p className="is-left text-left">
The Arbitrary Message Bridge Live Monitoring application provides real-time status updates for messages
bridged between {HOME_NETWORK_NAME} and {FOREIGN_NETWORK_NAME}. You can check current tx status, view
validator info, and troubleshoot potential issues with bridge transfers.
</p>
<AlertP className="is-left text-left">
For more information refer to&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']}

View File

@@ -1,5 +1,5 @@
import React, { useEffect } from 'react'
import { Link, useHistory, useParams } from 'react-router-dom'
import { useHistory, useParams } from 'react-router-dom'
import { useTransactionStatus } from '../hooks/useTransactionStatus'
import { formatTxHash, getExplorerTxUrl, getTransactionStatusDescription, validTxHash } from '../utils/networks'
import { TRANSACTION_STATUS } from '../config/constants'
@@ -8,23 +8,8 @@ import { Loading } from './commons/Loading'
import { useStateProvider } from '../state/StateProvider'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { ConfirmationsContainer } from './ConfirmationsContainer'
import { LeftArrow } from './commons/LeftArrow'
import styled from 'styled-components'
import { TransactionReceipt } from 'web3-eth'
const BackButton = styled.button`
color: var(--button-color);
border-color: var(--font-color);
margin-top: 10px;
&:focus {
outline: var(--button-color);
}
`
const BackLabel = styled.label`
margin-left: 5px;
cursor: pointer;
`
import { BackButton } from './commons/BackButton'
export interface StatusContainerParam {
onBackToMain: () => void
@@ -79,10 +64,6 @@ export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptPar
const displayReference = multiMessageSelected ? messages[selectedMessageId].id : txHash
const formattedMessageId = formatTxHash(displayReference)
const displayedDescription = multiMessageSelected
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
: description
const isHome = chainId === home.chainId.toString()
const txExplorerLink = getExplorerTxUrl(txHash, isHome)
const displayExplorerLink = status !== TRANSACTION_STATUS.NOT_FOUND
@@ -90,33 +71,39 @@ export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptPar
const displayConfirmations = status === TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE || multiMessageSelected
const messageToConfirm =
messages.length > 1 ? messages[selectedMessageId] : messages.length > 0 ? messages[0] : { id: '', data: '' }
let displayedDescription: string = multiMessageSelected
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
: description
let link
const descArray = displayedDescription.split('%link')
if (descArray.length > 1) {
displayedDescription = descArray[0]
link = (
<ExplorerTxLink href={descArray[1]} target="_blank" rel="noopener noreferrer">
{descArray[1]}
</ExplorerTxLink>
)
}
return (
<div>
{status && (
<p>
The request{' '}
The transaction{' '}
{displayExplorerLink && (
<ExplorerTxLink href={txExplorerLink} target="blank">
<ExplorerTxLink href={txExplorerLink} target="_blank">
{formattedMessageId}
</ExplorerTxLink>
)}
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription}
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription} {link}
</p>
)}
{displayMessageSelector && <MessageSelector messages={messages} onMessageSelected={onMessageSelected} />}
{displayConfirmations && (
<ConfirmationsContainer message={messageToConfirm} receipt={receipt} fromHome={isHome} timestamp={timestamp} />
)}
<div className="row is-center">
<div className="col-9">
<Link to="/" onClick={onBackToMain}>
<BackButton className="button outline is-left">
<LeftArrow />
<BackLabel>Search another transaction</BackLabel>
</BackButton>
</Link>
</div>
</div>
<BackButton onBackToMain={onBackToMain} />
</div>
)
}

View File

@@ -5,13 +5,23 @@ import { TRANSACTION_STATUS } from '../config/constants'
import { TransactionReceipt } from 'web3-eth'
import { Loading } from './commons/Loading'
import { NetworkTransactionSelector } from './NetworkTransactionSelector'
import { BackButton } from './commons/BackButton'
import { TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
import { MultiLine } from './commons/MultiLine'
import styled from 'styled-components'
const StyledMultiLine = styled(MultiLine)`
margin-bottom: 40px;
`
export const TransactionSelector = ({
txHash,
onSelected
onSelected,
onBack
}: {
txHash: string
onSelected: (chainId: number, receipt: TransactionReceipt) => void
onBack: () => void
}) => {
const { home, foreign } = useStateProvider()
const { receipt: homeReceipt, status: homeStatus } = useTransactionFinder({ txHash, web3: home.web3 })
@@ -43,5 +53,15 @@ export const TransactionSelector = ({
return <NetworkTransactionSelector onNetworkSelected={onSelectedNetwork} />
}
if (foreignStatus === TRANSACTION_STATUS.NOT_FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
const message = TRANSACTION_STATUS_DESCRIPTION[TRANSACTION_STATUS.NOT_FOUND]
return (
<div>
<StyledMultiLine>{message}</StyledMultiLine>
<BackButton onBackToMain={onBack} />
</div>
)
}
return <Loading />
}

View File

@@ -1,16 +1,13 @@
import React from 'react'
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
import { useWindowWidth } from '@react-hook/window-size'
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { SEARCHING_TX, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { SimpleLoading } from './commons/Loading'
import styled from 'styled-components'
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
import { Thead, AgeTd, StatusTd } from './commons/Table'
const RequiredConfirmations = styled.label`
font-size: 14px;
@@ -20,12 +17,14 @@ export interface ValidatorsConfirmationsParams {
confirmations: Array<ConfirmationParam>
requiredSignatures: number
validatorList: string[]
waitingBlocksResolved: boolean
}
export const ValidatorsConfirmations = ({
confirmations,
requiredSignatures,
validatorList
validatorList,
waitingBlocksResolved
}: ValidatorsConfirmationsParams) => {
const windowWidth = useWindowWidth()
@@ -40,7 +39,11 @@ export const ValidatorsConfirmations = ({
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
return <GreyLabel>{validatorStatus}</GreyLabel>
default:
return <SimpleLoading />
return waitingBlocksResolved ? (
<GreyLabel>{VALIDATOR_CONFIRMATION_STATUS.WAITING}</GreyLabel>
) : (
<SimpleLoading />
)
}
}
@@ -63,21 +66,28 @@ export const ValidatorsConfirmations = ({
const elementIfNoTimestamp =
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.WAITING &&
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED ? (
<SimpleLoading />
(displayedStatus === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || displayedStatus === '') &&
waitingBlocksResolved ? (
SEARCHING_TX
) : (
<SimpleLoading />
)
) : (
''
)
return (
<tr key={i}>
<td>{windowWidth < 850 ? formatTxHash(validator) : validator}</td>
<td className="text-center">{getValidatorStatusElement(displayedStatus)}</td>
<td className="text-center">
<ExplorerTxLink href={explorerLink} target="_blank">
{confirmation && confirmation.timestamp > 0
? formatTimestamp(confirmation.timestamp)
: elementIfNoTimestamp}
</ExplorerTxLink>
</td>
<StatusTd className="text-center">{getValidatorStatusElement(displayedStatus)}</StatusTd>
<AgeTd className="text-center">
{confirmation && confirmation.timestamp > 0 ? (
<ExplorerTxLink href={explorerLink} target="_blank">
{formatTimestamp(confirmation.timestamp)}
</ExplorerTxLink>
) : (
elementIfNoTimestamp
)}
</AgeTd>
</tr>
)
})}

View File

@@ -0,0 +1,35 @@
import { Link } from 'react-router-dom'
import { LeftArrow } from './LeftArrow'
import React from 'react'
import styled from 'styled-components'
const StyledButton = styled.button`
color: var(--button-color);
border-color: var(--font-color);
margin-top: 10px;
&:focus {
outline: var(--button-color);
}
`
const BackLabel = styled.label`
margin-left: 5px;
cursor: pointer;
`
export interface BackButtonParam {
onBackToMain: () => void
}
export const BackButton = ({ onBackToMain }: BackButtonParam) => (
<div className="row is-center">
<div className="col-9">
<Link to="/" onClick={onBackToMain}>
<StyledButton className="button outline is-left">
<LeftArrow />
<BackLabel>Search another transaction</BackLabel>
</StyledButton>
</Link>
</div>
</div>
)

View File

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

View File

@@ -0,0 +1,31 @@
import React from 'react'
import styled from 'styled-components'
import { InfoIcon } from './InfoIcon'
import { CloseIcon } from './CloseIcon'
const StyledInfoAlert = styled.div`
border: 1px solid var(--button-color);
border-radius: 4px;
margin-bottom: 20px;
padding-top: 10px;
`
const CloseIconContainer = styled.div`
cursor: pointer;
`
const TextContainer = styled.div`
flex-direction: column;
`
export const InfoAlert = ({ onClick, children }: { onClick: () => void; children: React.ReactChild[] }) => (
<div className="row is-center">
<StyledInfoAlert className="col-10 is-vertical-align row">
<InfoIcon />
<TextContainer className="col-10">{children}</TextContainer>
<CloseIconContainer className="col-1 is-vertical-align is-center" onClick={onClick}>
<CloseIcon />
</CloseIconContainer>
</StyledInfoAlert>
</div>
)

View File

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

View File

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

View File

@@ -0,0 +1,13 @@
import styled from 'styled-components'
export const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
export const StatusTd = styled.td`
width: 150px;
`
export const AgeTd = styled.td`
width: 180px;
`

View File

@@ -45,7 +45,9 @@ export const CONFIRMATIONS_STATUS = {
EXECUTION_WAITING: 'EXECUTION_WAITING',
FAILED: 'FAILED',
PENDING: 'PENDING',
WAITING: 'WAITING',
SEARCHING: 'SEARCHING',
WAITING_VALIDATORS: 'WAITING_VALIDATORS',
WAITING_CHAIN: 'WAITING_CHAIN',
UNDEFINED: 'UNDEFINED'
}
@@ -57,3 +59,5 @@ export const VALIDATOR_CONFIRMATION_STATUS = {
NOT_REQUIRED: 'Not required',
UNDEFINED: 'UNDEFINED'
}
export const SEARCHING_TX = 'Searching Transaction...'

View File

@@ -2,35 +2,72 @@
export const TRANSACTION_STATUS_DESCRIPTION: { [key: string]: string } = {
SUCCESS_MULTIPLE_MESSAGES: 'was initiated %t and contains several bridge messages. Specify one of them:',
SUCCESS_ONE_MESSAGE: 'was initiated %t',
SUCCESS_NO_MESSAGES: 'execution succeeded %t but it does not contain any bridge messages',
SUCCESS_NO_MESSAGES:
'successfully mined %t but it does not seem to contain any request to the bridge, \nso nothing needs to be confirmed by the validators. \nIf you are sure that the transaction should contain a request to the bridge,\ncontact to the validators by \nmessaging on %linkhttps://forum.poa.network/c/support',
FAILED: 'failed %t',
NOT_FOUND: 'was not found'
NOT_FOUND:
'Transaction not found. \n1. Check that the transaction hash is correct. \n2. Wait several blocks for the transaction to be\nmined, gas price affects mining speed.'
}
export const CONFIRMATIONS_STATUS_LABEL: { [key: string]: string } = {
SUCCESS: 'Success',
SUCCESS_MESSAGE_FAILED: 'Success',
FAILED: 'Failed',
PENDING: 'Pending',
WAITING_VALIDATORS: 'Waiting',
SEARCHING: 'Waiting',
WAITING_CHAIN: 'Waiting'
}
export const CONFIRMATIONS_STATUS_LABEL_HOME: { [key: string]: string } = {
SUCCESS: 'Success',
SUCCESS_MESSAGE_FAILED: 'Success',
EXECUTION_FAILED: 'Execution failed',
EXECUTION_PENDING: 'Execution pending',
EXECUTION_WAITING: 'Execution waiting',
FAILED: 'Failed',
PENDING: 'Pending',
WAITING: 'Waiting'
FAILED: 'Confirmation Failed',
PENDING: 'Confirmation Pending',
WAITING_VALIDATORS: 'Confirmation Waiting',
SEARCHING: 'Confirmation Waiting',
WAITING_CHAIN: 'Confirmation Waiting'
}
// %homeChain will be replaced by the home network name
// %foreignChain will be replaced by the foreign network name
// use %link to identify a link
export const CONFIRMATIONS_STATUS_DESCRIPTION: { [key: string]: string } = {
SUCCESS: '',
SUCCESS_MESSAGE_FAILED:
'Signatures have been collected in the %homeChain and they were successfully sent to the %foreignChain but the contained message execution failed.',
EXECUTION_FAILED:
'Signatures have been collected in the %homeChain and they were sent to the %foreignChain but the transaction with signatures failed',
EXECUTION_PENDING:
'Signatures have been collected in the %homeChain and they were sent to the %foreignChain but the transaction is in the pending state (transactions congestion or low gas price)',
EXECUTION_WAITING: 'Execution waiting',
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
FAILED:
'Some validators sent improper transactions as so they were failed, collected confirmations are not enough to execute the relay request',
PENDING: 'Some confirmations are in pending state',
WAITING: 'Validators are waiting for the chain finalization'
'The specified transaction was included in a block,\nbut confirmations sent by a majority of validators\nfailed. The cross-chain relay request will not be\nprocessed. Contact to the validators by\nmessaging on %linkhttps://forum.poa.network/c/support',
PENDING:
'The specified transaction was included in a block. A\nmajority of validators sent confirmations which have\nnot yet been added to a block.',
WAITING_VALIDATORS:
'The specified transaction was included in a block.\nSome validators have sent confirmations, others are\nwaiting for chain finalization.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
SEARCHING:
'The specified transaction was included in a block. The app is looking for confirmations. Either\n1. Validators are waiting for chain finalization before sending their signatures.\n2. Validators are not active.\n3. The bridge was stopped.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
WAITING_CHAIN:
'The specified transaction was included in a block.\nValidators are waiting for chain finalization before\nsending their confirmations.'
}
// use %link to identify a link
export const CONFIRMATIONS_STATUS_DESCRIPTION_HOME: { [key: string]: string } = {
SUCCESS: '',
SUCCESS_MESSAGE_FAILED:
'The specified transaction was included in a block,\nthe validators collected signatures and the cross-chain relay was executed correctly,\nbut the contained message execution failed.\nContact the support of the application you used to produce the transaction for the clarifications.',
EXECUTION_FAILED:
'The specified transaction was included in a block\nand the validators collected signatures. The\nvalidators 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\nvalidators transaction with collected signatures was\nsent but is not yet added to a block.',
EXECUTION_WAITING:
'The specified transaction was included in a block\nand the validators collected signatures. Either\n1. One of the validators is waiting for chain finalization.\n2. A validator skipped its duty to relay signatures.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
FAILED:
'The specified transaction was included in a block,\nbut transactions with signatures sent by a majority of\nvalidators failed. The cross-chain relay request will\nnot be processed. Contact to the validators by\nmessaging on %linkhttps://forum.poa.network/c/support',
PENDING:
'The specified transaction was included in a block.\nA majority of validators sent signatures which have not\nyet been added to a block.',
WAITING_VALIDATORS:
'The specified transaction was included in a block.\nSome validators have sent signatures, others are\nwaiting for chain finalization.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
SEARCHING:
'The specified transaction was included in a block. The app is looking for confirmations. Either\n1. Validators are waiting for chain finalization before sending their signatures.\n2. Validators are not active.\n3. The bridge was stopped.\nCheck status again after a few blocks. If the issue still persists contact to the validators by messaging on %linkhttps://forum.poa.network/c/support',
WAITING_CHAIN:
'The specified transaction was included in a block.\nValidators are waiting for chain finalization\nbefore sending their signatures.'
}

View File

@@ -3,6 +3,7 @@ import { TransactionReceipt } from 'web3-eth'
import { useStateProvider } from '../state/StateProvider'
import { Contract } from 'web3-eth-contract'
import { getRequiredBlockConfirmations } from '../utils/contract'
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
export interface UseBlockConfirmationsParams {
fromHome: boolean
@@ -17,17 +18,19 @@ export const useBlockConfirmations = ({ receipt, fromHome }: UseBlockConfirmatio
const callRequireBlockConfirmations = async (
contract: Contract,
receipt: TransactionReceipt,
setResult: Function
setResult: Function,
snapshotProvider: SnapshotProvider
) => {
const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber)
const result = await getRequiredBlockConfirmations(contract, receipt.blockNumber, snapshotProvider)
setResult(result)
}
useEffect(
() => {
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
if (!bridgeContract || !receipt) return
callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations)
callRequireBlockConfirmations(bridgeContract, receipt, setBlockConfirmations, snapshotProvider)
},
[home.bridgeContract, foreign.bridgeContract, receipt, fromHome]
)

View File

@@ -83,6 +83,13 @@ export const useMessageConfirmations = ({
const [pendingConfirmations, setPendingConfirmations] = useState(false)
const [pendingExecution, setPendingExecution] = useState(false)
const existsConfirmation = (confirmationArray: ConfirmationParam[]) => {
const filteredList = confirmationArray.filter(
c => c.status !== VALIDATOR_CONFIRMATION_STATUS.UNDEFINED && c.status !== VALIDATOR_CONFIRMATION_STATUS.WAITING
)
return filteredList.length > 0
}
// Check if the validators are waiting for block confirmations to verify the message
useEffect(
() => {
@@ -306,7 +313,7 @@ export const useMessageConfirmations = ({
// Sets the message status based in the collected information
useEffect(
() => {
if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS) {
if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS && existsConfirmation(confirmations)) {
const newStatus = executionData.executionResult
? CONFIRMATIONS_STATUS.SUCCESS
: CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED
@@ -319,18 +326,24 @@ export const useMessageConfirmations = ({
setStatus(CONFIRMATIONS_STATUS.EXECUTION_FAILED)
} else if (pendingExecution) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_PENDING)
} else if (waitingBlocksForExecutionResolved) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
}
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
}
} else if (waitingBlocks) {
setStatus(CONFIRMATIONS_STATUS.WAITING)
setStatus(CONFIRMATIONS_STATUS.WAITING_CHAIN)
} else if (failedConfirmations) {
setStatus(CONFIRMATIONS_STATUS.FAILED)
} else if (pendingConfirmations) {
setStatus(CONFIRMATIONS_STATUS.PENDING)
} else if (waitingBlocksResolved && existsConfirmation(confirmations)) {
setStatus(CONFIRMATIONS_STATUS.WAITING_VALIDATORS)
} else if (waitingBlocksResolved) {
setStatus(CONFIRMATIONS_STATUS.SEARCHING)
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
}
@@ -344,7 +357,10 @@ export const useMessageConfirmations = ({
failedConfirmations,
failedExecution,
pendingConfirmations,
pendingExecution
pendingExecution,
waitingBlocksResolved,
confirmations,
waitingBlocksForExecutionResolved
]
)
@@ -352,6 +368,7 @@ export const useMessageConfirmations = ({
confirmations,
status,
signatureCollected,
executionData
executionData,
waitingBlocksResolved
}
}

View File

@@ -1,7 +1,8 @@
import { useEffect, useState } from 'react'
import { getWeb3 } from '../utils/web3'
import { getChainId, getWeb3 } from '../utils/web3'
import { SnapshotProvider } from '../services/SnapshotProvider'
export const useNetwork = (url: string) => {
export const useNetwork = (url: string, snapshotProvider: SnapshotProvider) => {
const [loading, setLoading] = useState(true)
const [chainId, setChainId] = useState(0)
const web3 = getWeb3(url)
@@ -9,14 +10,14 @@ export const useNetwork = (url: string) => {
useEffect(
() => {
setLoading(true)
const getChainId = async () => {
const id = await web3.eth.getChainId()
const getWeb3ChainId = async () => {
const id = await getChainId(web3, snapshotProvider)
setChainId(id)
setLoading(false)
}
getChainId()
getWeb3ChainId()
},
[web3.eth]
[web3, snapshotProvider]
)
return {

View File

@@ -5,6 +5,7 @@ import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '..
import { BRIDGE_VALIDATORS_ABI } from '../abis'
import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth'
import { foreignSnapshotProvider, homeSnapshotProvider, SnapshotProvider } from '../services/SnapshotProvider'
export interface useValidatorContractParams {
fromHome: boolean
@@ -28,16 +29,22 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract
const callRequiredSignatures = async (
contract: Maybe<Contract>,
receipt: TransactionReceipt,
setResult: Function
setResult: Function,
snapshotProvider: SnapshotProvider
) => {
if (!contract) return
const result = await getRequiredSignatures(contract, receipt.blockNumber)
const result = await getRequiredSignatures(contract, receipt.blockNumber, snapshotProvider)
setResult(result)
}
const callValidatorList = async (contract: Maybe<Contract>, receipt: TransactionReceipt, setResult: Function) => {
const callValidatorList = async (
contract: Maybe<Contract>,
receipt: TransactionReceipt,
setResult: Function,
snapshotProvider: SnapshotProvider
) => {
if (!contract) return
const result = await getValidatorList(contract, receipt.blockNumber)
const result = await getValidatorList(contract, receipt.blockNumber, snapshotProvider)
setResult(result)
}
@@ -55,10 +62,11 @@ export const useValidatorContract = ({ receipt, fromHome }: useValidatorContract
useEffect(
() => {
if (!receipt) return
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures)
callValidatorList(validatorContract, receipt, setValidatorList)
const snapshotProvider = fromHome ? homeSnapshotProvider : foreignSnapshotProvider
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures, snapshotProvider)
callValidatorList(validatorContract, receipt, setValidatorList, snapshotProvider)
},
[validatorContract, receipt]
[validatorContract, receipt, fromHome]
)
return {

View File

@@ -33,7 +33,7 @@ export class BlockNumberProvider {
}
stop() {
this.running = this.running - 1
this.running = this.running > 0 ? this.running - 1 : 0
if (!this.running) {
clearTimeout(this.ref)

View File

@@ -0,0 +1,69 @@
const initialValue = {
chainId: 0,
RequiredBlockConfirmationChanged: [],
RequiredSignaturesChanged: [],
ValidatorAdded: [],
ValidatorRemoved: [],
snapshotBlockNumber: 0
}
export interface SnapshotEvent {
blockNumber: number
returnValues: any
}
export interface SnapshotValidatorEvent {
blockNumber: number
returnValues: any
event: string
}
export interface Snapshot {
chainId: number
RequiredBlockConfirmationChanged: SnapshotEvent[]
RequiredSignaturesChanged: SnapshotEvent[]
ValidatorAdded: SnapshotValidatorEvent[]
ValidatorRemoved: SnapshotValidatorEvent[]
snapshotBlockNumber: number
}
export class SnapshotProvider {
private data: Snapshot
constructor(side: string) {
let data = initialValue
try {
data = require(`../snapshots/${side}.json`)
} catch (e) {
console.log('Snapshot not found')
}
this.data = data
}
chainId() {
return this.data.chainId
}
snapshotBlockNumber() {
return this.data.snapshotBlockNumber
}
requiredBlockConfirmationEvents(toBlock: number) {
return this.data.RequiredBlockConfirmationChanged.filter(e => e.blockNumber <= toBlock)
}
requiredSignaturesEvents(toBlock: number) {
return this.data.RequiredSignaturesChanged.filter(e => e.blockNumber <= toBlock)
}
validatorAddedEvents(fromBlock: number) {
return this.data.ValidatorAdded.filter(e => e.blockNumber >= fromBlock)
}
validatorRemovedEvents(fromBlock: number) {
return this.data.ValidatorRemoved.filter(e => e.blockNumber >= fromBlock)
}
}
export const homeSnapshotProvider = new SnapshotProvider('home')
export const foreignSnapshotProvider = new SnapshotProvider('foreign')

View File

View File

@@ -11,6 +11,7 @@ import {
import Web3 from 'web3'
import { useBridgeContracts } from '../hooks/useBridgeContracts'
import { Contract } from 'web3-eth-contract'
import { foreignSnapshotProvider, homeSnapshotProvider } from '../services/SnapshotProvider'
export interface BaseNetworkParams {
chainId: number
@@ -47,8 +48,8 @@ const initialState = {
const StateContext = createContext<StateContext>(initialState)
export const StateProvider = ({ children }: { children: ReactNode }) => {
const homeNetwork = useNetwork(HOME_RPC_URL)
const foreignNetwork = useNetwork(FOREIGN_RPC_URL)
const homeNetwork = useNetwork(HOME_RPC_URL, homeSnapshotProvider)
const foreignNetwork = useNetwork(FOREIGN_RPC_URL, foreignSnapshotProvider)
const { homeBridge, foreignBridge } = useBridgeContracts({
homeWeb3: homeNetwork.web3,
foreignWeb3: foreignNetwork.web3

View File

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

View File

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

View File

@@ -0,0 +1,705 @@
import 'jest'
import { getConfirmationsForTx } from '../getConfirmationsForTx'
import * as helpers from '../validatorConfirmationHelpers'
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
import { APIPendingTransaction, APITransaction } from '../explorer'
import { VALIDATOR_CONFIRMATION_STATUS } from '../../config/constants'
import { BasicConfirmationParam } from '../../hooks/useMessageConfirmations'
jest.mock('../validatorConfirmationHelpers')
const getValidatorSuccessTransaction = helpers.getValidatorSuccessTransaction as jest.Mock<any>
const getValidatorConfirmation = helpers.getValidatorConfirmation as jest.Mock<any>
const getValidatorFailedTransaction = helpers.getValidatorFailedTransaction as jest.Mock<any>
const getValidatorPendingTransaction = helpers.getValidatorPendingTransaction as jest.Mock<any>
const messageData = '0x111111111'
const web3 = {
utils: {
soliditySha3Raw: (data: string) => `0xaaaa${data.replace('0x', '')}`
}
} as Web3
const validator1 = '0x45b96809336A8b714BFbdAB3E4B5e0fe5d839908'
const validator2 = '0xAe8bFfc8BBc6AAa9E21ED1E4e4957fe798BEA25f'
const validator3 = '0x285A6eB779be4db94dA65e2F3518B1c5F0f71244'
const validatorList = [validator1, validator2, validator3]
const bridgeContract = {} as Contract
const confirmationContractMethod = () => {}
const requiredSignatures = 2
const waitingBlocksResolved = true
let subscriptions: Array<number> = []
const timestamp = 1594045859
const getFailedTransactions = (): Promise<APITransaction[]> => Promise.resolve([])
const getPendingTransactions = (): Promise<APIPendingTransaction[]> => Promise.resolve([])
const getSuccessTransactions = (): Promise<APITransaction[]> => Promise.resolve([])
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
beforeEach(() => {
// Clear all instances and calls to constructor and all methods:
getValidatorSuccessTransaction.mockClear()
getValidatorConfirmation.mockClear()
getValidatorFailedTransaction.mockClear()
getValidatorPendingTransaction.mockClear()
subscriptions = []
})
describe('getConfirmationsForTx', () => {
test('should set validator confirmations status when signatures collected even if validator transactions not found yet and set remaining validator as not required', async () => {
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: '',
timestamp: 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(1)
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getValidatorSuccessTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
expect(setResult.mock.calls[0][0]()).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
])
)
expect(setResult.mock.calls[1][0]).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
])
)
})
test('should set validator confirmations status when signatures not collected even if validator transactions not found yet', async () => {
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator === validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: '',
timestamp: 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getValidatorSuccessTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
})
test('should set validator confirmations status, validator transactions and not retry', async () => {
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator !== validator3 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator !== validator3 ? '0x123' : '',
timestamp: validatorData.validator !== validator3 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(0)
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getValidatorSuccessTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
expect(setResult.mock.calls[0][0]()).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
])
)
expect(setResult.mock.calls[1][0]).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
])
)
})
test('should set validator confirmations status, validator transactions, keep failed found transaction and not retry', async () => {
const validator4 = '0x9d2dC11C342F4eF3C5491A048D0f0eBCd2D8f7C3'
const validatorList = [validator1, validator2, validator3, validator4]
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status:
validator !== validator3 && validator !== validator4
? VALIDATOR_CONFIRMATION_STATUS.SUCCESS
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? '0x123' : '',
timestamp: validatorData.validator !== validator3 && validatorData.validator !== validator4 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator3
? VALIDATOR_CONFIRMATION_STATUS.FAILED
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator3 ? '0x123' : '',
timestamp: validatorData.validator === validator3 ? 123 : 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(0)
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getValidatorSuccessTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(0)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
expect(setResult.mock.calls[0][0]()).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED }
])
)
expect(setResult.mock.calls[1][0]).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator4, status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED }
])
)
})
test('should look for failed and pending transactions for not confirmed validators', async () => {
// Validator1 success
// Validator2 failed
// Validator3 Pending
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator === validator1 ? '0x123' : '',
timestamp: validatorData.validator === validator1 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator2
? VALIDATOR_CONFIRMATION_STATUS.FAILED
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator2 ? '0x123' : '',
timestamp: validatorData.validator === validator2 ? 123 : 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator3
? VALIDATOR_CONFIRMATION_STATUS.PENDING
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator3 ? '0x123' : '',
timestamp: validatorData.validator === validator3 ? 123 : 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getValidatorSuccessTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
expect(setResult.mock.calls[0][0]()).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
])
)
expect(setResult.mock.calls[1][0]).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
])
)
})
test('should set as failed if enough signatures failed', async () => {
// Validator1 success
// Validator2 failed
// Validator3 failed
getValidatorConfirmation.mockImplementation(() => async (validator: string) => ({
validator,
status: validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getValidatorSuccessTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator === validator1 ? '0x123' : '',
timestamp: validatorData.validator === validator1 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator !== validator1
? VALIDATOR_CONFIRMATION_STATUS.FAILED
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator !== validator1 ? '0x123' : '',
timestamp: validatorData.validator !== validator1 ? 123 : 0
}))
getValidatorPendingTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(subscriptions.length).toEqual(0)
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getValidatorSuccessTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(true)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(false)
expect(setResult.mock.calls[0][0]()).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 }
])
)
expect(setResult.mock.calls[1][0]).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 }
])
)
})
test('should remove pending state after transaction mined', async () => {
// Validator1 success
// Validator2 failed
// Validator3 Pending
getValidatorConfirmation
.mockImplementationOnce(() => async (validator: string) => ({
validator,
status:
validator === validator1 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
.mockImplementation(() => async (validator: string) => ({
validator,
status:
validator !== validator2 ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
}))
getValidatorSuccessTransaction
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator === validator1 ? '0x123' : '',
timestamp: validatorData.validator === validator1 ? 123 : 0
}))
.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS,
txHash: validatorData.validator !== validator2 ? '0x123' : '',
timestamp: validatorData.validator !== validator2 ? 123 : 0
}))
getValidatorFailedTransaction.mockImplementation(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator2
? VALIDATOR_CONFIRMATION_STATUS.FAILED
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator2 ? '0x123' : '',
timestamp: validatorData.validator === validator2 ? 123 : 0
}))
getValidatorPendingTransaction
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status:
validatorData.validator === validator3
? VALIDATOR_CONFIRMATION_STATUS.PENDING
: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: validatorData.validator === validator3 ? '0x123' : '',
timestamp: validatorData.validator === validator3 ? 123 : 0
}))
.mockImplementationOnce(() => async (validatorData: BasicConfirmationParam) => ({
validator: validatorData.validator,
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
txHash: '',
timestamp: 0
}))
const setResult = jest.fn()
const setSignatureCollected = jest.fn()
const setFailedConfirmations = jest.fn()
const setPendingConfirmations = jest.fn()
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(2)
expect(getValidatorConfirmation).toBeCalledTimes(1)
expect(getValidatorSuccessTransaction).toBeCalledTimes(1)
expect(setSignatureCollected).toBeCalledTimes(1)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(getValidatorFailedTransaction).toBeCalledTimes(1)
expect(setFailedConfirmations).toBeCalledTimes(1)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(1)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
expect(setResult.mock.calls[0][0]()).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
])
)
expect(setResult.mock.calls[1][0]).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.PENDING, txHash: '0x123', timestamp: 123 }
])
)
await getConfirmationsForTx(
messageData,
web3,
validatorList,
bridgeContract,
confirmationContractMethod,
setResult,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getFailedTransactions,
setFailedConfirmations,
getPendingTransactions,
setPendingConfirmations,
getSuccessTransactions
)
unsubscribe()
expect(setResult).toBeCalledTimes(4)
expect(getValidatorConfirmation).toBeCalledTimes(2)
expect(getValidatorSuccessTransaction).toBeCalledTimes(2)
expect(setSignatureCollected).toBeCalledTimes(2)
expect(setSignatureCollected.mock.calls[0][0]).toEqual(false)
expect(setSignatureCollected.mock.calls[1][0]).toEqual(true)
expect(getValidatorFailedTransaction).toBeCalledTimes(2)
expect(setFailedConfirmations).toBeCalledTimes(2)
expect(setFailedConfirmations.mock.calls[0][0]).toEqual(false)
expect(setFailedConfirmations.mock.calls[1][0]).toEqual(false)
expect(getValidatorPendingTransaction).toBeCalledTimes(1)
expect(setPendingConfirmations).toBeCalledTimes(2)
expect(setPendingConfirmations.mock.calls[0][0]).toEqual(true)
expect(setPendingConfirmations.mock.calls[1][0]).toEqual(false)
expect(setResult.mock.calls[2][0]()).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS }
])
)
expect(setResult.mock.calls[3][0]).toEqual(
expect.arrayContaining([
{ validator: validator1, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 },
{ validator: validator2, status: VALIDATOR_CONFIRMATION_STATUS.FAILED, txHash: '0x123', timestamp: 123 },
{ validator: validator3, status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS, txHash: '0x123', timestamp: 123 }
])
)
})
})

View File

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

View File

@@ -1,10 +1,24 @@
import { Contract } from 'web3-eth-contract'
import { EventData } from 'web3-eth-contract'
import { SnapshotProvider } from '../services/SnapshotProvider'
export const getRequiredBlockConfirmations = async (contract: Contract, blockNumber: number) => {
const events = await contract.getPastEvents('RequiredBlockConfirmationChanged', {
fromBlock: 0,
toBlock: blockNumber
})
export const getRequiredBlockConfirmations = async (
contract: Contract,
blockNumber: number,
snapshotProvider: SnapshotProvider
) => {
const eventsFromSnapshot = snapshotProvider.requiredBlockConfirmationEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
let contractEvents: EventData[] = []
if (blockNumber > snapshotBlockNumber) {
contractEvents = await contract.getPastEvents('RequiredBlockConfirmationChanged', {
fromBlock: snapshotBlockNumber + 1,
toBlock: blockNumber
})
}
const events = [...eventsFromSnapshot, ...contractEvents]
let blockConfirmations
if (events.length > 0) {
@@ -21,11 +35,23 @@ export const getRequiredBlockConfirmations = async (contract: Contract, blockNum
export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call()
export const getRequiredSignatures = async (contract: Contract, blockNumber: number) => {
const events = await contract.getPastEvents('RequiredSignaturesChanged', {
fromBlock: 0,
toBlock: blockNumber
})
export const getRequiredSignatures = async (
contract: Contract,
blockNumber: number,
snapshotProvider: SnapshotProvider
) => {
const eventsFromSnapshot = snapshotProvider.requiredSignaturesEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
let contractEvents: EventData[] = []
if (blockNumber > snapshotBlockNumber) {
contractEvents = await contract.getPastEvents('RequiredSignaturesChanged', {
fromBlock: snapshotBlockNumber + 1,
toBlock: blockNumber
})
}
const events = [...eventsFromSnapshot, ...contractEvents]
// Use the value form last event before the transaction
const event = events[events.length - 1]
@@ -33,19 +59,26 @@ export const getRequiredSignatures = async (contract: Contract, blockNumber: num
return parseInt(requiredSignatures)
}
export const getValidatorList = async (contract: Contract, blockNumber: number) => {
let currentList: string[] = await contract.methods.validatorList().call()
const [added, removed] = await Promise.all([
export const getValidatorList = async (contract: Contract, blockNumber: number, snapshotProvider: SnapshotProvider) => {
const addedEventsFromSnapshot = snapshotProvider.validatorAddedEvents(blockNumber)
const removedEventsFromSnapshot = snapshotProvider.validatorRemovedEvents(blockNumber)
const snapshotBlockNumber = snapshotProvider.snapshotBlockNumber()
const fromBlock = snapshotBlockNumber > blockNumber ? snapshotBlockNumber + 1 : blockNumber
const [currentList, added, removed] = await Promise.all([
contract.methods.validatorList().call(),
contract.getPastEvents('ValidatorAdded', {
fromBlock: blockNumber
fromBlock
}),
contract.getPastEvents('ValidatorRemoved', {
fromBlock: blockNumber
fromBlock
})
])
// Ordered desc
const orderedEvents = [...added, ...removed].sort(({ blockNumber: prev }, { blockNumber: next }) => next - prev)
const orderedEvents = [...addedEventsFromSnapshot, ...added, ...removedEventsFromSnapshot, ...removed].sort(
({ blockNumber: prev }, { blockNumber: next }) => next - prev
)
// Stored as a Set to avoid duplicates
const validatorList = new Set(currentList)

View File

@@ -22,8 +22,8 @@ export const checkWaitingBlocksForExecution = async (
timestamp: 0,
executionResult: false
})
setWaitingBlocksForExecution(false)
setWaitingBlocksForExecutionResolved(true)
setWaitingBlocksForExecution(false)
blockProvider.stop()
} else {
let nextInterval = interval

View File

@@ -217,14 +217,11 @@ export const getValidatorSuccessTransactionsForMessage = async ({
return filterValidatorSignatureTransaction(transactions, messageData)
}
export const getExecutionFailedTransactionForMessage = async ({
account,
to,
messageData,
startTimestamp,
endTimestamp
}: GetFailedTransactionParams): Promise<APITransaction[]> => {
const failedTransactions = await getFailedTransactions(
export const getExecutionFailedTransactionForMessage = async (
{ account, to, messageData, startTimestamp, endTimestamp }: GetFailedTransactionParams,
getFailedTransactionsMethod = getFailedTransactions
): Promise<APITransaction[]> => {
const failedTransactions = await getFailedTransactionsMethod(
account,
to,
startTimestamp,
@@ -237,12 +234,11 @@ export const getExecutionFailedTransactionForMessage = async ({
return failedTransactions.filter(t => t.input.includes(EXECUTE_SIGNATURES_HASH) && t.input.includes(messageDataValue))
}
export const getValidatorPendingTransactionsForMessage = async ({
account,
to,
messageData
}: GetPendingTransactionParams): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactions({
export const getValidatorPendingTransactionsForMessage = async (
{ account, to, messageData }: GetPendingTransactionParams,
fetchPendingTransactionsMethod = fetchPendingTransactions
): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactionsMethod({
account,
api: HOME_EXPLORER_API
})
@@ -258,12 +254,11 @@ export const getValidatorPendingTransactionsForMessage = async ({
)
}
export const getExecutionPendingTransactionsForMessage = async ({
account,
to,
messageData
}: GetPendingTransactionParams): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactions({
export const getExecutionPendingTransactionsForMessage = async (
{ account, to, messageData }: GetPendingTransactionParams,
fetchPendingTransactionsMethod = fetchPendingTransactions
): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactionsMethod({
account,
api: FOREIGN_EXPLORER_API
})

View File

@@ -1,176 +1,19 @@
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
import validatorsCache from '../services/ValidatorsCache'
import {
CACHE_KEY_FAILED,
CACHE_KEY_SUCCESS,
HOME_RPC_POLLING_INTERVAL,
ONE_DAY_TIMESTAMP,
VALIDATOR_CONFIRMATION_STATUS
} from '../config/constants'
import { HOME_RPC_POLLING_INTERVAL, VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import {
GetFailedTransactionParams,
APITransaction,
APIPendingTransaction,
GetPendingTransactionParams
} from './explorer'
import { BasicConfirmationParam, ConfirmationParam } from '../hooks/useMessageConfirmations'
export const getValidatorConfirmation = (
web3: Web3,
hashMsg: string,
bridgeContract: Contract,
confirmationContractMethod: Function
) => async (validator: string): Promise<BasicConfirmationParam> => {
const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg)
const signatureFromCache = validatorsCache.get(hashSenderMsg)
if (signatureFromCache) {
return {
validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS
}
}
const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg)
const status = confirmed ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
// If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change
if (confirmed) {
validatorsCache.set(hashSenderMsg, confirmed)
}
return {
validator,
status
}
}
export const getValidatorSuccessTransaction = (
bridgeContract: Contract,
messageData: string,
timestamp: number,
getSuccessTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
const { validator } = validatorData
const validatorCacheKey = `${CACHE_KEY_SUCCESS}${validatorData.validator}-${messageData}`
const fromCache = validatorsCache.getData(validatorCacheKey)
if (fromCache && fromCache.txHash) {
return fromCache
}
const transactions = await getSuccessTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData,
startTimestamp: timestamp,
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
})
let txHashTimestamp = 0
let txHash = ''
const status = VALIDATOR_CONFIRMATION_STATUS.SUCCESS
if (transactions.length > 0) {
const tx = transactions[0]
txHashTimestamp = parseInt(tx.timeStamp)
txHash = tx.hash
// cache the result
validatorsCache.setData(validatorCacheKey, {
validator,
status,
txHash,
timestamp: txHashTimestamp
})
}
return {
validator,
status,
txHash,
timestamp: txHashTimestamp
}
}
export const getValidatorFailedTransaction = (
bridgeContract: Contract,
messageData: string,
timestamp: number,
getFailedTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
const validatorCacheKey = `${CACHE_KEY_FAILED}${validatorData.validator}-${messageData}`
const failedFromCache = validatorsCache.getData(validatorCacheKey)
if (failedFromCache && failedFromCache.txHash) {
return failedFromCache
}
const failedTransactions = await getFailedTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData,
startTimestamp: timestamp,
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
})
const newStatus =
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.FAILED : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
let txHashTimestamp = 0
let txHash = ''
// If validator signature failed, we cache the result to avoid doing future requests for a result that won't change
if (failedTransactions.length > 0) {
const failedTx = failedTransactions[0]
txHashTimestamp = parseInt(failedTx.timeStamp)
txHash = failedTx.hash
validatorsCache.setData(validatorCacheKey, {
validator: validatorData.validator,
status: newStatus,
txHash,
timestamp: txHashTimestamp
})
}
return {
validator: validatorData.validator,
status: newStatus,
txHash,
timestamp: txHashTimestamp
}
}
export const getValidatorPendingTransaction = (
bridgeContract: Contract,
messageData: string,
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
const failedTransactions = await getPendingTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData
})
const newStatus =
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.PENDING : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
let timestamp = 0
let txHash = ''
if (failedTransactions.length > 0) {
const failedTx = failedTransactions[0]
timestamp = Math.floor(new Date().getTime() / 1000.0)
txHash = failedTx.hash
}
return {
validator: validatorData.validator,
status: newStatus,
txHash,
timestamp
}
}
import {
getValidatorConfirmation,
getValidatorFailedTransaction,
getValidatorPendingTransaction,
getValidatorSuccessTransaction
} from './validatorConfirmationHelpers'
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
export const getConfirmationsForTx = async (
messageData: string,
@@ -201,9 +44,22 @@ export const getConfirmationsForTx = async (
const successConfirmations = validatorConfirmations.filter(c => c.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
setResult((prevConfirmations: ConfirmationParam[]) => {
if (prevConfirmations && prevConfirmations.length) {
successConfirmations.forEach(validatorData => {
const index = prevConfirmations.findIndex(e => e.validator === validatorData.validator)
validatorConfirmations[index] = validatorData
})
return prevConfirmations
} else {
return validatorConfirmations
}
})
const notSuccessConfirmations = validatorConfirmations.filter(c => c.status !== VALIDATOR_CONFIRMATION_STATUS.SUCCESS)
// If signatures not collected, it needs to retry in the next blocks
// If signatures not collected, look for pending transactions
let pendingConfirmationsResult = false
if (successConfirmations.length !== requiredSignatures) {
// Check if confirmation is pending
const validatorPendingConfirmationsChecks = await Promise.all(
@@ -219,51 +75,56 @@ export const getConfirmationsForTx = async (
})
if (validatorPendingConfirmations.length > 0) {
setPendingConfirmations(true)
pendingConfirmationsResult = true
}
}
const undefinedConfirmations = validatorConfirmations.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
)
// Check if confirmation failed
const validatorFailedConfirmationsChecks = await Promise.all(
undefinedConfirmations.map(
getValidatorFailedTransaction(bridgeContract, messageData, timestamp, getFailedTransactions)
)
)
const validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED
)
validatorFailedConfirmations.forEach(validatorData => {
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
validatorConfirmations[index] = validatorData
})
const messageConfirmationsFailed = validatorFailedConfirmations.length > validatorList.length - requiredSignatures
if (messageConfirmationsFailed) {
setFailedConfirmations(true)
}
const undefinedConfirmations = validatorConfirmations.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
)
const missingConfirmations = validatorConfirmations.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
// Check if confirmation failed
let failedConfirmationsResult = false
const validatorFailedConfirmationsChecks = await Promise.all(
undefinedConfirmations.map(
getValidatorFailedTransaction(bridgeContract, messageData, timestamp, getFailedTransactions)
)
)
const validatorFailedConfirmations = validatorFailedConfirmationsChecks.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.FAILED
)
validatorFailedConfirmations.forEach(validatorData => {
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
validatorConfirmations[index] = validatorData
})
const messageConfirmationsFailed = validatorFailedConfirmations.length > validatorList.length - requiredSignatures
if (messageConfirmationsFailed) {
failedConfirmationsResult = true
}
if (missingConfirmations.length > 0) {
shouldRetry = true
}
} else {
// If signatures collected, it should set other signatures as not required
const notRequiredConfirmations = notSuccessConfirmations.map(c => ({
const missingConfirmations = validatorConfirmations.filter(
c => c.status === VALIDATOR_CONFIRMATION_STATUS.UNDEFINED || c.status === VALIDATOR_CONFIRMATION_STATUS.PENDING
)
if (successConfirmations.length !== requiredSignatures && missingConfirmations.length > 0) {
shouldRetry = true
}
let signatureCollectedResult = false
if (successConfirmations.length === requiredSignatures) {
// If signatures collected, it should set other signatures not found as not required
const notRequiredConfirmations = missingConfirmations.map(c => ({
validator: c.validator,
status: VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED
}))
validatorConfirmations = [...successConfirmations, ...notRequiredConfirmations]
setSignatureCollected(true)
notRequiredConfirmations.forEach(validatorData => {
const index = validatorConfirmations.findIndex(e => e.validator === validatorData.validator)
validatorConfirmations[index] = validatorData
})
signatureCollectedResult = true
}
// Set confirmations to update UI and continue requesting the transactions for the signatures
setResult(validatorConfirmations)
// get transactions from success signatures
const successConfirmationWithData = await Promise.all(
validatorConfirmations
@@ -282,7 +143,11 @@ export const getConfirmationsForTx = async (
})
}
// Set results
setResult(updatedValidatorConfirmations)
setFailedConfirmations(failedConfirmationsResult)
setPendingConfirmations(pendingConfirmationsResult)
setSignatureCollected(signatureCollectedResult)
// Retry if not all transaction were found for validator confirmations
if (successConfirmationWithTxFound.length < successConfirmationWithData.length) {

View File

@@ -1,5 +1,9 @@
import { formatDistance } from 'date-fns'
import { CONFIRMATIONS_STATUS_DESCRIPTION, TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
import {
CONFIRMATIONS_STATUS_DESCRIPTION,
CONFIRMATIONS_STATUS_DESCRIPTION_HOME,
TRANSACTION_STATUS_DESCRIPTION
} from '../config/descriptions'
import { FOREIGN_EXPLORER_TX_TEMPLATE, HOME_EXPLORER_TX_TEMPLATE } from '../config/constants'
export const validTxHash = (txHash: string) => /^0x[a-fA-F0-9]{64}$/.test(txHash)
@@ -31,11 +35,8 @@ export const getTransactionStatusDescription = (status: string, timestamp: Maybe
return description
}
export const getConfirmationsStatusDescription = (status: string, home: string, foreign: string) => {
let description = CONFIRMATIONS_STATUS_DESCRIPTION[status]
export const getConfirmationsStatusDescription = (status: string, home: string, foreign: string, fromHome: boolean) => {
const statusDescription = fromHome ? CONFIRMATIONS_STATUS_DESCRIPTION_HOME : CONFIRMATIONS_STATUS_DESCRIPTION
description = description.replace('%homeChain', home)
description = description.replace('%foreignChain', foreign)
return description
return statusDescription[status]
}

View File

@@ -14,8 +14,8 @@ export const checkSignaturesWaitingForBLocks = async (
const currentBlock = blockProvider.get()
if (currentBlock && currentBlock >= targetBlock) {
setWaitingStatus(false)
setWaitingBlocksResolved(true)
setWaitingStatus(false)
blockProvider.stop()
} else {
let nextInterval = interval

View File

@@ -0,0 +1,172 @@
import Web3 from 'web3'
import { Contract } from 'web3-eth-contract'
import { BasicConfirmationParam, ConfirmationParam } from '../hooks/useMessageConfirmations'
import validatorsCache from '../services/ValidatorsCache'
import {
CACHE_KEY_FAILED,
CACHE_KEY_SUCCESS,
ONE_DAY_TIMESTAMP,
VALIDATOR_CONFIRMATION_STATUS
} from '../config/constants'
import {
APIPendingTransaction,
APITransaction,
GetFailedTransactionParams,
GetPendingTransactionParams
} from './explorer'
export const getValidatorConfirmation = (
web3: Web3,
hashMsg: string,
bridgeContract: Contract,
confirmationContractMethod: Function
) => async (validator: string): Promise<BasicConfirmationParam> => {
const hashSenderMsg = web3.utils.soliditySha3Raw(validator, hashMsg)
const signatureFromCache = validatorsCache.get(hashSenderMsg)
if (signatureFromCache) {
return {
validator,
status: VALIDATOR_CONFIRMATION_STATUS.SUCCESS
}
}
const confirmed = await confirmationContractMethod(bridgeContract, hashSenderMsg)
const status = confirmed ? VALIDATOR_CONFIRMATION_STATUS.SUCCESS : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
// If validator confirmed signature, we cache the result to avoid doing future requests for a result that won't change
if (confirmed) {
validatorsCache.set(hashSenderMsg, confirmed)
}
return {
validator,
status
}
}
export const getValidatorSuccessTransaction = (
bridgeContract: Contract,
messageData: string,
timestamp: number,
getSuccessTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
const { validator } = validatorData
const validatorCacheKey = `${CACHE_KEY_SUCCESS}${validatorData.validator}-${messageData}`
const fromCache = validatorsCache.getData(validatorCacheKey)
if (fromCache && fromCache.txHash) {
return fromCache
}
const transactions = await getSuccessTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData,
startTimestamp: timestamp,
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
})
let txHashTimestamp = 0
let txHash = ''
const status = VALIDATOR_CONFIRMATION_STATUS.SUCCESS
if (transactions.length > 0) {
const tx = transactions[0]
txHashTimestamp = parseInt(tx.timeStamp)
txHash = tx.hash
// cache the result
validatorsCache.setData(validatorCacheKey, {
validator,
status,
txHash,
timestamp: txHashTimestamp
})
}
return {
validator,
status,
txHash,
timestamp: txHashTimestamp
}
}
export const getValidatorFailedTransaction = (
bridgeContract: Contract,
messageData: string,
timestamp: number,
getFailedTransactions: (args: GetFailedTransactionParams) => Promise<APITransaction[]>
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
const validatorCacheKey = `${CACHE_KEY_FAILED}${validatorData.validator}-${messageData}`
const failedFromCache = validatorsCache.getData(validatorCacheKey)
if (failedFromCache && failedFromCache.txHash) {
return failedFromCache
}
const failedTransactions = await getFailedTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData,
startTimestamp: timestamp,
endTimestamp: timestamp + ONE_DAY_TIMESTAMP
})
const newStatus =
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.FAILED : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
let txHashTimestamp = 0
let txHash = ''
// If validator signature failed, we cache the result to avoid doing future requests for a result that won't change
if (failedTransactions.length > 0) {
const failedTx = failedTransactions[0]
txHashTimestamp = parseInt(failedTx.timeStamp)
txHash = failedTx.hash
validatorsCache.setData(validatorCacheKey, {
validator: validatorData.validator,
status: newStatus,
txHash,
timestamp: txHashTimestamp
})
}
return {
validator: validatorData.validator,
status: newStatus,
txHash,
timestamp: txHashTimestamp
}
}
export const getValidatorPendingTransaction = (
bridgeContract: Contract,
messageData: string,
getPendingTransactions: (args: GetPendingTransactionParams) => Promise<APIPendingTransaction[]>
) => async (validatorData: BasicConfirmationParam): Promise<ConfirmationParam> => {
const failedTransactions = await getPendingTransactions({
account: validatorData.validator,
to: bridgeContract.options.address,
messageData
})
const newStatus =
failedTransactions.length > 0 ? VALIDATOR_CONFIRMATION_STATUS.PENDING : VALIDATOR_CONFIRMATION_STATUS.UNDEFINED
let timestamp = 0
let txHash = ''
if (failedTransactions.length > 0) {
const failedTx = failedTransactions[0]
timestamp = Math.floor(new Date().getTime() / 1000.0)
txHash = failedTx.hash
}
return {
validator: validatorData.validator,
status: newStatus,
txHash,
timestamp
}
}

View File

@@ -5,6 +5,7 @@ import { AbiItem } from 'web3-utils'
import memoize from 'fast-memoize'
import promiseRetry from 'promise-retry'
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
import { SnapshotProvider } from '../services/SnapshotProvider'
export interface MessageObject {
id: string
@@ -61,3 +62,11 @@ export const getBlock = async (web3: Web3, blockNumber: number): Promise<BlockTr
}
return result
})
export const getChainId = async (web3: Web3, snapshotProvider: SnapshotProvider) => {
let id = snapshotProvider.chainId()
if (id === 0) {
id = await web3.eth.getChainId()
}
return id
}

View File

@@ -2,19 +2,37 @@ import React from 'react'
import ReactDOM from 'react-dom'
import BurnerCore from '@burner-wallet/core'
import { InjectedSigner, LocalSigner } from '@burner-wallet/core/signers'
import { InfuraGateway, InjectedGateway } from '@burner-wallet/core/gateways'
import { XDaiBridge } from '@burner-wallet/exchange'
import { xdai } from '@burner-wallet/assets'
import { InfuraGateway, InjectedGateway, XDaiGateway } from '@burner-wallet/core/gateways'
import Exchange from '@burner-wallet/exchange'
import ModernUI from '@burner-wallet/modern-ui'
import { Etc, Wetc, TokenBridgeGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
import {
Etc,
Wetc,
Dai,
qDai,
MOON,
xMOON,
TokenBridgeGateway,
WETCBridge,
QDAIBridge,
MOONBridge
} from '@poanet/tokenbridge-bw-exchange'
import MetamaskPlugin from '@burner-wallet/metamask-plugin'
const core = new BurnerCore({
signers: [new InjectedSigner(), new LocalSigner()],
gateways: [new InjectedGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY), new TokenBridgeGateway()],
assets: [Wetc, Etc]
gateways: [
new InjectedGateway(),
new XDaiGateway(),
new InfuraGateway(process.env.REACT_APP_INFURA_KEY),
new TokenBridgeGateway()
],
assets: [xdai, Wetc, Etc, Dai, qDai, MOON, xMOON]
})
const exchange = new Exchange([new WETCBridge()])
const exchange = new Exchange([new XDaiBridge(), new WETCBridge(), new QDAIBridge(), new MOONBridge()])
const BurnerWallet = () => <ModernUI title="Staging Wallet" core={core} plugins={[exchange, new MetamaskPlugin()]} />

View File

@@ -93,8 +93,7 @@ if (process.env.REACT_APP_MODE === 'AMB_NATIVE_TO_ERC677') {
network: process.env.REACT_APP_FOREIGN_NETWORK,
// @ts-ignore
address: process.env.REACT_APP_FOREIGN_TOKEN_ADDRESS,
// @ts-ignore
bridgeAddress: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
bridgeModes: ['erc-to-native-amb']
})
testBridge = new MediatorErcToNative({

View File

@@ -4,12 +4,16 @@ This plugin defines a Bridge trading pair to be used in the Exchange Plugin.
Bridge trading pairs and assets supported:
* ETC - WETC Bridge
* MOON - xMOON Bridge
* DAI - qDAI Bridge (For qDAI Bridge, it's necessary to use a custom DAI token from this repo instead of the DAI asset provided by burner-wallet)
It also provides some generic resources that can be used and extended:
* **ERC677Asset** - A representation of an Erc677 token
* **ERC677Asset** - A representation of an Erc677 token.
* **BridgeableERC20Asset** - A representation of Erc20 token with a possibility of bridging it via a call to `relayTokens`.
* **NativeMediatorAsset** - Represents a native token that interacts with a Mediator extension.
* **Mediator Pair** - Represents an Exchange Pair that interacts with mediators extensions.
* **TokenBridgeGateway** - A gateway to operate with ETC, POA Sokol and POA Core networks.
* **MediatorErcToNative Pair** - Represents a modified Mediator Pair that interacts with a tokenbridge erc-to-native mediators contracts.
* **TokenBridgeGateway** - A gateway to operate with ETC, POA Sokol, POA Core and qDAI networks.
### Install package
```
@@ -18,13 +22,22 @@ yarn add @poanet/tokenbridge-bw-exchange
### Usage
#### WETCBridge example
In this example, we use `TokenBridgeGateway` for connecting to the Ethereum Classic and `InfuraGateway` for connecting to the Ethereum Mainnet.
`WETCBridge` operates with two assets: `WETC` (Ethereum Mainnet) and `ETC` (Ethereum Classic), they should be added in the assets list.
```javascript
import { Etc, Wetc, EtcGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
import BurnerCore from '@burner-wallet/core'
import Exchange from '@burner-wallet/exchange'
import { LocalSigner } from '@burner-wallet/core/signers'
import { Etc, Wetc, TokenBridgeGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
import { InfuraGateway } from '@burner-wallet/core/gateways'
const core = new BurnerCore({
...
gateways: [new EtcGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
assets: [Etc, Wetc]
signers: [new LocalSigner()],
gateways: [new TokenBridgeGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
assets: [Wetc, Etc]
})
const exchange = new Exchange({
@@ -32,6 +45,33 @@ const exchange = new Exchange({
})
```
### Using several exchanges simultaneously
In this example, we use `TokenBridgeGateway` for connecting to the qDAI chain, `XDaiGatewai` for connecting to the xDAI chain and `InfuraGateway` for connecting to the Ethereum Mainnet and Rinkeby Network.
`QDAIBridge` operates with two assets: `qDAI` (qDAI chain) and `DAI` (Ethereum Mainnet). Note that we use a custom DAI token from the `@poanet/tokenbridge-bw-exchange`, this is necessary for allowing bridge operations on this token.
`MOONBridge` operates with two assets: `MOON` (Rinkeby network) and `xMOON` (xDAI chain).
All four assets should be added to the assets list.
```javascript
import BurnerCore from '@burner-wallet/core'
import Exchange from '@burner-wallet/exchange'
import { LocalSigner } from '@burner-wallet/core/signers'
import { InfuraGateway, XDaiGateway } from '@burner-wallet/core/gateways'
import { Dai, qDai, MOON, xMOON, TokenBridgeGateway, QDAIBridge, MOONBridge } from '@poanet/tokenbridge-bw-exchange'
const core = new BurnerCore({
signers: [new LocalSigner()],
gateways: [new TokenBridgeGateway(), new XDaiGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
assets: [Dai, qDai, MOON, xMOON]
})
const exchange = new Exchange({
pairs: [new QDAIBridge(), new MOONBridge()]
})
```
This is how the exchange plugin will look like:
![exchange-wetc](https://user-images.githubusercontent.com/4614574/80991095-e40d0900-8e0d-11ea-9915-1b4e4a052694.png)

View File

@@ -1,6 +1,6 @@
{
"name": "@poanet/tokenbridge-bw-exchange",
"version": "1.0.0",
"version": "1.1.0",
"license": "GPL-3.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",

View File

@@ -0,0 +1,12 @@
import { Mediator } from '../burner-wallet'
export default class MOONBridge extends Mediator {
constructor() {
super({
assetA: 'xmoon',
assetABridge: '0x1E0507046130c31DEb20EC2f870ad070Ff266079',
assetB: 'moon',
assetBBridge: '0xFEaB457D95D9990b7eb6c943c839258245541754'
})
}
}

View File

@@ -0,0 +1,12 @@
import { MediatorErcToNative } from '../burner-wallet'
export default class QDAIBridge extends MediatorErcToNative {
constructor() {
super({
assetA: 'qdai',
assetABridge: '0xFEaB457D95D9990b7eb6c943c839258245541754',
assetB: 'dai',
assetBBridge: '0xf6edFA16926f30b0520099028A145F4E06FD54ed'
})
}
}

View File

@@ -1,6 +1,6 @@
import { Mediator } from '../burner-wallet'
import { HOME_NATIVE_TO_ERC_ABI, FOREIGN_NATIVE_TO_ERC_ABI } from '../utils'
import { waitForEvent, isBridgeContract, constants } from '../utils'
import { waitForEvent, isVanillaBridgeContract, constants } from '../utils'
import { ValueTypes } from '@burner-wallet/exchange'
import { toBN, fromWei } from 'web3-utils'
@@ -19,7 +19,7 @@ export default class WETCBridge extends Mediator {
.getAsset(this.assetA)
.getWeb3()
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.assetABridge)
const listenToBridgeEvent = await isBridgeContract(contract)
const listenToBridgeEvent = await isVanillaBridgeContract(contract)
if (listenToBridgeEvent) {
await waitForEvent(web3, contract, 'AffirmationCompleted', this.processBridgeEvents(sendResult.txHash))
} else {
@@ -32,7 +32,7 @@ export default class WETCBridge extends Mediator {
.getAsset(this.assetB)
.getWeb3()
const contract = new web3.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, this.assetBBridge)
const listenToBridgeEvent = await isBridgeContract(contract)
const listenToBridgeEvent = await isVanillaBridgeContract(contract)
if (listenToBridgeEvent) {
await waitForEvent(web3, contract, 'RelayedMessage', this.processBridgeEvents(sendResult.txHash))
} else {
@@ -53,7 +53,7 @@ export default class WETCBridge extends Mediator {
.getWeb3()
const contract = new web3.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, this.assetBBridge)
const useBridgeContract = await isBridgeContract(contract)
const useBridgeContract = await isVanillaBridgeContract(contract)
if (useBridgeContract) {
const fee = toBN(await contract.methods.getHomeFee().call())
@@ -79,7 +79,7 @@ export default class WETCBridge extends Mediator {
.getWeb3()
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.assetABridge)
const useBridgeContract = await isBridgeContract(contract)
const useBridgeContract = await isVanillaBridgeContract(contract)
if (useBridgeContract) {
const fee = toBN(await contract.methods.getForeignFee().call())

View File

@@ -0,0 +1,3 @@
export { default as WETCBridge } from './WETCBridge'
export { default as QDAIBridge } from './QDAIBridge'
export { default as MOONBridge } from './MOONBridge'

View File

@@ -1,42 +1,55 @@
import { ERC20Asset } from '@burner-wallet/assets'
import { MEDIATOR_ABI, constants } from '../../utils'
import { toBN } from 'web3-utils'
import { AssetConstructor } from '@burner-wallet/assets/Asset'
import { MEDIATOR_ABI, constants, isBridgeContract } from '../../utils'
import { toBN, soliditySha3 } from 'web3-utils'
interface BridgeableERC20Constructor {
interface BridgeableERC20Constructor extends AssetConstructor {
abi?: object
address: string
id: string
name: string
network: string
bridgeAddress: string
bridgeModes: string[]
}
export default class BridgeableERC20Asset extends ERC20Asset {
protected bridgeAddress: string
private _bridge
private _bridgeModes
private _bridges: { [addr: string]: object | false }
constructor({ bridgeAddress, ...params }: BridgeableERC20Constructor) {
super({ ...params })
this.bridgeAddress = bridgeAddress.toLowerCase()
public set bridgeModes(bridgeModes: string[]) {
this._bridgeModes = bridgeModes.map(s => soliditySha3(s)!.slice(0, 10))
}
getBridgeContract() {
if (!this._bridge) {
public get bridgeModes() {
return this._bridgeModes
}
constructor({
bridgeModes = ['erc-to-native-core', 'erc-to-native-amb', 'erc-to-erc-core', 'erc-to-erc-amb'],
...params
}: BridgeableERC20Constructor) {
super(params)
this._bridges = {}
this.bridgeModes = bridgeModes
}
async getBridgeContract(addr: string) {
if (typeof this._bridges[addr] === 'undefined') {
const Contract = this.getWeb3().eth.Contract
this._bridge = new Contract(MEDIATOR_ABI, this.bridgeAddress)
const bridge = new Contract(MEDIATOR_ABI, addr)
if (await isBridgeContract(bridge, this.bridgeModes)) {
return (this._bridges[addr] = bridge)
}
return (this._bridges[addr] = false)
}
return this._bridge
return this._bridges[addr]
}
async _send({ from, to, value }) {
if (to.toLowerCase() === this.bridgeAddress) {
const bridge = await this.getBridgeContract(to)
if (bridge) {
const allowance = await this.allowance(from, to)
if (toBN(allowance).lt(toBN(value))) {
await this.approve(from, to, value)
}
const receipt = await this.getBridgeContract()
.methods.relayTokens(from, value)
.send({ from })
const receipt = await bridge.methods.relayTokens(from, value).send({ from })
const transferLog = Object.values(receipt.events as object).find(
e => e.raw.topics[0] === constants.TRANSFER_TOPIC
)

View File

@@ -0,0 +1,11 @@
import BridgeableERC20Asset from './BridgeableERC20Asset'
export default new BridgeableERC20Asset({
id: 'dai',
name: 'Dai',
network: '1',
address: '0x6b175474e89094c44da98b954eedeac495271d0f',
usdPrice: 1,
icon: 'https://static.burnerfactory.com/icons/mcd.svg',
bridgeModes: ['erc-to-native-amb']
})

View File

@@ -1,14 +1,12 @@
import { ERC20Asset } from '@burner-wallet/assets'
import { AssetConstructor } from '@burner-wallet/assets/Asset'
import { ERC677_ABI, constants } from '../../utils'
const BLOCK_LOOKBACK = 250
interface ERC677Constructor {
interface ERC677Constructor extends AssetConstructor {
abi?: object
address: string
id: string
name: string
network: string
}
export default class ERC677Asset extends ERC20Asset {

View File

@@ -1,5 +1,5 @@
import NativeMediatorAsset from './NativeMediatorAsset'
import { isBridgeContract, HOME_NATIVE_TO_ERC_ABI } from '../../utils'
import { isVanillaBridgeContract, HOME_NATIVE_TO_ERC_ABI } from '../../utils'
class EtcNativeAsset extends NativeMediatorAsset {
constructor(props) {
@@ -9,7 +9,7 @@ class EtcNativeAsset extends NativeMediatorAsset {
async scanMediatorEvents(address, fromBlock, toBlock) {
const web3 = this.getWeb3()
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.mediatorAddress)
const listenToBridgeEvent = await isBridgeContract(contract)
const listenToBridgeEvent = await isVanillaBridgeContract(contract)
if (listenToBridgeEvent && this.mediatorAddress != '') {
const events = await contract.getPastEvents('AffirmationCompleted', {
fromBlock,

View File

@@ -0,0 +1,10 @@
import BridgeableERC20Asset from './BridgeableERC20Asset'
export default new BridgeableERC20Asset({
id: 'moon',
name: 'MOON',
network: '4',
address: '0xDF82c9014F127243CE1305DFE54151647d74B27A',
icon: 'https://blockscout.com/poa/xdai/images/icons/moon.png',
bridgeModes: ['erc-to-erc-amb']
})

View File

@@ -1,12 +1,10 @@
import { NativeAsset } from '@burner-wallet/assets'
import { Contract, EventData } from 'web3-eth-contract'
import { MEDIATOR_ABI } from '../../utils'
import { AssetConstructor } from '@burner-wallet/assets/Asset'
interface NativeMediatorConstructor {
interface NativeMediatorConstructor extends AssetConstructor {
mediatorAddress?: string
id: string
name: string
network: string
}
export default class NativeMediatorAsset extends NativeAsset {

View File

@@ -0,0 +1,9 @@
import NativeMediatorAsset from './NativeMediatorAsset'
export default new NativeMediatorAsset({
id: 'qdai',
name: 'qDai',
network: '181',
usdPrice: 1,
mediatorAddress: '0xFEaB457D95D9990b7eb6c943c839258245541754'
})

View File

@@ -0,0 +1,9 @@
import { default as ERC677Asset } from './ERC677Asset'
export default new ERC677Asset({
id: 'xmoon',
name: 'xMOON',
network: '100',
address: '0x1e16aa4Df73d29C029d94CeDa3e3114EC191E25A',
icon: 'https://blockscout.com/poa/xdai/images/icons/moon.png'
})

View File

@@ -9,7 +9,8 @@ export default class TokenBridgeGateway extends Gateway {
this.providerStrings = {
'61': `https://www.ethercluster.com/etc`,
'77': 'https://sokol.poa.network',
'99': 'https://core.poa.network'
'99': 'https://core.poa.network',
'181': 'https://quorum-rpc.tokenbridge.net'
}
this.providers = {}
}
@@ -19,7 +20,7 @@ export default class TokenBridgeGateway extends Gateway {
}
getNetworks() {
return ['61', '77', '99']
return ['61', '77', '99', '181']
}
_provider(network) {

View File

@@ -1,6 +1,10 @@
export { default as sPOA } from './assets/sPOA'
export { default as Etc } from './assets/Etc'
export { default as Wetc } from './assets/Wetc'
export { default as Dai } from './assets/Dai'
export { default as qDai } from './assets/qDai'
export { default as MOON } from './assets/MOON'
export { default as xMOON } from './assets/xMOON'
export { default as ERC677Asset } from './assets/ERC677Asset'
export { default as BridgeableERC20Asset } from './assets/BridgeableERC20Asset'
export { default as NativeMediatorAsset } from './assets/NativeMediatorAsset'

View File

@@ -5,8 +5,12 @@ export {
sPOA,
Etc,
Wetc,
qDai,
Dai,
MOON,
xMOON,
TokenBridgeGateway,
Mediator,
MediatorErcToNative
} from './burner-wallet'
export { WETCBridge } from './wetc-bridge'
export { WETCBridge, QDAIBridge, MOONBridge } from './bridges'

View File

@@ -52,5 +52,19 @@ export default [
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getBridgeMode',
outputs: [
{
name: '',
type: 'bytes4'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
}
]

View File

@@ -1,4 +1,4 @@
export { constants, wait, waitForEvent, isBridgeContract } from './utils'
export { constants, wait, waitForEvent, isVanillaBridgeContract, isBridgeContract } from './utils'
export {
ERC677_ABI,
FOREIGN_NATIVE_TO_ERC_ABI,

View File

@@ -34,7 +34,7 @@ export const waitForEvent = async (web3, contract: Contract, event: string, call
}
}
export const isBridgeContract = async (contract: Contract): Promise<boolean> => {
export const isVanillaBridgeContract = async (contract: Contract): Promise<boolean> => {
try {
await contract.methods.deployedAtBlock().call()
return true
@@ -42,3 +42,15 @@ export const isBridgeContract = async (contract: Contract): Promise<boolean> =>
return false
}
}
export const isBridgeContract = async (contract: Contract, allowedModes?: string[]): Promise<boolean> => {
try {
const mode = await contract.methods.getBridgeMode().call()
if (typeof allowedModes === 'undefined') {
return true
}
return allowedModes.includes(mode)
} catch (e) {
return false
}
}

View File

@@ -1 +0,0 @@
export { default as WETCBridge } from './WETCBridge'

View File

@@ -1,12 +0,0 @@
---
version: '3.0'
services:
molecule_runner:
build:
context: ..
dockerfile: deployment-e2e/Dockerfile
restart: 'no'
privileged: true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ..:/mono

View File

@@ -1,9 +1,11 @@
#!/usr/bin/env bash
cd $(dirname $0)
cd ./e2e-commons
set -e # exit when any command fails
docker-compose pull molecule_runner
docker network create --driver bridge ultimate || true
while [ "$1" != "" ]; do
docker-compose build && docker-compose run molecule_runner /bin/bash -c "molecule test --scenario-name $1"
docker-compose run molecule_runner /bin/bash -c "molecule test --scenario-name $1"
shift # Shift all the parameters down by one
done

View File

@@ -4,7 +4,7 @@ ORACLE_BRIDGE_MODE: "NATIVE_TO_ERC"
UI_NATIVE_TOKEN_DISPLAY_NAME: "ETC"
## Home contract
COMMON_HOME_RPC_URL: "https://ethereumclassic.network"
COMMON_HOME_RPC_URL: "https://www.ethercluster.com/etc"
UI_HOME_NETWORK_DISPLAY_NAME: "Ethereum Classic"
UI_HOME_WITHOUT_EVENTS: false
COMMON_HOME_BRIDGE_ADDRESS: "0x073081832B4Ecdce79d4D6753565c85Ba4b3BeA9"

View File

@@ -47,6 +47,9 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR={{ COMMON_FOREIGN_GAS_PRICE_FACTOR }}
ORACLE_ALLOW_HTTP_FOR_RPC={{ "yes" if ORACLE_ALLOW_HTTP_FOR_RPC else "no" }}
ORACLE_QUEUE_URL={{ ORACLE_QUEUE_URL }}
ORACLE_REDIS_URL={{ ORACLE_REDIS_URL }}
{% if ORACLE_TX_REDUNDANCY | default('') != '' %}
ORACLE_TX_REDUNDANCY={{ ORACLE_TX_REDUNDANCY }}
{% endif %}
{% if ORACLE_HOME_START_BLOCK | default('') != '' %}
ORACLE_HOME_START_BLOCK={{ ORACLE_HOME_START_BLOCK }}

View File

@@ -0,0 +1,7 @@
ARG DOCKER_LOGIN
ARG CIRCLE_BRANCH
FROM ${DOCKER_LOGIN}/tokenbridge-e2e-ui:${CIRCLE_BRANCH}
ARG DOT_ENV_PATH
COPY ${DOT_ENV_PATH} ./.env

View File

@@ -22,9 +22,16 @@ Shut down and cleans up containers, networks, services, running scripts:
| --- | --- |
| deploy | Deploys the Smart Contracts |
| oracle | Launches Oracle containers |
| oracle-validator-2 | Launches Oracle containers for second validator |
| oracle-validator-3 | Launches Oracle containers for third validator |
| ui | Launches UI containers |
| blocks | Auto mines blocks |
| monitor | Launches Monitor containers |
| native-to-erc | Creates infrastructure for ultimate e2e testing, for native-to-erc type of bridge |
| erc-to-native | Creates infrastructure for ultimate e2e testing, for erc-to-native type of bridge |
| erc-to-erc | Creates infrastructure for ultimate e2e testing, for erc-to-erc type of bridge |
| amb | Creates infrastructure for ultimate e2e testing, for arbitrary message type of bridge |
| ultimate-amb-stake-erc-to-erc | Creates infrastructure for ultimate e2e testing, for stake token bridge |
#### Ultimate e2e testing

View File

@@ -15,13 +15,13 @@ It runs the e2e tests on components deployed using the deployment playbooks.
Run the Parity nodes, deploy the bridge contracts, deploy Oracle using the deployment playbook.
```bash
./up.sh deploy native-to-erc
./up.sh deploy native-to-erc blocks
```
### 2. Run the E2E tests
```
docker-compose run e2e yarn workspace oracle-e2e run native-to-erc
cd ui-e2e; yarn mocha -g "NATIVE_TO_ERC" -b ./test.js
```
## Diagram

View File

@@ -0,0 +1,10 @@
COMMON_HOME_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
COMMON_FOREIGN_BRIDGE_ADDRESS=0x0AEe1FCD12dDFab6265F7f8956e6E012A9Fe4Aa0
COMMON_HOME_RPC_URL=http://localhost:8541
COMMON_FOREIGN_RPC_URL=http://localhost:8542
ALM_HOME_NETWORK_NAME=Parity1
ALM_FOREIGN_NETWORK_NAME=Parity2
PORT=3000

View File

@@ -27,6 +27,7 @@ services:
networks:
- ultimate
oracle:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-oracle:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: oracle/Dockerfile
@@ -37,6 +38,7 @@ services:
networks:
- ultimate
oracle-erc20:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-oracle:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: oracle/Dockerfile
@@ -47,6 +49,7 @@ services:
networks:
- ultimate
oracle-erc20-native:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-oracle:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: oracle/Dockerfile
@@ -57,6 +60,7 @@ services:
networks:
- ultimate
oracle-amb:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-oracle:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: oracle/Dockerfile
@@ -67,6 +71,7 @@ services:
networks:
- ultimate
ui:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-ui:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: ui/Dockerfile
@@ -78,8 +83,10 @@ services:
ui-erc20:
build:
context: ..
dockerfile: ui/Dockerfile
dockerfile: e2e-commons/Dockerfile.ui
args:
DOCKER_LOGIN: ${DOCKER_LOGIN}
CIRCLE_BRANCH: ${CIRCLE_BRANCH}
DOT_ENV_PATH: e2e-commons/components-envs/ui-erc20.env
command: "true"
networks:
@@ -87,8 +94,10 @@ services:
ui-erc20-native:
build:
context: ..
dockerfile: ui/Dockerfile
dockerfile: e2e-commons/Dockerfile.ui
args:
DOCKER_LOGIN: ${DOCKER_LOGIN}
CIRCLE_BRANCH: ${CIRCLE_BRANCH}
DOT_ENV_PATH: e2e-commons/components-envs/ui-erc20-native.env
command: "true"
networks:
@@ -96,13 +105,26 @@ services:
ui-amb-stake-erc20-erc20:
build:
context: ..
dockerfile: ui/Dockerfile
dockerfile: e2e-commons/Dockerfile.ui
args:
DOCKER_LOGIN: ${DOCKER_LOGIN}
CIRCLE_BRANCH: ${CIRCLE_BRANCH}
DOT_ENV_PATH: e2e-commons/components-envs/ui-amb-stake-erc20-erc20.env
command: "true"
networks:
- ultimate
alm:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-alm:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: alm/Dockerfile
args:
DOT_ENV_PATH: e2e-commons/components-envs/alm.env
command: "true"
networks:
- ultimate
monitor:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-monitor:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: monitor/Dockerfile
@@ -113,6 +135,7 @@ services:
networks:
- ultimate
monitor-erc20:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-monitor:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: monitor/Dockerfile
@@ -123,6 +146,7 @@ services:
networks:
- ultimate
monitor-erc20-native:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-monitor:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: monitor/Dockerfile
@@ -133,6 +157,7 @@ services:
networks:
- ultimate
monitor-amb:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-monitor:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: monitor/Dockerfile
@@ -143,6 +168,7 @@ services:
networks:
- ultimate
e2e:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-e2e:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: Dockerfile.e2e
@@ -150,9 +176,20 @@ services:
networks:
- ultimate
blocks:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-e2e:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: Dockerfile.e2e
entrypoint: node e2e-commons/scripts/blocks.js
networks:
- ultimate
molecule_runner:
image: ${DOCKER_LOGIN}/tokenbridge-e2e-molecule_runner:${CIRCLE_BRANCH}
build:
context: ..
dockerfile: deployment-e2e/Dockerfile
restart: 'no'
privileged: true
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ..:/mono

17
e2e-commons/pull.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env bash
cd $(dirname $0)
set -e # exit when any command fails
docker-compose pull e2e
while [ "$1" != "" ]; do
if [ "$1" == "oracle" ]; then
docker-compose pull oracle
elif [ "$1" == "monitor" ]; then
docker-compose pull monitor
elif [ "$1" == "ui" ]; then
docker-compose pull ui
elif [ "$1" == "alm" ]; then
docker-compose pull alm
fi
shift
done

View File

@@ -23,10 +23,10 @@ function main() {
setTimeout(async () => {
try {
generateNewBlock(homeWeb3, blockGenerator.address)
} catch {} // in case of Transaction with the same hash was already imported.
} catch (_) {} // in case of Transaction with the same hash was already imported.
try {
generateNewBlock(foreignWeb3, blockGenerator.address)
} catch {} // in case of Transaction with the same hash was already imported.
} catch (_) {} // in case of Transaction with the same hash was already imported.
main()
}, 1000)
}

View File

@@ -3,14 +3,12 @@ cd $(dirname $0)
set -e # exit when any command fails
./down.sh
docker-compose build
docker-compose build parity1 parity2
test -n "$NODOCKERPULL" || ./pull.sh $@
docker network create --driver bridge ultimate || true
docker-compose up -d parity1 parity2 e2e
startValidator () {
# make sure that old image tags are not cached
docker-compose $1 build
docker-compose $1 run -d --name $4 redis
docker-compose $1 run -d --name $5 rabbit
docker-compose $1 run $2 $3 -d oracle yarn watcher:signature-request
@@ -34,9 +32,19 @@ startValidator () {
docker-compose $1 run $2 $3 -d oracle-erc20-native yarn sender:foreign
}
startAMBValidator () {
docker-compose $1 run -d --name $4 redis
docker-compose $1 run -d --name $5 rabbit
docker-compose $1 run $2 $3 -d oracle-amb yarn watcher:signature-request
docker-compose $1 run $2 $3 -d oracle-amb yarn watcher:collected-signatures
docker-compose $1 run $2 $3 -d oracle-amb yarn watcher:affirmation-request
docker-compose $1 run $2 $3 -d oracle-amb yarn sender:home
docker-compose $1 run $2 $3 -d oracle-amb yarn sender:foreign
}
while [ "$1" != "" ]; do
if [ "$1" == "oracle" ]; then
docker-compose up -d redis rabbit oracle oracle-erc20 oracle-erc20-native oracle-amb
docker-compose up -d redis rabbit
docker-compose run -d oracle yarn watcher:signature-request
docker-compose run -d oracle yarn watcher:collected-signatures
@@ -74,6 +82,9 @@ while [ "$1" != "" ]; do
fi
if [ "$1" == "ui" ]; then
# this should only rebuild last 3 steps from ui/Dockerfile
docker-compose build ui-erc20 ui-erc20-native ui-amb-stake-erc20-erc20
docker-compose up -d ui ui-erc20 ui-erc20-native ui-amb-stake-erc20-erc20
docker-compose run -d -p 3000:3000 ui yarn start
@@ -82,6 +93,12 @@ while [ "$1" != "" ]; do
docker-compose run -d -p 3003:3000 ui-amb-stake-erc20-erc20 yarn start
fi
if [ "$1" == "alm" ]; then
docker-compose up -d alm
docker-compose run -d -p 3004:3000 alm serve -p 3000 -s .
fi
if [ "$1" == "deploy" ]; then
docker-compose run e2e e2e-commons/scripts/deploy.sh
fi
@@ -114,5 +131,25 @@ while [ "$1" != "" ]; do
../deployment-e2e/molecule.sh ultimate-amb-stake-erc-to-erc
fi
if [ "$1" == "alm-e2e" ]; then
docker-compose up -d redis rabbit
docker-compose run -d oracle-amb yarn watcher:signature-request
docker-compose run -d oracle-amb yarn watcher:collected-signatures
docker-compose run -d oracle-amb yarn watcher:affirmation-request
docker-compose run -d oracle-amb yarn sender:home
docker-compose run -d oracle-amb yarn sender:foreign
oracle2name="-p validator2"
oracle2Values="-e ORACLE_VALIDATOR_ADDRESS=0xdCC784657C78054aa61FbcFFd2605F32374816A4 -e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=5a5c3645d0f04e9eb4f27f94ed4c244a225587405b8838e7456f7781ce3a9513"
oracle2comp="-e ORACLE_QUEUE_URL=amqp://rabbit2 -e ORACLE_REDIS_URL=redis://redis2"
startAMBValidator "$oracle2name" "$oracle2Values" "$oracle2comp" "redis2" "rabbit2"
oracle3name="-p validator3"
oracle3Values="-e ORACLE_VALIDATOR_ADDRESS=0xDcef88209a20D52165230104B245803C3269454d -e ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=f877f62a1c19f852cff1d29f0fb1ecac18821c0080d4cc0520c60c098293dca1"
oracle3comp="-e ORACLE_QUEUE_URL=amqp://rabbit3 -e ORACLE_REDIS_URL=redis://redis3"
startAMBValidator "$oracle3name" "$oracle3Values" "$oracle3comp" "redis3" "rabbit3"
fi
shift # Shift all the parameters down by one
done

View File

@@ -9,11 +9,12 @@
},
"author": "",
"license": "ISC",
"dependencies": {},
"dependencies": {
"mocha": "^5.2.0",
"axios": "0.19.0"
},
"engines": {
"node": ">= 8.9"
},
"devDependencies": {
"axios": "0.19.0"
}
"devDependencies": {}
}

View File

@@ -1,7 +1,7 @@
while true; do
sleep 3
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor yarn check-all
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20 yarn check-all
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20-native yarn check-all
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-amb yarn check-all
COMPOSE_INTERACTIVE_NO_CLI=1 nohup docker-compose -f ../e2e-commons/docker-compose.yml exec monitor yarn check-all
COMPOSE_INTERACTIVE_NO_CLI=1 nohup docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20 yarn check-all
COMPOSE_INTERACTIVE_NO_CLI=1 nohup docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20-native yarn check-all
COMPOSE_INTERACTIVE_NO_CLI=1 nohup docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-amb yarn check-all
done

View File

@@ -1,19 +1,27 @@
FROM node:8 as contracts
WORKDIR /mono
COPY contracts/package.json contracts/package-lock.json ./contracts/
WORKDIR /mono/contracts
RUN npm install --only=prod
COPY ./contracts/truffle-config.js ./
COPY ./contracts/contracts ./contracts
RUN npm run compile
FROM node:8
WORKDIR /mono
COPY package.json .
COPY contracts/package.json ./contracts/
COPY --from=contracts /mono/contracts/build ./contracts/build
COPY commons/package.json ./commons/
COPY monitor/package.json ./monitor/
COPY yarn.lock .
RUN yarn install --frozen-lockfile --production
COPY ./contracts ./contracts
RUN yarn run compile:contracts
RUN mv ./contracts/build ./ && rm -rf ./contracts/* ./contracts/.[!.]* && mv ./build ./contracts/
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production
COPY ./commons ./commons
COPY ./monitor ./monitor
WORKDIR /mono/monitor

View File

@@ -6,11 +6,13 @@
"scripts": {
"start": "mocha",
"lint": "eslint . --ignore-path ../.eslintignore",
"amb": "ULTIMATE=true mocha test/amb.js"
"amb": "ULTIMATE=true mocha test/amb.js",
"alm": "mocha test/amb.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"mocha": "^5.2.0",
"chalk": "^2.4.1",
"dotenv": "^6.0.0",
"promise-retry": "^1.1.1",

View File

@@ -1,3 +1,16 @@
FROM node:8 as contracts
WORKDIR /mono
COPY contracts/package.json contracts/package-lock.json ./contracts/
WORKDIR /mono/contracts
RUN npm install --only=prod
COPY ./contracts/truffle-config.js ./
COPY ./contracts/contracts ./contracts
RUN npm run compile
FROM node:8
RUN apt-get update
@@ -9,18 +22,13 @@ RUN apt-get clean
WORKDIR /mono
COPY package.json .
COPY contracts/package.json ./contracts/
COPY --from=contracts /mono/contracts/build ./contracts/build
COPY commons/package.json ./commons/
COPY oracle/package.json ./oracle/
COPY yarn.lock .
RUN yarn install --production --frozen-lockfile
COPY ./contracts ./contracts
RUN yarn run compile:contracts
RUN mv ./contracts/build ./ && rm -rf ./contracts/* ./contracts/.[!.]* && mv ./build ./contracts/
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile --production
COPY ./commons ./commons
COPY ./oracle ./oracle
WORKDIR /mono/oracle

View File

@@ -30,7 +30,6 @@
"dotenv": "^5.0.1",
"http-list-provider": "0.0.5",
"ioredis": "^3.2.2",
"lodash": "^4.17.10",
"node-fetch": "^2.1.2",
"pino": "^4.17.3",
"pino-pretty": "^2.0.1",

View File

@@ -7,7 +7,7 @@ const rpcUrlsManager = require('./services/getRpcUrlsManager')
const { getNonce, getChainId, getEventsFromTx } = require('./tx/web3')
const { sendTx } = require('./tx/sendTx')
const { checkHTTPS, watchdog, syncForEach, addExtraGas } = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE } = require('./utils/constants')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
const { ORACLE_VALIDATOR_ADDRESS, ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY, ORACLE_ALLOW_HTTP_FOR_RPC } = process.env
@@ -143,7 +143,12 @@ async function sendJobTx(jobs) {
let nonce = await getNonce(web3Instance, ORACLE_VALIDATOR_ADDRESS)
await syncForEach(jobs, async job => {
const gasLimit = addExtraGas(job.gasEstimate, EXTRA_GAS_PERCENTAGE)
let gasLimit
if (typeof job.extraGas === 'number') {
gasLimit = addExtraGas(job.gasEstimate + job.extraGas, 0, MAX_GAS_LIMIT)
} else {
gasLimit = addExtraGas(job.gasEstimate, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT)
}
try {
logger.info(`Sending transaction with nonce ${nonce}`)

View File

@@ -3,14 +3,23 @@ const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = req
const logger = require('../../services/logger').child({
module: 'processAffirmationRequests:estimateGas'
})
const { parseAMBHeader } = require('../../utils/message')
const { strip0x } = require('../../../../commons')
const {
AMB_AFFIRMATION_REQUEST_EXTRA_GAS_ESTIMATOR: estimateExtraGas,
MIN_AMB_HEADER_LENGTH
} = require('../../utils/constants')
async function estimateGas({ web3, homeBridge, validatorContract, message, address }) {
try {
const gasEstimate = await homeBridge.methods.executeAffirmation(message).estimateGas({
from: address
})
const msgGasLimit = parseAMBHeader(message).gasLimit
// message length in bytes
const len = strip0x(message).length / 2 - MIN_AMB_HEADER_LENGTH
return gasEstimate
return gasEstimate + msgGasLimit + estimateExtraGas(len)
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e

View File

@@ -4,7 +4,7 @@ const promiseLimit = require('promise-limit')
const rootLogger = require('../../services/logger')
const { web3Home } = require('../../services/web3')
const bridgeValidatorsABI = require('../../../../contracts/build/contracts/BridgeValidators').abi
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
const { EXIT_CODES, MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
const estimateGas = require('./estimateGas')
const { parseAMBMessage } = require('../../../../commons')
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
@@ -75,6 +75,7 @@ function processAffirmationRequestsBuilder(config) {
txToSend.push({
data,
gasEstimate,
extraGas: EXTRA_GAS_ABSOLUTE,
transactionReference: affirmationRequest.transactionHash,
to: config.homeBridgeAddress
})

View File

@@ -4,6 +4,7 @@ const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError
const logger = require('../../services/logger').child({
module: 'processCollectedSignatures:estimateGas'
})
const { parseAMBHeader } = require('../../utils/message')
const web3 = new Web3()
const { toBN } = Web3.utils
@@ -24,7 +25,12 @@ async function estimateGas({
const gasEstimate = await foreignBridge.methods.executeSignatures(message, signatures).estimateGas({
from: address
})
return gasEstimate
const msgGasLimit = parseAMBHeader(message).gasLimit
// + estimateExtraGas(len)
// is not needed here, since estimateGas will already take into account gas
// needed for memory expansion, message processing, etc.
return gasEstimate + msgGasLimit
} catch (e) {
if (e instanceof HttpListProviderError) {
throw e

View File

@@ -8,7 +8,7 @@ const { signatureToVRS, packSignatures } = require('../../utils/message')
const { parseAMBMessage } = require('../../../../commons')
const estimateGas = require('./estimateGas')
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
const { MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
const { MAX_CONCURRENT_EVENTS, EXTRA_GAS_ABSOLUTE } = require('../../utils/constants')
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
@@ -107,6 +107,7 @@ function processCollectedSignaturesBuilder(config) {
txToSend.push({
data,
gasEstimate,
extraGas: EXTRA_GAS_ABSOLUTE,
transactionReference: colSignature.transactionHash,
to: config.foreignBridgeAddress
})

View File

@@ -16,7 +16,7 @@ const {
watchdog,
nonceError
} = require('./utils/utils')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE } = require('./utils/constants')
const { EXIT_CODES, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT } = require('./utils/constants')
const { ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY } = process.env
@@ -106,7 +106,12 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry }) {
logger.debug(`Sending ${txArray.length} transactions`)
await syncForEach(txArray, async job => {
const gasLimit = addExtraGas(job.gasEstimate, EXTRA_GAS_PERCENTAGE)
let gasLimit
if (typeof job.extraGas === 'number') {
gasLimit = addExtraGas(job.gasEstimate + job.extraGas, 0, MAX_GAS_LIMIT)
} else {
gasLimit = addExtraGas(job.gasEstimate, EXTRA_GAS_PERCENTAGE, MAX_GAS_LIMIT)
}
try {
logger.info(`Sending transaction with nonce ${nonce}`)

View File

@@ -1,7 +1,7 @@
const _ = require('lodash')
const promiseRetry = require('promise-retry')
const tryEach = require('../utils/tryEach')
const { RETRY_CONFIG } = require('../utils/constants')
const { promiseAny } = require('../utils/utils')
function RpcUrlsManager(homeUrls, foreignUrls) {
if (!homeUrls) {
@@ -15,19 +15,22 @@ function RpcUrlsManager(homeUrls, foreignUrls) {
this.foreignUrls = foreignUrls.split(',')
}
RpcUrlsManager.prototype.tryEach = async function(chain, f) {
RpcUrlsManager.prototype.tryEach = async function(chain, f, redundant = false) {
if (chain !== 'home' && chain !== 'foreign') {
throw new Error(`Invalid argument chain: '${chain}'`)
}
// save homeUrls to avoid race condition
const urls = chain === 'home' ? _.cloneDeep(this.homeUrls) : _.cloneDeep(this.foreignUrls)
// save urls to avoid race condition
const urls = chain === 'home' ? [...this.homeUrls] : [...this.foreignUrls]
const [result, index] = await promiseRetry(retry =>
tryEach(urls, f).catch(() => {
retry()
}, RETRY_CONFIG)
)
if (redundant) {
// result from first responded node will be returned immediately
// remaining nodes will continue to retry queries in separate promises
// promiseAny will throw only if all urls reached max retry number
return promiseAny(urls.map(url => promiseRetry(retry => f(url).catch(retry), RETRY_CONFIG)))
}
const [result, index] = await promiseRetry(retry => tryEach(urls, f).catch(retry), RETRY_CONFIG)
if (index > 0) {
// rotate urls

View File

@@ -2,6 +2,8 @@ const Web3Utils = require('web3-utils')
const fetch = require('node-fetch')
const rpcUrlsManager = require('../services/getRpcUrlsManager')
const { ORACLE_TX_REDUNDANCY } = process.env
// eslint-disable-next-line consistent-return
async function sendTx({ chain, privateKey, data, nonce, gasPrice, amount, gasLimit, to, chainId, web3 }) {
const serializedTx = await web3.eth.accounts.signTransaction(
@@ -26,27 +28,31 @@ async function sendTx({ chain, privateKey, data, nonce, gasPrice, amount, gasLim
// eslint-disable-next-line consistent-return
async function sendRawTx({ chain, params, method }) {
const result = await rpcUrlsManager.tryEach(chain, async url => {
// curl -X POST --data '{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":[{see above}],"id":1}'
const response = await fetch(url, {
headers: {
'Content-type': 'application/json'
},
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method,
params,
id: Math.floor(Math.random() * 100) + 1
const result = await rpcUrlsManager.tryEach(
chain,
async url => {
// curl -X POST --data '{"jsonrpc":"2.0","method":"eth_sendRawTransaction","params":[{see above}],"id":1}'
const response = await fetch(url, {
headers: {
'Content-type': 'application/json'
},
method: 'POST',
body: JSON.stringify({
jsonrpc: '2.0',
method,
params,
id: Math.floor(Math.random() * 100) + 1
})
})
})
if (!response.ok) {
throw new Error(response.statusText)
}
if (!response.ok) {
throw new Error(response.statusText)
}
return response
})
return response
},
ORACLE_TX_REDUNDANCY === 'true' && method === 'eth_sendRawTransaction'
)
const json = await result.json()
if (json.error) {

View File

@@ -1,5 +1,9 @@
module.exports = {
EXTRA_GAS_PERCENTAGE: 4,
EXTRA_GAS_ABSOLUTE: 200000,
AMB_AFFIRMATION_REQUEST_EXTRA_GAS_ESTIMATOR: len => Math.floor(0.0035 * len ** 2 + 40 * len),
MIN_AMB_HEADER_LENGTH: 32 + 20 + 20 + 4 + 2 + 1 + 2,
MAX_GAS_LIMIT: 10000000,
MAX_CONCURRENT_EVENTS: 50,
RETRY_CONFIG: {
retries: 20,

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