Merge branch 'master' into amb-oracle
# Conflicts: # .prettierrc # commons/abis.js # commons/package.json # e2e-commons/docker-compose.yml # monitor/eventsStats.js # monitor/utils/events.js # oracle/src/services/gasPrice.js # oracle/test/gasPrice.test.js # yarn.lock
This commit is contained in:
commit
d22a3e8bae
@ -1,8 +1,84 @@
|
||||
version: 2
|
||||
version: 2.1
|
||||
|
||||
orbs:
|
||||
tokenbridge-orb:
|
||||
commands:
|
||||
install-chrome:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Chrome
|
||||
command: |
|
||||
wget -O chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
sudo dpkg -i chrome.deb
|
||||
install-node:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Node
|
||||
command: |
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
|
||||
|
||||
nvm install 11.4.0 && nvm alias default 11.4.0
|
||||
|
||||
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
|
||||
echo ' [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||
install-yarn:
|
||||
steps:
|
||||
- run:
|
||||
name: Install Yarn
|
||||
command: |
|
||||
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
sudo apt-get update && sudo apt-get -y install yarn
|
||||
yarn-install-cached-on-machine:
|
||||
steps:
|
||||
- restore_cache:
|
||||
name: Restore Machine Yarn Package Cache
|
||||
keys:
|
||||
- yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
- run:
|
||||
name: Install npm dependencies using Yarn
|
||||
command: nvm use default; yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
name: Save Machine Yarn Package Cache
|
||||
key: yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
wait-for-oracle:
|
||||
parameters:
|
||||
redis-key:
|
||||
type: string
|
||||
steps:
|
||||
- run:
|
||||
name: Install redis tools
|
||||
command: sudo apt-get install -y redis-tools
|
||||
- run:
|
||||
name: Wait for the Oracle to start
|
||||
command: |
|
||||
set +e
|
||||
i=0
|
||||
while [[ $(redis-cli GET << parameters.redis-key >> ) ]]; do
|
||||
((i++))
|
||||
if [ "$i" -gt 30 ]
|
||||
then
|
||||
exit -1
|
||||
fi
|
||||
|
||||
echo "Sleeping..."
|
||||
sleep 3
|
||||
done
|
||||
executors:
|
||||
docker-node:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
machine-with-docker-caching:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
|
||||
jobs:
|
||||
initialize:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
@ -36,35 +112,30 @@ jobs:
|
||||
paths:
|
||||
- ~/project
|
||||
initialize-root:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- checkout
|
||||
- run: sudo su - -c 'export CI=true && cd /home/circleci/project && yarn initialize && yarn test'
|
||||
build:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run build
|
||||
lint:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run lint
|
||||
test:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn run test
|
||||
oracle-e2e:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
@ -72,75 +143,75 @@ jobs:
|
||||
docker_layer_caching: true
|
||||
- run: yarn run oracle-e2e
|
||||
ui-e2e:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: |
|
||||
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
|
||||
echo ' [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
|
||||
- run: nvm install 11.4.0 && nvm alias default 11.4.0
|
||||
- run: curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
|
||||
- run: echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
|
||||
- run: sudo apt-get update && sudo apt-get install yarn
|
||||
- run: wget https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
|
||||
- run: sudo dpkg -i google-chrome-stable_current_amd64.deb
|
||||
- tokenbridge-orb/install-node
|
||||
- tokenbridge-orb/install-yarn
|
||||
- tokenbridge-orb/install-chrome
|
||||
- run: git submodule update --init
|
||||
- restore_cache:
|
||||
name: Restore Machine Yarn Package Cache
|
||||
keys:
|
||||
- yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
- run: yarn install --frozen-lockfile
|
||||
- save_cache:
|
||||
name: Save Machine Yarn Package Cache
|
||||
key: yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
|
||||
paths:
|
||||
- ~/.cache/yarn
|
||||
- tokenbridge-orb/yarn-install-cached-on-machine
|
||||
- run: yarn run ui-e2e
|
||||
monitor-e2e:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- run: ./monitor-e2e/run-tests.sh
|
||||
cover:
|
||||
docker:
|
||||
- image: circleci/node:10.15
|
||||
executor: tokenbridge-orb/docker-node
|
||||
steps:
|
||||
- restore_cache:
|
||||
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
|
||||
- run: yarn workspace ui run coverage
|
||||
- run: yarn workspace ui run coveralls
|
||||
deployment-oracle:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: deployment/molecule/molecule.sh oracle
|
||||
deployment-ui:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: deployment/molecule/molecule.sh ui
|
||||
ultimate-native-to-erc:
|
||||
machine:
|
||||
image: circleci/classic:latest
|
||||
docker_layer_caching: true
|
||||
deployment-monitor:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
steps:
|
||||
- checkout
|
||||
- run: deployment/molecule/molecule.sh monitor
|
||||
ultimate:
|
||||
executor: tokenbridge-orb/machine-with-docker-caching
|
||||
parameters:
|
||||
scenario-name:
|
||||
description: "Molecule scenario name used to create the infrastructure"
|
||||
type: string
|
||||
redis-key:
|
||||
description: "Redis key checked for non-emptiness to assert if Oracle is running"
|
||||
type: string
|
||||
ui-e2e-grep:
|
||||
description: "Mocha grep string used to run ui-e2e tests specific to given type of bridge"
|
||||
type: string
|
||||
steps:
|
||||
- checkout
|
||||
- run: git submodule update --init
|
||||
- tokenbridge-orb/install-node
|
||||
- tokenbridge-orb/install-chrome
|
||||
- tokenbridge-orb/install-yarn
|
||||
- tokenbridge-orb/yarn-install-cached-on-machine
|
||||
- run:
|
||||
name: Prepare the infrastructure
|
||||
command: e2e-commons/up.sh deploy native-to-erc
|
||||
- run: 'echo "TODO - Run the e2e tests on top of the infrastructure created by previous step"'
|
||||
command: e2e-commons/up.sh deploy << parameters.scenario-name >>
|
||||
no_output_timeout: 30m
|
||||
- tokenbridge-orb/wait-for-oracle:
|
||||
redis-key: << parameters.redis-key >>
|
||||
- run:
|
||||
name: Run the ui-e2e tests
|
||||
command: |
|
||||
nvm use default;
|
||||
node ./e2e-commons/scripts/blocks.js &
|
||||
cd ui-e2e; yarn mocha -g "<< parameters.ui-e2e-grep >>" -b ./test.js
|
||||
workflows:
|
||||
version: 2
|
||||
tokenbridge:
|
||||
jobs:
|
||||
- initialize
|
||||
@ -168,4 +239,19 @@ workflows:
|
||||
- monitor-e2e
|
||||
- deployment-oracle
|
||||
- deployment-ui
|
||||
- ultimate-native-to-erc
|
||||
- deployment-monitor
|
||||
- ultimate:
|
||||
name: "ultimate: native to erc"
|
||||
scenario-name: native-to-erc
|
||||
redis-key: native-erc-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "NATIVE TO ERC"
|
||||
- ultimate:
|
||||
name: "ultimate: erc to native"
|
||||
scenario-name: erc-to-native
|
||||
redis-key: erc-native-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "ERC TO NATIVE"
|
||||
- ultimate:
|
||||
name: "ultimate: erc to erc"
|
||||
scenario-name: erc-to-erc
|
||||
redis-key: erc-erc-collected-signatures:lastProcessedBlock
|
||||
ui-e2e-grep: "ERC TO ERC"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"printWidth": 120,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
|
10
README.md
10
README.md
@ -3,15 +3,15 @@
|
||||
[![License: LGPL v3.0](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
|
||||
|
||||
# Tokenbridge
|
||||
Welcome to the **POA Token Bridge** monorepository!
|
||||
Welcome to the **POA TokenBridge** monorepository!
|
||||
|
||||
Please note that this repository as a **work in progress**.
|
||||
|
||||
## Overview
|
||||
|
||||
The POA Token Bridge allows users to transfer assets between two chains in the Ethereum ecosystem. It is composed of several elements which are contained within this monorepository.
|
||||
The POA TokenBridge allows users to transfer assets between two chains in the Ethereum ecosystem. It is composed of several elements which are contained within this monorepository.
|
||||
|
||||
For a complete picture of the POA Token Bridge functionality, it is useful to explore each subrepository.
|
||||
For a complete picture of the POA TokenBridge functionality, it is useful to explore each subrepository.
|
||||
|
||||
## Structure
|
||||
|
||||
@ -35,9 +35,9 @@ Additionally there are [Smart Contracts](https://github.com/poanetwork/poa-bridg
|
||||
|
||||
| **Launched by POA** | **Launched by 3rd parties** |
|
||||
| ---------- | ---------- |
|
||||
| [POA20 Bridge](https://bridge.poa.net/) | [Ocean Token Bridge](https://bridge.oceanprotocol.com/) |
|
||||
| [POA20 Bridge](https://bridge.poa.net/) | [Ocean TokenBridge](https://bridge.oceanprotocol.com/) |
|
||||
| [xDai Bridge](https://dai-bridge.poa.network/) | [Thunder bridge](https://ui.stormdapps.com/) |
|
||||
| [WETC Bridge](https://wetc.app/) | [Volta Token Bridge](https://vt.volta.bridge.eth.events/) & [DAI bridge to Volta Chain](https://dai.volta.bridge.eth.events/) |
|
||||
| [WETC Bridge](https://wetc.app/) | [Volta TokenBridge](https://vt.volta.bridge.eth.events/) & [DAI bridge to Volta Chain](https://dai.volta.bridge.eth.events/) |
|
||||
| | [Artis Brige](https://bridge.artis.network/) |
|
||||
| | [Tenda bridge](https://bridge-mainnet.tenda.network) & [xDai-to-Tenda bridge](https://bridge-xdai.tenda.network/) |
|
||||
|
||||
|
Binary file not shown.
@ -4,7 +4,8 @@
|
||||
"../.eslintrc"
|
||||
],
|
||||
"rules": {
|
||||
"no-unused-expressions": "off"
|
||||
"no-unused-expressions": "off",
|
||||
"import/no-extraneous-dependencies": "off"
|
||||
},
|
||||
"env": {
|
||||
"mocha": true
|
||||
|
@ -1,2 +1,2 @@
|
||||
# POA Token Bridge / Commons
|
||||
# POA TokenBridge / Commons
|
||||
Interfaces, constants and utilities shared between the sub-repositories
|
||||
|
@ -1,12 +1,9 @@
|
||||
const HOME_NATIVE_TO_ERC_ABI = require('../contracts/build/contracts/HomeBridgeNativeToErc').abi
|
||||
const FOREIGN_NATIVE_TO_ERC_ABI = require('../contracts/build/contracts/ForeignBridgeNativeToErc')
|
||||
.abi
|
||||
const FOREIGN_NATIVE_TO_ERC_ABI = require('../contracts/build/contracts/ForeignBridgeNativeToErc').abi
|
||||
const HOME_ERC_TO_ERC_ABI = require('../contracts/build/contracts/HomeBridgeErcToErc').abi
|
||||
const FOREIGN_ERC_TO_ERC_ABI = require('../contracts/build/contracts/ForeignBridgeErc677ToErc677')
|
||||
.abi
|
||||
const FOREIGN_ERC_TO_ERC_ABI = require('../contracts/build/contracts/ForeignBridgeErc677ToErc677').abi
|
||||
const HOME_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/HomeBridgeErcToNative').abi
|
||||
const FOREIGN_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/ForeignBridgeErcToNative')
|
||||
.abi
|
||||
const FOREIGN_ERC_TO_NATIVE_ABI = require('../contracts/build/contracts/ForeignBridgeErcToNative').abi
|
||||
const ERC20_ABI = require('../contracts/build/contracts/ERC20').abi
|
||||
const ERC677_ABI = require('../contracts/build/contracts/ERC677').abi
|
||||
const ERC677_BRIDGE_TOKEN_ABI = require('../contracts/build/contracts/ERC677BridgeToken').abi
|
||||
@ -17,9 +14,40 @@ const HOME_AMB_ABI = require('../contracts/build/contracts/HomeAMB').abi
|
||||
const FOREIGN_AMB_ABI = require('../contracts/build/contracts/ForeignAMB').abi
|
||||
const BOX_ABI = require('../contracts/build/contracts/Box').abi
|
||||
|
||||
const { homeV1Abi, foreignViAbi } = require('./v1Abis')
|
||||
const { HOME_V1_ABI, FOREIGN_V1_ABI } = require('./v1Abis')
|
||||
const { BRIDGE_MODES } = require('./constants')
|
||||
|
||||
const ERC20_BYTES32_ABI = [
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'name',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
},
|
||||
{
|
||||
constant: true,
|
||||
inputs: [],
|
||||
name: 'symbol',
|
||||
outputs: [
|
||||
{
|
||||
name: '',
|
||||
type: 'bytes32'
|
||||
}
|
||||
],
|
||||
payable: false,
|
||||
stateMutability: 'view',
|
||||
type: 'function'
|
||||
}
|
||||
]
|
||||
|
||||
function getBridgeABIs(bridgeMode) {
|
||||
let HOME_ABI = null
|
||||
let FOREIGN_ABI = null
|
||||
@ -33,8 +61,8 @@ function getBridgeABIs(bridgeMode) {
|
||||
HOME_ABI = HOME_ERC_TO_NATIVE_ABI
|
||||
FOREIGN_ABI = FOREIGN_ERC_TO_NATIVE_ABI
|
||||
} else if (bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1) {
|
||||
HOME_ABI = homeV1Abi
|
||||
FOREIGN_ABI = foreignViAbi
|
||||
HOME_ABI = HOME_V1_ABI
|
||||
FOREIGN_ABI = FOREIGN_V1_ABI
|
||||
} else if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
|
||||
HOME_ABI = HOME_AMB_ABI
|
||||
FOREIGN_ABI = FOREIGN_AMB_ABI
|
||||
@ -59,6 +87,9 @@ module.exports = {
|
||||
BLOCK_REWARD_ABI,
|
||||
BRIDGE_VALIDATORS_ABI,
|
||||
REWARDABLE_VALIDATORS_ABI,
|
||||
HOME_V1_ABI,
|
||||
FOREIGN_V1_ABI,
|
||||
ERC20_BYTES32_ABI,
|
||||
HOME_AMB_ABI,
|
||||
FOREIGN_AMB_ABI,
|
||||
BOX_ABI
|
||||
|
162
commons/test/gas.js
Normal file
162
commons/test/gas.js
Normal file
@ -0,0 +1,162 @@
|
||||
const { expect } = require('chai')
|
||||
const Web3Utils = require('web3-utils')
|
||||
const { gasPriceWithinLimits, normalizeGasPrice } = require('..')
|
||||
|
||||
const GAS_PRICE_BOUNDARIES = {
|
||||
MIN: 1,
|
||||
MAX: 250
|
||||
}
|
||||
|
||||
describe('gas', () => {
|
||||
describe('normalizeGasPrice', () => {
|
||||
it('should work with oracle gas price in gwei', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 30
|
||||
const factor = 1
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal('30000000000')
|
||||
})
|
||||
it('should work with oracle gas price not in gwei', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 300
|
||||
const factor = 0.1
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal('30000000000')
|
||||
})
|
||||
it('should increase gas price value from oracle', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 20
|
||||
const factor = 1.5
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal('30000000000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('gasPriceWithinLimits', () => {
|
||||
it('should return gas price if gas price is between boundaries', () => {
|
||||
// given
|
||||
const minGasPrice = 1
|
||||
const middleGasPrice = 10
|
||||
const maxGasPrice = 250
|
||||
|
||||
// when
|
||||
const minGasPriceWithinLimits = gasPriceWithinLimits(minGasPrice, GAS_PRICE_BOUNDARIES)
|
||||
const middleGasPriceWithinLimits = gasPriceWithinLimits(middleGasPrice, GAS_PRICE_BOUNDARIES)
|
||||
const maxGasPriceWithinLimits = gasPriceWithinLimits(maxGasPrice, GAS_PRICE_BOUNDARIES)
|
||||
|
||||
// then
|
||||
expect(minGasPriceWithinLimits).to.equal(minGasPrice)
|
||||
expect(middleGasPriceWithinLimits).to.equal(middleGasPrice)
|
||||
expect(maxGasPriceWithinLimits).to.equal(maxGasPrice)
|
||||
})
|
||||
|
||||
it('should return min limit if gas price is below min boundary', () => {
|
||||
// Given
|
||||
const initialGasPrice = 0.5
|
||||
|
||||
// When
|
||||
const gasPrice = gasPriceWithinLimits(initialGasPrice, GAS_PRICE_BOUNDARIES)
|
||||
|
||||
// Then
|
||||
expect(gasPrice).to.equal(GAS_PRICE_BOUNDARIES.MIN)
|
||||
})
|
||||
|
||||
it('should return max limit if gas price is above max boundary', () => {
|
||||
// Given
|
||||
const initialGasPrice = 260
|
||||
|
||||
// When
|
||||
const gasPrice = gasPriceWithinLimits(initialGasPrice, GAS_PRICE_BOUNDARIES)
|
||||
|
||||
// Then
|
||||
expect(gasPrice).to.equal(GAS_PRICE_BOUNDARIES.MAX)
|
||||
})
|
||||
|
||||
it('should return gas price if boundaries not provided', () => {
|
||||
// Given
|
||||
const initialGasPrice = 260
|
||||
|
||||
// When
|
||||
const gasPrice = gasPriceWithinLimits(initialGasPrice)
|
||||
|
||||
// Then
|
||||
expect(gasPrice).to.equal(initialGasPrice)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizeGasPrice', () => {
|
||||
it('should work with oracle gas price in gwei', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 20
|
||||
const factor = 1
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal('20000000000')
|
||||
})
|
||||
|
||||
it('should work with oracle gas price not in gwei', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 200
|
||||
const factor = 0.1
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal('20000000000')
|
||||
})
|
||||
|
||||
it('should increase gas price value from oracle', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 20
|
||||
const factor = 1.5
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal('30000000000')
|
||||
})
|
||||
|
||||
it('should respect gas price max limit', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 200
|
||||
const factor = 4
|
||||
const maxInWei = Web3Utils.toWei(GAS_PRICE_BOUNDARIES.MAX.toString(), 'gwei')
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor, GAS_PRICE_BOUNDARIES).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal(maxInWei)
|
||||
})
|
||||
|
||||
it('should respect gas price min limit', () => {
|
||||
// Given
|
||||
const oracleGasPrice = 1
|
||||
const factor = 0.01
|
||||
const minInWei = Web3Utils.toWei(GAS_PRICE_BOUNDARIES.MIN.toString(), 'gwei')
|
||||
|
||||
// When
|
||||
const result = normalizeGasPrice(oracleGasPrice, factor, GAS_PRICE_BOUNDARIES).toString()
|
||||
|
||||
// Then
|
||||
expect(result).to.equal(minInWei)
|
||||
})
|
||||
})
|
||||
})
|
64
commons/test/getTokenType.js
Normal file
64
commons/test/getTokenType.js
Normal file
@ -0,0 +1,64 @@
|
||||
const { expect } = require('chai')
|
||||
const { getTokenType, ERC_TYPES } = require('..')
|
||||
|
||||
describe('getTokenType', () => {
|
||||
it('should return ERC677 if bridgeContract is equal to bridgeAddress', async () => {
|
||||
// Given
|
||||
const bridgeAddress = '0xCecBE80Ed3548dE11D7d2D922a36576eA40C4c26'
|
||||
const contract = {
|
||||
methods: {
|
||||
bridgeContract: () => {
|
||||
return {
|
||||
call: () => Promise.resolve(bridgeAddress)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
const type = await getTokenType(contract, bridgeAddress)
|
||||
|
||||
// Then
|
||||
expect(type).to.equal(ERC_TYPES.ERC677)
|
||||
})
|
||||
|
||||
it('should return ERC20 if bridgeContract is not equal to bridgeAddress', async () => {
|
||||
// Given
|
||||
const bridgeAddress = '0xCecBE80Ed3548dE11D7d2D922a36576eA40C4c26'
|
||||
const contract = {
|
||||
methods: {
|
||||
bridgeContract: () => {
|
||||
return {
|
||||
call: () => Promise.resolve('0xBFCb120F7B1de491262CA4D9D8Eba70438b6896E')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
const type = await getTokenType(contract, bridgeAddress)
|
||||
|
||||
// Then
|
||||
expect(type).to.equal(ERC_TYPES.ERC20)
|
||||
})
|
||||
|
||||
it('should return ERC20 if bridgeContract is not present', async () => {
|
||||
// Given
|
||||
const bridgeAddress = '0xCecBE80Ed3548dE11D7d2D922a36576eA40C4c26'
|
||||
const contract = {
|
||||
methods: {
|
||||
bridgeContract: () => {
|
||||
return {
|
||||
call: () => Promise.reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When
|
||||
const type = await getTokenType(contract, bridgeAddress)
|
||||
|
||||
// Then
|
||||
expect(type).to.equal(ERC_TYPES.ERC20)
|
||||
})
|
||||
})
|
@ -1,4 +1,5 @@
|
||||
import { processValidatorsEvents, parseValidatorEvent } from '../contract'
|
||||
const { expect } = require('chai')
|
||||
const { processValidatorsEvents, parseValidatorEvent } = require('..')
|
||||
|
||||
describe('parseValidatorEvent', () => {
|
||||
it('should parse ValidatorAdded event from v1', () => {
|
||||
@ -14,8 +15,8 @@ describe('parseValidatorEvent', () => {
|
||||
parseValidatorEvent(event)
|
||||
|
||||
// Then
|
||||
expect(event.event).toBe('ValidatorAdded')
|
||||
expect(event.returnValues.validator).toBe('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
expect(event.event).to.be.equal('ValidatorAdded')
|
||||
expect(event.returnValues.validator).to.be.equal('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
})
|
||||
it('should parse ValidatorAdded event', () => {
|
||||
// Given
|
||||
@ -33,8 +34,8 @@ describe('parseValidatorEvent', () => {
|
||||
parseValidatorEvent(event)
|
||||
|
||||
// Then
|
||||
expect(event.event).toBe('ValidatorAdded')
|
||||
expect(event.returnValues.validator).toBe('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
expect(event.event).to.be.equal('ValidatorAdded')
|
||||
expect(event.returnValues.validator).to.be.equal('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
})
|
||||
it('should parse ValidatorAdded event from rewardableValidators', () => {
|
||||
// Given
|
||||
@ -52,8 +53,8 @@ describe('parseValidatorEvent', () => {
|
||||
parseValidatorEvent(event)
|
||||
|
||||
// Then
|
||||
expect(event.event).toBe('ValidatorAdded')
|
||||
expect(event.returnValues.validator).toBe('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
expect(event.event).to.be.equal('ValidatorAdded')
|
||||
expect(event.returnValues.validator).to.be.equal('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
})
|
||||
it('should parse ValidatorRemoved event', () => {
|
||||
// Given
|
||||
@ -71,8 +72,8 @@ describe('parseValidatorEvent', () => {
|
||||
parseValidatorEvent(event)
|
||||
|
||||
// Then
|
||||
expect(event.event).toBe('ValidatorRemoved')
|
||||
expect(event.returnValues.validator).toBe('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
expect(event.event).to.be.equal('ValidatorRemoved')
|
||||
expect(event.returnValues.validator).to.be.equal('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
})
|
||||
it('should parse ValidatorRemoved event from v1', () => {
|
||||
// Given
|
||||
@ -87,8 +88,8 @@ describe('parseValidatorEvent', () => {
|
||||
parseValidatorEvent(event)
|
||||
|
||||
// Then
|
||||
expect(event.event).toBe('ValidatorRemoved')
|
||||
expect(event.returnValues.validator).toBe('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
expect(event.event).to.be.equal('ValidatorRemoved')
|
||||
expect(event.returnValues.validator).to.be.equal('0xcfef0c6bb765321529ffe81507f6d099693cd225')
|
||||
})
|
||||
})
|
||||
describe('processValidatorsEvents', () => {
|
||||
@ -169,9 +170,9 @@ describe('processValidatorsEvents', () => {
|
||||
const validatorList = processValidatorsEvents(events)
|
||||
|
||||
// Then
|
||||
expect(validatorList.length).toBe(3)
|
||||
expect(validatorList[0]).toBe('0xCbd25A2a5708051747a052dBB1b291865Fc0e474')
|
||||
expect(validatorList[1]).toBe('0xBac68A86Cf596E3b124781E0bdbC47bb458bec62')
|
||||
expect(validatorList[2]).toBe('0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955')
|
||||
expect(validatorList.length).to.be.equal(3)
|
||||
expect(validatorList[0]).to.be.equal('0xCbd25A2a5708051747a052dBB1b291865Fc0e474')
|
||||
expect(validatorList[1]).to.be.equal('0xBac68A86Cf596E3b124781E0bdbC47bb458bec62')
|
||||
expect(validatorList[2]).to.be.equal('0xf4BEF13F9f4f2B203FAF0C3cBbaAbe1afE056955')
|
||||
})
|
||||
})
|
206
commons/utils.js
206
commons/utils.js
@ -1,4 +1,6 @@
|
||||
const { BRIDGE_MODES, FEE_MANAGER_MODE } = require('./constants')
|
||||
const { toWei, toBN } = require('web3-utils')
|
||||
const { BRIDGE_MODES, FEE_MANAGER_MODE, ERC_TYPES } = require('./constants')
|
||||
const { REWARDABLE_VALIDATORS_ABI } = require('./abis')
|
||||
|
||||
function decodeBridgeMode(bridgeModeHash) {
|
||||
switch (bridgeModeHash) {
|
||||
@ -35,6 +37,19 @@ async function getBridgeMode(contract) {
|
||||
}
|
||||
}
|
||||
|
||||
const getTokenType = async (bridgeTokenContract, bridgeAddress) => {
|
||||
try {
|
||||
const resultBridgeAddress = await bridgeTokenContract.methods.bridgeContract().call()
|
||||
if (resultBridgeAddress === bridgeAddress) {
|
||||
return ERC_TYPES.ERC677
|
||||
} else {
|
||||
return ERC_TYPES.ERC20
|
||||
}
|
||||
} catch (e) {
|
||||
return ERC_TYPES.ERC20
|
||||
}
|
||||
}
|
||||
|
||||
const getUnit = bridgeMode => {
|
||||
let unitHome = null
|
||||
let unitForeign = null
|
||||
@ -54,9 +69,196 @@ const getUnit = bridgeMode => {
|
||||
return { unitHome, unitForeign }
|
||||
}
|
||||
|
||||
const parseValidatorEvent = event => {
|
||||
if (
|
||||
event.event === undefined &&
|
||||
event.raw &&
|
||||
event.raw.topics &&
|
||||
(event.raw.topics[0] === '0xe366c1c0452ed8eec96861e9e54141ebff23c9ec89fe27b996b45f5ec3884987' ||
|
||||
event.raw.topics[0] === '0x8064a302796c89446a96d63470b5b036212da26bd2debe5bec73e0170a9a5e83')
|
||||
) {
|
||||
const rawAddress = event.raw.topics.length > 1 ? event.raw.topics[1] : event.raw.data
|
||||
const address = '0x' + rawAddress.slice(26)
|
||||
event.event = 'ValidatorAdded'
|
||||
event.returnValues.validator = address
|
||||
} else if (
|
||||
event.event === undefined &&
|
||||
event.raw &&
|
||||
event.raw.topics &&
|
||||
event.raw.topics[0] === '0xe1434e25d6611e0db941968fdc97811c982ac1602e951637d206f5fdda9dd8f1'
|
||||
) {
|
||||
const rawAddress = event.raw.data === '0x' ? event.raw.topics[1] : event.raw.data
|
||||
const address = '0x' + rawAddress.slice(26)
|
||||
event.event = 'ValidatorRemoved'
|
||||
event.returnValues.validator = address
|
||||
}
|
||||
}
|
||||
|
||||
const processValidatorsEvents = events => {
|
||||
const validatorList = new Set()
|
||||
events.forEach(event => {
|
||||
parseValidatorEvent(event)
|
||||
|
||||
if (event.event === 'ValidatorAdded') {
|
||||
validatorList.add(event.returnValues.validator)
|
||||
} else if (event.event === 'ValidatorRemoved') {
|
||||
validatorList.delete(event.returnValues.validator)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(validatorList)
|
||||
}
|
||||
|
||||
const tryCall = async (method, fallbackValue) => {
|
||||
try {
|
||||
return await method.call()
|
||||
} catch (e) {
|
||||
return fallbackValue
|
||||
}
|
||||
}
|
||||
|
||||
const getDeployedAtBlock = async contract => tryCall(contract.methods.deployedAtBlock(), 0)
|
||||
|
||||
const getPastEvents = async (
|
||||
contract,
|
||||
{ event = 'allEvents', fromBlock = toBN(0), toBlock = 'latest', options = {} }
|
||||
) => {
|
||||
let events
|
||||
try {
|
||||
events = await contract.getPastEvents(event, {
|
||||
...options,
|
||||
fromBlock,
|
||||
toBlock
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.message.includes('query returned more than') && toBlock !== 'latest') {
|
||||
const middle = toBN(fromBlock)
|
||||
.add(toBlock)
|
||||
.divRound(toBN(2))
|
||||
const middlePlusOne = middle.add(toBN(1))
|
||||
|
||||
const firstHalfEvents = await getPastEvents(contract, {
|
||||
...options,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock: middle
|
||||
})
|
||||
const secondHalfEvents = await getPastEvents(contract, {
|
||||
...options,
|
||||
event,
|
||||
fromBlock: middlePlusOne,
|
||||
toBlock
|
||||
})
|
||||
events = [...firstHalfEvents, ...secondHalfEvents]
|
||||
} else {
|
||||
throw new Error(e)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
const getValidatorList = async (address, eth, options) => {
|
||||
options.logger && options.logger.debug && options.logger.debug('getting validatorList')
|
||||
|
||||
const validatorsContract = new eth.Contract(REWARDABLE_VALIDATORS_ABI, address) // in monitor, BRIDGE_VALIDATORS_ABI was used
|
||||
const validators = await tryCall(validatorsContract.methods.validatorList(), [])
|
||||
|
||||
if (validators.length) {
|
||||
return validators
|
||||
}
|
||||
|
||||
options.logger && options.logger.debug && options.logger.debug('getting validatorsEvents')
|
||||
|
||||
const deployedAtBlock = await tryCall(validatorsContract.methods.deployedAtBlock(), 0)
|
||||
const fromBlock = options.fromBlock || Number(deployedAtBlock) || 0
|
||||
const toBlock = options.toBlock || 'latest'
|
||||
|
||||
const validatorsEvents = await getPastEvents(new eth.Contract([], address), {
|
||||
event: 'allEvents',
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options: {}
|
||||
})
|
||||
|
||||
return processValidatorsEvents(validatorsEvents)
|
||||
}
|
||||
|
||||
const gasPriceWithinLimits = (gasPrice, limits) => {
|
||||
if (!limits) {
|
||||
return gasPrice
|
||||
}
|
||||
if (gasPrice < limits.MIN) {
|
||||
return limits.MIN
|
||||
} else if (gasPrice > limits.MAX) {
|
||||
return limits.MAX
|
||||
} else {
|
||||
return gasPrice
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeGasPrice = (oracleGasPrice, factor, limits = null) => {
|
||||
let gasPrice = oracleGasPrice * factor
|
||||
gasPrice = gasPriceWithinLimits(gasPrice, limits)
|
||||
return toBN(toWei(gasPrice.toFixed(2).toString(), 'gwei'))
|
||||
}
|
||||
|
||||
// fetchFn has to be supplied (instead of just url to oracle),
|
||||
// because this utility function is shared between Browser and Node,
|
||||
// we use built-in 'fetch' on browser side, and `node-fetch` package in Node.
|
||||
const gasPriceFromOracle = async (fetchFn, options = {}) => {
|
||||
try {
|
||||
const response = await fetchFn()
|
||||
const json = await response.json()
|
||||
const oracleGasPrice = json[options.speedType]
|
||||
|
||||
if (!oracleGasPrice) {
|
||||
options.logger &&
|
||||
options.logger.error &&
|
||||
options.logger.error(`Response from Oracle didn't include gas price for ${options.speedType} type.`)
|
||||
return null
|
||||
}
|
||||
|
||||
const normalizedGasPrice = normalizeGasPrice(oracleGasPrice, options.factor, options.limits)
|
||||
|
||||
options.logger &&
|
||||
options.logger.debug &&
|
||||
options.logger.debug({ oracleGasPrice, normalizedGasPrice }, 'Gas price updated using the API')
|
||||
|
||||
return normalizedGasPrice
|
||||
} catch (e) {
|
||||
options.logger && options.logger.error && options.logger.error(`Gas Price API is not available. ${e.message}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
const gasPriceFromContract = async (bridgeContract, options = {}) => {
|
||||
try {
|
||||
const gasPrice = await bridgeContract.methods.gasPrice().call()
|
||||
options.logger &&
|
||||
options.logger.debug &&
|
||||
options.logger.debug({ gasPrice }, 'Gas price updated using the contracts')
|
||||
return gasPrice
|
||||
} catch (e) {
|
||||
options.logger &&
|
||||
options.logger.error &&
|
||||
options.logger.error(`There was a problem getting the gas price from the contract. ${e.message}`)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
decodeBridgeMode,
|
||||
decodeFeeManagerMode,
|
||||
getBridgeMode,
|
||||
getUnit
|
||||
getTokenType,
|
||||
getUnit,
|
||||
parseValidatorEvent,
|
||||
processValidatorsEvents,
|
||||
getValidatorList,
|
||||
getPastEvents,
|
||||
getDeployedAtBlock,
|
||||
normalizeGasPrice,
|
||||
gasPriceFromOracle,
|
||||
gasPriceFromContract,
|
||||
gasPriceWithinLimits
|
||||
}
|
||||
|
@ -158,6 +158,6 @@ const foreignViAbi = [
|
||||
]
|
||||
|
||||
module.exports = {
|
||||
homeV1Abi,
|
||||
foreignViAbi
|
||||
HOME_V1_ABI: homeV1Abi,
|
||||
FOREIGN_V1_ABI: foreignViAbi
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
# POA Token Bridge / Deployment Configuration
|
||||
# POA TokenBridge / Deployment Configuration
|
||||
|
||||
Please see the [Oracle](../oracle/README.md) for additional configuration and execution details.
|
||||
|
||||
@ -46,6 +46,8 @@ cp hosts.yml.example hosts.yml
|
||||
hosts:
|
||||
<host_ip_B>:
|
||||
ansible_user: <user>
|
||||
#syslog_server_port: "<protocol>://<ip>:<port>"
|
||||
#monitor_cron_schedule: "*/4 * * * *" # When this parameter is set, it will overwrite default schedule for performing checks
|
||||
```
|
||||
|
||||
The config above would install the Oracle on `<host_ip_A>`, UI on `<host_ip_C>`, and both Oracle, UI and Monitor on `<host_ip_B>`.
|
||||
@ -85,13 +87,13 @@ Example config for installing only UI:
|
||||
|
||||
2. You can also add the following parameters in the `group_vars` to change the default behavior of the playbooks:
|
||||
|
||||
2.1 `compose_service_user` - specifies the user created by the playbooks. This user runs the Token Bridge Oracle.
|
||||
2.1 `compose_service_user` - specifies the user created by the playbooks. This user runs the TokenBridge Oracle.
|
||||
|
||||
2.2 `bridge_repo` contains the address of the Token Bridge Oracle repository. The default value is https://github.com/poanetwork/tokenbridge.
|
||||
2.2 `bridge_repo` contains the address of the TokenBridge Oracle repository. The default value is https://github.com/poanetwork/tokenbridge.
|
||||
|
||||
2.3 `bridge_repo_branch` points to the specific branch or commit to use with the `bridge_repo`. If `bridge_repo_branch` is not specified, the default (`master`) branch is used.
|
||||
|
||||
2.4 `bridge_path` sets the path where the Token Bridge Oracle is installed. By default, it points. to the home folder of `compose_service_user`
|
||||
2.4 `bridge_path` sets the path where the TokenBridge Oracle is installed. By default, it points. to the home folder of `compose_service_user`
|
||||
|
||||
2.5 `docker_compose_version` - specifies a version of docker-compose to be installed.
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
# POA Token Bridge / Deployment Execution
|
||||
# POA TokenBridge / Deployment Execution
|
||||
|
||||
Please refer to the [Configuration](./CONFIGURATION.md) first.
|
||||
|
||||
@ -74,6 +74,49 @@ where the _<watcher>_ could be one of the following:
|
||||
- `collected-signatures`
|
||||
- `affirmation-request`
|
||||
|
||||
## Reset nonce counters
|
||||
In case some tx from your bridge validator account were done outside the bridge, you might need to update nonce counters.
|
||||
|
||||
1. ssh to your bridge node and run:
|
||||
```
|
||||
$ sudo su poadocker
|
||||
$ cd ~/bridge
|
||||
```
|
||||
1. stop running docker containers using the nonce by running:
|
||||
```
|
||||
$ docker-compose stop bridge_senderhome bridge_senderforeign
|
||||
```
|
||||
1. Connect to the redis container:
|
||||
```
|
||||
$ docker-compose exec redis /bin/bash
|
||||
```
|
||||
you should get a shell prompt from inside the docker container, similar to this:
|
||||
```
|
||||
root@redis:/data#
|
||||
```
|
||||
1. connect to redis database by running `redis-cli`, prompt should change once again to
|
||||
```
|
||||
127.0.0.1:6379>
|
||||
```
|
||||
1. list existing keys by running `keys *`, output should look like this:
|
||||
```
|
||||
127.0.0.1:6379> keys *
|
||||
1) "erc-native-affirmation-request:lastProcessedBlock"
|
||||
2) "erc-native-collected-signatures:lastProcessedBlock"
|
||||
3) "erc-native-signature-request:lastProcessedBlock"
|
||||
4) "home:nonce"
|
||||
5) "foreign:nonce"
|
||||
```
|
||||
1. delete keys containing last used nonces on both networks
|
||||
```
|
||||
127.0.0.1:6379> del "home:nonce" "foreign:nonce"
|
||||
```
|
||||
1. exit from redis container by running `exit` twice
|
||||
1. start the rest of bridge containers:
|
||||
```
|
||||
$ docker-compose start bridge_senderhome bridge_senderforeign
|
||||
```
|
||||
|
||||
## Logs
|
||||
|
||||
If the `syslog_server_port` option in the hosts.yml file is not set, all logs will be stored in `/var/log/docker/` folder in the set of folders with the `bridge_` prefix.
|
||||
|
@ -1,8 +1,8 @@
|
||||
# POA Token Bridge / Deployment
|
||||
# POA TokenBridge / Deployment
|
||||
Ansible playbooks for deploying cross-chain bridges.
|
||||
|
||||
## Overview
|
||||
Please refer to the [POA Token Bridge](../README.md) overview first of all.
|
||||
Please refer to the [POA TokenBridge](../README.md) overview first of all.
|
||||
|
||||
These playbooks are designed to automate the deployment process for cross-chain bridges on bridge validator nodes. This process installs the bridge as a service and sets .env configurations on a remote server.
|
||||
|
||||
|
@ -51,4 +51,3 @@ MONITOR_FOREIGN_GAS_LIMIT: 300000
|
||||
MONITOR_HOME_GAS_PRICE_FALLBACK: 0
|
||||
MONITOR_FOREIGN_GAS_PRICE_FALLBACK: 10000000000
|
||||
MONITOR_LEFT_TX_THRESHOLD: 100
|
||||
MONITOR_CRON_SCHEDULE: "* * * * *"
|
||||
|
7
deployment/group_vars/erc-to-erc.yml
Normal file
7
deployment/group_vars/erc-to-erc.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
BRIDGE_MODE: "ERC_TO_ERC"
|
||||
HOME_BRIDGE_ADDRESS: "0x1feB40aD9420b186F019A717c37f5546165d411E"
|
||||
FOREIGN_BRIDGE_ADDRESS: "0x4a58D6d8D416a5fBCAcf3dC52eb8bE8948E25127"
|
||||
ERC20_TOKEN_ADDRESS: "0x3C665A31199694Bf723fD08844AD290207B5797f"
|
||||
UI_PORT: 3001
|
||||
MONITOR_PORT: 3011
|
7
deployment/group_vars/erc-to-native.yml
Normal file
7
deployment/group_vars/erc-to-native.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
BRIDGE_MODE: "ERC_TO_NATIVE"
|
||||
HOME_BRIDGE_ADDRESS: "0x488Af810997eD1730cB3a3918cD83b3216E6eAda"
|
||||
FOREIGN_BRIDGE_ADDRESS: "0x488Af810997eD1730cB3a3918cD83b3216E6eAda"
|
||||
ERC20_TOKEN_ADDRESS: "0x3C665A31199694Bf723fD08844AD290207B5797f"
|
||||
UI_PORT: 3002
|
||||
MONITOR_PORT: 3012
|
@ -52,4 +52,3 @@ MONITOR_FOREIGN_GAS_LIMIT: 300000
|
||||
MONITOR_HOME_GAS_PRICE_FALLBACK: 1000000000
|
||||
MONITOR_FOREIGN_GAS_PRICE_FALLBACK: 1000000000
|
||||
MONITOR_LEFT_TX_THRESHOLD: 100
|
||||
MONITOR_CRON_SCHEDULE: "* * * * *"
|
||||
|
7
deployment/group_vars/native-to-erc.yml
Normal file
7
deployment/group_vars/native-to-erc.yml
Normal file
@ -0,0 +1,7 @@
|
||||
---
|
||||
BRIDGE_MODE: "NATIVE_TO_ERC"
|
||||
HOME_BRIDGE_ADDRESS: "0x32198D570fffC7033641F8A9094FFDCaAEF42624"
|
||||
FOREIGN_BRIDGE_ADDRESS: "0x2B6871b9B02F73fa24F4864322CdC78604207769"
|
||||
ERC20_TOKEN_ADDRESS: "0xdbeE25CbE97e4A5CC6c499875774dc7067E9426B"
|
||||
UI_PORT: 3000
|
||||
MONITOR_PORT: 3010
|
56
deployment/group_vars/ultimate.yml
Normal file
56
deployment/group_vars/ultimate.yml
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
## General settings
|
||||
# BRIDGE_MODE: ""
|
||||
HOME_NATIVE_NAME: "POA"
|
||||
ALLOW_HTTP: yes
|
||||
LOG_LEVEL: debug
|
||||
|
||||
## Home contract
|
||||
HOME_RPC_URL: "https://sokol.poa.network"
|
||||
HOME_NAME: "POA Sokol"
|
||||
HOME_WITHOUT_EVENTS: false
|
||||
# HOME_BRIDGE_ADDRESS: "0x32198D570fffC7033641F8A9094FFDCaAEF42624"
|
||||
HOME_POLLING_INTERVAL: 5000
|
||||
|
||||
## Foreign contract
|
||||
FOREIGN_RPC_URL: "https://sokol.poa.network"
|
||||
FOREIGN_NAME: "Kovan"
|
||||
FOREIGN_WITHOUT_EVENTS: false
|
||||
# FOREIGN_BRIDGE_ADDRESS: "0x2B6871b9B02F73fa24F4864322CdC78604207769"
|
||||
# ERC20_TOKEN_ADDRESS: "0xdbeE25CbE97e4A5CC6c499875774dc7067E9426B"
|
||||
FOREIGN_POLLING_INTERVAL: 1000
|
||||
|
||||
## Home Gasprice
|
||||
HOME_GAS_PRICE_ORACLE_URL: "https://gasprice.poa.network/"
|
||||
HOME_GAS_PRICE_SPEED_TYPE: "standard"
|
||||
HOME_GAS_PRICE_FALLBACK: 1000000000 # in wei
|
||||
HOME_GAS_PRICE_UPDATE_INTERVAL: 600000
|
||||
HOME_GAS_PRICE_FACTOR: 1
|
||||
|
||||
## Foreign Gasprice
|
||||
FOREIGN_GAS_PRICE_ORACLE_URL: "https://gasprice.poa.network/"
|
||||
FOREIGN_GAS_PRICE_SPEED_TYPE: "standard"
|
||||
FOREIGN_GAS_PRICE_FALLBACK: 1000000000 # in wei
|
||||
FOREIGN_GAS_PRICE_UPDATE_INTERVAL: 600000
|
||||
FOREIGN_GAS_PRICE_FACTOR: 1
|
||||
|
||||
## UI
|
||||
UI_TITLE: "TokenBridge UI app - %c"
|
||||
UI_DESCRIPTION: "The POA cross-chain bridge serves as a method of transferring POA native tokens from the POA Network to the Ethereum network in a quick and cost-efficient manner."
|
||||
# UI_PORT: 3000
|
||||
UI_HOME_EXPLORER_TX_TEMPLATE: https://blockscout.com/poa/sokol/tx/%s
|
||||
UI_FOREIGN_EXPLORER_TX_TEMPLATE: https://blockscout.com/eth/kovan/tx/%s
|
||||
UI_HOME_EXPLORER_ADDRESS_TEMPLATE: https://blockscout.com/poa/sokol/address/%s
|
||||
UI_FOREIGN_EXPLORER_ADDRESS_TEMPLATE: https://blockscout.com/eth/kovan/address/%s
|
||||
UI_HOME_GAS_PRICE_FALLBACK: 1000000000
|
||||
UI_FOREIGN_GAS_PRICE_FALLBACK: 1000000000
|
||||
|
||||
## Monitor
|
||||
# MONITOR_PORT: 3003
|
||||
MONITOR_HOME_DEPLOYMENT_BLOCK: 0
|
||||
MONITOR_FOREIGN_DEPLOYMENT_BLOCK: 0
|
||||
MONITOR_HOME_GAS_LIMIT: 300000
|
||||
MONITOR_FOREIGN_GAS_LIMIT: 300000
|
||||
MONITOR_HOME_GAS_PRICE_FALLBACK: 1000000000
|
||||
MONITOR_FOREIGN_GAS_PRICE_FALLBACK: 1000000000
|
||||
MONITOR_LEFT_TX_THRESHOLD: 100
|
@ -52,4 +52,3 @@ MONITOR_FOREIGN_GAS_LIMIT: 300000
|
||||
MONITOR_HOME_GAS_PRICE_FALLBACK: 15000000000
|
||||
MONITOR_FOREIGN_GAS_PRICE_FALLBACK: 10000000000
|
||||
MONITOR_LEFT_TX_THRESHOLD: 100
|
||||
MONITOR_CRON_SCHEDULE: "* * * * *"
|
||||
|
@ -16,3 +16,5 @@ sokol-kovan:
|
||||
hosts:
|
||||
127.0.0.1:
|
||||
ansible_user: ubuntu
|
||||
#syslog_server_port: "<protocol>://<ip>:<port>"
|
||||
#monitor_cron_schedule: "*/4 * * * *"
|
||||
|
@ -1,4 +1,4 @@
|
||||
# POA Token Bridge / Deployment Testing
|
||||
# POA TokenBridge / Deployment Testing
|
||||
|
||||
The deployment playbooks are tested using [Molecule](https://molecule.readthedocs.io).
|
||||
|
||||
@ -30,3 +30,7 @@ Scenario | Description
|
||||
--- | ---
|
||||
oracle | Deploys and checks standalone Oracle on Ubuntu host
|
||||
ui | Deploys and checks standalone UI on Ubuntu host
|
||||
|
||||
## Ultimate E2E tests
|
||||
|
||||
For information on the Ultimate tests, please refer to [Ultimate](../../e2e-commons/ULTIMATE.md).
|
||||
|
@ -7,6 +7,5 @@ services:
|
||||
dockerfile: molecule/Dockerfile
|
||||
restart: 'no'
|
||||
privileged: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
@ -3,10 +3,9 @@ cd $(dirname $0)
|
||||
set -e # exit when any command fails
|
||||
|
||||
CODEBASE_BRANCH=${CIRCLE_BRANCH-$(git symbolic-ref --short HEAD)}
|
||||
DOCKER_LOCALHOST=${DOCKER_LOCALHOST-localhost}
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
docker-compose build && docker-compose run -e CODEBASE_BRANCH=$CODEBASE_BRANCH -e DOCKER_LOCALHOST=$DOCKER_LOCALHOST molecule_runner /bin/bash -c "molecule test --scenario-name $1"
|
||||
docker-compose build && docker-compose run -e CODEBASE_BRANCH=$CODEBASE_BRANCH molecule_runner /bin/bash -c "molecule test --scenario-name $1"
|
||||
|
||||
shift # Shift all the parameters down by one
|
||||
done
|
||||
|
14
deployment/molecule/monitor/Dockerfile.j2
Normal file
14
deployment/molecule/monitor/Dockerfile.j2
Normal file
@ -0,0 +1,14 @@
|
||||
# Molecule managed
|
||||
|
||||
{% if item.registry is defined %}
|
||||
FROM {{ item.registry.url }}/{{ item.image }}
|
||||
{% else %}
|
||||
FROM {{ item.image }}
|
||||
{% endif %}
|
||||
|
||||
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \
|
||||
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash && dnf clean all; \
|
||||
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
|
||||
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \
|
||||
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
|
||||
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
|
56
deployment/molecule/monitor/molecule.yml
Normal file
56
deployment/molecule/monitor/molecule.yml
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
dependency:
|
||||
name: galaxy
|
||||
driver:
|
||||
name: docker
|
||||
lint:
|
||||
name: yamllint
|
||||
enabled: True
|
||||
options:
|
||||
config-data:
|
||||
ignore: ../../hosts.yml
|
||||
platforms:
|
||||
- name: monitor-host
|
||||
groups:
|
||||
- example
|
||||
children:
|
||||
- monitor
|
||||
image: ubuntu:16.04
|
||||
privileged: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
provisioner:
|
||||
name: ansible
|
||||
lint:
|
||||
name: ansible-lint
|
||||
enabled: True
|
||||
options:
|
||||
r: ["bug"]
|
||||
playbooks:
|
||||
prepare: ../prepare.yml
|
||||
converge: ../../site.yml
|
||||
inventory:
|
||||
host_vars:
|
||||
monitor-host:
|
||||
bridge_repo_branch: $CODEBASE_BRANCH
|
||||
syslog_server_port: "udp://127.0.0.1:514"
|
||||
verifier:
|
||||
name: testinfra
|
||||
lint:
|
||||
name: flake8
|
||||
additional_files_or_dirs:
|
||||
- ../../tests/*
|
||||
scenario:
|
||||
name: monitor
|
||||
test_sequence:
|
||||
- lint
|
||||
- cleanup
|
||||
- destroy
|
||||
- dependency
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
||||
- verify
|
||||
- destroy
|
54
deployment/molecule/monitor/tests/test_monitor.py
Normal file
54
deployment/molecule/monitor/tests/test_monitor.py
Normal file
@ -0,0 +1,54 @@
|
||||
import os
|
||||
import pytest
|
||||
import testinfra.utils.ansible_runner
|
||||
|
||||
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
|
||||
os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('monitor')
|
||||
|
||||
|
||||
@pytest.mark.parametrize("name", [
|
||||
("monitor_monitor_1")
|
||||
])
|
||||
def test_docker_containers(host, name):
|
||||
container = host.docker(name)
|
||||
assert container.is_running
|
||||
|
||||
|
||||
@pytest.mark.parametrize("service", [
|
||||
("tokenbridge-monitor"),
|
||||
("rsyslog")
|
||||
])
|
||||
def test_services(host, service):
|
||||
assert host.service(service).is_enabled
|
||||
assert host.service(service).is_running
|
||||
|
||||
|
||||
@pytest.mark.parametrize("filename", [
|
||||
("/etc/rsyslog.d/33-monitor-docker.conf"),
|
||||
("/etc/rsyslog.d/38-monitor-remote-logging.conf")
|
||||
])
|
||||
def test_logging(host, filename):
|
||||
assert host.file(filename).exists
|
||||
assert host.file(filename).mode == 0o0644
|
||||
|
||||
|
||||
def test_home_exists(host):
|
||||
assert host.run_test(
|
||||
'curl -s http://localhost:3003 | '
|
||||
'grep -q -i "home"'
|
||||
)
|
||||
|
||||
|
||||
def test_foreign_exists(host):
|
||||
assert host.run_test(
|
||||
'curl -s http://localhost:3003 | '
|
||||
'grep -q -i "foreign"'
|
||||
)
|
||||
|
||||
|
||||
def test_no_error(host):
|
||||
assert host.run_expect(
|
||||
[1],
|
||||
'curl -s http://localhost:3003 | '
|
||||
'grep -i -q "error"'
|
||||
)
|
5
deployment/molecule/ultimate-commons/converge.yml
Normal file
5
deployment/molecule/ultimate-commons/converge.yml
Normal file
@ -0,0 +1,5 @@
|
||||
---
|
||||
- import_playbook: ../../site.yml
|
||||
# The docker-compose files have to be modified, in order to join the docker containers over network with the parity containers
|
||||
- import_playbook: ./oracle-docker-compose.yml
|
||||
- import_playbook: ./ui-docker-compose.yml
|
@ -0,0 +1,36 @@
|
||||
---
|
||||
- name: Overwrite Oracle the docker-compose
|
||||
hosts: oracle
|
||||
become: true
|
||||
tasks:
|
||||
- name: stop the service
|
||||
shell: service poabridge stop
|
||||
|
||||
- name: Slurp docker compose file
|
||||
slurp:
|
||||
src: "/home/poadocker/bridge/oracle/docker-compose.yml"
|
||||
register: docker_compose_slurp
|
||||
- name: Parse docker compose file
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_slurp['content'] | b64decode | from_yaml }}"
|
||||
|
||||
- name: Add the external network used to connect to Parity nodes
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_parsed |combine({'networks': {'ultimate': {'external': 'true'}}}, recursive=True) }}"
|
||||
|
||||
- name: Add all Oracle containers to the network
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_parsed | combine({'services': {item: {'networks': docker_compose_parsed.services[item].networks | union(['ultimate'])}}}, recursive=True) }}"
|
||||
with_items: "{{ docker_compose_parsed.services }}"
|
||||
|
||||
- name: Expose Redis port to allow connecting from redis-cli
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_parsed | combine({'services': {'redis': {'ports': ['6379:6379']}}}, recursive=True) }}"
|
||||
|
||||
- name: Write new docker-compose file
|
||||
copy:
|
||||
content: "{{ docker_compose_parsed | to_yaml }}"
|
||||
dest: "/home/poadocker/bridge/oracle/docker-compose.yml"
|
||||
|
||||
- name: start the service
|
||||
shell: service poabridge start
|
32
deployment/molecule/ultimate-commons/ui-docker-compose.yml
Normal file
32
deployment/molecule/ultimate-commons/ui-docker-compose.yml
Normal file
@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: Overwrite UI the docker-compose
|
||||
hosts: ui
|
||||
become: true
|
||||
tasks:
|
||||
- name: stop the service
|
||||
shell: service tokenbridge-ui stop
|
||||
|
||||
- name: Slurp docker compose file
|
||||
slurp:
|
||||
src: "/home/poadocker/bridge/ui/docker-compose.yml"
|
||||
register: docker_compose_slurp
|
||||
- name: Parse docker compose file
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_slurp['content'] | b64decode | from_yaml }}"
|
||||
|
||||
- name: Add the external network used to connect to Parity nodes
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_parsed |combine({'networks': {'ultimate': {'external': 'true'}}}, recursive=True) }}"
|
||||
|
||||
- name: Add all UI containers to the network
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_parsed | combine({'services': {item: {'networks': ['ultimate']}}}, recursive=True) }}"
|
||||
with_items: "{{ docker_compose_parsed.services }}"
|
||||
|
||||
- name: Write new docker-compose file
|
||||
copy:
|
||||
content: "{{ docker_compose_parsed | to_yaml }}"
|
||||
dest: "/home/poadocker/bridge/ui/docker-compose.yml"
|
||||
|
||||
- name: start the service
|
||||
shell: service tokenbridge-ui start
|
14
deployment/molecule/ultimate-erc-to-erc/Dockerfile.j2
Normal file
14
deployment/molecule/ultimate-erc-to-erc/Dockerfile.j2
Normal file
@ -0,0 +1,14 @@
|
||||
# Molecule managed
|
||||
|
||||
{% if item.registry is defined %}
|
||||
FROM {{ item.registry.url }}/{{ item.image }}
|
||||
{% else %}
|
||||
FROM {{ item.image }}
|
||||
{% endif %}
|
||||
|
||||
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \
|
||||
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash && dnf clean all; \
|
||||
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
|
||||
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \
|
||||
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
|
||||
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
|
56
deployment/molecule/ultimate-erc-to-erc/molecule.yml
Normal file
56
deployment/molecule/ultimate-erc-to-erc/molecule.yml
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
platforms:
|
||||
- name: oracle-erc-to-erc-host
|
||||
groups:
|
||||
- ultimate
|
||||
- erc-to-erc
|
||||
children:
|
||||
- oracle
|
||||
image: ubuntu:16.04
|
||||
privileged: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- name: ui-erc-to-erc-host
|
||||
groups:
|
||||
- ultimate
|
||||
- erc-to-erc
|
||||
children:
|
||||
- ui
|
||||
image: ubuntu:16.04
|
||||
privileged: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
prepare: ../prepare.yml
|
||||
converge: ../ultimate-commons/converge.yml
|
||||
inventory:
|
||||
host_vars:
|
||||
oracle-erc-to-erc-host:
|
||||
bridge_repo_branch: $CODEBASE_BRANCH
|
||||
HOME_RPC_URL: "http://parity1:8545"
|
||||
FOREIGN_RPC_URL: "http://parity2:8545"
|
||||
VALIDATOR_ADDRESS: "0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"
|
||||
VALIDATOR_ADDRESS_PRIVATE_KEY: "8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
|
||||
ui-erc-to-erc-host:
|
||||
bridge_repo_branch: $CODEBASE_BRANCH
|
||||
HOME_RPC_URL: "http://localhost:8541"
|
||||
FOREIGN_RPC_URL: "http://localhost:8542"
|
||||
verifier:
|
||||
name: testinfra
|
||||
lint:
|
||||
name: flake8
|
||||
scenario:
|
||||
name: ultimate-erc-to-erc
|
||||
test_sequence:
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
14
deployment/molecule/ultimate-erc-to-native/Dockerfile.j2
Normal file
14
deployment/molecule/ultimate-erc-to-native/Dockerfile.j2
Normal file
@ -0,0 +1,14 @@
|
||||
# Molecule managed
|
||||
|
||||
{% if item.registry is defined %}
|
||||
FROM {{ item.registry.url }}/{{ item.image }}
|
||||
{% else %}
|
||||
FROM {{ item.image }}
|
||||
{% endif %}
|
||||
|
||||
RUN if [ $(command -v apt-get) ]; then apt-get update && apt-get install -y python sudo bash ca-certificates && apt-get clean; \
|
||||
elif [ $(command -v dnf) ]; then dnf makecache && dnf --assumeyes install python sudo python-devel python*-dnf bash && dnf clean all; \
|
||||
elif [ $(command -v yum) ]; then yum makecache fast && yum install -y python sudo yum-plugin-ovl bash && sed -i 's/plugins=0/plugins=1/g' /etc/yum.conf && yum clean all; \
|
||||
elif [ $(command -v zypper) ]; then zypper refresh && zypper install -y python sudo bash python-xml && zypper clean -a; \
|
||||
elif [ $(command -v apk) ]; then apk update && apk add --no-cache python sudo bash ca-certificates; \
|
||||
elif [ $(command -v xbps-install) ]; then xbps-install -Syu && xbps-install -y python sudo bash ca-certificates && xbps-remove -O; fi
|
56
deployment/molecule/ultimate-erc-to-native/molecule.yml
Normal file
56
deployment/molecule/ultimate-erc-to-native/molecule.yml
Normal file
@ -0,0 +1,56 @@
|
||||
---
|
||||
driver:
|
||||
name: docker
|
||||
platforms:
|
||||
- name: oracle-erc-to-native-host
|
||||
groups:
|
||||
- ultimate
|
||||
- erc-to-native
|
||||
children:
|
||||
- oracle
|
||||
image: ubuntu:16.04
|
||||
privileged: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- name: ui-erc-to-native-host
|
||||
groups:
|
||||
- ultimate
|
||||
- erc-to-native
|
||||
children:
|
||||
- ui
|
||||
image: ubuntu:16.04
|
||||
privileged: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
prepare: ../prepare.yml
|
||||
converge: ../ultimate-commons/converge.yml
|
||||
inventory:
|
||||
host_vars:
|
||||
oracle-erc-to-native-host:
|
||||
bridge_repo_branch: $CODEBASE_BRANCH
|
||||
HOME_RPC_URL: "http://parity1:8545"
|
||||
FOREIGN_RPC_URL: "http://parity2:8545"
|
||||
VALIDATOR_ADDRESS: "0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"
|
||||
VALIDATOR_ADDRESS_PRIVATE_KEY: "8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
|
||||
ui-erc-to-native-host:
|
||||
bridge_repo_branch: $CODEBASE_BRANCH
|
||||
HOME_RPC_URL: "http://localhost:8541"
|
||||
FOREIGN_RPC_URL: "http://localhost:8542"
|
||||
verifier:
|
||||
name: testinfra
|
||||
lint:
|
||||
name: flake8
|
||||
scenario:
|
||||
name: ultimate-erc-to-native
|
||||
test_sequence:
|
||||
- cleanup
|
||||
- destroy
|
||||
- syntax
|
||||
- create
|
||||
- prepare
|
||||
- converge
|
@ -4,7 +4,8 @@ driver:
|
||||
platforms:
|
||||
- name: oracle-native-to-erc-host
|
||||
groups:
|
||||
- example
|
||||
- ultimate
|
||||
- native-to-erc
|
||||
children:
|
||||
- oracle
|
||||
image: ubuntu:16.04
|
||||
@ -12,26 +13,34 @@ platforms:
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- name: ui-native-to-erc-host
|
||||
groups:
|
||||
- ultimate
|
||||
- native-to-erc
|
||||
children:
|
||||
- ui
|
||||
image: ubuntu:16.04
|
||||
privileged: true
|
||||
network_mode: host
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
provisioner:
|
||||
name: ansible
|
||||
playbooks:
|
||||
prepare: ../prepare.yml
|
||||
converge: ../../site.yml
|
||||
converge: ../ultimate-commons/converge.yml
|
||||
inventory:
|
||||
host_vars:
|
||||
oracle-native-to-erc-host:
|
||||
bridge_repo_branch: $CODEBASE_BRANCH
|
||||
HOME_RPC_URL: "http://parity1:8545"
|
||||
FOREIGN_RPC_URL: "http://parity2:8545"
|
||||
VALIDATOR_ADDRESS: "0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"
|
||||
VALIDATOR_ADDRESS_PRIVATE_KEY: "8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
|
||||
HOME_BRIDGE_ADDRESS: "0x32198D570fffC7033641F8A9094FFDCaAEF42624"
|
||||
FOREIGN_BRIDGE_ADDRESS: "0x2B6871b9B02F73fa24F4864322CdC78604207769"
|
||||
ERC20_TOKEN_ADDRESS: "0xdbeE25CbE97e4A5CC6c499875774dc7067E9426B"
|
||||
QUEUE_URL: "amqp://$DOCKER_LOCALHOST"
|
||||
REDIS_URL: "redis://$DOCKER_LOCALHOST:6379"
|
||||
HOME_RPC_URL: "http://$DOCKER_LOCALHOST:8541"
|
||||
FOREIGN_RPC_URL: "http://$DOCKER_LOCALHOST:8542"
|
||||
ALLOW_HTTP: yes
|
||||
LOG_LEVEL: debug
|
||||
ui-native-to-erc-host:
|
||||
bridge_repo_branch: $CODEBASE_BRANCH
|
||||
HOME_RPC_URL: "http://localhost:8541"
|
||||
FOREIGN_RPC_URL: "http://localhost:8542"
|
||||
verifier:
|
||||
name: testinfra
|
||||
lint:
|
||||
|
@ -1,5 +1,4 @@
|
||||
{
|
||||
"disable-legacy-registry": true,
|
||||
"live-restore": true,
|
||||
"no-new-privileges": true
|
||||
}
|
||||
|
@ -6,3 +6,7 @@
|
||||
force: no
|
||||
update: no
|
||||
version: "{{ bridge_repo_branch }}"
|
||||
- name: Initialize submodules
|
||||
shell: git submodule update --init
|
||||
args:
|
||||
chdir: "{{ bridge_path }}"
|
||||
|
2
deployment/roles/monitor/defaults/main.yml
Normal file
2
deployment/roles/monitor/defaults/main.yml
Normal file
@ -0,0 +1,2 @@
|
||||
---
|
||||
monitor_cron_schedule: "*/4 * * * *"
|
18
deployment/roles/monitor/tasks/cron.yml
Normal file
18
deployment/roles/monitor/tasks/cron.yml
Normal file
@ -0,0 +1,18 @@
|
||||
---
|
||||
- name: Parse cron schedule
|
||||
set_fact:
|
||||
minute: "{{ monitor_cron_schedule.split(' ')[0] }}"
|
||||
hour: "{{ monitor_cron_schedule.split(' ')[1] }}"
|
||||
day: "{{ monitor_cron_schedule.split(' ')[2] }}"
|
||||
month: "{{ monitor_cron_schedule.split(' ')[3] }}"
|
||||
weekday: "{{ monitor_cron_schedule.split(' ')[4] }}"
|
||||
job: "/bin/bash -c 'cd {{ bridge_path }}/monitor/scripts; ./checkDocker.sh >cronWorker.out 2>cronWorker.err'"
|
||||
- name: Add cron entry
|
||||
cron:
|
||||
name: "RUN_MONITOR_CHECKS"
|
||||
minute: "{{ minute }}"
|
||||
hour: "{{ hour }}"
|
||||
day: "{{ day }}"
|
||||
month: "{{ month }}"
|
||||
weekday: "{{ weekday }}"
|
||||
job: "{{ job }}"
|
41
deployment/roles/monitor/tasks/logging.yml
Normal file
41
deployment/roles/monitor/tasks/logging.yml
Normal file
@ -0,0 +1,41 @@
|
||||
---
|
||||
- name: Slurp docker compose file
|
||||
slurp:
|
||||
src: "{{ bridge_path }}/monitor/docker-compose.yml"
|
||||
register: docker_compose_slurp
|
||||
|
||||
- name: Parse docker compose file
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_slurp['content'] | b64decode | from_yaml }}"
|
||||
|
||||
- name: Set logger to remote server
|
||||
set_fact:
|
||||
docker_compose_parsed: "{{ docker_compose_parsed |combine({'services': {item: {'logging': {'driver': 'syslog','options': {'tag': '{{.Name}}/{{.ID}}'}}}}}, recursive=True) }}"
|
||||
with_items: "{{ docker_compose_parsed.services }}"
|
||||
|
||||
- name: Write new docker-compose file
|
||||
copy:
|
||||
content: "{{ docker_compose_parsed | to_yaml }}"
|
||||
dest: "{{ bridge_path }}/monitor/docker-compose.yml"
|
||||
|
||||
- name: Set the local container logs configuration file
|
||||
template:
|
||||
src: 33-monitor-docker.conf.j2
|
||||
dest: /etc/rsyslog.d/33-monitor-docker.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
|
||||
- name: Set the log configuration file to send container logs to remote server
|
||||
template:
|
||||
src: 38-monitor-remote-logging.conf.j2
|
||||
dest: /etc/rsyslog.d/38-monitor-remote-logging.conf
|
||||
owner: root
|
||||
group: root
|
||||
mode: 0644
|
||||
when: syslog_server_port is defined
|
||||
|
||||
- name: restart rsyslog
|
||||
service:
|
||||
name: rsyslog
|
||||
state: restarted
|
@ -1,4 +1,6 @@
|
||||
---
|
||||
- include_tasks: pre_config.yml
|
||||
- include_tasks: logging.yml
|
||||
- include_tasks: jumpbox.yml
|
||||
- include_tasks: servinstall.yml
|
||||
- include_tasks: cron.yml
|
||||
|
@ -22,4 +22,3 @@ FOREIGN_GAS_PRICE_FALLBACK={{ MONITOR_FOREIGN_GAS_PRICE_FALLBACK }}
|
||||
FOREIGN_GAS_PRICE_FACTOR={{ FOREIGN_GAS_PRICE_FACTOR }}
|
||||
LEFT_TX_THRESHOLD={{ MONITOR_LEFT_TX_THRESHOLD }}
|
||||
PORT={{ MONITOR_PORT }}
|
||||
CRON_SCHEDULE={{ MONITOR_CRON_SCHEDULE }}
|
||||
|
11
deployment/roles/monitor/templates/33-monitor-docker.conf.j2
Normal file
11
deployment/roles/monitor/templates/33-monitor-docker.conf.j2
Normal file
@ -0,0 +1,11 @@
|
||||
$FileCreateMode 0644
|
||||
template(name="DockerLogFileName_Monitor" type="list") {
|
||||
constant(value="/var/log/docker/")
|
||||
property(name="syslogtag" securepath="replace" regex.type="ERE" regex.submatch="1" regex.expression="monitor_(.*)\\/[a-zA-Z0-9]+\\[")
|
||||
constant(value="/docker.log")
|
||||
}
|
||||
|
||||
if $programname startswith 'monitor_' then \
|
||||
?DockerLogFileName_Monitor
|
||||
|
||||
$FileCreateMode 0600
|
@ -0,0 +1,15 @@
|
||||
if $programname startswith 'monitor_' then {
|
||||
action(
|
||||
type="omfwd"
|
||||
protocol="{{ syslog_server_port.split(":")[0] }}"
|
||||
target="{{ (syslog_server_port.split(":")[1])[2:] }}"
|
||||
port="{{ syslog_server_port.split(":")[2] }}"
|
||||
template="RemoteForwardFormat"
|
||||
queue.SpoolDirectory="/var/spool/rsyslog"
|
||||
queue.FileName="remote"
|
||||
queue.MaxDiskSpace="1g"
|
||||
queue.SaveOnShutdown="on"
|
||||
queue.Type="LinkedList"
|
||||
ResendLastMSGOnReconnect="on"
|
||||
)
|
||||
}
|
@ -3,5 +3,4 @@ bridge_path: "/home/{{ compose_service_user }}/bridge"
|
||||
ALLOW_HTTP: no
|
||||
QUEUE_URL: amqp://rabbit
|
||||
REDIS_URL: redis://redis
|
||||
syslog_server_port: udp://127.0.0.1:514
|
||||
keyfile_path: "/root/.key"
|
||||
|
@ -1,4 +1,4 @@
|
||||
# POA Token Bridge / E2E-Commons
|
||||
# POA TokenBridge / E2E-Commons
|
||||
|
||||
Common scripts and configuration for the end-to-end tests.
|
||||
|
||||
@ -25,3 +25,7 @@ Shut down and cleans up containers, networks, services, running scripts:
|
||||
| ui | Launches UI containers |
|
||||
| blocks | Auto mines blocks |
|
||||
| native-to-erc | Creates infrastructure for ultimate e2e testing, for native-to-erc type of bridge |
|
||||
|
||||
#### Ultimate e2e testing
|
||||
|
||||
For more information on the Ultimate e2e testing, please refer to [Ultimate](./ULTIMATE.md).
|
||||
|
29
e2e-commons/ULTIMATE.md
Normal file
29
e2e-commons/ULTIMATE.md
Normal file
@ -0,0 +1,29 @@
|
||||
# POA TokenBridge / Ultimate E2E
|
||||
|
||||
Documentation regarding the Ultimate end-to-end tests.
|
||||
|
||||
## Overview
|
||||
|
||||
The ultimate e2e test scenario covers native-to-erc type of bridge.
|
||||
It runs the e2e tests on components deployed using the deployment playbooks.
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Prepare the infrastructure
|
||||
|
||||
Run the Parity nodes, deploy the bridge contracts, deploy Oracle using the deployment playbook.
|
||||
|
||||
```bash
|
||||
./up.sh deploy native-to-erc
|
||||
```
|
||||
|
||||
### 2. Run the E2E tests
|
||||
|
||||
```
|
||||
docker-compose run e2e yarn workspace oracle-e2e run native-to-erc
|
||||
```
|
||||
|
||||
## Diagram
|
||||
|
||||
![diagram](./ultimate.png)
|
@ -11,6 +11,10 @@
|
||||
"address": "0xaaB52d66283F7A1D5978bcFcB55721ACB467384b",
|
||||
"privateKey": "0x8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9"
|
||||
},
|
||||
"blockGenerator": {
|
||||
"address": "0xB4579fd5AfEaB60B03Db3F408AAdD315035943f7",
|
||||
"privateKey": "0xd6143d390d8b28c33601bb0fe29392fb1c35c24ccfe8722c09c2bdd6ada2699f"
|
||||
},
|
||||
"nativeToErcBridge": {
|
||||
"home": "0x32198D570fffC7033641F8A9094FFDCaAEF42624",
|
||||
"foreign": "0x2B6871b9B02F73fa24F4864322CdC78604207769",
|
||||
|
@ -1,22 +1,33 @@
|
||||
---
|
||||
version: '3'
|
||||
networks:
|
||||
ultimate:
|
||||
external: true
|
||||
services:
|
||||
parity1:
|
||||
build: ../parity
|
||||
ports:
|
||||
- "8541:8545"
|
||||
networks:
|
||||
- ultimate
|
||||
parity2:
|
||||
build:
|
||||
context: ../parity
|
||||
dockerfile: Dockerfile-foreign
|
||||
ports:
|
||||
- "8542:8545"
|
||||
networks:
|
||||
- ultimate
|
||||
redis:
|
||||
image: "redis:4"
|
||||
networks:
|
||||
- ultimate
|
||||
rabbit:
|
||||
image: "rabbitmq:3-management"
|
||||
ports:
|
||||
- "15672:15672"
|
||||
networks:
|
||||
- ultimate
|
||||
oracle:
|
||||
build:
|
||||
context: ..
|
||||
@ -47,6 +58,8 @@ services:
|
||||
- FOREIGN_POLLING_INTERVAL=500
|
||||
- ALLOW_HTTP=yes
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
oracle-erc20:
|
||||
build:
|
||||
context: ..
|
||||
@ -78,6 +91,8 @@ services:
|
||||
- FOREIGN_POLLING_INTERVAL=500
|
||||
- ALLOW_HTTP=yes
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
oracle-erc20-native:
|
||||
build:
|
||||
context: ..
|
||||
@ -109,6 +124,8 @@ services:
|
||||
- FOREIGN_POLLING_INTERVAL=500
|
||||
- ALLOW_HTTP=yes
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
oracle-amb:
|
||||
build:
|
||||
context: ..
|
||||
@ -137,6 +154,8 @@ services:
|
||||
- FOREIGN_POLLING_INTERVAL=500
|
||||
- ALLOW_HTTP=yes
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
ui:
|
||||
build:
|
||||
context: ..
|
||||
@ -165,6 +184,8 @@ services:
|
||||
- REACT_APP_FOREIGN_GAS_PRICE_FACTOR=1
|
||||
- PORT=3000
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
ui-erc20:
|
||||
build:
|
||||
context: ..
|
||||
@ -193,6 +214,8 @@ services:
|
||||
- REACT_APP_FOREIGN_GAS_PRICE_FACTOR=1
|
||||
- PORT=3000
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
ui-erc20-native:
|
||||
build:
|
||||
context: ..
|
||||
@ -221,6 +244,8 @@ services:
|
||||
- REACT_APP_FOREIGN_GAS_PRICE_FACTOR=1
|
||||
- PORT=3000
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
monitor:
|
||||
build:
|
||||
context: ..
|
||||
@ -243,12 +268,72 @@ services:
|
||||
- FOREIGN_GAS_PRICE_FALLBACK=1000000000
|
||||
- FOREIGN_GAS_PRICE_FACTOR=1
|
||||
- LEFT_TX_THRESHOLD=100
|
||||
- PORT=3003
|
||||
- PORT=3010
|
||||
entrypoint: yarn start
|
||||
ports:
|
||||
- "3003:3003"
|
||||
- "3010:3010"
|
||||
networks:
|
||||
- ultimate
|
||||
monitor-erc20:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: monitor/Dockerfile
|
||||
environment:
|
||||
- HOME_RPC_URL=http://parity1:8545
|
||||
- FOREIGN_RPC_URL=http://parity2:8545
|
||||
- HOME_BRIDGE_ADDRESS=0x1feB40aD9420b186F019A717c37f5546165d411E
|
||||
- FOREIGN_BRIDGE_ADDRESS=0x4a58D6d8D416a5fBCAcf3dC52eb8bE8948E25127
|
||||
- HOME_DEPLOYMENT_BLOCK=0
|
||||
- FOREIGN_DEPLOYMENT_BLOCK=0
|
||||
- HOME_GAS_LIMIT=300000
|
||||
- HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
|
||||
- HOME_GAS_PRICE_SPEED_TYPE=standard
|
||||
- HOME_GAS_PRICE_FALLBACK=1000000000
|
||||
- HOME_GAS_PRICE_FACTOR=1
|
||||
- FOREIGN_GAS_LIMIT=300000
|
||||
- FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
|
||||
- FOREIGN_GAS_PRICE_SPEED_TYPE=standard
|
||||
- FOREIGN_GAS_PRICE_FALLBACK=1000000000
|
||||
- FOREIGN_GAS_PRICE_FACTOR=1
|
||||
- LEFT_TX_THRESHOLD=100
|
||||
- PORT=3011
|
||||
entrypoint: yarn start
|
||||
ports:
|
||||
- "3011:3011"
|
||||
networks:
|
||||
- ultimate
|
||||
monitor-erc20-native:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: monitor/Dockerfile
|
||||
environment:
|
||||
- HOME_RPC_URL=http://parity1:8545
|
||||
- FOREIGN_RPC_URL=http://parity2:8545
|
||||
- HOME_BRIDGE_ADDRESS=0x488Af810997eD1730cB3a3918cD83b3216E6eAda
|
||||
- FOREIGN_BRIDGE_ADDRESS=0x488Af810997eD1730cB3a3918cD83b3216E6eAda
|
||||
- HOME_DEPLOYMENT_BLOCK=0
|
||||
- FOREIGN_DEPLOYMENT_BLOCK=0
|
||||
- HOME_GAS_LIMIT=300000
|
||||
- HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
|
||||
- HOME_GAS_PRICE_SPEED_TYPE=standard
|
||||
- HOME_GAS_PRICE_FALLBACK=1000000000
|
||||
- HOME_GAS_PRICE_FACTOR=1
|
||||
- FOREIGN_GAS_LIMIT=300000
|
||||
- FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
|
||||
- FOREIGN_GAS_PRICE_SPEED_TYPE=standard
|
||||
- FOREIGN_GAS_PRICE_FALLBACK=1000000000
|
||||
- FOREIGN_GAS_PRICE_FACTOR=1
|
||||
- LEFT_TX_THRESHOLD=100
|
||||
- PORT=3012
|
||||
entrypoint: yarn start
|
||||
ports:
|
||||
- "3012:3012"
|
||||
networks:
|
||||
- ultimate
|
||||
e2e:
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile.e2e
|
||||
command: "true"
|
||||
networks:
|
||||
- ultimate
|
||||
|
@ -5,3 +5,4 @@ if [ $CI ]; then exit $rc; fi
|
||||
|
||||
ps | grep node | grep -v grep | awk '{print "kill " $1}' | /bin/bash
|
||||
docker-compose down
|
||||
docker network rm ultimate || true
|
||||
|
@ -3,16 +3,19 @@ const { generateNewBlock } = require('../utils')
|
||||
|
||||
const homeWeb3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8541'))
|
||||
const foreignWeb3 = new Web3(new Web3.providers.HttpProvider('http://localhost:8542'))
|
||||
const {user} = require('../constants.json');
|
||||
const {user, blockGenerator} = require('../constants.json');
|
||||
|
||||
homeWeb3.eth.accounts.wallet.add(user.privateKey)
|
||||
foreignWeb3.eth.accounts.wallet.add(user.privateKey)
|
||||
homeWeb3.eth.accounts.wallet.add(blockGenerator.privateKey)
|
||||
foreignWeb3.eth.accounts.wallet.add(blockGenerator.privateKey)
|
||||
|
||||
function main() {
|
||||
setTimeout(async () => {
|
||||
generateNewBlock(homeWeb3, user.address)
|
||||
generateNewBlock(foreignWeb3, user.address)
|
||||
generateNewBlock(homeWeb3, blockGenerator.address)
|
||||
generateNewBlock(foreignWeb3, blockGenerator.address)
|
||||
main()
|
||||
}, 5000)
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
main()
|
||||
|
BIN
e2e-commons/ultimate.png
Normal file
BIN
e2e-commons/ultimate.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 259 KiB |
@ -4,14 +4,10 @@ set -e # exit when any command fails
|
||||
|
||||
./down.sh
|
||||
docker-compose build
|
||||
docker network create --driver bridge ultimate || true
|
||||
docker-compose up -d parity1 parity2 e2e
|
||||
export DOCKER_LOCALHOST="localhost"
|
||||
|
||||
while [ "$1" != "" ]; do
|
||||
if [ "$1" == "macos" ]; then
|
||||
export DOCKER_LOCALHOST="host.docker.internal"
|
||||
fi
|
||||
|
||||
if [ "$1" == "oracle" ]; then
|
||||
docker-compose up -d redis rabbit oracle oracle-erc20 oracle-erc20-native oracle-amb
|
||||
|
||||
@ -48,12 +44,20 @@ while [ "$1" != "" ]; do
|
||||
fi
|
||||
|
||||
if [ "$1" == "monitor" ]; then
|
||||
docker-compose up -d monitor
|
||||
docker-compose up -d monitor monitor-erc20 monitor-erc20-native
|
||||
fi
|
||||
|
||||
if [ "$1" == "native-to-erc" ]; then
|
||||
../deployment/molecule/molecule.sh ultimate-native-to-erc
|
||||
fi
|
||||
|
||||
if [ "$1" == "erc-to-native" ]; then
|
||||
../deployment/molecule/molecule.sh ultimate-erc-to-native
|
||||
fi
|
||||
|
||||
if [ "$1" == "erc-to-erc" ]; then
|
||||
../deployment/molecule/molecule.sh ultimate-erc-to-erc
|
||||
fi
|
||||
|
||||
shift # Shift all the parameters down by one
|
||||
done
|
||||
|
@ -1,6 +1,6 @@
|
||||
# POA Token Bridge / Monitor-E2E
|
||||
# POA TokenBridge / Monitor-E2E
|
||||
|
||||
End to end tests for the POA Token Bridge [Monitor](../monitor/README.md).
|
||||
End to end tests for the POA TokenBridge [Monitor](../monitor/README.md).
|
||||
|
||||
## Running
|
||||
|
||||
|
26
monitor-e2e/erc-to-erc.sh
Executable file
26
monitor-e2e/erc-to-erc.sh
Executable file
@ -0,0 +1,26 @@
|
||||
set -e # exit when any command fails
|
||||
|
||||
echo "MONITOR E2E - ERC TO ERC"
|
||||
export URL=localhost:3011
|
||||
|
||||
echo "Test case - Web Interface should return balances"
|
||||
OUTPUT=$(curl -s http://$URL/)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return validators"
|
||||
OUTPUT=$(curl -s http://$URL/validators)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return eventsStats"
|
||||
OUTPUT=$(curl -s http://$URL/eventsStats)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return alerts"
|
||||
OUTPUT=$(curl -s http://$URL/alerts)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
26
monitor-e2e/erc-to-native.sh
Executable file
26
monitor-e2e/erc-to-native.sh
Executable file
@ -0,0 +1,26 @@
|
||||
set -e # exit when any command fails
|
||||
|
||||
echo "MONITOR E2E - ERC TO NATIVE"
|
||||
export URL=localhost:3012
|
||||
|
||||
echo "Test case - Web Interface should return balances"
|
||||
OUTPUT=$(curl -s http://$URL/)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return validators"
|
||||
OUTPUT=$(curl -s http://$URL/validators)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return eventsStats"
|
||||
OUTPUT=$(curl -s http://$URL/eventsStats)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return alerts"
|
||||
OUTPUT=$(curl -s http://$URL/alerts)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
26
monitor-e2e/native-to-erc.sh
Executable file
26
monitor-e2e/native-to-erc.sh
Executable file
@ -0,0 +1,26 @@
|
||||
set -e # exit when any command fails
|
||||
|
||||
echo "MONITOR E2E - NATIVE TO ERC"
|
||||
export URL="localhost:3010"
|
||||
|
||||
echo "Test case - Web Interface should return balances"
|
||||
OUTPUT=$(curl -s http://$URL/)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return validators"
|
||||
OUTPUT=$(curl -s http://$URL/validators)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return eventsStats"
|
||||
OUTPUT=$(curl -s http://$URL/eventsStats)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return alerts"
|
||||
OUTPUT=$(curl -s http://$URL/alerts)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
@ -17,6 +17,8 @@ check_files_exist() {
|
||||
for f in "${FILES[@]}"; do
|
||||
command="test -f responses/$f"
|
||||
(docker-compose -f ../e2e-commons/docker-compose.yml exec monitor /bin/bash -c "$command") || rc=1
|
||||
(docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20 /bin/bash -c "$command") || rc=1
|
||||
(docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20-native /bin/bash -c "$command") || rc=1
|
||||
done
|
||||
return $rc
|
||||
}
|
||||
@ -24,38 +26,24 @@ check_files_exist() {
|
||||
|
||||
##### Initialization #####
|
||||
|
||||
../e2e-commons/up.sh oracle deploy monitor
|
||||
../e2e-commons/up.sh deploy oracle monitor
|
||||
|
||||
|
||||
##### Initial checks #####
|
||||
|
||||
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor /bin/bash -c "yarn check-all"
|
||||
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20 /bin/bash -c "yarn check-all"
|
||||
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor-erc20-native /bin/bash -c "yarn check-all"
|
||||
check_files_exist
|
||||
|
||||
|
||||
##### Test cases #####
|
||||
|
||||
echo "Test case - CheckWorker scripts should work and create files in responses/ directory"
|
||||
(! check_files_exist)
|
||||
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor /bin/bash -c "node checkWorker.js"
|
||||
docker-compose -f ../e2e-commons/docker-compose.yml exec monitor /bin/bash -c "node checkWorker2.js"
|
||||
check_files_exist
|
||||
./native-to-erc.sh
|
||||
|
||||
echo "Test case - Web Interface should return balances"
|
||||
OUTPUT=$(curl -s http://localhost:3003/)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
./erc-to-erc.sh
|
||||
|
||||
echo "Test case - Web Interface should return validators"
|
||||
OUTPUT=$(curl -s http://localhost:3003/validators)
|
||||
grep -q home <<< "$OUTPUT"
|
||||
grep -q foreign <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return eventsStats"
|
||||
OUTPUT=$(curl -s http://localhost:3003/eventsStats)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
|
||||
echo "Test case - Web Interface should return alerts"
|
||||
OUTPUT=$(curl -s http://localhost:3003/alerts)
|
||||
grep -q lastChecked <<< "$OUTPUT"
|
||||
(! grep -q error <<< "$OUTPUT")
|
||||
./erc-to-native.sh
|
||||
|
||||
|
||||
##### Cleanup #####
|
||||
|
@ -1,12 +1,12 @@
|
||||
# POA Token Bridge / Monitor
|
||||
# POA TokenBridge / Monitor
|
||||
Tool for checking balances and unprocessed events in bridged networks.
|
||||
|
||||
## Overview
|
||||
Please refer to the [POA Token Bridge](../README.md) overview first of all.
|
||||
Please refer to the [POA TokenBridge](../README.md) overview first of all.
|
||||
|
||||
- Deployed version: https://bridge-monitoring.poa.net/
|
||||
|
||||
This tool allows you to spin up a NODE.JS server to monitor for health of the Token Bridge contracts: check for the balance difference, discover inconsistency in the validators list, catch unhandled transactions.
|
||||
This tool allows you to spin up a NODE.JS server to monitor for health of the TokenBridge contracts: check for the balance difference, discover inconsistency in the validators list, catch unhandled transactions.
|
||||
|
||||
## Examples
|
||||
|
||||
|
@ -45,25 +45,15 @@ async function main() {
|
||||
)
|
||||
|
||||
logger.debug('extracting most recent transactionHash')
|
||||
const { transactionHash: xSignaturesMostRecentTxHash = '' } =
|
||||
xSignatures.sort(sortEvents).reverse()[0] || {}
|
||||
const { transactionHash: xAffirmationsMostRecentTxHash = '' } =
|
||||
xAffirmations.sort(sortEvents).reverse()[0] || {}
|
||||
const { transactionHash: xSignaturesMostRecentTxHash = '' } = xSignatures.sort(sortEvents).reverse()[0] || {}
|
||||
const { transactionHash: xAffirmationsMostRecentTxHash = '' } = xAffirmations.sort(sortEvents).reverse()[0] || {}
|
||||
|
||||
logger.debug('building transaction objects')
|
||||
const foreignValidators = await Promise.all(
|
||||
xSignatures.map(event => findTxSender(web3Foreign)(event))
|
||||
)
|
||||
const homeValidators = await Promise.all(
|
||||
xAffirmations.map(event => findTxSender(web3Home)(event))
|
||||
)
|
||||
const foreignValidators = await Promise.all(xSignatures.map(event => findTxSender(web3Foreign)(event)))
|
||||
const homeValidators = await Promise.all(xAffirmations.map(event => findTxSender(web3Home)(event)))
|
||||
|
||||
const xSignaturesTxs = xSignatures
|
||||
.map(normalizeEventInformation)
|
||||
.reduce(buildTxList(foreignValidators), {})
|
||||
const xAffirmationsTxs = xAffirmations
|
||||
.map(normalizeEventInformation)
|
||||
.reduce(buildTxList(homeValidators), {})
|
||||
const xSignaturesTxs = xSignatures.map(normalizeEventInformation).reduce(buildTxList(foreignValidators), {})
|
||||
const xAffirmationsTxs = xAffirmations.map(normalizeEventInformation).reduce(buildTxList(homeValidators), {})
|
||||
|
||||
logger.debug('Done')
|
||||
|
||||
@ -181,9 +171,7 @@ const findDifferences = src => dest => {
|
||||
return (
|
||||
src
|
||||
.map(normalizeEventInformation)
|
||||
.filter(
|
||||
a => a.referenceTx === b.referenceTx && a.recipient === b.recipient && a.value === b.value
|
||||
).length === 0
|
||||
.filter(a => a.referenceTx === b.referenceTx && a.recipient === b.recipient && a.value === b.value).length === 0
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -26,18 +26,12 @@ async function checkWorker() {
|
||||
const foreign = Object.assign({}, balances.foreign, events.foreign)
|
||||
const status = Object.assign({}, balances, events, { home }, { foreign })
|
||||
if (!status) throw new Error('status is empty: ' + JSON.stringify(status))
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '/responses/getBalances.json'),
|
||||
JSON.stringify(status, null, 4)
|
||||
)
|
||||
fs.writeFileSync(path.join(__dirname, '/responses/getBalances.json'), JSON.stringify(status, null, 4))
|
||||
|
||||
logger.debug('calling validators()')
|
||||
const vBalances = await validators(bridgeMode)
|
||||
if (!vBalances) throw new Error('vBalances is empty: ' + JSON.stringify(vBalances))
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '/responses/validators.json'),
|
||||
JSON.stringify(vBalances, null, 4)
|
||||
)
|
||||
fs.writeFileSync(path.join(__dirname, '/responses/validators.json'), JSON.stringify(vBalances, null, 4))
|
||||
logger.debug('Done')
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
|
@ -9,17 +9,11 @@ async function checkWorker2() {
|
||||
logger.debug('calling eventsStats()')
|
||||
const evStats = await eventsStats()
|
||||
if (!evStats) throw new Error('evStats is empty: ' + JSON.stringify(evStats))
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '/responses/eventsStats.json'),
|
||||
JSON.stringify(evStats, null, 4)
|
||||
)
|
||||
fs.writeFileSync(path.join(__dirname, '/responses/eventsStats.json'), JSON.stringify(evStats, null, 4))
|
||||
logger.debug('calling alerts()')
|
||||
const _alerts = await alerts()
|
||||
if (!_alerts) throw new Error('alerts is empty: ' + JSON.stringify(_alerts))
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '/responses/alerts.json'),
|
||||
JSON.stringify(_alerts, null, 4)
|
||||
)
|
||||
fs.writeFileSync(path.join(__dirname, '/responses/alerts.json'), JSON.stringify(_alerts, null, 4))
|
||||
logger.debug('Done x2')
|
||||
} catch (e) {
|
||||
logger.error(e)
|
||||
|
@ -9,10 +9,7 @@ async function checkWorker3() {
|
||||
const transfers = await stuckTransfers()
|
||||
// console.log(transfers)
|
||||
if (!transfers) throw new Error('transfers is empty: ' + JSON.stringify(transfers))
|
||||
fs.writeFileSync(
|
||||
path.join(__dirname, '/responses/stuckTransfers.json'),
|
||||
JSON.stringify(transfers, null, 4)
|
||||
)
|
||||
fs.writeFileSync(path.join(__dirname, '/responses/stuckTransfers.json'), JSON.stringify(transfers, null, 4))
|
||||
logger.debug('Done')
|
||||
} catch (e) {
|
||||
logger.error('checkWorker3.js', e)
|
||||
|
@ -27,10 +27,7 @@ const {
|
||||
|
||||
async function main(bridgeMode) {
|
||||
if (bridgeMode === BRIDGE_MODES.ERC_TO_ERC) {
|
||||
const foreignBridge = new web3Foreign.eth.Contract(
|
||||
FOREIGN_ERC_TO_ERC_ABI,
|
||||
FOREIGN_BRIDGE_ADDRESS
|
||||
)
|
||||
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ERC_TO_ERC_ABI, FOREIGN_BRIDGE_ADDRESS)
|
||||
const erc20Address = await foreignBridge.methods.erc20token().call()
|
||||
const erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
|
||||
logger.debug('calling erc20Contract.methods.balanceOf')
|
||||
@ -55,15 +52,9 @@ async function main(bridgeMode) {
|
||||
balanceDiff: Number(Web3Utils.fromWei(diff)),
|
||||
lastChecked: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
} else if (
|
||||
bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC ||
|
||||
bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1
|
||||
) {
|
||||
} else if (bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC_V1) {
|
||||
logger.debug('calling web3Home.eth.getBalance')
|
||||
const foreignBridge = new web3Foreign.eth.Contract(
|
||||
FOREIGN_NATIVE_TO_ERC_ABI,
|
||||
FOREIGN_BRIDGE_ADDRESS
|
||||
)
|
||||
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, FOREIGN_BRIDGE_ADDRESS)
|
||||
const erc20Address = await foreignBridge.methods.erc677token().call()
|
||||
const homeBalance = await web3Home.eth.getBalance(HOME_BRIDGE_ADDRESS)
|
||||
const tokenContract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
|
||||
@ -84,10 +75,7 @@ async function main(bridgeMode) {
|
||||
lastChecked: Math.floor(Date.now() / 1000)
|
||||
}
|
||||
} else if (bridgeMode === BRIDGE_MODES.ERC_TO_NATIVE) {
|
||||
const foreignBridge = new web3Foreign.eth.Contract(
|
||||
FOREIGN_ERC_TO_NATIVE_ABI,
|
||||
FOREIGN_BRIDGE_ADDRESS
|
||||
)
|
||||
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, FOREIGN_BRIDGE_ADDRESS)
|
||||
const erc20Address = await foreignBridge.methods.erc20token().call()
|
||||
const erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
|
||||
logger.debug('calling erc20Contract.methods.balanceOf')
|
||||
|
@ -3,9 +3,7 @@ const eventsInfo = require('./utils/events')
|
||||
const { BRIDGE_MODES } = require('../commons')
|
||||
|
||||
async function main(bridgeMode) {
|
||||
const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo(
|
||||
bridgeMode
|
||||
)
|
||||
const { foreignDeposits, homeDeposits, homeWithdrawals, foreignWithdrawals } = await eventsInfo(bridgeMode)
|
||||
|
||||
if (bridgeMode === BRIDGE_MODES.ARBITRARY_MESSAGE) {
|
||||
return {
|
||||
|
@ -91,8 +91,7 @@ app.get('/eventsStats', async (req, res, next) => {
|
||||
app.get('/alerts', async (req, res, next) => {
|
||||
try {
|
||||
const results = await readFile('./responses/alerts.json')
|
||||
results.ok =
|
||||
!results.executeAffirmations.mostRecentTxHash && !results.executeSignatures.mostRecentTxHash
|
||||
results.ok = !results.executeAffirmations.mostRecentTxHash && !results.executeSignatures.mostRecentTxHash
|
||||
res.json(results)
|
||||
} catch (e) {
|
||||
next(e)
|
||||
|
@ -3,12 +3,7 @@ module.exports = function logger(name) {
|
||||
|
||||
function log(...args) {
|
||||
const now = new Date()
|
||||
console.log(
|
||||
now.toISOString(),
|
||||
`(+${lastlog ? now.getTime() - lastlog : 0}ms)`,
|
||||
`[${name}]`,
|
||||
...args
|
||||
)
|
||||
console.log(now.toISOString(), `(+${lastlog ? now.getTime() - lastlog : 0}ms)`, `[${name}]`, ...args)
|
||||
lastlog = now.getTime()
|
||||
}
|
||||
|
||||
|
9
monitor/scripts/checkDocker.sh
Executable file
9
monitor/scripts/checkDocker.sh
Executable file
@ -0,0 +1,9 @@
|
||||
#!/bin/bash
|
||||
cd $(dirname $0)/..
|
||||
|
||||
if /usr/local/bin/docker-compose ps | grep -q -i 'monitor'; then
|
||||
# https://github.com/docker/compose/issues/3352
|
||||
COMPOSE_INTERACTIVE_NO_CLI=1 /usr/local/bin/docker-compose exec -T monitor /bin/bash -c 'yarn check-all'
|
||||
else
|
||||
echo "Monitor is not running, skipping checks."
|
||||
fi
|
@ -1,7 +1,7 @@
|
||||
require('dotenv').config()
|
||||
const Web3 = require('web3')
|
||||
const logger = require('./logger')('stuckTransfers.js')
|
||||
const { foreignViAbi } = require('../commons/abis')
|
||||
const { FOREIGN_V1_ABI } = require('../commons/abis')
|
||||
|
||||
const { FOREIGN_RPC_URL, FOREIGN_BRIDGE_ADDRESS } = process.env
|
||||
const FOREIGN_DEPLOYMENT_BLOCK = Number(process.env.FOREIGN_DEPLOYMENT_BLOCK) || 0
|
||||
@ -75,7 +75,7 @@ function compareTransfers(transfersNormal) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const foreignBridge = new web3Foreign.eth.Contract(foreignViAbi, FOREIGN_BRIDGE_ADDRESS)
|
||||
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_V1_ABI, FOREIGN_BRIDGE_ADDRESS)
|
||||
const erc20Address = await foreignBridge.methods.erc677token().call()
|
||||
const tokenContract = new web3Foreign.eth.Contract(ABITransferWithoutData, erc20Address)
|
||||
const tokenContractWithData = new web3Foreign.eth.Contract(ABIWithData, erc20Address)
|
||||
|
@ -1,43 +1,5 @@
|
||||
const { toBN } = require('web3').utils
|
||||
|
||||
const ONE = toBN(1)
|
||||
const TWO = toBN(2)
|
||||
|
||||
async function getPastEvents({ contract, event, fromBlock, toBlock, options }) {
|
||||
let events
|
||||
try {
|
||||
events = await contract.getPastEvents(event, {
|
||||
...options,
|
||||
fromBlock,
|
||||
toBlock
|
||||
})
|
||||
} catch (e) {
|
||||
if (e.message.includes('query returned more than 1000 results')) {
|
||||
const middle = fromBlock.add(toBlock).divRound(TWO)
|
||||
const middlePlusOne = middle.add(ONE)
|
||||
|
||||
const firstHalfEvents = await getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock,
|
||||
toBlock: middle,
|
||||
options
|
||||
})
|
||||
const secondHalfEvents = await getPastEvents({
|
||||
contract,
|
||||
event,
|
||||
fromBlock: middlePlusOne,
|
||||
toBlock,
|
||||
options
|
||||
})
|
||||
events = [...firstHalfEvents, ...secondHalfEvents]
|
||||
} else {
|
||||
throw new Error(e)
|
||||
}
|
||||
}
|
||||
return events
|
||||
}
|
||||
|
||||
const getBlockNumberCall = web3 => web3.eth.getBlockNumber()
|
||||
|
||||
async function getBlockNumber(web3Home, web3Foreign) {
|
||||
@ -45,6 +7,5 @@ async function getBlockNumber(web3Home, web3Foreign) {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getPastEvents,
|
||||
getBlockNumber
|
||||
}
|
||||
|
@ -1,18 +0,0 @@
|
||||
const { ERC_TYPES } = require('../../commons')
|
||||
|
||||
const getTokenType = async (contract, bridgeAddress) => {
|
||||
try {
|
||||
const bridgeContract = await contract.methods.bridgeContract().call()
|
||||
if (bridgeContract === bridgeAddress) {
|
||||
return ERC_TYPES.ERC677
|
||||
} else {
|
||||
return ERC_TYPES.ERC20
|
||||
}
|
||||
} catch (e) {
|
||||
return ERC_TYPES.ERC20
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getTokenType
|
||||
}
|
@ -8,9 +8,11 @@ const {
|
||||
getBridgeABIs,
|
||||
getBridgeMode,
|
||||
HOME_ERC_TO_ERC_ABI,
|
||||
ERC20_ABI
|
||||
ERC20_ABI,
|
||||
ERC677_BRIDGE_TOKEN_ABI,
|
||||
getTokenType,
|
||||
getPastEvents
|
||||
} = require('../../commons')
|
||||
const { getTokenType } = require('./ercUtils')
|
||||
|
||||
const { HOME_RPC_URL, FOREIGN_RPC_URL, HOME_BRIDGE_ADDRESS, FOREIGN_BRIDGE_ADDRESS } = process.env
|
||||
const HOME_DEPLOYMENT_BLOCK = toBN(Number(process.env.HOME_DEPLOYMENT_BLOCK) || 0)
|
||||
@ -22,7 +24,7 @@ const web3Home = new Web3(homeProvider)
|
||||
const foreignProvider = new Web3.providers.HttpProvider(FOREIGN_RPC_URL)
|
||||
const web3Foreign = new Web3(foreignProvider)
|
||||
|
||||
const { getPastEvents, getBlockNumber } = require('./contract')
|
||||
const { getBlockNumber } = require('./contract')
|
||||
|
||||
async function main(mode) {
|
||||
const homeErcBridge = new web3Home.eth.Contract(HOME_ERC_TO_ERC_ABI, HOME_BRIDGE_ADDRESS)
|
||||
@ -38,7 +40,11 @@ async function main(mode) {
|
||||
isExternalErc20 = tokenType === ERC_TYPES.ERC20
|
||||
const erc20MethodName =
|
||||
bridgeMode === BRIDGE_MODES.NATIVE_TO_ERC || v1Bridge ? 'erc677token' : 'erc20token'
|
||||
const erc20Address = await foreignBridge.methods[erc20MethodName]().call()
|
||||
const erc20Address = await foreignBridge.methods[erc20MethodName]().call()const tokenType = await getTokenType(
|
||||
new web3Foreign.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, erc20Address),
|
||||
FOREIGN_BRIDGE_ADDRESS
|
||||
)
|
||||
const isExternalErc20 = tokenType === ERC_TYPES.ERC20
|
||||
erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
|
||||
}
|
||||
|
||||
@ -46,36 +52,29 @@ async function main(mode) {
|
||||
const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign)
|
||||
|
||||
logger.debug("calling homeBridge.getPastEvents('UserRequestForSignature')")
|
||||
const homeDeposits = await getPastEvents({
|
||||
contract: homeBridge,
|
||||
const homeDeposits = await getPastEvents(homeBridge, {
|
||||
event: v1Bridge ? 'Deposit' : 'UserRequestForSignature',
|
||||
fromBlock: HOME_DEPLOYMENT_BLOCK,
|
||||
toBlock: homeBlockNumber,
|
||||
options: {}
|
||||
toBlock: homeBlockNumber
|
||||
})
|
||||
|
||||
logger.debug("calling foreignBridge.getPastEvents('RelayedMessage')")
|
||||
const foreignDeposits = await getPastEvents({
|
||||
contract: foreignBridge,
|
||||
const foreignDeposits = await getPastEvents(foreignBridge, {
|
||||
event: v1Bridge ? 'Deposit' : 'RelayedMessage',
|
||||
fromBlock: FOREIGN_DEPLOYMENT_BLOCK,
|
||||
toBlock: foreignBlockNumber,
|
||||
options: {}
|
||||
toBlock: foreignBlockNumber
|
||||
})
|
||||
|
||||
logger.debug("calling homeBridge.getPastEvents('AffirmationCompleted')")
|
||||
const homeWithdrawals = await getPastEvents({
|
||||
contract: homeBridge,
|
||||
const homeWithdrawals = await getPastEvents(homeBridge, {
|
||||
event: v1Bridge ? 'Withdraw' : 'AffirmationCompleted',
|
||||
fromBlock: HOME_DEPLOYMENT_BLOCK,
|
||||
toBlock: homeBlockNumber,
|
||||
options: {}
|
||||
toBlock: homeBlockNumber
|
||||
})
|
||||
|
||||
logger.debug("calling foreignBridge.getPastEvents('UserRequestForAffirmation')")
|
||||
const foreignWithdrawals = isExternalErc20
|
||||
? await getPastEvents({
|
||||
contract: erc20Contract,
|
||||
? await getPastEvents(erc20Contract, {
|
||||
event: 'Transfer',
|
||||
fromBlock: FOREIGN_DEPLOYMENT_BLOCK,
|
||||
toBlock: foreignBlockNumber,
|
||||
@ -83,12 +82,10 @@ async function main(mode) {
|
||||
filter: { to: FOREIGN_BRIDGE_ADDRESS }
|
||||
}
|
||||
})
|
||||
: await getPastEvents({
|
||||
contract: foreignBridge,
|
||||
: await getPastEvents(foreignBridge, {
|
||||
event: v1Bridge ? 'Withdraw' : 'UserRequestForAffirmation',
|
||||
fromBlock: FOREIGN_DEPLOYMENT_BLOCK,
|
||||
toBlock: foreignBlockNumber,
|
||||
options: {}
|
||||
toBlock: foreignBlockNumber
|
||||
})
|
||||
logger.debug('Done')
|
||||
return {
|
||||
|
@ -1,79 +0,0 @@
|
||||
/* eslint no-param-reassign: ["error", { "props": false }] */
|
||||
|
||||
const { BRIDGE_VALIDATORS_ABI } = require('../../commons')
|
||||
const logger = require('../logger')('validatorsUtils')
|
||||
const { getPastEvents } = require('./contract')
|
||||
|
||||
const parseValidatorEvent = event => {
|
||||
if (
|
||||
event.event === undefined &&
|
||||
event.raw &&
|
||||
event.raw.topics &&
|
||||
(event.raw.topics[0] === '0xe366c1c0452ed8eec96861e9e54141ebff23c9ec89fe27b996b45f5ec3884987' ||
|
||||
event.raw.topics[0] === '0x8064a302796c89446a96d63470b5b036212da26bd2debe5bec73e0170a9a5e83')
|
||||
) {
|
||||
const rawAddress = event.raw.topics.length > 1 ? event.raw.topics[1] : event.raw.data
|
||||
const address = '0x' + rawAddress.slice(26)
|
||||
event.event = 'ValidatorAdded'
|
||||
event.returnValues.validator = address
|
||||
} else if (
|
||||
event.event === undefined &&
|
||||
event.raw &&
|
||||
event.raw.topics &&
|
||||
event.raw.topics[0] === '0xe1434e25d6611e0db941968fdc97811c982ac1602e951637d206f5fdda9dd8f1'
|
||||
) {
|
||||
const rawAddress = event.raw.data === '0x' ? event.raw.topics[1] : event.raw.data
|
||||
const address = '0x' + rawAddress.slice(26)
|
||||
event.event = 'ValidatorRemoved'
|
||||
event.returnValues.validator = address
|
||||
}
|
||||
}
|
||||
|
||||
const processValidatorsEvents = events => {
|
||||
const validatorList = new Set()
|
||||
events.forEach(event => {
|
||||
parseValidatorEvent(event)
|
||||
|
||||
if (event.event === 'ValidatorAdded') {
|
||||
validatorList.add(event.returnValues.validator)
|
||||
} else if (event.event === 'ValidatorRemoved') {
|
||||
validatorList.delete(event.returnValues.validator)
|
||||
}
|
||||
})
|
||||
|
||||
return Array.from(validatorList)
|
||||
}
|
||||
|
||||
const validatorList = async contract => {
|
||||
try {
|
||||
return await contract.methods.validatorList().call()
|
||||
} catch (e) {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
const getValidatorList = async (address, eth, fromBlock, toBlock) => {
|
||||
logger.debug('getting validatorList')
|
||||
const validatorsContract = new eth.Contract(BRIDGE_VALIDATORS_ABI, address)
|
||||
const validators = await validatorList(validatorsContract)
|
||||
|
||||
if (validators.length) {
|
||||
return validators
|
||||
}
|
||||
|
||||
logger.debug('getting validatorsEvents')
|
||||
const contract = new eth.Contract([], address)
|
||||
const validatorsEvents = await getPastEvents({
|
||||
contract,
|
||||
event: 'allEvents',
|
||||
fromBlock,
|
||||
toBlock,
|
||||
options: {}
|
||||
})
|
||||
|
||||
return processValidatorsEvents(validatorsEvents)
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getValidatorList
|
||||
}
|
@ -2,8 +2,7 @@ require('dotenv').config()
|
||||
const Web3 = require('web3')
|
||||
const fetch = require('node-fetch')
|
||||
const logger = require('./logger')('validators')
|
||||
const { getBridgeABIs, BRIDGE_VALIDATORS_ABI } = require('../commons')
|
||||
const { getValidatorList } = require('./utils/validatorUtils')
|
||||
const { getBridgeABIs, BRIDGE_VALIDATORS_ABI, getValidatorList, gasPriceFromOracle } = require('../commons')
|
||||
const { getBlockNumber } = require('./utils/contract')
|
||||
|
||||
const {
|
||||
@ -33,75 +32,51 @@ const web3Home = new Web3(homeProvider)
|
||||
const foreignProvider = new Web3.providers.HttpProvider(FOREIGN_RPC_URL)
|
||||
const web3Foreign = new Web3(foreignProvider)
|
||||
|
||||
const homeGasOracleOpts = {
|
||||
speedType: HOME_GAS_PRICE_SPEED_TYPE,
|
||||
factor: HOME_GAS_PRICE_FACTOR,
|
||||
logger
|
||||
}
|
||||
|
||||
const foreignGasOracleOpts = {
|
||||
speedType: FOREIGN_GAS_PRICE_SPEED_TYPE,
|
||||
factor: FOREIGN_GAS_PRICE_FACTOR,
|
||||
logger
|
||||
}
|
||||
|
||||
const asyncForEach = async (array, callback) => {
|
||||
for (let index = 0; index < array.length; index++) {
|
||||
await callback(array[index], index, array)
|
||||
}
|
||||
}
|
||||
|
||||
function getGasPrices(url, type, factor, fallback) {
|
||||
if (!url) {
|
||||
return Web3Utils.toBN(fallback)
|
||||
}
|
||||
return fetchGasPrices(url, type, factor, fallback)
|
||||
}
|
||||
|
||||
async function fetchGasPrices(url, type, factor, fallback) {
|
||||
try {
|
||||
const response = await fetch(url)
|
||||
const json = await response.json()
|
||||
logger.log('Fetched gasprice: ' + json[type])
|
||||
const gasPrice = json[type]
|
||||
if (!gasPrice) {
|
||||
throw new Error(`Response from Oracle didn't include gas price for ${type} type.`)
|
||||
}
|
||||
return normalizeGasPrice(gasPrice, factor)
|
||||
} catch (e) {
|
||||
logger.error('Gas Price API is not available', e)
|
||||
return Web3Utils.toBN(fallback)
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGasPrice(oracleGasPrice, factor) {
|
||||
const gasPrice = oracleGasPrice * factor
|
||||
return Web3Utils.toBN(Web3Utils.toWei(gasPrice.toFixed(2).toString(), 'gwei'))
|
||||
}
|
||||
|
||||
async function main(bridgeMode) {
|
||||
const { HOME_ABI, FOREIGN_ABI } = getBridgeABIs(bridgeMode)
|
||||
const homeBridge = new web3Home.eth.Contract(HOME_ABI, HOME_BRIDGE_ADDRESS)
|
||||
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ABI, FOREIGN_BRIDGE_ADDRESS)
|
||||
const homeValidatorsAddress = await homeBridge.methods.validatorContract().call()
|
||||
const homeBridgeValidators = new web3Home.eth.Contract(
|
||||
BRIDGE_VALIDATORS_ABI,
|
||||
homeValidatorsAddress
|
||||
)
|
||||
const homeBridgeValidators = new web3Home.eth.Contract(BRIDGE_VALIDATORS_ABI, homeValidatorsAddress)
|
||||
|
||||
logger.debug('getting last block numbers')
|
||||
const [homeBlockNumber, foreignBlockNumber] = await getBlockNumber(web3Home, web3Foreign)
|
||||
|
||||
logger.debug('calling foreignBridge.methods.validatorContract().call()')
|
||||
const foreignValidatorsAddress = await foreignBridge.methods.validatorContract().call()
|
||||
const foreignBridgeValidators = new web3Foreign.eth.Contract(
|
||||
BRIDGE_VALIDATORS_ABI,
|
||||
foreignValidatorsAddress
|
||||
)
|
||||
const foreignBridgeValidators = new web3Foreign.eth.Contract(BRIDGE_VALIDATORS_ABI, foreignValidatorsAddress)
|
||||
|
||||
logger.debug('calling foreignBridgeValidators getValidatorList()')
|
||||
const foreignValidators = await getValidatorList(
|
||||
foreignValidatorsAddress,
|
||||
web3Foreign.eth,
|
||||
FOREIGN_DEPLOYMENT_BLOCK,
|
||||
foreignBlockNumber
|
||||
)
|
||||
const foreignValidators = await getValidatorList(foreignValidatorsAddress, web3Foreign.eth, {
|
||||
from: FOREIGN_DEPLOYMENT_BLOCK,
|
||||
to: foreignBlockNumber,
|
||||
logger
|
||||
})
|
||||
|
||||
logger.debug('calling homeBridgeValidators getValidatorList()')
|
||||
const homeValidators = await getValidatorList(
|
||||
homeValidatorsAddress,
|
||||
web3Home.eth,
|
||||
HOME_DEPLOYMENT_BLOCK,
|
||||
homeBlockNumber
|
||||
)
|
||||
const homeValidators = await getValidatorList(homeValidatorsAddress, web3Home.eth, {
|
||||
from: HOME_DEPLOYMENT_BLOCK,
|
||||
to: homeBlockNumber,
|
||||
logger
|
||||
})
|
||||
|
||||
const homeBalances = {}
|
||||
logger.debug('calling asyncForEach homeValidators homeBalances')
|
||||
@ -112,22 +87,16 @@ async function main(bridgeMode) {
|
||||
const homeVBalances = {}
|
||||
|
||||
logger.debug('calling home getGasPrices')
|
||||
const homeGasPrice = await getGasPrices(
|
||||
HOME_GAS_PRICE_ORACLE_URL,
|
||||
HOME_GAS_PRICE_SPEED_TYPE,
|
||||
HOME_GAS_PRICE_FACTOR,
|
||||
HOME_GAS_PRICE_FALLBACK
|
||||
)
|
||||
const homeGasPrice =
|
||||
(await gasPriceFromOracle(() => fetch(HOME_GAS_PRICE_ORACLE_URL), homeGasOracleOpts)) ||
|
||||
Web3Utils.toBN(HOME_GAS_PRICE_FALLBACK)
|
||||
const homeGasPriceGwei = Web3Utils.fromWei(homeGasPrice.toString(), 'gwei')
|
||||
const homeTxCost = homeGasPrice.mul(Web3Utils.toBN(HOME_GAS_LIMIT))
|
||||
|
||||
logger.debug('calling foreign getGasPrices')
|
||||
const foreignGasPrice = await getGasPrices(
|
||||
FOREIGN_GAS_PRICE_ORACLE_URL,
|
||||
FOREIGN_GAS_PRICE_SPEED_TYPE,
|
||||
FOREIGN_GAS_PRICE_FACTOR,
|
||||
FOREIGN_GAS_PRICE_FALLBACK
|
||||
)
|
||||
const foreignGasPrice =
|
||||
(await gasPriceFromOracle(() => fetch(FOREIGN_GAS_PRICE_ORACLE_URL), foreignGasOracleOpts)) ||
|
||||
Web3Utils.toBN(FOREIGN_GAS_PRICE_FALLBACK)
|
||||
const foreignGasPriceGwei = Web3Utils.fromWei(foreignGasPrice.toString(), 'gwei')
|
||||
const foreignTxCost = foreignGasPrice.mul(Web3Utils.toBN(FOREIGN_GAS_LIMIT))
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
# POA Token Bridge / Oracle-E2E
|
||||
# POA TokenBridge / Oracle-E2E
|
||||
|
||||
End to end tests for the POA Token Bridge [Oracle](../oracle/README.md).
|
||||
End to end tests for the POA TokenBridge [Oracle](../oracle/README.md).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
|
@ -5,7 +5,8 @@
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "mocha",
|
||||
"lint": "eslint . --ignore-path ../.eslintignore"
|
||||
"lint": "eslint . --ignore-path ../.eslintignore",
|
||||
"native-to-erc": "mocha test/nativeToErc.js"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
|
@ -16,10 +16,7 @@ const { toBN } = foreignWeb3.utils
|
||||
homeWeb3.eth.accounts.wallet.add(user.privateKey)
|
||||
foreignWeb3.eth.accounts.wallet.add(user.privateKey)
|
||||
|
||||
const erc20Token = new foreignWeb3.eth.Contract(
|
||||
ERC677_BRIDGE_TOKEN_ABI,
|
||||
ercToErcBridge.foreignToken
|
||||
)
|
||||
const erc20Token = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ercToErcBridge.foreignToken)
|
||||
const erc677Token = new homeWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ercToErcBridge.homeToken)
|
||||
|
||||
describe('erc to erc', () => {
|
||||
|
@ -16,10 +16,7 @@ const { toBN } = foreignWeb3.utils
|
||||
homeWeb3.eth.accounts.wallet.add(user.privateKey)
|
||||
foreignWeb3.eth.accounts.wallet.add(user.privateKey)
|
||||
|
||||
const erc20Token = new foreignWeb3.eth.Contract(
|
||||
ERC677_BRIDGE_TOKEN_ABI,
|
||||
ercToNativeBridge.foreignToken
|
||||
)
|
||||
const erc20Token = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ercToNativeBridge.foreignToken)
|
||||
|
||||
describe('erc to native', () => {
|
||||
it('should convert tokens in foreign to coins in home', async () => {
|
||||
|
@ -1,8 +1,8 @@
|
||||
# POA Token Bridge / Oracle
|
||||
# POA TokenBridge / Oracle
|
||||
Oracle responsible for listening to bridge related events and authorizing asset transfers.
|
||||
|
||||
## Overview
|
||||
Please refer to the [POA Token Bridge](../README.md) overview first of all.
|
||||
Please refer to the [POA TokenBridge](../README.md) overview first of all.
|
||||
|
||||
The Oracle is deployed on specified validator nodes (only nodes whose private keys correspond to addresses specified in the smart contracts) in the network. It connects to two chains via a Remote Procedure Call (RPC) and is responsible for:
|
||||
- listening to events related to bridge contracts
|
||||
|
@ -1,6 +1,5 @@
|
||||
const baseConfig = require('./base.config')
|
||||
const { ERC20_ABI } = require('../../commons')
|
||||
const { ERC_TYPES } = require('../src/utils/constants')
|
||||
const { ERC20_ABI, ERC_TYPES } = require('../../commons')
|
||||
|
||||
const initialChecksJson = process.argv[3]
|
||||
|
||||
@ -22,8 +21,7 @@ if (baseConfig.id === 'erc-erc' && initialChecks.foreignERC === ERC_TYPES.ERC677
|
||||
const id = `${baseConfig.id}-affirmation-request`
|
||||
|
||||
module.exports =
|
||||
(baseConfig.id === 'erc-erc' && initialChecks.foreignERC === ERC_TYPES.ERC20) ||
|
||||
baseConfig.id === 'erc-native'
|
||||
(baseConfig.id === 'erc-erc' && initialChecks.foreignERC === ERC_TYPES.ERC20) || baseConfig.id === 'erc-native'
|
||||
? {
|
||||
...baseConfig.bridgeConfig,
|
||||
...baseConfig.foreignConfig,
|
||||
|
@ -56,8 +56,7 @@ let maxProcessingTime = null
|
||||
if (String(process.env.MAX_PROCESSING_TIME) === '0') {
|
||||
maxProcessingTime = 0
|
||||
} else if (!process.env.MAX_PROCESSING_TIME) {
|
||||
maxProcessingTime =
|
||||
4 * Math.max(process.env.HOME_POLLING_INTERVAL, process.env.FOREIGN_POLLING_INTERVAL)
|
||||
maxProcessingTime = 4 * Math.max(process.env.HOME_POLLING_INTERVAL, process.env.FOREIGN_POLLING_INTERVAL)
|
||||
} else {
|
||||
maxProcessingTime = Number(process.env.MAX_PROCESSING_TIME)
|
||||
}
|
||||
|
@ -61,10 +61,7 @@ async function main() {
|
||||
console.log('Signature Requests')
|
||||
console.log(signatureRequestsStats)
|
||||
|
||||
const collectedSignaturesStats = computeCollectedSignaturesStats(
|
||||
collectedSignatures,
|
||||
senderForeign
|
||||
)
|
||||
const collectedSignaturesStats = computeCollectedSignaturesStats(collectedSignatures, senderForeign)
|
||||
console.log('Collected Signatures')
|
||||
console.log(collectedSignaturesStats)
|
||||
}
|
||||
|
@ -14,8 +14,7 @@ const {
|
||||
HOME_TEST_TX_GAS_PRICE
|
||||
} = process.env
|
||||
|
||||
const NUMBER_OF_WITHDRAWALS_TO_SEND =
|
||||
process.argv[2] || process.env.NUMBER_OF_WITHDRAWALS_TO_SEND || 1
|
||||
const NUMBER_OF_WITHDRAWALS_TO_SEND = process.argv[2] || process.env.NUMBER_OF_WITHDRAWALS_TO_SEND || 1
|
||||
|
||||
const BRIDGEABLE_TOKEN_ABI = [
|
||||
{
|
||||
|
@ -17,10 +17,7 @@ async function getStartBlock(rpcUrl, bridgeAddress, bridgeAbi) {
|
||||
const deployedAtBlock = await bridgeContract.methods.deployedAtBlock().call()
|
||||
|
||||
const validatorContractAddress = await bridgeContract.methods.validatorContract().call()
|
||||
const validatorContract = new web3Instance.eth.Contract(
|
||||
BRIDGE_VALIDATORS_ABI,
|
||||
validatorContractAddress
|
||||
)
|
||||
const validatorContract = new web3Instance.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorContractAddress)
|
||||
|
||||
const validatorDeployedAtBlock = await validatorContract.methods.deployedAtBlock().call()
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
require('../env')
|
||||
const Web3 = require('web3')
|
||||
const { ERC677_BRIDGE_TOKEN_ABI } = require('../../commons')
|
||||
const { ERC_TYPES } = require('../src/utils/constants')
|
||||
const { ERC677_BRIDGE_TOKEN_ABI, getTokenType } = require('../../commons')
|
||||
|
||||
async function initialChecks() {
|
||||
const { ERC20_TOKEN_ADDRESS, BRIDGE_MODE, FOREIGN_RPC_URL, FOREIGN_BRIDGE_ADDRESS } = process.env
|
||||
@ -9,17 +8,9 @@ async function initialChecks() {
|
||||
|
||||
if (BRIDGE_MODE === 'ERC_TO_ERC') {
|
||||
const foreignWeb3 = new Web3(new Web3.providers.HttpProvider(FOREIGN_RPC_URL))
|
||||
const tokenContract = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ERC20_TOKEN_ADDRESS)
|
||||
try {
|
||||
const bridgeContract = await tokenContract.methods.bridgeContract().call()
|
||||
if (bridgeContract === FOREIGN_BRIDGE_ADDRESS) {
|
||||
result.foreignERC = ERC_TYPES.ERC677
|
||||
} else {
|
||||
result.foreignERC = ERC_TYPES.ERC20
|
||||
}
|
||||
} catch (e) {
|
||||
result.foreignERC = ERC_TYPES.ERC20
|
||||
}
|
||||
const bridgeTokenContract = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, ERC20_TOKEN_ADDRESS)
|
||||
|
||||
result.foreignERC = await getTokenType(bridgeTokenContract, FOREIGN_BRIDGE_ADDRESS)
|
||||
}
|
||||
console.log(JSON.stringify(result))
|
||||
return result
|
||||
|
@ -13,8 +13,7 @@ const {
|
||||
FOREIGN_TEST_TX_GAS_PRICE
|
||||
} = process.env
|
||||
|
||||
const NUMBER_OF_WITHDRAWALS_TO_SEND =
|
||||
process.argv[2] || process.env.NUMBER_OF_WITHDRAWALS_TO_SEND || 1
|
||||
const NUMBER_OF_WITHDRAWALS_TO_SEND = process.argv[2] || process.env.NUMBER_OF_WITHDRAWALS_TO_SEND || 1
|
||||
|
||||
const ERC677_ABI = [
|
||||
{
|
||||
|
@ -1,28 +1,14 @@
|
||||
const { HttpListProviderError } = require('http-list-provider')
|
||||
const {
|
||||
AlreadyProcessedError,
|
||||
AlreadySignedError,
|
||||
InvalidValidatorError
|
||||
} = require('../../utils/errors')
|
||||
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const logger = require('../../services/logger').child({
|
||||
module: 'processAffirmationRequests:estimateGas'
|
||||
})
|
||||
|
||||
async function estimateGas({
|
||||
web3,
|
||||
homeBridge,
|
||||
validatorContract,
|
||||
recipient,
|
||||
value,
|
||||
txHash,
|
||||
address
|
||||
}) {
|
||||
async function estimateGas({ web3, homeBridge, validatorContract, recipient, value, txHash, address }) {
|
||||
try {
|
||||
const gasEstimate = await homeBridge.methods
|
||||
.executeAffirmation(recipient, value, txHash)
|
||||
.estimateGas({
|
||||
from: address
|
||||
})
|
||||
const gasEstimate = await homeBridge.methods.executeAffirmation(recipient, value, txHash).estimateGas({
|
||||
from: address
|
||||
})
|
||||
|
||||
return gasEstimate
|
||||
} catch (e) {
|
||||
@ -36,9 +22,7 @@ async function estimateGas({
|
||||
// Check if minimum number of validations was already reached
|
||||
logger.debug('Check if minimum number of validations was already reached')
|
||||
const numAffirmationsSigned = await homeBridge.methods.numAffirmationsSigned(messageHash).call()
|
||||
const alreadyProcessed = await homeBridge.methods
|
||||
.isAlreadyProcessed(numAffirmationsSigned)
|
||||
.call()
|
||||
const alreadyProcessed = await homeBridge.methods.isAlreadyProcessed(numAffirmationsSigned).call()
|
||||
|
||||
if (alreadyProcessed) {
|
||||
throw new AlreadyProcessedError(e.message)
|
||||
|
@ -7,11 +7,7 @@ const { web3Home } = require('../../services/web3')
|
||||
const { BRIDGE_VALIDATORS_ABI } = require('../../../../commons')
|
||||
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
|
||||
const estimateGas = require('./estimateGas')
|
||||
const {
|
||||
AlreadyProcessedError,
|
||||
AlreadySignedError,
|
||||
InvalidValidatorError
|
||||
} = require('../../utils/errors')
|
||||
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
|
||||
|
||||
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
|
||||
|
||||
@ -40,10 +36,7 @@ function processAffirmationRequestsBuilder(config) {
|
||||
eventTransactionHash: affirmationRequest.transactionHash
|
||||
})
|
||||
|
||||
logger.info(
|
||||
{ sender: recipient, value },
|
||||
`Processing affirmationRequest ${affirmationRequest.transactionHash}`
|
||||
)
|
||||
logger.info({ sender: recipient, value }, `Processing affirmationRequest ${affirmationRequest.transactionHash}`)
|
||||
|
||||
let gasEstimate
|
||||
try {
|
||||
@ -60,9 +53,7 @@ function processAffirmationRequestsBuilder(config) {
|
||||
logger.debug({ gasEstimate }, 'Gas estimated')
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw new Error(
|
||||
'RPC Connection Error: submitSignature Gas Estimate cannot be obtained.'
|
||||
)
|
||||
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
|
||||
} else if (e instanceof InvalidValidatorError) {
|
||||
logger.fatal({ address: config.validatorAddress }, 'Invalid validator')
|
||||
process.exit(EXIT_CODES.INCOMPATIBILITY)
|
||||
@ -71,9 +62,7 @@ function processAffirmationRequestsBuilder(config) {
|
||||
return
|
||||
} else if (e instanceof AlreadyProcessedError) {
|
||||
logger.info(
|
||||
`affirmationRequest ${
|
||||
affirmationRequest.transactionHash
|
||||
} was already processed by other validators`
|
||||
`affirmationRequest ${affirmationRequest.transactionHash} was already processed by other validators`
|
||||
)
|
||||
return
|
||||
} else {
|
||||
|
@ -1,10 +1,6 @@
|
||||
const Web3 = require('web3')
|
||||
const { HttpListProviderError } = require('http-list-provider')
|
||||
const {
|
||||
AlreadyProcessedError,
|
||||
IncompatibleContractError,
|
||||
InvalidValidatorError
|
||||
} = require('../../utils/errors')
|
||||
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const { parseMessage } = require('../../utils/message')
|
||||
const logger = require('../../services/logger').child({
|
||||
module: 'processCollectedSignatures:estimateGas'
|
||||
@ -13,19 +9,9 @@ const logger = require('../../services/logger').child({
|
||||
const web3 = new Web3()
|
||||
const { toBN } = Web3.utils
|
||||
|
||||
async function estimateGas({
|
||||
foreignBridge,
|
||||
validatorContract,
|
||||
message,
|
||||
numberOfCollectedSignatures,
|
||||
v,
|
||||
r,
|
||||
s
|
||||
}) {
|
||||
async function estimateGas({ foreignBridge, validatorContract, message, numberOfCollectedSignatures, v, r, s }) {
|
||||
try {
|
||||
const gasEstimate = await foreignBridge.methods
|
||||
.executeSignatures(v, r, s, message)
|
||||
.estimateGas()
|
||||
const gasEstimate = await foreignBridge.methods.executeSignatures(v, r, s, message).estimateGas()
|
||||
return gasEstimate
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
|
@ -6,11 +6,7 @@ const rootLogger = require('../../services/logger')
|
||||
const { web3Home, web3Foreign } = require('../../services/web3')
|
||||
const { signatureToVRS } = require('../../utils/message')
|
||||
const estimateGas = require('./estimateGas')
|
||||
const {
|
||||
AlreadyProcessedError,
|
||||
IncompatibleContractError,
|
||||
InvalidValidatorError
|
||||
} = require('../../utils/errors')
|
||||
const { AlreadyProcessedError, IncompatibleContractError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const { MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
|
||||
|
||||
const limit = promiseLimit(MAX_CONCURRENT_EVENTS)
|
||||
@ -20,10 +16,7 @@ let validatorContract = null
|
||||
function processCollectedSignaturesBuilder(config) {
|
||||
const homeBridge = new web3Home.eth.Contract(config.homeBridgeAbi, config.homeBridgeAddress)
|
||||
|
||||
const foreignBridge = new web3Foreign.eth.Contract(
|
||||
config.foreignBridgeAbi,
|
||||
config.foreignBridgeAddress
|
||||
)
|
||||
const foreignBridge = new web3Foreign.eth.Contract(config.foreignBridgeAbi, config.foreignBridgeAddress)
|
||||
|
||||
return async function processCollectedSignatures(signatures) {
|
||||
const txToSend = []
|
||||
@ -33,28 +26,19 @@ function processCollectedSignaturesBuilder(config) {
|
||||
const validatorContractAddress = await foreignBridge.methods.validatorContract().call()
|
||||
rootLogger.debug({ validatorContractAddress }, 'Validator contract address obtained')
|
||||
|
||||
validatorContract = new web3Foreign.eth.Contract(
|
||||
BRIDGE_VALIDATORS_ABI,
|
||||
validatorContractAddress
|
||||
)
|
||||
validatorContract = new web3Foreign.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorContractAddress)
|
||||
}
|
||||
|
||||
rootLogger.debug(`Processing ${signatures.length} CollectedSignatures events`)
|
||||
const callbacks = signatures.map(colSignature =>
|
||||
limit(async () => {
|
||||
const {
|
||||
authorityResponsibleForRelay,
|
||||
messageHash,
|
||||
NumberOfCollectedSignatures
|
||||
} = colSignature.returnValues
|
||||
const { authorityResponsibleForRelay, messageHash, NumberOfCollectedSignatures } = colSignature.returnValues
|
||||
|
||||
const logger = rootLogger.child({
|
||||
eventTransactionHash: colSignature.transactionHash
|
||||
})
|
||||
|
||||
if (
|
||||
authorityResponsibleForRelay === web3Home.utils.toChecksumAddress(config.validatorAddress)
|
||||
) {
|
||||
if (authorityResponsibleForRelay === web3Home.utils.toChecksumAddress(config.validatorAddress)) {
|
||||
logger.info(`Processing CollectedSignatures ${colSignature.transactionHash}`)
|
||||
const message = await homeBridge.methods.message(messageHash).call()
|
||||
|
||||
@ -90,16 +74,11 @@ function processCollectedSignaturesBuilder(config) {
|
||||
logger.debug({ gasEstimate }, 'Gas estimated')
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw new Error(
|
||||
'RPC Connection Error: submitSignature Gas Estimate cannot be obtained.'
|
||||
)
|
||||
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
|
||||
} else if (e instanceof AlreadyProcessedError) {
|
||||
logger.info(`Already processed CollectedSignatures ${colSignature.transactionHash}`)
|
||||
return
|
||||
} else if (
|
||||
e instanceof IncompatibleContractError ||
|
||||
e instanceof InvalidValidatorError
|
||||
) {
|
||||
} else if (e instanceof IncompatibleContractError || e instanceof InvalidValidatorError) {
|
||||
logger.error(`The message couldn't be processed; skipping: ${e.message}`)
|
||||
return
|
||||
} else {
|
||||
@ -115,11 +94,7 @@ function processCollectedSignaturesBuilder(config) {
|
||||
to: config.foreignBridgeAddress
|
||||
})
|
||||
} else {
|
||||
logger.info(
|
||||
`Validator not responsible for relaying CollectedSignatures ${
|
||||
colSignature.transactionHash
|
||||
}`
|
||||
)
|
||||
logger.info(`Validator not responsible for relaying CollectedSignatures ${colSignature.transactionHash}`)
|
||||
}
|
||||
})
|
||||
)
|
||||
|
@ -1,9 +1,5 @@
|
||||
const { HttpListProviderError } = require('http-list-provider')
|
||||
const {
|
||||
AlreadyProcessedError,
|
||||
AlreadySignedError,
|
||||
InvalidValidatorError
|
||||
} = require('../../utils/errors')
|
||||
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const logger = require('../../services/logger').child({
|
||||
module: 'processSignatureRequests:estimateGas'
|
||||
})
|
||||
|
@ -6,11 +6,7 @@ const rootLogger = require('../../services/logger')
|
||||
const { web3Home } = require('../../services/web3')
|
||||
const { createMessage } = require('../../utils/message')
|
||||
const estimateGas = require('./estimateGas')
|
||||
const {
|
||||
AlreadyProcessedError,
|
||||
AlreadySignedError,
|
||||
InvalidValidatorError
|
||||
} = require('../../utils/errors')
|
||||
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
|
||||
|
||||
const { VALIDATOR_ADDRESS_PRIVATE_KEY } = process.env
|
||||
@ -47,10 +43,7 @@ function processSignatureRequestsBuilder(config) {
|
||||
eventTransactionHash: signatureRequest.transactionHash
|
||||
})
|
||||
|
||||
logger.info(
|
||||
{ sender: recipient, value },
|
||||
`Processing signatureRequest ${signatureRequest.transactionHash}`
|
||||
)
|
||||
logger.info({ sender: recipient, value }, `Processing signatureRequest ${signatureRequest.transactionHash}`)
|
||||
|
||||
const message = createMessage({
|
||||
recipient,
|
||||
@ -76,9 +69,7 @@ function processSignatureRequestsBuilder(config) {
|
||||
logger.debug({ gasEstimate }, 'Gas estimated')
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw new Error(
|
||||
'RPC Connection Error: submitSignature Gas Estimate cannot be obtained.'
|
||||
)
|
||||
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
|
||||
} else if (e instanceof InvalidValidatorError) {
|
||||
logger.fatal({ address: config.validatorAddress }, 'Invalid validator')
|
||||
process.exit(EXIT_CODES.INCOMPATIBILITY)
|
||||
@ -87,9 +78,7 @@ function processSignatureRequestsBuilder(config) {
|
||||
return
|
||||
} else if (e instanceof AlreadyProcessedError) {
|
||||
logger.info(
|
||||
`signatureRequest ${
|
||||
signatureRequest.transactionHash
|
||||
} was already processed by other validators`
|
||||
`signatureRequest ${signatureRequest.transactionHash} was already processed by other validators`
|
||||
)
|
||||
return
|
||||
} else {
|
||||
|
@ -4,11 +4,7 @@ const { HttpListProviderError } = require('http-list-provider')
|
||||
const { BRIDGE_VALIDATORS_ABI } = require('../../../../commons')
|
||||
const rootLogger = require('../../services/logger')
|
||||
const { web3Home } = require('../../services/web3')
|
||||
const {
|
||||
AlreadyProcessedError,
|
||||
AlreadySignedError,
|
||||
InvalidValidatorError
|
||||
} = require('../../utils/errors')
|
||||
const { AlreadyProcessedError, AlreadySignedError, InvalidValidatorError } = require('../../utils/errors')
|
||||
const { EXIT_CODES, MAX_CONCURRENT_EVENTS } = require('../../utils/constants')
|
||||
const estimateGas = require('../processAffirmationRequests/estimateGas')
|
||||
|
||||
@ -56,9 +52,7 @@ function processTransfersBuilder(config) {
|
||||
logger.debug({ gasEstimate }, 'Gas estimated')
|
||||
} catch (e) {
|
||||
if (e instanceof HttpListProviderError) {
|
||||
throw new Error(
|
||||
'RPC Connection Error: submitSignature Gas Estimate cannot be obtained.'
|
||||
)
|
||||
throw new Error('RPC Connection Error: submitSignature Gas Estimate cannot be obtained.')
|
||||
} else if (e instanceof InvalidValidatorError) {
|
||||
logger.fatal({ address: config.validatorAddress }, 'Invalid validator')
|
||||
process.exit(EXIT_CODES.INCOMPATIBILITY)
|
||||
@ -66,9 +60,7 @@ function processTransfersBuilder(config) {
|
||||
logger.info(`Already signed transfer ${transfer.transactionHash}`)
|
||||
return
|
||||
} else if (e instanceof AlreadyProcessedError) {
|
||||
logger.info(
|
||||
`transfer ${transfer.transactionHash} was already processed by other validators`
|
||||
)
|
||||
logger.info(`transfer ${transfer.transactionHash} was already processed by other validators`)
|
||||
return
|
||||
} else {
|
||||
logger.error(e, 'Unknown error while processing transaction')
|
||||
|
@ -63,9 +63,7 @@ async function initialize() {
|
||||
}
|
||||
|
||||
function resume(newBalance) {
|
||||
logger.info(
|
||||
`Validator balance changed. New balance is ${newBalance}. Resume messages processing.`
|
||||
)
|
||||
logger.info(`Validator balance changed. New balance is ${newBalance}. Resume messages processing.`)
|
||||
initialize()
|
||||
}
|
||||
|
||||
@ -164,9 +162,7 @@ async function main({ msg, ackMsg, nackMsg, channel, scheduleForRetry }) {
|
||||
logger.debug(`Finished processing msg`)
|
||||
|
||||
if (insufficientFunds) {
|
||||
logger.warn(
|
||||
'Insufficient funds. Stop sending transactions until the account has the minimum balance'
|
||||
)
|
||||
logger.warn('Insufficient funds. Stop sending transactions until the account has the minimum balance')
|
||||
channel.close()
|
||||
waitForFunds(web3Instance, VALIDATOR_ADDRESS, minimumBalance, resume, logger)
|
||||
}
|
||||
|
@ -59,14 +59,7 @@ function connectSenderToQueue({ queueName, cb }) {
|
||||
})
|
||||
}
|
||||
|
||||
async function generateRetry({
|
||||
data,
|
||||
msgRetries,
|
||||
channelWrapper,
|
||||
channel,
|
||||
queueName,
|
||||
deadLetterExchange
|
||||
}) {
|
||||
async function generateRetry({ data, msgRetries, channelWrapper, channel, queueName, deadLetterExchange }) {
|
||||
const retries = msgRetries + 1
|
||||
const delay = getRetrySequence(retries) * 1000
|
||||
const retryQueue = `${queueName}-retry-${delay}`
|
||||
|
@ -1,18 +1,13 @@
|
||||
require('../../env')
|
||||
const fetch = require('node-fetch')
|
||||
const Web3Utils = require('web3-utils')
|
||||
const { web3Home, web3Foreign } = require('../services/web3')
|
||||
const { bridgeConfig } = require('../../config/base.config')
|
||||
const logger = require('../services/logger').child({
|
||||
module: 'gasPrice'
|
||||
})
|
||||
const { setIntervalAndRun } = require('../utils/utils')
|
||||
const {
|
||||
DEFAULT_UPDATE_INTERVAL,
|
||||
GAS_PRICE_BOUNDARIES,
|
||||
DEFAULT_GAS_PRICE_FACTOR
|
||||
} = require('../utils/constants')
|
||||
const { GAS_PRICE_OPTIONS } = require('../../../commons')
|
||||
const { DEFAULT_UPDATE_INTERVAL, GAS_PRICE_BOUNDARIES, DEFAULT_GAS_PRICE_FACTOR } = require('../utils/constants')
|
||||
const { gasPriceFromOracle, gasPriceFromContract, GAS_PRICE_OPTIONS } = require('../../../commons')
|
||||
|
||||
const HomeABI = bridgeConfig.homeBridgeAbi
|
||||
const ForeignABI = bridgeConfig.foreignBridgeAbi
|
||||
@ -39,62 +34,20 @@ const foreignBridge = new web3Foreign.eth.Contract(ForeignABI, FOREIGN_BRIDGE_AD
|
||||
let cachedGasPrice = null
|
||||
let cachedGasPriceOracleSpeeds = null
|
||||
|
||||
function gasPriceWithinLimits(gasPrice) {
|
||||
if (gasPrice < GAS_PRICE_BOUNDARIES.MIN) {
|
||||
return GAS_PRICE_BOUNDARIES.MIN
|
||||
} else if (gasPrice > GAS_PRICE_BOUNDARIES.MAX) {
|
||||
return GAS_PRICE_BOUNDARIES.MAX
|
||||
} else {
|
||||
return gasPrice
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeGasPrice(oracleGasPrice, factor) {
|
||||
const gasPriceGwei = oracleGasPrice * factor
|
||||
const gasPrice = gasPriceWithinLimits(gasPriceGwei)
|
||||
return Web3Utils.toWei(gasPrice.toFixed(2).toString(), 'gwei')
|
||||
}
|
||||
|
||||
async function fetchGasPriceFromOracle(oracleUrl, speedType, factor) {
|
||||
const response = await fetch(oracleUrl)
|
||||
const json = await response.json()
|
||||
const oracleGasPrice = json[speedType]
|
||||
if (!oracleGasPrice) {
|
||||
throw new Error(`Response from Oracle didn't include gas price for ${speedType} type.`)
|
||||
}
|
||||
|
||||
return {
|
||||
oracleGasPrice: normalizeGasPrice(oracleGasPrice, factor),
|
||||
oracleResponse: json
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGasPrice({ bridgeContract, oracleFn }) {
|
||||
let gasPrice = null
|
||||
let oracleGasPriceSpeeds = null
|
||||
try {
|
||||
const { oracleGasPrice, oracleResponse } = await oracleFn()
|
||||
gasPrice = oracleGasPrice
|
||||
oracleGasPriceSpeeds = oracleResponse
|
||||
logger.debug({ gasPrice }, 'Gas price updated using the oracle')
|
||||
} catch (e) {
|
||||
logger.error(`Gas Price API is not available. ${e.message}`)
|
||||
|
||||
try {
|
||||
gasPrice = await bridgeContract.methods.gasPrice().call()
|
||||
logger.debug({ gasPrice }, 'Gas price updated using the contracts')
|
||||
} catch (e) {
|
||||
logger.error(`There was a problem getting the gas price from the contract. ${e.message}`)
|
||||
}
|
||||
}
|
||||
return {
|
||||
gasPrice,
|
||||
oracleGasPriceSpeeds
|
||||
}
|
||||
}
|
||||
|
||||
let fetchGasPriceInterval = null
|
||||
|
||||
const fetchGasPrice = async (speedType, factor, bridgeContract, oracleFetchFn) => {
|
||||
const contractOptions = { logger }
|
||||
const oracleOptions = { speedType, factor, limits: GAS_PRICE_BOUNDARIES, logger, returnAllSpeeds: true }
|
||||
const { gasPrice, oracleGasPriceSpeeds } = await gasPriceFromOracle(oracleFetchFn, oracleOptions)
|
||||
cachedGasPrice =
|
||||
gasPrice ||
|
||||
(await gasPriceFromContract(bridgeContract, contractOptions)) ||
|
||||
cachedGasPrice
|
||||
cachedGasPriceOracleSpeeds = oracleGasPriceSpeeds || cachedGasPriceOracleSpeeds
|
||||
return cachedGasPrice
|
||||
}
|
||||
|
||||
async function start(chainId) {
|
||||
clearInterval(fetchGasPriceInterval)
|
||||
|
||||
@ -123,14 +76,10 @@ async function start(chainId) {
|
||||
throw new Error(`Unrecognized chainId '${chainId}'`)
|
||||
}
|
||||
|
||||
fetchGasPriceInterval = setIntervalAndRun(async () => {
|
||||
const { gasPrice, oracleGasPriceSpeeds } = await fetchGasPrice({
|
||||
bridgeContract,
|
||||
oracleFn: () => fetchGasPriceFromOracle(oracleUrl, speedType, factor)
|
||||
})
|
||||
cachedGasPrice = gasPrice || cachedGasPrice
|
||||
cachedGasPriceOracleSpeeds = oracleGasPriceSpeeds || cachedGasPriceOracleSpeeds
|
||||
}, updateInterval)
|
||||
fetchGasPriceInterval = setIntervalAndRun(
|
||||
() => fetchGasPrice(speedType, factor, bridgeContract, () => fetch(oracleUrl)),
|
||||
updateInterval
|
||||
)
|
||||
}
|
||||
|
||||
function getPrice(options) {
|
||||
@ -152,9 +101,7 @@ function processGasPriceOptions({ options, cachedGasPrice, cachedGasPriceOracleS
|
||||
|
||||
module.exports = {
|
||||
start,
|
||||
fetchGasPrice,
|
||||
getPrice,
|
||||
processGasPriceOptions,
|
||||
gasPriceWithinLimits,
|
||||
normalizeGasPrice
|
||||
fetchGasPrice
|
||||
}
|
||||
|
@ -1,8 +1,7 @@
|
||||
const pino = require('pino')
|
||||
const path = require('path')
|
||||
|
||||
const config =
|
||||
process.env.NODE_ENV !== 'test' ? require(path.join('../../config/', process.argv[2])) : {}
|
||||
const config = process.env.NODE_ENV !== 'test' ? require(path.join('../../config/', process.argv[2])) : {}
|
||||
|
||||
const logger = pino({
|
||||
enabled: process.env.NODE_ENV !== 'test',
|
||||
|
@ -3,18 +3,7 @@ const fetch = require('node-fetch')
|
||||
const rpcUrlsManager = require('../services/getRpcUrlsManager')
|
||||
|
||||
// eslint-disable-next-line consistent-return
|
||||
async function sendTx({
|
||||
chain,
|
||||
privateKey,
|
||||
data,
|
||||
nonce,
|
||||
gasPrice,
|
||||
amount,
|
||||
gasLimit,
|
||||
to,
|
||||
chainId,
|
||||
web3
|
||||
}) {
|
||||
async function sendTx({ chain, privateKey, data, nonce, gasPrice, amount, gasLimit, to, chainId, web3 }) {
|
||||
const serializedTx = await web3.eth.accounts.signTransaction(
|
||||
{
|
||||
nonce: Number(nonce),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user