Compare commits

...

21 Commits

Author SHA1 Message Date
Alexander Kolotov
4117f29a74 Merge the develop branch to the master branch, preparation to v2.1.0 2020-05-07 22:32:05 +03:00
dependabot[bot]
1921087ad1 Bump acorn from 5.7.3 to 5.7.4 (#305) 2020-05-07 20:48:59 +03:00
Kirill Fedoseev
b4abbc4910 Cache monitor RPC calls to increase performance (#322) 2020-05-07 20:42:27 +03:00
Alexander Kolotov
bcbe34a839 Update the contract's submodule to the release 5.0.0-rc0 (#321) 2020-05-05 20:03:12 +03:00
Gerardo Nardelli
738442e4cf Add Burner Wallet Plugin for WETC Bridge (#306) 2020-05-04 23:35:46 +03:00
Gerardo Nardelli
64c5a5670f Update contract's submodule and e2e tests (#313) 2020-04-24 21:01:07 +03:00
Gerardo Nardelli
c8c589536b Fix monitor balances displayed for erc to native (#312) 2020-04-18 21:30:38 +03:00
Gerardo Nardelli
7b0edff624 Update monitor half duplex transfer filter (#311) 2020-04-16 23:19:03 +03:00
Alexander Kolotov
b02b879b02 Update the contract's submodule to the release 4.1.0 (#307) 2020-04-09 17:34:37 +03:00
Alexander Kolotov
5b4b01a79b Merge the develop branch to the master branch, preparation to v2.0.0 2020-03-23 16:43:32 +03:00
Alexander Kolotov
881fafe9a8 Update the contract's submodule to the release 4.0.1 (#301) 2020-03-14 00:22:52 +03:00
Kirill Fedoseev
c1ed6f21e6 Correct handling of Chai to Dai swaps in oracle and monitor components (#302) 2020-03-13 01:12:48 +03:00
Alexander Kolotov
8ae0fa82d5 Merge the develop branch to the master branch, preparation to v2.0.0-rc1 2020-02-21 18:42:19 +03:00
Alexander Kolotov
dbf3d3d90d Composer files to build oracle and monitor docker images (#299) 2020-02-21 16:58:05 +03:00
Alexander Kolotov
8977ed6d3b Removal of putting .env file into the docker image for monitor (#289) 2020-02-21 16:56:55 +03:00
Alexander Kolotov
df0dc1c313 Update the contract's submodule to the release 4.0.0-rc1 (#298) 2020-02-21 10:17:18 +03:00
Gerardo Nardelli
7b2bebbcf0 Update metamask extension in ui e2e tests (#297) 2020-02-20 22:13:18 +03:00
Kirill Fedoseev
85104d67c0 Support of Chai token by monitor in erc-to-native (#287) 2020-02-18 19:17:01 +03:00
Kirill Fedoseev
2a3d7c8e08 Oracle support for erc-to-native with Chai integrated (#286) 2020-02-17 23:48:43 +03:00
Kirill Fedoseev
e6052f162a Fix some of the possibles failure reasons in e2e tests (#294) 2020-02-11 20:50:34 +03:00
Gerardo Nardelli
52ed4e85e2 Upgrade pip version in deployment common (#296) 2020-02-10 19:31:36 +03:00
105 changed files with 25970 additions and 754 deletions

View File

@@ -245,7 +245,7 @@ jobs:
- tokenbridge-orb/yarn-install-cached-on-machine
- run:
name: Prepare the infrastructure
command: e2e-commons/up.sh deploy << parameters.scenario-name >>
command: e2e-commons/up.sh deploy << parameters.scenario-name >> blocks
no_output_timeout: 50m
- tokenbridge-orb/wait-for-oracle:
redis-key: << parameters.redis-key >>
@@ -256,7 +256,6 @@ jobs:
name: Run the ui-e2e tests
command: |
nvm use default;
node ./e2e-commons/scripts/blocks.js &
cd ui-e2e; yarn mocha -g "<< parameters.ui-e2e-grep >>" -b ./test.js
- when:
condition: << parameters.oracle-e2e-script >>

View File

@@ -2,3 +2,4 @@ node_modules
submodules
coverage
lib
dist

3
.gitignore vendored
View File

@@ -6,6 +6,7 @@ coverage
# production
build
dist
# misc
.DS_Store
@@ -48,4 +49,4 @@ __pycache__
#monitor
monitor/responses/*
!monitor/.gitkeep
!monitor/.gitkeep

View File

@@ -70,3 +70,4 @@ MONITOR_VALIDATOR_FOREIGN_TX_LIMIT | Average gas usage of a transaction sent by
MONITOR_TX_NUMBER_THRESHOLD | If estimated number of transaction is equal to or below this value, the monitor will report that the validator has less funds than it is required. | integer
MONITOR_PORT | The port for the Monitor. | integer
MONITOR_BRIDGE_NAME | The name to be used in the url path for the bridge | string
MONITOR_CACHE_EVENTS | If set to true, monitor will cache obtained events for other workers runs

View File

@@ -66,11 +66,13 @@ Clone the repository:
git clone https://github.com/poanetwork/tokenbridge
```
Initialize submodules, install dependencies, compile the Smart Contracts:
If there is no need to build docker images for the TokenBridge components (oracle, monitor, UI), initialize submodules, install dependencies, compile the Smart Contracts:
```
yarn initialize
```
Then refer to the corresponding README files to get information about particular TokenBridge component.
## Linting
Running linter for all JS projects:

View File

@@ -0,0 +1,31 @@
module.exports = {
parser: "@typescript-eslint/parser", // Specifies the ESLint parser
extends: [
"plugin:react/recommended",
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from @typescript-eslint/eslint-plugin
"../.eslintrc"
],
parserOptions: {
ecmaVersion: 2018, // Allows for the parsing of modern ECMAScript features
sourceType: "module", // Allows for the use of imports
ecmaFeatures: {
jsx: true // Allows for the parsing of JSX
}
},
rules: {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off", // Reduce the use of 'any'
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-var-requires": "off",
"react/prop-types": "off",
"@typescript-eslint/ban-ts-ignore": "off",
"@typescript-eslint/member-delimiter-style": "off",
"@typescript-eslint/indent": "off",
"@typescript-eslint/explicit-member-accessibility": "off"
},
settings: {
react: {
version: "detect",
}
}
};

View File

@@ -0,0 +1,30 @@
FROM node:12 as plugin-base
WORKDIR /mono
COPY package.json .
RUN mkdir -p contracts/node_modules
COPY burner-wallet-plugin/package.json ./burner-wallet-plugin/
COPY burner-wallet-plugin/lerna.json ./burner-wallet-plugin/
COPY burner-wallet-plugin/yarn.lock ./burner-wallet-plugin/
COPY burner-wallet-plugin/tsconfig.json ./burner-wallet-plugin/
COPY burner-wallet-plugin/tokenbridge-bw-exchange/package.json ./burner-wallet-plugin/tokenbridge-bw-exchange/
COPY burner-wallet-plugin/staging/package.json ./burner-wallet-plugin/staging/
COPY burner-wallet-plugin/testing/package.json ./burner-wallet-plugin/testing/
COPY yarn.lock .
RUN yarn install --production --frozen-lockfile
COPY ./burner-wallet-plugin/tokenbridge-bw-exchange ./burner-wallet-plugin/tokenbridge-bw-exchange
RUN yarn build:plugin
FROM plugin-base as testing
COPY ./burner-wallet-plugin/testing ./burner-wallet-plugin/testing
WORKDIR /mono/burner-wallet-plugin
CMD ["yarn", "start-testing"]
FROM plugin-base as staging
COPY ./burner-wallet-plugin/staging ./burner-wallet-plugin/staging
WORKDIR /mono/burner-wallet-plugin
CMD ["yarn", "start-staging"]

View File

@@ -0,0 +1,41 @@
# TokenBridge Burner Wallet 2 Plugin
Please refer to the [Plugin README](./tokenrbdige-bw-exchange/README.md) for resources provided, instructions to install and use the plugin.
### Setup
1. [Initialize](../README.md#initializing-the-monorepository) the monorepository.
2. Run `yarn build` or from the monorepository root `yarn build:plugin`
### Run Burner Wallet with the plugin in Mainnet & Classic
1. Create `.env` file in `staging` folder and set `REACT_APP_INFURA_KEY=<your key from infura.com>`
2. Run `yarn start-staging` to start the wallet connected to Mainnet & Classic and interact with the ETH - WETC Bridge.
### Run Burner Wallet with the plugin in Sokol & Kovan
1. Create `.env` file in `testing` folder and set `REACT_APP_INFURA_KEY=<your key from infura.com>`.
Also, a private key can be set to start the wallet with the specified account `REACT_APP_PK=0x...`
2. Run `yarn start-testing` to start the wallet connected to Sokol & Kovan and interact with a test bridge
that works on top of the AMB bridge.
### Docker Setup
Docker can be used to build the services and run the testing and staging wallets.
First you may want to create the `.env` files for testing and staging as mentioned before. This is optional before building the containers, variables can be passes later using `--env-file` or `--env` parameters in `docker run`.
Build the services with docker-compose:
```bash
docker-compose build
```
### Run Burner Wallet with the plugin in Mainnet & Classic using Docker
```bash
docker run -ti -p 8080:8080 -e PORT=8080 --rm burner-wallet-plugin_staging
```
### Run Burner Wallet with the plugin in Sokol & Kovan using Docker
```bash
docker run -ti -p 8080:8080 -e PORT=8080 --rm burner-wallet-plugin_testing
```
### Publish to npm
In order to make this plugin accessible, it should be available as a npm package. Follow the [instructions](publish.md) to publish
the package to npm registry.

View File

@@ -0,0 +1,17 @@
---
version: '2.4'
services:
staging:
build:
context: ..
dockerfile: burner-wallet-plugin/Dockerfile
target: staging
environment:
- NODE_ENV=production
testing:
build:
context: ..
dockerfile: burner-wallet-plugin/Dockerfile
target: testing
environment:
- NODE_ENV=production

View File

@@ -0,0 +1,10 @@
{
"packages": [
"basic-wallet",
"local-wallet",
"tokenbridge-bw-exchange"
],
"npmClient": "yarn",
"useWorkspaces": true,
"version": "independent"
}

View File

@@ -0,0 +1,28 @@
{
"name": "burner-wallet-plugin",
"description": "Burner Wallet 2 plugin",
"version": "1.0.0",
"license": "GPL-3.0-only",
"private": true,
"scripts": {
"install": "lerna bootstrap",
"build": "lerna run --ignore testing --ignore staging build --stream",
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore",
"start-staging": "lerna run --scope staging start --stream",
"start-testing": "lerna run --scope testing start --stream",
"test": "lerna run --ignore testing --ignore staging test --stream"
},
"workspaces": [
"staging",
"testing",
"tokenbridge-bw-exchange"
],
"dependencies": {
"@types/color": "3.0.0",
"@typescript-eslint/eslint-plugin": "1.13.0",
"@typescript-eslint/parser": "1.13.0",
"eslint-plugin-react": "7.19.0",
"lerna": "3.16.4",
"typescript": "3.5.3"
}
}

View File

@@ -0,0 +1,36 @@
## Plugin Package Information
The package to be published gets its configuration from `tokenbridge/burner-wallet-plugin/tokenbridge-bw-exchange/package.json`
```json
{
"name": "tokenbridge-bw-exchange",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
]
}
```
- `name` is the name of how package will be available in npm.
- `main` is entry point for the package
- `types` is the entry point for typescript types
- `files` is the list of files included when publishing the package. So we have to run `yarn build` first to
generate the `dist` folder.
## Steps to publish to npm
1. Create account in https://www.npmjs.com/
2. Go to `tokenbridge/burner-wallet-plugin/tokenbridge-bw-exchange/`
3. Run `yarn build`. Make sure it generates the `dist` folder
4. Update `version` in `tokenbridge/burner-wallet-plugin/tokenbridge-bw-exchange/package.json`
5. Run `yarn login` and fill login information if required.
6. Run `yarn publish --access public`.
The prompt will ask for the new version, complete it with the version from `package.json`
More information in https://classic.yarnpkg.com/en/docs/publishing-a-package/

View File

@@ -0,0 +1 @@
REACT_APP_INFURA_KEY=

View File

@@ -0,0 +1,40 @@
{
"name": "staging",
"version": "0.1.0",
"private": true,
"dependencies": {
"@burner-wallet/assets": "^1.1.10",
"@burner-wallet/core": "^1.1.0",
"@burner-wallet/exchange": "^1.1.4",
"@burner-wallet/metamask-plugin": "^1.0.0",
"@burner-wallet/modern-ui": "^1.0.7",
"@poanet/tokenbridge-bw-exchange": "^1.0.0",
"@types/node": "12.0.4",
"@types/react": "*",
"@types/react-dom": "16.8.4",
"@types/react-router-dom": "^4.3.3",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"typescript": "3.5.1"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -0,0 +1,15 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,21 @@
import React from 'react'
import ReactDOM from 'react-dom'
import BurnerCore from '@burner-wallet/core'
import { InjectedSigner, LocalSigner } from '@burner-wallet/core/signers'
import { InfuraGateway, InjectedGateway } from '@burner-wallet/core/gateways'
import Exchange from '@burner-wallet/exchange'
import ModernUI from '@burner-wallet/modern-ui'
import { Etc, Wetc, TokenBridgeGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
import MetamaskPlugin from '@burner-wallet/metamask-plugin'
const core = new BurnerCore({
signers: [new InjectedSigner(), new LocalSigner()],
gateways: [new InjectedGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY), new TokenBridgeGateway()],
assets: [Wetc, Etc]
})
const exchange = new Exchange([new WETCBridge()])
const BurnerWallet = () => <ModernUI title="Staging Wallet" core={core} plugins={[exchange, new MetamaskPlugin()]} />
ReactDOM.render(<BurnerWallet />, document.getElementById('root'))

View File

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

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve"
},
"include": [
"src"
]
}

View File

@@ -0,0 +1,14 @@
REACT_APP_INFURA_KEY=
#REACT_APP_PK=0x
REACT_APP_MODE=AMB_NATIVE_TO_ERC677
REACT_APP_HOME_TOKEN_NAME=sPOA
REACT_APP_HOME_NETWORK=77
REACT_APP_HOME_MEDIATOR_ADDRESS=0x867949C3F2f66D827Ed40847FaA7B3a369370e13
REACT_APP_HOME_TOKEN_ADDRESS=
REACT_APP_FOREIGN_TOKEN_NAME=ksPOA
REACT_APP_FOREIGN_NETWORK=42
REACT_APP_FOREIGN_MEDIATOR_ADDRESS=0x99FB1a25caeB9c3a5Bf132686E2fe5e27BC0e2dd
REACT_APP_FOREIGN_TOKEN_ADDRESS=0xff94183659f549D6273349696d73686Ee1d2AC83

View File

@@ -0,0 +1,43 @@
{
"name": "testing",
"version": "0.1.0",
"private": true,
"dependencies": {
"@burner-wallet/assets": "^1.1.10",
"@burner-wallet/core": "^1.1.0",
"@burner-wallet/exchange": "^1.1.4",
"@burner-wallet/metamask-plugin": "^1.0.0",
"@burner-wallet/modern-ui": "^1.0.7",
"@poanet/tokenbridge-bw-exchange": "^1.0.0",
"@types/node": "12.0.4",
"@types/react": "16.8.19",
"@types/react-dom": "16.8.4",
"@types/react-router-dom": "^4.3.3",
"react": "^16.8.6",
"react-dom": "^16.8.6",
"react-scripts": "3.0.1",
"typescript": "3.5.1",
"web3": "1.0.0-beta.55"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"axios": "^0.19.0"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

View File

@@ -0,0 +1,15 @@
{
"short_name": "Burner Wallet",
"name": "Burner Wallet",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

View File

@@ -0,0 +1,73 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { Asset } from '@burner-wallet/assets'
import BurnerCore from '@burner-wallet/core'
import { InjectedSigner, LocalSigner } from '@burner-wallet/core/signers'
import { InfuraGateway, InjectedGateway } from '@burner-wallet/core/gateways'
import ModernUI from '@burner-wallet/modern-ui'
import Exchange from '@burner-wallet/exchange'
import { Mediator, sPOA, ERC677Asset, TokenBridgeGateway } from '@poanet/tokenbridge-bw-exchange'
import MetamaskPlugin from '@burner-wallet/metamask-plugin'
let assetIdAtHome = 'assetAtHome'
const assetIdAtForeign = 'assetAtForeign'
let assetAtHome: Asset
let assetAtForeign: Asset
if (process.env.REACT_APP_MODE === 'AMB_NATIVE_TO_ERC677') {
sPOA.setMediatorAddress(process.env.REACT_APP_HOME_MEDIATOR_ADDRESS)
assetAtHome = sPOA
assetIdAtHome = sPOA.id
assetAtForeign = new ERC677Asset({
id: 'assetAtForeign',
// @ts-ignore
name: process.env.REACT_APP_FOREIGN_TOKEN_NAME,
// @ts-ignore
network: process.env.REACT_APP_FOREIGN_NETWORK,
// @ts-ignore
address: process.env.REACT_APP_FOREIGN_TOKEN_ADDRESS
})
} else {
// process.env.REACT_APP_MODE === 'AMB_ERC677_TO_ERC677'
assetAtHome = new ERC677Asset({
id: 'assetAtHome',
// @ts-ignore
name: process.env.REACT_APP_HOME_TOKEN_NAME,
// @ts-ignore
network: process.env.REACT_APP_HOME_NETWORK,
// @ts-ignore
address: process.env.REACT_APP_HOME_TOKEN_ADDRESS
})
assetAtForeign = new ERC677Asset({
id: 'assetAtForeign',
// @ts-ignore
name: process.env.REACT_APP_FOREIGN_TOKEN_NAME,
// @ts-ignore
network: process.env.REACT_APP_FOREIGN_NETWORK,
// @ts-ignore
address: process.env.REACT_APP_FOREIGN_TOKEN_ADDRESS
})
}
const testBridge = new Mediator({
assetA: assetIdAtHome,
// @ts-ignore
assetABridge: process.env.REACT_APP_HOME_MEDIATOR_ADDRESS,
assetB: assetIdAtForeign,
// @ts-ignore
assetBBridge: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
})
const core = new BurnerCore({
signers: [new InjectedSigner(), new LocalSigner({ privateKey: process.env.REACT_APP_PK, saveKey: false })],
gateways: [new InjectedGateway(), new TokenBridgeGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
assets: [assetAtHome, assetAtForeign]
})
const exchange = new Exchange([testBridge])
const BurnerWallet = () => <ModernUI title="Testing Wallet" core={core} plugins={[exchange, new MetamaskPlugin()]} />
ReactDOM.render(<BurnerWallet />, document.getElementById('root'))

View File

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

View File

@@ -0,0 +1,25 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve"
},
"include": [
"src"
]
}

View File

@@ -0,0 +1,37 @@
# TokenBridge Burner Wallet 2 Plugin
This plugin defines a Bridge trading pair to be used in the Exchange Plugin.
Bridge trading pairs and assets supported:
* ETC - WETC Bridge
It also provides some generic resources that can be used and extended:
* **ERC677Asset** - A representation of an Erc677 token
* **NativeMediatorAsset** - Represents a native token that interacts with a Mediator extension.
* **Mediator Pair** - Represents an Exchange Pair that interacts with mediators extensions.
* **TokenBridgeGateway** - A gateway to operate with ETC, POA Sokol and POA Core networks.
### Install package
```
yarn add @poanet/tokenbridge-bw-exchange
```
### Usage
```javascript
import { Etc, Wetc, EtcGateway, WETCBridge } from '@poanet/tokenbridge-bw-exchange'
const core = new BurnerCore({
...
gateways: [new EtcGateway(), new InfuraGateway(process.env.REACT_APP_INFURA_KEY)],
assets: [Etc, Wetc]
})
const exchange = new Exchange({
pairs: [new WETCBridge()]
})
```
This is how the exchange plugin will look like:
![exchange-wetc](https://user-images.githubusercontent.com/4614574/80991095-e40d0900-8e0d-11ea-9915-1b4e4a052694.png)

View File

@@ -0,0 +1,40 @@
{
"name": "@poanet/tokenbridge-bw-exchange",
"version": "1.0.0",
"license": "GPL-3.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist",
"README.md"
],
"scripts": {
"build": "tsc",
"start-basic": "tsc -w",
"start-local": "tsc -w",
"test": "TS_NODE_PROJECT=\"tsconfig.testing.json\" mocha -r ts-node/register test/**/*.spec.ts"
},
"dependencies": {
"@burner-wallet/assets": "^1.1.10",
"@burner-wallet/core": "^1.1.9",
"@burner-wallet/exchange": "^1.1.4",
"@burner-wallet/types": "^1.0.6"
},
"devDependencies": {
"@types/mocha": "^7.0.2",
"chai": "^4.2.0",
"mocha": "^5.2.0",
"ts-node": "^8.8.2",
"typescript": "^3.5.2"
},
"repository": {
"type": "git",
"url": "https://github.com/poanetwork/tokenbridge.git",
"directory": "burner-wallet-plugin/tokenbridge-bw-exchange"
},
"homepage": "https://tokenbridge.net/",
"keywords": [
"tokenbridge",
"burner-wallet"
]
}

View File

@@ -0,0 +1,79 @@
import { ERC20Asset } from '@burner-wallet/assets'
import { ERC677_ABI } from '../../utils'
const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
const BLOCK_LOOKBACK = 250
interface ERC677Constructor {
abi?: object
address: string
id: string
name: string
network: string
}
export default class ERC677Asset extends ERC20Asset {
constructor({ abi = ERC677_ABI, ...params }: ERC677Constructor) {
super({ abi, type: 'erc677', ...params })
}
async _send({ from, to, value }) {
const receipt = await this.getContract()
.methods.transferAndCall(to, value, '0x')
.send({ from })
return {
...receipt,
txHash: receipt.transactionHash,
id: `${receipt.transactionHash}-${receipt.events.Transfer.logIndex}`
}
}
/**
* Overrides ERC20Asset `startWatchingAddress` to get the `Transfer` events by topic instead of
* the event name because ERC677 abi has two events definitions named `Transfer` and
* `getPastEvents` method does not provide a way to choose the correct one to use.
* @param address
*/
startWatchingAddress(address) {
let block = 0
return this.poll(async () => {
const currentBlock = await this.getWeb3().eth.getBlockNumber()
if (block === 0) {
block = Math.max(currentBlock - BLOCK_LOOKBACK, 0)
}
const allTransferEvents = await this.getContract().getPastEvents('allEvents', {
fromBlock: block,
toBlock: currentBlock,
topics: [TRANSFER_TOPIC]
})
// Manually filter `to` parameter because `filter` option does not work with allEvents
const events = allTransferEvents.filter(e => e.returnValues.to.toLowerCase() === address.toLowerCase())
await events.map(async event =>
this.core.addHistoryEvent({
id: `${event.transactionHash}-${event.logIndex}`,
asset: this.id,
type: 'send',
value: event.returnValues.value.toString(),
from: event.returnValues.from,
to: event.returnValues.to,
tx: event.transactionHash,
timestamp: await this._getBlockTimestamp(event.blockNumber)
})
)
block = currentBlock
}, this._pollInterval)
}
async getTx(txHash) {
const historyEvents = this.core.getHistoryEvents({ asset: this.id })
const eventMatch = historyEvents.filter(e => e.tx === txHash)
if (eventMatch.length > 0) {
return eventMatch[0]
} else {
return super.getTx(txHash)
}
}
}

View File

@@ -0,0 +1,45 @@
import NativeMediatorAsset from './NativeMediatorAsset'
import { isBridgeContract, HOME_NATIVE_TO_ERC_ABI } from '../../utils'
class EtcNativeAsset extends NativeMediatorAsset {
constructor(props) {
super({ mediatorAddress: '0x073081832B4Ecdce79d4D6753565c85Ba4b3BeA9', ...props })
}
async scanMediatorEvents(address, fromBlock, toBlock) {
const web3 = this.getWeb3()
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.mediatorAddress)
const listenToBridgeEvent = await isBridgeContract(contract)
if (listenToBridgeEvent && this.mediatorAddress != '') {
const events = await contract.getPastEvents('AffirmationCompleted', {
fromBlock,
toBlock
})
const filteredEvents = events.filter(
event => event.returnValues.recipient.toLowerCase() === address.toLowerCase()
)
for (const event of filteredEvents) {
this.core.addHistoryEvent({
id: `${event.transactionHash}-${event.logIndex}`,
asset: this.id,
type: 'send',
value: event.returnValues.value.toString(),
from: this.mediatorAddress,
to: event.returnValues.recipient,
tx: event.transactionHash,
timestamp: await this._getBlockTimestamp(event.blockNumber)
})
}
} else {
await super.scanMediatorEvents(address, fromBlock, toBlock)
}
}
}
export default new EtcNativeAsset({
id: 'etc',
name: 'ETC',
network: '61',
icon: 'https://user-images.githubusercontent.com/4614574/77648741-666cf800-6f47-11ea-8cb4-01b9db00c264.png'
})

View File

@@ -0,0 +1,65 @@
import { NativeAsset } from '@burner-wallet/assets'
import { Contract, EventData } from 'web3-eth-contract'
import { MEDIATOR_ABI } from '../../utils'
interface NativeMediatorConstructor {
mediatorAddress?: string
id: string
name: string
network: string
}
export default class NativeMediatorAsset extends NativeAsset {
protected mediatorAddress: string
constructor({ mediatorAddress = '', ...params }: NativeMediatorConstructor) {
super({ ...params })
this.mediatorAddress = mediatorAddress
}
async scanBlocks(address, fromBlock, toBlock) {
await super.scanBlocks(address, fromBlock, toBlock)
await this.scanMediatorEvents(address, fromBlock, toBlock)
}
async getTx(txHash) {
const historyEvents = this.core.getHistoryEvents({ asset: this.id, account: this.mediatorAddress })
const eventMatch = historyEvents.filter(e => e.tx === txHash)
if (eventMatch.length > 0) {
return eventMatch[0]
} else {
return super.getTx(txHash)
}
}
async scanMediatorEvents(address, fromBlock, toBlock) {
if (this.mediatorAddress != '') {
const web3 = this.getWeb3()
const contract: Contract = new web3.eth.Contract(MEDIATOR_ABI, this.mediatorAddress)
const events: EventData[] = await contract.getPastEvents('TokensBridged', {
fromBlock,
toBlock,
filter: {
recipient: address
}
})
for (const event of events) {
this.core.addHistoryEvent({
id: `${event.transactionHash}-${event.logIndex}`,
asset: this.id,
type: 'send',
value: event.returnValues.value.toString(),
from: this.mediatorAddress,
to: event.returnValues.recipient,
tx: event.transactionHash,
timestamp: await this._getBlockTimestamp(event.blockNumber)
})
}
}
}
setMediatorAddress(mediatorAddress) {
this.mediatorAddress = mediatorAddress
}
}

View File

@@ -0,0 +1,8 @@
import { default as ERC677Asset } from './ERC677Asset'
export default new ERC677Asset({
id: 'wetc',
name: 'WETC',
network: '1',
address: '0x86aabcc646f290b9fc9bd05ce17c3858d1511da1'
})

View File

@@ -0,0 +1,7 @@
import NativeMediatorAsset from './NativeMediatorAsset'
export default new NativeMediatorAsset({
id: 'spoa',
name: 'sPOA',
network: '77'
})

View File

@@ -0,0 +1,54 @@
import { Gateway } from '@burner-wallet/core/gateways'
import Web3 from 'web3'
export default class TokenBridgeGateway extends Gateway {
private readonly providers: object
private readonly providerStrings: { [id: string]: string }
constructor() {
super()
this.providerStrings = {
'61': `https://www.ethercluster.com/etc`,
'77': 'https://sokol.poa.network',
'99': 'https://core.poa.network'
}
this.providers = {}
}
isAvailable() {
return true
}
getNetworks() {
return ['61', '77', '99']
}
_provider(network) {
if (!this.providers[network]) {
this._makeProvider(network)
}
return this.providers[network]
}
_makeProvider(network) {
if (!this.providerStrings[network]) {
throw new Error(`Network ${network} not supported by TokenBridgeGateway`)
}
this.providers[network] = new Web3.providers.HttpProvider(this.providerStrings[network])
}
send(network, payload) {
return new Promise((resolve, reject) => {
if (this.getNetworks().indexOf(network) === -1) {
return reject(new Error('TokenBridgeGateway does not support this network'))
}
this._provider(network).send(payload, (err, response) => {
if (err) {
return reject(err)
}
return resolve(response.result)
})
})
}
}

View File

@@ -0,0 +1,7 @@
export { default as sPOA } from './assets/sPOA'
export { default as Etc } from './assets/Etc'
export { default as Wetc } from './assets/Wetc'
export { default as ERC677Asset } from './assets/ERC677Asset'
export { default as NativeMediatorAsset } from './assets/NativeMediatorAsset'
export { default as TokenBridgeGateway } from './gateways/TokenBridgeGateway'
export { default as Mediator } from './pairs/Mediator'

View File

@@ -0,0 +1,107 @@
import BN from 'bn.js'
import { Bridge, EstimateReturn, ValueTypes } from '@burner-wallet/exchange'
import { waitForEvent, constants } from '../../utils'
import { MEDIATOR_ABI, MEDIATOR_FEE_MANAGER_ABI } from '../../utils'
import { fromWei, toBN } from 'web3-utils'
interface MediatorConstructor {
assetA: string
assetABridge: string
assetB: string
assetBBridge: string
}
export default class Mediator extends Bridge {
constructor({ assetA, assetABridge, assetB, assetBBridge }: MediatorConstructor) {
super({ assetA, assetABridge, assetB, assetBBridge })
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async detectExchangeBToAFinished(account, value, sendResult) {
const asset = this.getExchange().getAsset(this.assetA)
const web3 = asset.getWeb3()
const contract = new web3.eth.Contract(MEDIATOR_ABI, this.assetABridge)
await waitForEvent(web3, contract, 'TokensBridged', this.processMediatorEvents(account))
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async detectExchangeAToBFinished(account, value, sendResult) {
const web3 = this.getExchange()
.getAsset(this.assetB)
.getWeb3()
const contract = new web3.eth.Contract(MEDIATOR_ABI, this.assetBBridge)
await waitForEvent(web3, contract, 'TokensBridged', this.processMediatorEvents(account))
}
processMediatorEvents(account) {
return events => {
const confirmationEvent = events.filter(
event => event.returnValues.recipient.toLowerCase() === account.toLowerCase()
)
return confirmationEvent.length > 0
}
}
async estimateAtoB(value: ValueTypes): Promise<EstimateReturn> {
const web3 = this.getExchange()
.getAsset(this.assetB)
.getWeb3()
const userAmount = this._getValue(value)
const contract = new web3.eth.Contract(MEDIATOR_ABI, this.assetBBridge)
const { feeAmount, feePercentage } = await this.getFee(web3, contract, userAmount)
const finalAmount = toBN(userAmount).sub(feeAmount)
const estimateInfo = feeAmount.isZero() ? null : `${constants.ESTIMATE_FEE_MESSAGE} Fee: ${feePercentage}%`
return {
estimate: finalAmount.toString(),
estimateInfo
}
}
async estimateBtoA(value: ValueTypes): Promise<EstimateReturn> {
const web3 = this.getExchange()
.getAsset(this.assetA)
.getWeb3()
const userAmount = this._getValue(value)
const contract = new web3.eth.Contract(MEDIATOR_ABI, this.assetABridge)
const { feeAmount, feePercentage } = await this.getFee(web3, contract, userAmount)
const finalAmount = toBN(userAmount).sub(feeAmount)
const estimateInfo = feeAmount.isZero() ? null : `${constants.ESTIMATE_FEE_MESSAGE} Fee: ${feePercentage}%`
return {
estimate: finalAmount.toString(),
estimateInfo
}
}
async getFee(web3, contract, value): Promise<{ feeAmount: BN; feePercentage: number }> {
const feeManagerAddress = await this.getFeeManagerContract(contract)
if (feeManagerAddress != constants.ZERO_ADDRESS) {
const feeManagerContract = new web3.eth.Contract(MEDIATOR_FEE_MANAGER_ABI, feeManagerAddress)
const fee = toBN(await feeManagerContract.methods.fee().call())
const feePercentage = Number(fromWei(fee, 'ether')) * 100
const feeAmount = toBN(await feeManagerContract.methods.calculateFee(value).call())
return {
feeAmount,
feePercentage
}
} else {
return {
feeAmount: toBN(0),
feePercentage: 0
}
}
}
async getFeeManagerContract(contract) {
try {
return await contract.methods.feeManagerContract().call()
} catch (e) {
return constants.ZERO_ADDRESS
}
}
}

View File

@@ -0,0 +1,2 @@
export { ERC677Asset, NativeMediatorAsset, sPOA, Etc, Wetc, TokenBridgeGateway, Mediator } from './burner-wallet'
export { WETCBridge } from './wetc-bridge'

View File

@@ -0,0 +1,5 @@
export { default as ERC677_ABI } from './abis/ERC677'
export { default as FOREIGN_NATIVE_TO_ERC_ABI } from './abis/ForeignBridgeNativeToErc'
export { default as HOME_NATIVE_TO_ERC_ABI } from './abis/HomeBridgeNativeToErc'
export { default as MEDIATOR_ABI } from './abis/Mediator'
export { default as MEDIATOR_FEE_MANAGER_ABI } from './abis/MediatorFeeManager'

View File

@@ -0,0 +1,275 @@
export default [
{
constant: false,
inputs: [
{
name: '_spender',
type: 'address'
},
{
name: '_value',
type: 'uint256'
}
],
name: 'approve',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'totalSupply',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_from',
type: 'address'
},
{
name: '_to',
type: 'address'
},
{
name: '_value',
type: 'uint256'
}
],
name: 'transferFrom',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_who',
type: 'address'
}
],
name: 'balanceOf',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_to',
type: 'address'
},
{
name: '_value',
type: 'uint256'
}
],
name: 'transfer',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_owner',
type: 'address'
},
{
name: '_spender',
type: 'address'
}
],
name: 'allowance',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'from',
type: 'address'
},
{
indexed: true,
name: 'to',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
},
{
indexed: false,
name: 'data',
type: 'bytes'
}
],
name: 'Transfer',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'owner',
type: 'address'
},
{
indexed: true,
name: 'spender',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
}
],
name: 'Approval',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'from',
type: 'address'
},
{
indexed: true,
name: 'to',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
}
],
name: 'Transfer',
type: 'event'
},
{
constant: false,
inputs: [
{
name: '',
type: 'address'
},
{
name: '',
type: 'uint256'
},
{
name: '',
type: 'bytes'
}
],
name: 'transferAndCall',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'spender',
type: 'address'
},
{
name: 'addedValue',
type: 'uint256'
}
],
name: 'increaseAllowance',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'spender',
type: 'address'
},
{
name: 'subtractedValue',
type: 'uint256'
}
],
name: 'decreaseAllowance',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
}
]

View File

@@ -0,0 +1,49 @@
export default [
{
constant: true,
inputs: [
{
name: '_txHash',
type: 'bytes32'
}
],
name: 'relayedMessages',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'deployedAtBlock',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getHomeFee',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
}
]

View File

@@ -0,0 +1,52 @@
export default [
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'recipient',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
},
{
indexed: false,
name: 'transactionHash',
type: 'bytes32'
}
],
name: 'AffirmationCompleted',
type: 'event'
},
{
constant: true,
inputs: [],
name: 'deployedAtBlock',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getForeignFee',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
}
]

View File

@@ -0,0 +1,38 @@
export default [
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'recipient',
type: 'address'
},
{
indexed: false,
name: 'value',
type: 'uint256'
},
{
indexed: true,
name: 'messageId',
type: 'bytes32'
}
],
name: 'TokensBridged',
type: 'event'
},
{
constant: true,
inputs: [],
name: 'feeManagerContract',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
}
]

View File

@@ -0,0 +1,35 @@
export default [
{
constant: true,
inputs: [
{
name: '',
type: 'uint256'
}
],
name: 'calculateFee',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'fee',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
}
]

View File

@@ -0,0 +1,8 @@
export { constants, wait, waitForEvent, isBridgeContract } from './utils'
export {
ERC677_ABI,
FOREIGN_NATIVE_TO_ERC_ABI,
HOME_NATIVE_TO_ERC_ABI,
MEDIATOR_ABI,
MEDIATOR_FEE_MANAGER_ABI
} from './abis'

View File

@@ -0,0 +1,40 @@
import { EventData, Contract } from 'web3-eth-contract'
import { toWei } from 'web3-utils'
export const wait = (time: number) => new Promise(resolve => setTimeout(resolve, time))
export const constants = {
EXCHANGE_TIMEOUT: 300000,
MAX_FEE: toWei('1', 'ether'),
ESTIMATE_FEE_MESSAGE: 'Estimation takes fee charges into consideration.',
ZERO_ADDRESS: '0x0000000000000000000000000000000000000000'
}
export const waitForEvent = async (web3, contract: Contract, event: string, callback: Function) => {
let fromBlock = await web3.eth.getBlockNumber()
const stopTime = Date.now() + constants.EXCHANGE_TIMEOUT
while (Date.now() <= stopTime) {
const currentBlock = await web3.eth.getBlockNumber()
const events: EventData[] = await contract.getPastEvents(event, {
fromBlock,
toBlock: currentBlock
})
const eventFound = await callback(events)
if (eventFound) {
return
}
fromBlock = currentBlock
await wait(10000)
}
}
export const isBridgeContract = async (contract: Contract): Promise<boolean> => {
try {
await contract.methods.deployedAtBlock().call()
return true
} catch (e) {
return false
}
}

View File

@@ -0,0 +1,100 @@
import { Mediator } from '../burner-wallet'
import { HOME_NATIVE_TO_ERC_ABI, FOREIGN_NATIVE_TO_ERC_ABI } from '../utils'
import { waitForEvent, isBridgeContract, constants } from '../utils'
import { ValueTypes } from '@burner-wallet/exchange'
import { toBN, fromWei } from 'web3-utils'
export default class WETCBridge extends Mediator {
constructor() {
super({
assetA: 'etc',
assetABridge: '0x073081832B4Ecdce79d4D6753565c85Ba4b3BeA9',
assetB: 'wetc',
assetBBridge: '0x0cB781EE62F815bdD9CD4c2210aE8600d43e7040'
})
}
async detectExchangeBToAFinished(account, value, sendResult) {
const web3 = this.getExchange()
.getAsset(this.assetA)
.getWeb3()
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.assetABridge)
const listenToBridgeEvent = await isBridgeContract(contract)
if (listenToBridgeEvent) {
await waitForEvent(web3, contract, 'AffirmationCompleted', this.processBridgeEvents(sendResult.txHash))
} else {
await super.detectExchangeBToAFinished(account, value, sendResult)
}
}
async detectExchangeAToBFinished(account, value, sendResult) {
const web3 = this.getExchange()
.getAsset(this.assetB)
.getWeb3()
const contract = new web3.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, this.assetBBridge)
const listenToBridgeEvent = await isBridgeContract(contract)
if (listenToBridgeEvent) {
await waitForEvent(web3, contract, 'RelayedMessage', this.processBridgeEvents(sendResult.txHash))
} else {
await super.detectExchangeAToBFinished(account, value, sendResult)
}
}
processBridgeEvents(txHash) {
return events => {
const confirmationEvent = events.filter(event => event.returnValues.transactionHash === txHash)
return confirmationEvent.length > 0
}
}
async estimateAtoB(value: ValueTypes) {
const web3 = this.getExchange()
.getAsset(this.assetB)
.getWeb3()
const contract = new web3.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, this.assetBBridge)
const useBridgeContract = await isBridgeContract(contract)
if (useBridgeContract) {
const fee = toBN(await contract.methods.getHomeFee().call())
const feeAmount = toBN(this._getValue(value))
.mul(fee)
.div(toBN(constants.MAX_FEE))
const finalAmount = toBN(this._getValue(value)).sub(feeAmount)
const feePercentage = Number(fromWei(fee, 'ether')) * 100
const estimateInfo = feeAmount.isZero() ? null : `${constants.ESTIMATE_FEE_MESSAGE} Fee: ${feePercentage}%`
return {
estimate: finalAmount.toString(),
estimateInfo
}
} else {
return await super.estimateAtoB(value)
}
}
async estimateBtoA(value: ValueTypes) {
const web3 = this.getExchange()
.getAsset(this.assetA)
.getWeb3()
const contract = new web3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, this.assetABridge)
const useBridgeContract = await isBridgeContract(contract)
if (useBridgeContract) {
const fee = toBN(await contract.methods.getForeignFee().call())
const feeAmount = toBN(this._getValue(value))
.mul(fee)
.div(toBN(constants.MAX_FEE))
const finalAmount = toBN(this._getValue(value)).sub(feeAmount)
const feePercentage = Number(fromWei(fee, 'ether')) * 100
const estimateInfo = feeAmount.isZero() ? null : `${constants.ESTIMATE_FEE_MESSAGE} Fee: ${feePercentage}%`
return {
estimate: finalAmount.toString(),
estimateInfo
}
} else {
return await super.estimateBtoA(value)
}
}
}

View File

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

View File

@@ -0,0 +1,112 @@
import { expect } from 'chai'
import 'mocha'
import ERC677Asset from '../src/burner-wallet/assets/ERC677Asset'
const ACCOUNT1 = '0x1010101010101010101010101010101010101010'
const ACCOUNT2 = '0x0000000000000000000000000000000000000001'
const TX_HASH = '0x376565f5614bd4483fd716c441aff43446b50f7772bef75496edef7faa070a85'
const ONE_ETH = '1000000000000000000'
const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
describe('ERC677Asset', () => {
it('should add an event when sent', done => {
const asset = new ERC677Asset({
id: 'test',
name: 'Test',
network: '5777',
address: '0xcbfaa26289d24a6b4c5fe562bdd9a1b623260359'
})
const testConditions = event => {
expect(event.asset).to.equal('test')
expect(event.type).to.equal('send')
expect(event.value).to.equal(ONE_ETH)
expect(event.from).to.equal(ACCOUNT2)
expect(event.to).to.equal(ACCOUNT1)
expect(event.id).to.equal(`${TX_HASH}-0`)
expect(event.tx).to.equal(TX_HASH)
done()
}
const burnerCore = {
addHistoryEvent: testConditions,
getWeb3: () => ({
eth: {
methods: {},
Contract: function Contract() {
this.methods = {
transferAndCall() {
return {
send() {
return {
transactionHash: TX_HASH,
events: {
Transfer: {
logIndex: 0
}
}
}
}
}
}
}
}
}
})
}
asset.setCore(burnerCore)
asset.send({ to: ACCOUNT1, from: ACCOUNT2, value: ONE_ETH })
})
it('should watch an address and add events for new transactions', done => {
const asset = new ERC677Asset({
id: 'test',
name: 'Test',
network: '5777',
address: '0xcbfaa26289d24a6b4c5fe562bdd9a1b623260359'
})
const testConditions = event => {
expect(event.asset).to.equal('test')
expect(event.type).to.equal('send')
expect(event.value).to.equal(ONE_ETH)
expect(event.from).to.equal(ACCOUNT2)
expect(event.to).to.equal(ACCOUNT1)
expect(event.tx).to.equal(TX_HASH)
expect(event.timestamp).to.equal(1571234034)
done()
}
const burnerCore = {
addHistoryEvent: testConditions,
getWeb3: () => ({
eth: {
getBlockNumber: () => 100,
getBlock: () => ({ timestamp: 1571234034 }),
Contract: function Contract() {
// @ts-ignore
this.getPastEvents = (eventName, { topics }) => {
expect(eventName).to.equal('allEvents')
expect(topics[0]).to.equal(TRANSFER_TOPIC)
return [
{
event: 'Transfer',
returnValues: {
to: ACCOUNT1,
from: ACCOUNT2,
value: ONE_ETH
},
transactionHash: TX_HASH
}
]
}
}
}
})
}
asset.setCore(burnerCore)
const unsubscribe = asset.startWatchingAddress(ACCOUNT1)
unsubscribe()
})
})

View File

@@ -0,0 +1,10 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "./dist"
},
"include": [
"./src"
]
}

View File

@@ -0,0 +1,6 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs"
}
}

View File

@@ -0,0 +1,27 @@
{
"compilerOptions": {
"module": "esnext",
"declaration": true,
"removeComments": true,
"noLib": false,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"esModuleInterop": true,
"jsx": "react",
"target": "esnext",
"sourceMap": true,
"strict": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noImplicitAny": false,
"lib": [
"es6",
"dom"
]
},
"exclude": [
"node_modules",
"**/*.spec.ts"
]
}

15931
burner-wallet-plugin/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@ In `group_vars/xdai.yml`
---
MONITOR_BRIDGE_NAME: "xdai"
MONITOR_PORT: 3003
MONITOR_CACHE_EVENTS: "true"
COMMON_HOME_RPC_URL: "https://dai.poa.network"
COMMON_HOME_BRIDGE_ADDRESS: "0x7301CFA0e1756B71869E93d4e4Dca5c7d0eb0AA6"
@@ -64,6 +65,7 @@ In `group_vars/wetc.yml`
```
---
MONITOR_BRIDGE_NAME: "wetc"
MONITOR_CACHE_EVENTS: "true"
COMMON_HOME_RPC_URL: "https://ethereumclassic.network"
COMMON_HOME_BRIDGE_ADDRESS: "0x073081832B4Ecdce79d4D6753565c85Ba4b3BeA9"

View File

@@ -44,6 +44,7 @@ UI_FOREIGN_GAS_PRICE_UPDATE_INTERVAL: 600000
## Monitor
MONITOR_BRIDGE_NAME: "xdai"
MONITOR_PORT: 3003
MONITOR_CACHE_EVENTS: "true"
MONITOR_HOME_START_BLOCK: 759
MONITOR_FOREIGN_START_BLOCK: 6478417
MONITOR_VALIDATOR_HOME_TX_LIMIT: 300000

View File

@@ -46,6 +46,7 @@ UI_FOREIGN_GAS_PRICE_UPDATE_INTERVAL: 600000
## Monitor
MONITOR_BRIDGE_NAME: "bridge"
MONITOR_PORT: 3003
MONITOR_CACHE_EVENTS: "false"
MONITOR_HOME_START_BLOCK: 0
MONITOR_FOREIGN_START_BLOCK: 0
MONITOR_VALIDATOR_HOME_TX_LIMIT: 300000

View File

@@ -43,6 +43,7 @@ UI_FOREIGN_GAS_PRICE_UPDATE_INTERVAL: 600000
#monitor
MONITOR_BRIDGE_NAME: "bridge"
MONITOR_CACHE_EVENTS: "true"
MONITOR_HOME_START_BLOCK: 0
MONITOR_FOREIGN_START_BLOCK: 0
MONITOR_VALIDATOR_HOME_TX_LIMIT: 300000

View File

@@ -45,6 +45,7 @@ UI_FOREIGN_GAS_PRICE_UPDATE_INTERVAL: 600000
## Monitor
MONITOR_BRIDGE_NAME: "wetc"
MONITOR_PORT: 3003
MONITOR_CACHE_EVENTS: "true"
MONITOR_HOME_START_BLOCK: 7703292
MONITOR_FOREIGN_START_BLOCK: 7412459
MONITOR_VALIDATOR_HOME_TX_LIMIT: 300000

View File

@@ -32,6 +32,9 @@
group: "root"
mode: "0755"
- name: Upgrade pip version
shell: pip3 install --upgrade pip
- name: Install python docker library
shell: pip3 install docker docker-compose setuptools

View File

@@ -32,3 +32,5 @@ MONITOR_FOREIGN_START_BLOCK={{ MONITOR_FOREIGN_START_BLOCK }}
MONITOR_VALIDATOR_HOME_TX_LIMIT={{ MONITOR_VALIDATOR_HOME_TX_LIMIT }}
MONITOR_VALIDATOR_FOREIGN_TX_LIMIT={{ MONITOR_VALIDATOR_FOREIGN_TX_LIMIT }}
MONITOR_TX_NUMBER_THRESHOLD={{ MONITOR_TX_NUMBER_THRESHOLD }}
MONITOR_CACHE_EVENTS={{ MONITOR_CACHE_EVENTS }}

View File

@@ -17,3 +17,4 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR=1
MONITOR_TX_NUMBER_THRESHOLD=100
MONITOR_PORT=3013
MONITOR_BRIDGE_NAME=bridge
MONITOR_CACHE_EVENTS=false

View File

@@ -17,3 +17,4 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR=1
MONITOR_TX_NUMBER_THRESHOLD=100
MONITOR_PORT=3012
MONITOR_BRIDGE_NAME=bridge
MONITOR_CACHE_EVENTS=false

View File

@@ -17,3 +17,4 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR=1
MONITOR_TX_NUMBER_THRESHOLD=100
MONITOR_PORT=3011
MONITOR_BRIDGE_NAME=bridge
MONITOR_CACHE_EVENTS=false

View File

@@ -17,3 +17,4 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR=1
MONITOR_TX_NUMBER_THRESHOLD=100
MONITOR_PORT=3010
MONITOR_BRIDGE_NAME=bridge
MONITOR_CACHE_EVENTS=false

View File

@@ -52,6 +52,7 @@
"foreignToken": "0x7cc4b1851c35959d34e635a470f6b5c43ba3c9c9",
"halfDuplexToken": "0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359",
"saiTop": "0x9b0ccf7C8994E19F39b2B4CF708e0A7DF65fA8a3",
"chaiToken": "0x06af07097c9eeb7fd685c692751d5c66db49c215",
"ui": "http://localhost:3002",
"monitor": "http://monitor-erc20-native:3012/bridge"
},

View File

@@ -32,7 +32,7 @@ FOREIGN_GAS_PRICE=10000000000
FOREIGN_REWARDABLE=false
BLOCK_REWARD_ADDRESS=0xF9698Eb93702dfdd0e2d802088d4c21822a8A977
ERC20_TOKEN_ADDRESS=0x89d24A6b4CcB1B6fAA2625fE562bDD9a23260359
ERC20_TOKEN_ADDRESS=0x7cc4b1851c35959d34e635a470f6b5c43ba3c9c9
REQUIRED_NUMBER_OF_VALIDATORS=1
VALIDATORS="0xaaB52d66283F7A1D5978bcFcB55721ACB467384b 0xdCC784657C78054aa61FbcFFd2605F32374816A4 0xDcef88209a20D52165230104B245803C3269454d"

View File

@@ -97,8 +97,7 @@ services:
build:
context: ..
dockerfile: monitor/Dockerfile
args:
DOT_ENV_PATH: e2e-commons/components-envs/monitor.env
env_file: ../e2e-commons/components-envs/monitor.env
entrypoint: yarn check-and-start
ports:
- "3010:3010"
@@ -108,8 +107,7 @@ services:
build:
context: ..
dockerfile: monitor/Dockerfile
args:
DOT_ENV_PATH: e2e-commons/components-envs/monitor-erc20.env
env_file: ../e2e-commons/components-envs/monitor-erc20.env
entrypoint: yarn check-and-start
ports:
- "3011:3011"
@@ -119,8 +117,7 @@ services:
build:
context: ..
dockerfile: monitor/Dockerfile
args:
DOT_ENV_PATH: e2e-commons/components-envs/monitor-erc20-native.env
env_file: ../e2e-commons/components-envs/monitor-erc20-native.env
entrypoint: yarn check-and-start
ports:
- "3012:3012"
@@ -130,8 +127,7 @@ services:
build:
context: ..
dockerfile: monitor/Dockerfile
args:
DOT_ENV_PATH: e2e-commons/components-envs/monitor-amb.env
env_file: ../e2e-commons/components-envs/monitor-amb.env
entrypoint: yarn check-and-start
ports:
- "3013:3013"
@@ -144,3 +140,10 @@ services:
command: "true"
networks:
- ultimate
blocks:
build:
context: ..
dockerfile: Dockerfile.e2e
entrypoint: node e2e-commons/scripts/blocks.js
networks:
- ultimate

View File

@@ -1,8 +1,7 @@
const Web3 = require('web3')
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 homeWeb3 = new Web3(new Web3.providers.HttpProvider('http://parity1:8545'))
const foreignWeb3 = new Web3(new Web3.providers.HttpProvider('http://parity2:8545'))
const {user, blockGenerator} = require('../constants.json');
homeWeb3.eth.accounts.wallet.add(user.privateKey)
@@ -10,10 +9,24 @@ foreignWeb3.eth.accounts.wallet.add(user.privateKey)
homeWeb3.eth.accounts.wallet.add(blockGenerator.privateKey)
foreignWeb3.eth.accounts.wallet.add(blockGenerator.privateKey)
function generateNewBlock(web3, address) {
return web3.eth.sendTransaction({
from: address,
to: '0x0000000000000000000000000000000000000000',
gasPrice: '1',
gas: '21000',
value: '1'
})
}
function main() {
setTimeout(async () => {
generateNewBlock(homeWeb3, blockGenerator.address)
generateNewBlock(foreignWeb3, blockGenerator.address)
try {
generateNewBlock(homeWeb3, blockGenerator.address)
} catch {} // in case of Transaction with the same hash was already imported.
try {
generateNewBlock(foreignWeb3, blockGenerator.address)
} catch {} // in case of Transaction with the same hash was already imported.
main()
}, 1000)
}

View File

@@ -6,6 +6,9 @@ CONTRACTS_PATH="../../contracts"
DEPLOY_PATH="$CONTRACTS_PATH/deploy"
ENVS_PATH="../contracts-envs"
# mock bridge validators contract with the one with deterministic isValidatorDuty
mv "$CONTRACTS_PATH/build/contracts/BridgeValidatorsDeterministic.json" "$CONTRACTS_PATH/build/contracts/BridgeValidators.json"
echo -e "\n\n############ Deploying native-to-erc ############\n"
cp "$ENVS_PATH/native-to-erc.env" "$DEPLOY_PATH/.env"
cd "$DEPLOY_PATH"

View File

@@ -23,6 +23,7 @@ startValidator () {
docker-compose $1 run $2 $3 -d oracle-erc20-native yarn watcher:transfer
docker-compose $1 run $2 $3 -d oracle-erc20-native yarn watcher:half-duplex-transfer
docker-compose $1 run $2 $3 -d oracle-erc20-native yarn worker:swap-tokens
docker-compose $1 run $2 $3 -d oracle-erc20-native yarn worker:convert-to-chai
docker-compose $1 run $2 $3 -d oracle-amb yarn watcher:signature-request
docker-compose $1 run $2 $3 -d oracle-amb yarn watcher:collected-signatures
docker-compose $1 run $2 $3 -d oracle-amb yarn watcher:affirmation-request
@@ -47,6 +48,7 @@ while [ "$1" != "" ]; do
docker-compose run -d oracle-erc20-native yarn watcher:transfer
docker-compose run -d oracle-erc20-native yarn watcher:half-duplex-transfer
docker-compose run -d oracle-erc20-native yarn worker:swap-tokens
docker-compose run -d oracle-erc20-native yarn worker:convert-to-chai
docker-compose run -d oracle-amb yarn watcher:signature-request
docker-compose run -d oracle-amb yarn watcher:collected-signatures
docker-compose run -d oracle-amb yarn watcher:affirmation-request
@@ -81,7 +83,7 @@ while [ "$1" != "" ]; do
fi
if [ "$1" == "blocks" ]; then
node ./scripts/blocks.js &
docker-compose up -d blocks
fi
if [ "$1" == "monitor" ]; then

View File

@@ -1,13 +1,18 @@
function generateNewBlock(web3, address) {
return web3.eth.sendTransaction({
from: address,
to: '0x0000000000000000000000000000000000000000',
gasPrice: '1',
gas: '21000',
value: '1'
const promiseRetry = require('promise-retry')
async function uniformRetry(f) {
return promiseRetry(f, {
forever: true,
factor: 1,
minTimeout: 500
})
}
module.exports = {
generateNewBlock
async function sleep(timeout) {
return new Promise(res => setTimeout(res, timeout))
}
module.exports = {
uniformRetry,
sleep
}

View File

@@ -1,6 +1,6 @@
cd $(dirname $0)
../e2e-commons/up.sh deploy monitor
../e2e-commons/up.sh deploy blocks monitor
./wait-for-monitor.sh
nohup ./periodically-check-all.sh < /dev/null > /dev/null 2>&1 &

View File

@@ -1,7 +1,14 @@
const assert = require('assert')
const axios = require('axios')
const { ercToNativeBridge, user, foreignRPC, validator } = require('../../e2e-commons/constants.json')
const { waitUntil, sendTokens, addValidator } = require('../utils')
const {
waitUntil,
sendTokens,
addValidator,
initializeChaiToken,
convertDaiToChai,
setMinDaiTokenBalance
} = require('../utils')
const baseUrl = ercToNativeBridge.monitor
@@ -24,12 +31,14 @@ describe('ERC TO NATIVE with changing state of contracts', () => {
assert((await axios.get(`${baseUrl}/validators`)).data.validatorsMatch === true)
})
it('should change balanceDiff', async () => {
await sendTokens(foreignRPC.URL, user, ercToNativeBridge.halfDuplexToken, ercToNativeBridge.foreign)
it('should change balanceDiff', async function() {
this.timeout(60000)
await sendTokens(foreignRPC.URL, user, ercToNativeBridge.foreignToken, ercToNativeBridge.foreign)
await waitUntil(async () => {
;({ data } = await axios.get(`${baseUrl}`))
return data.balanceDiff !== 0
const { erc20Balance, investedErc20Balance } = data.foreign
return data.balanceDiff === 0.01 && erc20Balance === '0.01' && investedErc20Balance === undefined
})
})
@@ -40,4 +49,49 @@ describe('ERC TO NATIVE with changing state of contracts', () => {
return data.validatorsMatch === false
})
})
it('should consider chai token balance', async function() {
this.timeout(60000)
await initializeChaiToken(foreignRPC.URL, ercToNativeBridge.foreign)
await sendTokens(foreignRPC.URL, user, ercToNativeBridge.foreignToken, ercToNativeBridge.foreign)
await waitUntil(async () => {
;({ data } = await axios.get(`${baseUrl}`))
const { erc20Balance, investedErc20Balance, accumulatedInterest } = data.foreign
return (
data.balanceDiff === 0.02 &&
erc20Balance === '0.02' &&
investedErc20Balance === '0' &&
accumulatedInterest === '0.001' // value of dsrBalance() is initially defined in genesis block as 0.001
)
})
await setMinDaiTokenBalance(foreignRPC.URL, ercToNativeBridge.foreign, '0.01')
await convertDaiToChai(foreignRPC.URL, ercToNativeBridge.foreign)
await waitUntil(async () => {
;({ data } = await axios.get(`${baseUrl}`))
const { erc20Balance, investedErc20Balance, accumulatedInterest } = data.foreign
return (
data.balanceDiff === 0.02 &&
erc20Balance === '0.01' &&
investedErc20Balance === '0.01' &&
accumulatedInterest === '0.001'
)
})
await setMinDaiTokenBalance(foreignRPC.URL, ercToNativeBridge.foreign, '0.005')
await convertDaiToChai(foreignRPC.URL, ercToNativeBridge.foreign)
await waitUntil(async () => {
;({ data } = await axios.get(`${baseUrl}`))
const { erc20Balance, investedErc20Balance, accumulatedInterest } = data.foreign
return (
data.balanceDiff === 0.02 &&
erc20Balance === '0.005' &&
investedErc20Balance === '0.015' &&
accumulatedInterest === '0.001'
)
})
})
})

View File

@@ -1,5 +1,12 @@
const Web3 = require('web3')
const { ERC677_BRIDGE_TOKEN_ABI, BRIDGE_VALIDATORS_ABI, FOREIGN_NATIVE_TO_ERC_ABI, BOX_ABI } = require('../commons')
const {
ERC677_BRIDGE_TOKEN_ABI,
BRIDGE_VALIDATORS_ABI,
FOREIGN_NATIVE_TO_ERC_ABI,
FOREIGN_ERC_TO_NATIVE_ABI,
BOX_ABI
} = require('../commons')
const { validator } = require('../e2e-commons/constants')
const waitUntil = async (predicate, step = 100, timeout = 20000) => {
const stopTime = Date.now() + timeout
@@ -60,10 +67,46 @@ const addValidator = async (rpcUrl, account, bridgeAddress) => {
})
}
const initializeChaiToken = async (rpcUrl, bridgeAddress) => {
const web3 = new Web3(new Web3.providers.HttpProvider(rpcUrl))
web3.eth.accounts.wallet.add(validator.privateKey)
const bridgeContract = new web3.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, bridgeAddress)
await bridgeContract.methods.initializeChaiToken().send({
from: validator.address,
gas: '1000000'
})
}
const setMinDaiTokenBalance = async (rpcUrl, bridgeAddress, limit) => {
const web3 = new Web3(new Web3.providers.HttpProvider(rpcUrl))
web3.eth.accounts.wallet.add(validator.privateKey)
const bridgeContract = new web3.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, bridgeAddress)
await bridgeContract.methods.setMinDaiTokenBalance(web3.utils.toWei(limit)).send({
from: validator.address,
gas: '1000000'
})
}
const convertDaiToChai = async (rpcUrl, bridgeAddress) => {
const web3 = new Web3(new Web3.providers.HttpProvider(rpcUrl))
web3.eth.accounts.wallet.add(validator.privateKey)
const bridgeContract = new web3.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, bridgeAddress)
await bridgeContract.methods.convertDaiToChai().send({
from: validator.address,
gas: '1000000'
})
}
module.exports = {
waitUntil,
sendEther,
sendTokens,
addValidator,
sendAMBMessage
sendAMBMessage,
initializeChaiToken,
setMinDaiTokenBalance,
convertDaiToChai
}

View File

@@ -21,3 +21,4 @@ COMMON_FOREIGN_GAS_PRICE_FACTOR=1
MONITOR_TX_NUMBER_THRESHOLD=100
MONITOR_PORT=3003
MONITOR_CACHE_EVENTS=true

View File

@@ -15,9 +15,9 @@ RUN mv ./contracts/build ./ && rm -rf ./contracts/* ./contracts/.[!.]* && mv ./b
COPY ./commons ./commons
COPY ./monitor ./monitor
ARG DOT_ENV_PATH=./monitor/.env
COPY ${DOT_ENV_PATH} ./monitor/.env
WORKDIR /mono/monitor
CMD echo "To start the monitor run:" \
"yarn check-and-start"
CMD echo "To start the monitor web service run:" \
"yarn start" \
"To run monitor scripts run:" \
"yarn check-all"

View File

@@ -4,7 +4,12 @@ Tool for checking balances and unprocessed events in bridged networks.
## Overview
Please refer to the [POA TokenBridge](../README.md) overview first of all.
- Deployed version: https://bridge-monitoring.poa.net/
- Deployed version serves several monitor configurations:
* https://bridge-monitoring.poa.net/poa
* https://bridge-monitoring.poa.net/xdai
* https://bridge-monitoring.poa.net/wetc
* https://bridge-monitoring.poa.net/amb-dai
* https://bridge-monitoring.poa.net/amb-poa
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.
@@ -122,14 +127,19 @@ yarn start
You can run web interface via [pm2](https://www.npmjs.com/package/pm2) or similar supervisor program.
Using Docker:
```
docker-compose up -d
```
* to run it very first time (or after changes related to the monitor code):
```
docker-compose -f docker-compose-build.yml -f docker-compose.yml up -d --build
```
* next time (or in case of usage of an official docker image)
```
docker-compose up -d
```
- The application will run on `http://localhost:MONITOR_PORT/MONITOR_BRIDGE_NAME`, where `MONITOR_PORT` and `MONITOR_BRIDGE_NAME` are specified in your `.env` file.
- To enabled debug logging, set `DEBUG=1` variable in `.env`.
## Check balances of contracts and validators, get unprocessed events
## Preparing statistic about balances of bridge contracts and validators, get unprocessed events
Using Yarn:
```
@@ -138,13 +148,32 @@ yarn check-all
Using Docker:
```
docker-compose exec monitor yarn check-all
docker run --rm --env-file .env -v $(pwd)/responses:/mono/monitor/responses \
poanetwork/tokenbridge-monitor:latest /bin/bash -c 'yarn check-all'
```
As soon as the process finishes, use the URL described above to get the statistic.
### Cron
You can create cron job to run workers (see `crontab.example` for reference):
## Ad-hoc monitoring
There is a possibility to get bridge statistics without running the web interface use the commands provided above. In this case the results will be located in the `responses` directory.
## Build the image without running the monitor
To build the image change the directory:
```
cd monitor
```
And run the docker composer:
```
docker-compose -f docker-compose-build.yml build
```
## Linting
Running linter:

View File

@@ -2,4 +2,4 @@
*/4 * * * * cd $HOME/tokenbridge/monitor; yarn check-all >>cronWorker.out 2>>cronWorker.err
# Docker:
*/4 * * * * cd $HOME/tokenbridge/monitor; docker-compose exec monitor yarn check-all >>cronWorker.out 2>>cronWorker.err
*/4 * * * * cd $HOME/tokenbridge/monitor; docker run --rm --env-file .env -v $(pwd)/responses:/mono/monitor/responses poanetwork/tokenbridge-monitor:latest /bin/bash -c 'yarn check-all' >>cronWorker.out 2>>cronWorker.err

View File

@@ -0,0 +1,6 @@
---
version: '2.4'
services:
monitor:
image: poanetwork/tokenbridge-monitor
build: .

View File

@@ -3,6 +3,7 @@ const BN = require('bignumber.js')
const Web3 = require('web3')
const logger = require('./logger')('getBalances')
const { BRIDGE_MODES } = require('../commons')
const { blockNumberHalfDuplexDisabled } = require('./utils/tokenUtils')
const Web3Utils = Web3.utils
@@ -83,7 +84,10 @@ async function main(bridgeMode) {
const foreignBridge = new web3Foreign.eth.Contract(FOREIGN_ERC_TO_NATIVE_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
const erc20Address = await foreignBridge.methods.erc20token().call()
const erc20Contract = new web3Foreign.eth.Contract(ERC20_ABI, erc20Address)
let investedAmountInDai = 0
let bridgeDsrBalance = 0
let foreignHalfDuplexErc20Balance = 0
let displayChaiToken = false
let displayHalfDuplexToken = false
let tokenSwapAllowed = false
try {
@@ -91,7 +95,9 @@ async function main(bridgeMode) {
if (halfDuplexTokenAddress !== erc20Address) {
const halfDuplexToken = new web3Foreign.eth.Contract(ERC20_ABI, halfDuplexTokenAddress)
logger.debug('calling halfDuplexToken.methods.balanceOf')
foreignHalfDuplexErc20Balance = await halfDuplexToken.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
foreignHalfDuplexErc20Balance = await halfDuplexToken.methods
.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS)
.call(null, blockNumberHalfDuplexDisabled)
logger.debug('getting last block numbers')
const block = await web3Foreign.eth.getBlock('latest')
@@ -103,6 +109,21 @@ async function main(bridgeMode) {
logger.debug('Methods for half duplex token are not present')
}
try {
logger.debug('calling foreignBridge.methods.isChaiTokenEnabled')
if (await foreignBridge.methods.isChaiTokenEnabled().call()) {
displayChaiToken = true
logger.debug('calling foreignBridge.methods.investedAmountInDai')
investedAmountInDai = await foreignBridge.methods.investedAmountInDai().call()
logger.debug('calling foreignBridge.methods.dsrBalance')
bridgeDsrBalance = await foreignBridge.methods.dsrBalance().call()
} else {
logger.debug('Chai token is currently disabled')
}
} catch (e) {
logger.debug('Methods for chai token are not present')
}
logger.debug('calling erc20Contract.methods.balanceOf')
const foreignErc20Balance = await erc20Contract.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
@@ -119,11 +140,13 @@ async function main(bridgeMode) {
const burntCoinsBN = new BN(burntCoins)
const totalSupplyBN = mintedCoinsBN.minus(burntCoinsBN)
const foreignErc20BalanceBN = new BN(foreignErc20Balance)
const halfDuplexErc20BalanceBN =
displayHalfDuplexToken && tokenSwapAllowed ? new BN(foreignHalfDuplexErc20Balance) : new BN(0)
const investedAmountInDaiBN = new BN(investedAmountInDai)
const bridgeDsrBalanceBN = new BN(bridgeDsrBalance)
const halfDuplexErc20BalanceBN = displayHalfDuplexToken ? new BN(foreignHalfDuplexErc20Balance) : new BN(0)
const diff = foreignErc20BalanceBN
.plus(halfDuplexErc20BalanceBN)
.plus(investedAmountInDaiBN)
.minus(totalSupplyBN)
.toFixed()
@@ -143,6 +166,11 @@ async function main(bridgeMode) {
}
}
if (displayChaiToken) {
foreign.investedErc20Balance = Web3Utils.fromWei(investedAmountInDai)
foreign.accumulatedInterest = Web3Utils.fromWei(bridgeDsrBalanceBN.minus(investedAmountInDaiBN).toString(10))
}
logger.debug('Done')
return {
home: {

View File

@@ -16,12 +16,14 @@ const {
} = require('../../commons')
const { normalizeEventInformation } = require('./message')
const { filterTransferBeforeES } = require('./tokenUtils')
const { writeFile, readCacheFile } = require('./file')
const {
COMMON_HOME_RPC_URL,
COMMON_FOREIGN_RPC_URL,
COMMON_HOME_BRIDGE_ADDRESS,
COMMON_FOREIGN_BRIDGE_ADDRESS
COMMON_FOREIGN_BRIDGE_ADDRESS,
MONITOR_CACHE_EVENTS
} = process.env
const MONITOR_HOME_START_BLOCK = toBN(Number(process.env.MONITOR_HOME_START_BLOCK) || 0)
const MONITOR_FOREIGN_START_BLOCK = toBN(Number(process.env.MONITOR_FOREIGN_START_BLOCK) || 0)
@@ -34,7 +36,17 @@ const web3Foreign = new Web3(foreignProvider)
const { getBlockNumber } = require('./contract')
const cacheFilePath = '/tmp/cachedEvents.json'
async function main(mode) {
if (MONITOR_CACHE_EVENTS === 'true') {
logger.debug('checking existing events cache')
const cachedEvents = readCacheFile(cacheFilePath)
if (cachedEvents !== false) {
logger.debug('returning events stored in cache')
return cachedEvents
}
}
const homeErcBridge = new web3Home.eth.Contract(HOME_ERC_TO_ERC_ABI, COMMON_HOME_BRIDGE_ADDRESS)
const bridgeMode = mode || (await getBridgeMode(homeErcBridge))
const { HOME_ABI, FOREIGN_ABI } = getBridgeABIs(bridgeMode)
@@ -112,7 +124,26 @@ async function main(mode) {
const bridgeTokensSwappedEvents = tokensSwappedEvents.filter(e => e.address === COMMON_FOREIGN_BRIDGE_ADDRESS)
// Get transfer events for each previous erc20
const uniqueTokenAddresses = [...new Set(bridgeTokensSwappedEvents.map(e => e.returnValues.from))]
const uniqueTokenAddressesSet = new Set(bridgeTokensSwappedEvents.map(e => e.returnValues.from))
// Exclude chai token from previous erc20
try {
logger.debug('calling foreignBridge.chaiToken()')
const chaiToken = await foreignBridge.methods.chaiToken().call()
uniqueTokenAddressesSet.delete(chaiToken)
} catch (e) {
logger.debug('call to foreignBridge.chaiToken() failed')
}
// Exclude dai token from previous erc20
try {
logger.debug('calling foreignBridge.erc20token()')
const daiToken = await foreignBridge.methods.erc20token().call()
uniqueTokenAddressesSet.delete(daiToken)
} catch (e) {
logger.debug('call to foreignBridge.erc20token() failed')
}
const uniqueTokenAddresses = [...uniqueTokenAddressesSet]
await Promise.all(
uniqueTokenAddresses.map(async tokenAddress => {
const halfDuplexTokenContract = new web3Foreign.eth.Contract(ERC20_ABI, tokenAddress)
@@ -127,11 +158,7 @@ async function main(mode) {
})).map(normalizeEvent)
// Remove events after the ES
const validHalfDuplexTransfers = await filterTransferBeforeES(
halfDuplexTransferEvents,
web3Foreign,
foreignBridge
)
const validHalfDuplexTransfers = await filterTransferBeforeES(halfDuplexTransferEvents)
transferEvents = [...validHalfDuplexTransfers, ...transferEvents]
})
@@ -155,7 +182,7 @@ async function main(mode) {
}
logger.debug('Done')
return {
const result = {
homeToForeignRequests,
homeToForeignConfirmations,
foreignToHomeConfirmations,
@@ -163,6 +190,12 @@ async function main(mode) {
isExternalErc20,
bridgeMode
}
if (MONITOR_CACHE_EVENTS === 'true') {
logger.debug('saving obtained events into cache file')
writeFile(cacheFilePath, result, false)
}
return result
}
module.exports = main

View File

@@ -15,8 +15,9 @@ async function readFile(filePath) {
}
}
function writeFile(filePath, object) {
fs.writeFileSync(path.join(process.cwd(), filePath), JSON.stringify(object, null, 4))
function writeFile(filePath, object, useCwd = true) {
const fullPath = useCwd ? path.join(process.cwd(), filePath) : filePath
fs.writeFileSync(fullPath, JSON.stringify(object, null, 4))
}
function createDir(dirPath) {
@@ -29,8 +30,17 @@ function createDir(dirPath) {
}
}
function readCacheFile(filePath) {
try {
return JSON.parse(fs.readFileSync(filePath))
} catch (_) {
return false
}
}
module.exports = {
readFile,
writeFile,
createDir
createDir,
readCacheFile
}

View File

@@ -1,32 +1,18 @@
let beforeESBiggestBlockNumber = 0
// https://etherscan.io/tx/0xd0c3c92c94e05bc71256055ce8c4c993e047f04e04f3283a04e4cb077b71f6c6
const blockNumberHalfDuplexDisabled = 9884448
/**
*
* Returns true if the event was before the Emergency Shutdown.
* The method has an optimization to avoid making request if a bigger block number is confirmed
* to be before the ES. Events should be iterated from newer to older order to use the optimization.
* Returns true if the event was before the bridge stopped supporting half duplex transfers.
*/
async function transferBeforeES(event, web3Foreign, foreignBridge) {
const { blockNumber } = event
if (blockNumber < beforeESBiggestBlockNumber) {
return true
}
const block = await web3Foreign.eth.getBlock(blockNumber)
const tokenSwapAllowed = await foreignBridge.methods.isTokenSwapAllowed(block.timestamp).call()
if (tokenSwapAllowed) {
beforeESBiggestBlockNumber = blockNumber
}
return tokenSwapAllowed
async function transferBeforeES(event) {
return event.blockNumber < blockNumberHalfDuplexDisabled
}
async function filterTransferBeforeES(array, web3Foreign, foreignBridge) {
async function filterTransferBeforeES(array) {
const newArray = []
// Iterate events from newer to older
for (let i = array.length - 1; i >= 0; i--) {
const beforeES = await transferBeforeES(array[i], web3Foreign, foreignBridge)
const beforeES = await transferBeforeES(array[i])
if (beforeES) {
// add element to first position so the new array will have the same order
newArray.unshift(array[i])
@@ -36,5 +22,6 @@ async function filterTransferBeforeES(array, web3Foreign, foreignBridge) {
}
module.exports = {
filterTransferBeforeES
filterTransferBeforeES,
blockNumberHalfDuplexDisabled
}

View File

@@ -1,6 +1,6 @@
cd $(dirname $0)
../e2e-commons/up.sh deploy oracle oracle-validator-2 oracle-validator-3
../e2e-commons/up.sh deploy blocks oracle oracle-validator-2 oracle-validator-3
docker-compose -f ../e2e-commons/docker-compose.yml run e2e yarn workspace oracle-e2e run start
rc=$?

View File

@@ -1,8 +1,7 @@
const Web3 = require('web3')
const assert = require('assert')
const promiseRetry = require('promise-retry')
const { user, homeRPC, foreignRPC, amb, validator } = require('../../e2e-commons/constants.json')
const { generateNewBlock } = require('../../e2e-commons/utils')
const { uniformRetry } = require('../../e2e-commons/utils')
const { BOX_ABI, HOME_AMB_ABI, FOREIGN_AMB_ABI } = require('../../commons')
const { setRequiredSignatures } = require('./utils')
@@ -59,7 +58,7 @@ describe('arbitrary message bridging', () => {
const initialValue = await foreignBox.methods.value().call()
assert(!toBN(initialValue).eq(toBN(newValue)), 'initial value should be different from new value')
const setValueTx = await homeBox.methods
await homeBox.methods
.setValueOnOtherNetwork(newValue, amb.home, amb.foreignBox)
.send({
from: user.address,
@@ -69,31 +68,8 @@ describe('arbitrary message bridging', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(homeWeb3, user.address)
// The bridge should create a new transaction with a CollectedSignatures
// event so we generate another trivial transaction
await promiseRetry(
async retry => {
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
if (lastBlockNumber >= setValueTx.blockNumber + 2) {
await generateNewBlock(homeWeb3, user.address)
} else {
retry()
}
},
{
forever: true,
factor: 1,
minTimeout: 500
}
)
// check that value changed and balance decreased
await promiseRetry(async retry => {
await generateNewBlock(homeWeb3, user.address)
await uniformRetry(async retry => {
const value = await foreignBox.methods.value().call()
if (!toBN(value).eq(toBN(newValue))) {
retry()
@@ -120,12 +96,8 @@ describe('arbitrary message bridging', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that value changed and balance decreased
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const value = await homeBox.methods.value().call()
if (!toBN(value).eq(toBN(newValue))) {
retry()

View File

@@ -1,9 +1,8 @@
const Web3 = require('web3')
const assert = require('assert')
const promiseRetry = require('promise-retry')
const { user, secondUser, ercToErcBridge, homeRPC, foreignRPC, validator } = require('../../e2e-commons/constants.json')
const { ERC677_BRIDGE_TOKEN_ABI, FOREIGN_ERC_TO_NATIVE_ABI, HOME_ERC_TO_ERC_ABI } = require('../../commons')
const { generateNewBlock } = require('../../e2e-commons/utils')
const { uniformRetry } = require('../../e2e-commons/utils')
const { setRequiredSignatures } = require('./utils')
const homeWeb3 = new Web3(new Web3.providers.HttpProvider(homeRPC.URL))
@@ -76,12 +75,8 @@ describe('erc to erc', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = await erc677Token.methods.balanceOf(user.address).call()
const recipientBalance = await erc677Token.methods.balanceOf(secondUser.address).call()
assert(toBN(balance).isZero(), 'User balance should be the same')
@@ -103,12 +98,8 @@ describe('erc to erc', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = await erc677Token.methods.balanceOf(user.address).call()
if (toBN(balance).isZero()) {
retry()
@@ -125,7 +116,7 @@ describe('erc to erc', () => {
assert(!toBN(balance).isZero(), 'Account should have tokens')
// send transaction to home bridge
const depositTx = await erc677Token.methods
await erc677Token.methods
.transferAndCall(COMMON_HOME_BRIDGE_ADDRESS, homeWeb3.utils.toWei('0.01'), '0x')
.send({
from: user.address,
@@ -135,31 +126,8 @@ describe('erc to erc', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(homeWeb3, user.address)
// The bridge should create a new transaction with a CollectedSignatures
// event so we generate another trivial transaction
await promiseRetry(
async retry => {
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
if (lastBlockNumber >= depositTx.blockNumber + 2) {
await generateNewBlock(homeWeb3, user.address)
} else {
retry()
}
},
{
forever: true,
factor: 1,
minTimeout: 500
}
)
// check that balance increases
await promiseRetry(async retry => {
await generateNewBlock(homeWeb3, user.address)
await uniformRetry(async retry => {
const balance = await erc20Token.methods.balanceOf(user.address).call()
if (toBN(balance).lte(toBN(originalBalance))) {
retry()

View File

@@ -9,8 +9,8 @@ const {
homeRPC,
foreignRPC
} = require('../../e2e-commons/constants.json')
const { ERC677_BRIDGE_TOKEN_ABI, FOREIGN_ERC_TO_NATIVE_ABI, SAI_TOP, HOME_ERC_TO_NATIVE_ABI } = require('../../commons')
const { generateNewBlock } = require('../../e2e-commons/utils')
const { ERC677_BRIDGE_TOKEN_ABI, FOREIGN_ERC_TO_NATIVE_ABI, HOME_ERC_TO_NATIVE_ABI } = require('../../commons')
const { uniformRetry } = require('../../e2e-commons/utils')
const { setRequiredSignatures } = require('./utils')
const homeWeb3 = new Web3(new Web3.providers.HttpProvider(homeRPC.URL))
@@ -59,92 +59,11 @@ describe('erc to native', () => {
}
})
})
it('should continue working after migration', async () => {
it('should not convert half duplex tokens to native tokens in home', async () => {
const originalBalanceOnHome = await homeWeb3.eth.getBalance(user.address)
const transferValue = homeWeb3.utils.toWei('0.01')
// erc20 token address and half duplex address are the same before migration
const tokenAddress = await foreignBridge.methods.erc20token().call()
const erc20AndhalfDuplexToken = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, tokenAddress)
// send tokens to foreign bridge
await erc20AndhalfDuplexToken.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, transferValue)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async (retry, number) => {
const balance = await homeWeb3.eth.getBalance(user.address)
await generateNewBlock(foreignWeb3, user.address)
// retry at least 4 times to check transfer is not double processed by the two watchers
if (toBN(balance).lte(toBN(originalBalanceOnHome)) || number < 4) {
retry()
} else {
assert(
toBN(balance).eq(toBN(originalBalanceOnHome).add(toBN(transferValue))),
'User balance should be increased only by second transfer'
)
}
})
// call migration
await foreignBridge.methods.migrateToMCD().send({
from: validator.address,
gas: '4000000'
})
// update min threshold for swap
await foreignBridge.methods.setMinHDTokenBalance(foreignWeb3.utils.toWei('2', 'ether')).send({
from: validator.address,
gas: '1000000'
})
const AfterMigrateBalance = await homeWeb3.eth.getBalance(user.address)
// send tokens to foreign bridge
await erc20Token.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, transferValue)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async (retry, number) => {
const balance = await homeWeb3.eth.getBalance(user.address)
await generateNewBlock(foreignWeb3, user.address)
// retry at least 4 times to check transfer is not double processed by the two watchers
if (toBN(balance).lte(toBN(AfterMigrateBalance)) || number < 4) {
retry()
} else {
assert(
toBN(balance).eq(toBN(AfterMigrateBalance).add(toBN(transferValue))),
'User balance should be increased only by second transfer'
)
}
})
const afterMigrateAndTransferBalance = await homeWeb3.eth.getBalance(user.address)
// send tokens to foreign bridge
await halfDuplexToken.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, transferValue)
@@ -156,24 +75,68 @@ describe('erc to native', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance does not increases
await promiseRetry(async (retry, number) => {
const balance = await homeWeb3.eth.getBalance(user.address)
// retry at least 4 times to check transfer is not processed
if (toBN(balance).eq(toBN(originalBalanceOnHome)) && number < 4) {
retry()
} else {
assert(toBN(balance).eq(toBN(originalBalanceOnHome)), 'User balance should not be increased')
}
})
// send tokens to foreign bridge
await erc20Token.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, transferValue)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// check that balance increases
await promiseRetry(async (retry, number) => {
const balance = await homeWeb3.eth.getBalance(user.address)
await generateNewBlock(foreignWeb3, user.address)
// retry at least 4 times to check transfer is not double processed by the two watchers
if (toBN(balance).lte(toBN(afterMigrateAndTransferBalance)) || number < 4) {
if (toBN(balance).lte(toBN(originalBalanceOnHome)) || number < 4) {
retry()
} else {
assert(
toBN(balance).eq(toBN(afterMigrateAndTransferBalance).add(toBN(transferValue))),
toBN(balance).eq(toBN(originalBalanceOnHome).add(toBN(transferValue))),
'User balance should be increased only by second transfer'
)
}
})
const afterTransferBalance = await homeWeb3.eth.getBalance(user.address)
// send tokens to foreign bridge
await erc20Token.methods
.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, transferValue)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// check that balance increases
await promiseRetry(async (retry, number) => {
const balance = await homeWeb3.eth.getBalance(user.address)
// retry at least 4 times to check transfer is not double processed by the two watchers
if (toBN(balance).lte(toBN(afterTransferBalance)) || number < 4) {
retry()
} else {
assert(
toBN(balance).eq(toBN(afterTransferBalance).add(toBN(transferValue))),
'User balance should be increased'
)
}
})
})
it('should convert tokens in foreign to coins in home', async () => {
const balance = await erc20Token.methods.balanceOf(user.address).call()
@@ -203,12 +166,8 @@ describe('erc to native', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = await homeWeb3.eth.getBalance(user.address)
const secondUserbalance = await homeWeb3.eth.getBalance(secondUser.address)
assert(toBN(balance).lte(toBN(originalBalanceOnHome)), 'User balance should be the same')
@@ -230,12 +189,8 @@ describe('erc to native', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = await homeWeb3.eth.getBalance(user.address)
if (toBN(balance).lte(toBN(originalBalanceOnHome))) {
retry()
@@ -247,238 +202,6 @@ describe('erc to native', () => {
}
})
})
it('should convert half duplex token in foreign to native token in home', async () => {
const originalBalanceOnHome = await homeWeb3.eth.getBalance(user.address)
const bridgeErc20TokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
const bridgeHalfDuplexBalance = await halfDuplexToken.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
const valueToTransfer = foreignWeb3.utils.toWei('1', 'ether')
// this transfer won't trigger a call to swap tokens
await halfDuplexToken.methods.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, valueToTransfer).send({
from: user.address,
gas: '1000000'
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
const balance = await homeWeb3.eth.getBalance(user.address)
if (toBN(balance).lte(toBN(originalBalanceOnHome))) {
retry()
} else {
assert(
toBN(balance).eq(toBN(originalBalanceOnHome).add(toBN(valueToTransfer))),
'User balance should be increased by the half duplex token transfer'
)
}
})
const updatedBalanceOnHome = await homeWeb3.eth.getBalance(user.address)
const updatedBridgeHalfDuplexBalance = await halfDuplexToken.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
assert(
toBN(updatedBridgeHalfDuplexBalance).eq(toBN(bridgeHalfDuplexBalance).add(toBN(valueToTransfer))),
'Bridge balance should reflect the transfer value'
)
// this transfer will trigger call to swap tokens
await halfDuplexToken.methods.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, valueToTransfer).send({
from: user.address,
gas: '1000000'
})
await generateNewBlock(foreignWeb3, user.address)
await promiseRetry(async retry => {
const userBalance = await homeWeb3.eth.getBalance(user.address)
const updatedBridgeErc20TokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
if (
toBN(userBalance).lte(toBN(updatedBalanceOnHome)) ||
toBN(updatedBridgeErc20TokenBalance).lte(toBN(bridgeErc20TokenBalance))
) {
retry()
} else {
assert(
toBN(userBalance).eq(toBN(updatedBalanceOnHome).add(toBN(valueToTransfer))),
'User balance should be increased by the half duplex token transfer'
)
const updatedBalance = await halfDuplexToken.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
assert(toBN(updatedBalance).isZero(), 'Half duplex bridge balance should be zero')
assert(
toBN(updatedBridgeErc20TokenBalance).eq(
toBN(bridgeErc20TokenBalance)
.add(toBN(bridgeHalfDuplexBalance))
.add(toBN(foreignWeb3.utils.toWei('2', 'ether')))
),
'Erc20 token balance should be correctly increased by the token swap'
)
}
})
})
it('should convert half duplex token in foreign to native token in home for alternative receiver ', async () => {
const originalBalanceOnHome = await homeWeb3.eth.getBalance(user.address)
const initialBalanceSecondUser = await homeWeb3.eth.getBalance(secondUser.address)
const bridgeErc20TokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
const valueToTransfer = foreignWeb3.utils.toWei('1', 'ether')
// approve tokens to foreign bridge
await halfDuplexToken.methods
.approve(COMMON_FOREIGN_BRIDGE_ADDRESS, valueToTransfer)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// call bridge method to transfer tokens to a different recipient
await foreignBridge.methods['relayTokens(address,uint256,address)'](
secondUser.address,
valueToTransfer,
halfDuplexTokenAddress
)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
const secondUserbalance = await homeWeb3.eth.getBalance(secondUser.address)
const updatedBridgeErc20TokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
const userbalance = await homeWeb3.eth.getBalance(user.address)
assert(toBN(userbalance).lte(toBN(originalBalanceOnHome)), 'User balance should be the same')
if (
toBN(secondUserbalance).lte(toBN(initialBalanceSecondUser)) ||
toBN(updatedBridgeErc20TokenBalance).lte(toBN(bridgeErc20TokenBalance))
) {
retry()
} else {
assert(
toBN(secondUserbalance).eq(toBN(initialBalanceSecondUser).add(toBN(valueToTransfer))),
'User balance should be increased by the half duplex token transfer'
)
const updatedHDBalance = await halfDuplexToken.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
assert(toBN(updatedHDBalance).isZero(), 'Half duplex bridge balance should be zero')
assert(
toBN(updatedBridgeErc20TokenBalance).eq(toBN(bridgeErc20TokenBalance).add(toBN(valueToTransfer))),
'Erc20 token balance should be correctly increased by the token swap'
)
}
})
})
it('should not relay half duplex token transfer after Emergency Shutdown', async () => {
const originalBalanceOnHome = await homeWeb3.eth.getBalance(user.address)
const bridgeErc20TokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
const bridgeHalfDuplexBalance = await halfDuplexToken.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
const block = await foreignWeb3.eth.getBlock('latest')
const saiTop = new foreignWeb3.eth.Contract(SAI_TOP, ercToNativeBridge.saiTop)
// Trigger Emergency Shutdown
await saiTop.methods
.setCaged(block.timestamp)
.send({
from: user.address,
gas: '1000000'
})
.catch(e => {
console.error(e)
})
const valueToTransfer = foreignWeb3.utils.toWei('1', 'ether')
await generateNewBlock(foreignWeb3, user.address)
// this transfer won't trigger a call to swap tokens
await halfDuplexToken.methods.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, valueToTransfer).send({
from: user.address,
gas: '1000000'
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that transfer and swap are not processed in the next blocks.
await promiseRetry(async (retry, number) => {
const balanceOnHome = await homeWeb3.eth.getBalance(user.address)
const currentBridgeErc20TokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
const currentBridgeHalfDuplexBalance = await halfDuplexToken.methods
.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS)
.call()
assert(toBN(balanceOnHome).eq(toBN(originalBalanceOnHome)), 'User balance should be the same')
assert(
toBN(currentBridgeHalfDuplexBalance).eq(toBN(bridgeHalfDuplexBalance).add(toBN(valueToTransfer))),
'Half duplex balance should be the value of transfer'
)
assert(toBN(currentBridgeErc20TokenBalance).eq(toBN(bridgeErc20TokenBalance)), 'erc20 balance should not change')
// generate new blocks
await generateNewBlock(foreignWeb3, user.address)
// after several retries, the state is corrects
if (number < 4) {
retry()
}
})
// let's undo the Emergency Shutdown to check that the oracle is still working
await saiTop.methods.setCaged('0').send({
from: user.address,
gas: '1000000'
})
const newValueToTransfer = foreignWeb3.utils.toWei('2', 'ether')
await halfDuplexToken.methods.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, newValueToTransfer).send({
from: user.address,
gas: '1000000'
})
await generateNewBlock(foreignWeb3, user.address)
await promiseRetry(async retry => {
const userBalance = await homeWeb3.eth.getBalance(user.address)
const updatedBridgeErc20TokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
if (
toBN(userBalance).lte(toBN(originalBalanceOnHome)) ||
toBN(updatedBridgeErc20TokenBalance).lte(toBN(bridgeErc20TokenBalance))
) {
retry()
} else {
assert(
toBN(userBalance).eq(toBN(originalBalanceOnHome).add(toBN(newValueToTransfer))),
'User balance should be increased by the half duplex token transfer'
)
const updatedHDBalance = await halfDuplexToken.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
assert(toBN(updatedHDBalance).isZero(), 'Half duplex bridge balance should be zero')
assert(
toBN(updatedBridgeErc20TokenBalance).eq(
toBN(bridgeErc20TokenBalance)
.add(toBN(valueToTransfer))
.add(toBN(newValueToTransfer))
),
'Erc20 token balance should be correctly increased by the token swap'
)
}
})
})
it('should convert coins in home to tokens in foreign', async () => {
const originalBalance = await erc20Token.methods.balanceOf(user.address).call()
@@ -487,7 +210,7 @@ describe('erc to native', () => {
assert(!toBN(balance).isZero(), 'Account should have tokens')
// send transaction to home bridge
const depositTx = await homeWeb3.eth.sendTransaction({
await homeWeb3.eth.sendTransaction({
from: user.address,
to: COMMON_HOME_BRIDGE_ADDRESS,
gasPrice: '1',
@@ -495,35 +218,217 @@ describe('erc to native', () => {
value: homeWeb3.utils.toWei('0.01')
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(homeWeb3, user.address)
// The bridge should create a new transaction with a CollectedSignatures
// event so we generate another trivial transaction
await promiseRetry(
async retry => {
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
if (lastBlockNumber >= depositTx.blockNumber + 2) {
await generateNewBlock(homeWeb3, user.address)
} else {
retry()
}
},
{
forever: true,
factor: 1,
minTimeout: 500
}
)
// check that balance increases
await promiseRetry(async retry => {
await generateNewBlock(homeWeb3, user.address)
await uniformRetry(async retry => {
const balance = await erc20Token.methods.balanceOf(user.address).call()
if (toBN(balance).lte(toBN(originalBalance))) {
retry()
}
})
})
it('should not invest dai when chai token is disabled', async () => {
const bridgeDaiTokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
await foreignBridge.methods.setMinDaiTokenBalance(foreignWeb3.utils.toWei('2', 'ether')).send({
from: validator.address,
gas: '1000000'
}) // set min limit for automatic investment to 2*2 dai
const valueToTransfer = foreignWeb3.utils.toWei('5', 'ether')
// this transfer won't trigger a call to convert to chai
await erc20Token.methods.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, valueToTransfer).send({
from: user.address,
gas: '1000000'
})
await promiseRetry(async (retry, number) => {
if (number < 4) {
retry()
} else {
const updatedBridgeDaiTokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
assert(
toBN(bridgeDaiTokenBalance)
.add(toBN(valueToTransfer))
.eq(toBN(updatedBridgeDaiTokenBalance)),
'Dai tokens should not be when chai is disabled'
)
}
})
})
it('should invest dai after enough tokens are collected on bridge account', async () => {
await foreignBridge.methods.initializeChaiToken().send({
from: validator.address,
gas: '1000000'
}) // initialize chai token
await foreignBridge.methods.setMinDaiTokenBalance('0').send({
from: validator.address,
gas: '1000000'
}) // set investing limit to 0
await foreignBridge.methods.convertDaiToChai().send({
from: validator.address,
gas: '1000000'
}) // convert all existing dai tokens on bridge account to chai, in order to start from zero balance
await foreignBridge.methods.setMinDaiTokenBalance(foreignWeb3.utils.toWei('2', 'ether')).send({
from: validator.address,
gas: '1000000'
}) // set investing limit to 2 dai, automatically invest should happen after 4 dai
const valueToTransfer = foreignWeb3.utils.toWei('3', 'ether')
// this transfer won't trigger a call to convert to chai
await erc20Token.methods.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, valueToTransfer).send({
from: user.address,
gas: '1000000'
})
await promiseRetry(async (retry, number) => {
if (number < 4) {
retry()
} else {
const bridgeDaiTokenBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
assert(
valueToTransfer === bridgeDaiTokenBalance,
'Dai tokens should not be invested automatically before twice limit is reached'
)
}
})
// this transfer will trigger call to convert to chai
await erc20Token.methods.transfer(COMMON_FOREIGN_BRIDGE_ADDRESS, valueToTransfer).send({
from: user.address,
gas: '1000000'
})
await promiseRetry(async retry => {
const updatedBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
if (toBN(updatedBalance).gte(toBN(valueToTransfer).add(toBN(valueToTransfer)))) {
retry()
} else {
const updatedBalance = await erc20Token.methods.balanceOf(COMMON_FOREIGN_BRIDGE_ADDRESS).call()
assert(
toBN(updatedBalance).eq(toBN(foreignWeb3.utils.toWei('2', 'ether'))),
'Dai bridge balance should be equal to limit'
)
}
})
})
describe('handling of chai swaps', async () => {
before(async () => {
// Next tests check validator nonces, this will force every validator to submit signature/affirmation
// Set 3 required signatures for home bridge
await setRequiredSignatures({
bridgeContract: homeBridge,
web3: homeWeb3,
requiredSignatures: 3,
options: {
from: validator.address,
gas: '4000000'
}
})
// Set 3 required signatures for foreign bridge
await setRequiredSignatures({
bridgeContract: foreignBridge,
web3: foreignWeb3,
requiredSignatures: 3,
options: {
from: validator.address,
gas: '4000000'
}
})
})
it('should not handle transfer event in paying interest', async () => {
await foreignBridge.methods.setInterestReceiver(user.address).send({
from: validator.address,
gas: '1000000'
})
const initialNonce = await homeWeb3.eth.getTransactionCount(validator.address)
await foreignBridge.methods.payInterest().send({
from: user.address,
gas: '1000000'
})
await promiseRetry(async (retry, number) => {
if (number < 6) {
retry()
} else {
const nonce = await homeWeb3.eth.getTransactionCount(validator.address)
assert(
nonce === initialNonce,
'Validator should not process transfer event originated during converting Chai => Dai'
)
}
})
})
it('should not handle chai withdrawal transfer event in executeSignatures as a regular transfer', async () => {
await foreignBridge.methods.setMinDaiTokenBalance('0').send({
from: validator.address,
gas: '1000000'
}) // set investing limit to 0
await foreignBridge.methods.convertDaiToChai().send({
from: validator.address,
gas: '1000000'
}) // convert all existing dai tokens on bridge account to chai, in order to start from zero balance
const initialNonce = await homeWeb3.eth.getTransactionCount(validator.address)
const originalBalance = await erc20Token.methods.balanceOf(user.address).call()
// send transaction to home bridge
await homeWeb3.eth.sendTransaction({
from: user.address,
to: COMMON_HOME_BRIDGE_ADDRESS,
gasPrice: '1',
gas: '1000000',
value: homeWeb3.utils.toWei('0.01')
})
// check that balance increases
await uniformRetry(async retry => {
const balance = await erc20Token.methods.balanceOf(user.address).call()
if (toBN(balance).lte(toBN(originalBalance))) {
retry()
}
})
await promiseRetry(async (retry, number) => {
if (number < 6) {
retry()
} else {
const nonce = await homeWeb3.eth.getTransactionCount(validator.address)
assert(
nonce === initialNonce + 1,
'Validator should not process transfer event originated during converting Chai => Dai'
)
}
})
})
after(async () => {
// Set 2 required signatures for home bridge
await setRequiredSignatures({
bridgeContract: homeBridge,
web3: homeWeb3,
requiredSignatures: 2,
options: {
from: validator.address,
gas: '4000000'
}
})
// Set 2 required signatures for foreign bridge
await setRequiredSignatures({
bridgeContract: foreignBridge,
web3: foreignWeb3,
requiredSignatures: 2,
options: {
from: validator.address,
gas: '4000000'
}
})
})
})
})

View File

@@ -1,6 +1,5 @@
const Web3 = require('web3')
const assert = require('assert')
const promiseRetry = require('promise-retry')
const {
user,
validator,
@@ -14,7 +13,7 @@ const {
foreignRPC
} = require('../../e2e-commons/constants.json')
const { ERC677_BRIDGE_TOKEN_ABI, HOME_NATIVE_TO_ERC_ABI, FOREIGN_NATIVE_TO_ERC_ABI } = require('../../commons')
const { generateNewBlock } = require('../../e2e-commons/utils')
const { uniformRetry, sleep } = require('../../e2e-commons/utils')
const { setRequiredSignatures } = require('./utils')
const homeWeb3 = new Web3(new Web3.providers.HttpProvider(homeRPC.URL))
@@ -24,6 +23,8 @@ const { toBN } = foreignWeb3.utils
const COMMON_HOME_BRIDGE_ADDRESS = nativeToErcBridge.home
const COMMON_FOREIGN_BRIDGE_ADDRESS = nativeToErcBridge.foreign
const validatorAddresses = [validator.address, secondValidator.address, thirdValidator.address]
homeWeb3.eth.accounts.wallet.add(user.privateKey)
homeWeb3.eth.accounts.wallet.add(validator.privateKey)
homeWeb3.eth.accounts.wallet.add(secondUser.privateKey)
@@ -43,8 +44,6 @@ const token = new foreignWeb3.eth.Contract(ERC677_BRIDGE_TOKEN_ABI, nativeToErcB
const homeBridge = new homeWeb3.eth.Contract(HOME_NATIVE_TO_ERC_ABI, COMMON_HOME_BRIDGE_ADDRESS)
const foreignBridge = new foreignWeb3.eth.Contract(FOREIGN_NATIVE_TO_ERC_ABI, COMMON_FOREIGN_BRIDGE_ADDRESS)
const sleep = timeout => new Promise(res => setTimeout(res, timeout))
describe('native to erc', () => {
before(async () => {
// Set 2 required signatures for home bridge
@@ -75,7 +74,7 @@ describe('native to erc', () => {
assert(toBN(balance).isZero(), 'Account should not have tokens yet')
// send transaction to home chain
const depositTx = await homeWeb3.eth.sendTransaction({
await homeWeb3.eth.sendTransaction({
from: user.address,
to: COMMON_HOME_BRIDGE_ADDRESS,
gasPrice: '1',
@@ -83,30 +82,8 @@ describe('native to erc', () => {
value: '1000000000000000000'
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(homeWeb3, user.address)
// The bridge should create a new transaction with a CollectedSignatures
// event so we generate another trivial transaction
await promiseRetry(
async retry => {
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
if (lastBlockNumber >= depositTx.blockNumber + 2) {
await generateNewBlock(homeWeb3, user.address)
} else {
retry()
}
},
{
forever: true,
factor: 1,
minTimeout: 500
}
)
// check that account has tokens in the foreign chain
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = await token.methods.balanceOf(user.address).call()
if (toBN(balance).isZero()) {
retry()
@@ -128,12 +105,8 @@ describe('native to erc', () => {
console.error(e)
})
// Send a trivial transaction to generate a new block since the watcher
// is configured to wait 1 confirmation block
await generateNewBlock(foreignWeb3, user.address)
// check that balance increases
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = await homeWeb3.eth.getBalance(user.address)
if (toBN(balance).lte(toBN(originalBalance))) {
retry()
@@ -150,8 +123,9 @@ describe('native to erc', () => {
await sendAllBalance(homeWeb3, secondValidator.address, thirdUser.address)
await sendAllBalance(homeWeb3, thirdValidator.address, fourthUser.address)
const nonces = await Promise.all(validatorAddresses.map(homeWeb3.eth.getTransactionCount))
// send transaction to home chain
const depositTx = await homeWeb3.eth.sendTransaction({
await homeWeb3.eth.sendTransaction({
from: user.address,
to: COMMON_HOME_BRIDGE_ADDRESS,
gasPrice: '1',
@@ -159,41 +133,20 @@ describe('native to erc', () => {
value: '1000000000000000000'
})
// Send a Tx to generate a new block
await generateNewBlock(homeWeb3, user.address)
// wait two seconds, no new blocks should have been generated
await sleep(2000)
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
const newNonces = await Promise.all(validatorAddresses.map(homeWeb3.eth.getTransactionCount))
const balance = toBN(await token.methods.balanceOf(user.address).call())
assert(lastBlockNumber === depositTx.blockNumber + 1, "Shouldn't have emitted a new block")
assert.deepStrictEqual(nonces, newNonces, "Shouldn't sent new tx")
assert(originalBalance.eq(balance), "Token balance shouldn't have changed")
// send funds back to validator
await sendAllBalance(homeWeb3, secondUser.address, validator.address)
await sendAllBalance(homeWeb3, thirdUser.address, secondValidator.address)
const sendBalanceBackTx = await sendAllBalance(homeWeb3, fourthUser.address, thirdValidator.address)
// expect Deposit event to be processed
await promiseRetry(
async retry => {
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
// check that a new block was created since the last transaction
if (lastBlockNumber >= sendBalanceBackTx.blockNumber + 1) {
await generateNewBlock(homeWeb3, user.address)
} else {
retry()
}
},
{
forever: true,
factor: 1,
minTimeout: 500
}
)
await sendAllBalance(homeWeb3, fourthUser.address, thirdValidator.address)
// check that token balance was incremented in foreign chain
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = toBN(await token.methods.balanceOf(user.address).call())
if (!balance.gt(originalBalance)) {
retry()
@@ -209,7 +162,7 @@ describe('native to erc', () => {
await sendAllBalance(foreignWeb3, validator.address, secondUser.address)
await sendAllBalance(foreignWeb3, secondValidator.address, thirdUser.address)
await sendAllBalance(foreignWeb3, thirdValidator.address, fourthUser.address)
const foreignBlockNumber = await foreignWeb3.eth.getBlockNumber()
const nonces = await Promise.all(validatorAddresses.map(foreignWeb3.eth.getTransactionCount))
// send transaction to home chain
await homeWeb3.eth.sendTransaction({
@@ -220,30 +173,10 @@ describe('native to erc', () => {
value: '1000000000000000000'
})
// Send a Tx to generate a new block
const lastHomeTx = await generateNewBlock(homeWeb3, user.address)
// wait for the deposit to be processed
await promiseRetry(
async retry => {
const lastBlockNumber = await homeWeb3.eth.getBlockNumber()
if (lastBlockNumber >= lastHomeTx.blockNumber + 1) {
await generateNewBlock(homeWeb3, user.address)
} else {
retry()
}
},
{
forever: true,
factor: 1,
minTimeout: 500
}
)
// tokens shouldn't be generated in the foreign chain because the validator doesn't have funds
await sleep(2000)
const lastForeignBlockNumber = await foreignWeb3.eth.getBlockNumber()
assert(lastForeignBlockNumber === foreignBlockNumber, "Shouldn't have emitted a new block")
const newNonces = await Promise.all(validatorAddresses.map(foreignWeb3.eth.getTransactionCount))
assert.deepStrictEqual(nonces, newNonces, "Shouldn't sent new tx")
// send funds back to validator
await sendAllBalance(foreignWeb3, secondUser.address, validator.address)
@@ -251,7 +184,7 @@ describe('native to erc', () => {
await sendAllBalance(foreignWeb3, fourthUser.address, thirdValidator.address)
// check that account has tokens in the foreign chain
await promiseRetry(async retry => {
await uniformRetry(async retry => {
const balance = toBN(await token.methods.balanceOf(user.address).call())
if (balance.eq(originalBalance)) {
retry()

View File

@@ -117,8 +117,18 @@ There are two options to run the TokenBridge processes:
### Docker
- While running the bridge containers for the first time use `ORACLE_VALIDATOR_ADDRESS=<validator address> ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=<validator address private key> docker-compose up -d --build`
- For further launches use `ORACLE_VALIDATOR_ADDRESS=<validator address> ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=<validator address private key> docker-compose up --detach`
- While running the bridge containers for the first time use
```
env ORACLE_VALIDATOR_ADDRESS=<validator address> \
env ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=<validator address private key> \
docker-compose -f docker-compose-build.yml -f docker-compose.yml up -d --build
```
- For further launches (or in case of usage an official docker image) use
```
env ORACLE_VALIDATOR_ADDRESS=<validator address> \
env ORACLE_VALIDATOR_ADDRESS_PRIVATE_KEY=<validator address private key> \
docker-compose up --d
```
All [watcher](#watcher) & [sender](#sender) services launch when `docker-compose` is called.
@@ -144,6 +154,18 @@ In case you need to reset your bridge or setup a new one (with different configu
See the [UI instructions](../ui/README.md) to configure and use the optional Bridge UI.
### Build the image without running the oracle
To build the image change the directory:
```
cd oracle
```
And run the docker composer:
```
docker-compose -f docker-compose-build.yml build
```
## Rollback the Last Processed Block in Redis
If the bridge does not handle an event properly (i.e. a transaction stalls due to a low gas price), the Redis DB can be rolled back. You must identify which watcher needs to re-run. For example, if the validator signatures were collected but the transaction with signatures was not sent to the Foreign network, the `collected-signatures` watcher must look at the block where the corresponding `CollectedSignatures` event was raised.

View File

@@ -0,0 +1,20 @@
const baseConfig = require('./base.config')
const { EXIT_CODES } = require('../src/utils/constants')
const id = `${baseConfig.id}-convert-to-chai`
const workerRequired = baseConfig.id === 'erc-native'
if (!workerRequired) {
console.error(`Convert to chai tokens worker not required for bridge mode ${process.env.ORACLE_BRIDGE_MODE}`)
process.exit(EXIT_CODES.WATCHER_NOT_REQUIRED)
}
module.exports = {
...baseConfig.bridgeConfig,
...baseConfig.foreignConfig,
workerQueue: 'convert-to-chai',
senderQueue: 'foreign',
name: `worker-${id}`,
id
}

View File

@@ -29,6 +29,11 @@ if (!transferWatcherRequired) {
process.exit(EXIT_CODES.WATCHER_NOT_REQUIRED)
}
const workerQueueConfig = {}
if (baseConfig.id === 'erc-native') {
workerQueueConfig.workerQueue = 'convert-to-chai'
}
module.exports = {
...baseConfig.bridgeConfig,
...baseConfig.foreignConfig,
@@ -37,6 +42,7 @@ module.exports = {
eventAbi: ERC20_ABI,
eventFilter: { to: process.env.COMMON_FOREIGN_BRIDGE_ADDRESS },
queue: 'home',
...workerQueueConfig,
name: `watcher-${id}`,
id
}

View File

@@ -0,0 +1,6 @@
---
version: '2.4'
services:
oracle:
image: poanetwork/tokenbridge-oracle
build: .

View File

@@ -9,6 +9,7 @@ services:
- net_rabbit_bridge_transfer
- net_rabbit_bridge_half_duplex_transfer
- net_rabbit_bridge_swap_tokens_worker
- net_rabbit_bridge_convert_to_chai_worker
redis:
extends:
file: docker-compose.yml
@@ -75,6 +76,18 @@ services:
entrypoint: yarn worker:swap-tokens
networks:
- net_rabbit_bridge_swap_tokens_worker
bridge_convert_to_chai_worker:
cpus: 0.1
mem_limit: 500m
image: poanetwork/tokenbridge-oracle:latest
env_file: ./.env
environment:
- NODE_ENV=production
- ORACLE_VALIDATOR_ADDRESS=${ORACLE_VALIDATOR_ADDRESS}
restart: unless-stopped
entrypoint: yarn worker:convert-to-chai
networks:
- net_rabbit_bridge_convert_to_chai_worker
bridge_senderhome:
extends:
file: docker-compose.yml
@@ -121,3 +134,5 @@ networks:
driver: bridge
net_rabbit_bridge_senderforeign:
driver: bridge
net_rabbit_bridge_convert_to_chai_worker:
driver: bridge

View File

@@ -11,6 +11,7 @@
"watcher:transfer": "./scripts/start-worker.sh watcher transfer-watcher",
"watcher:half-duplex-transfer": "./scripts/start-worker.sh watcher half-duplex-transfer-watcher",
"worker:swap-tokens": "./scripts/start-worker.sh worker swap-tokens-worker",
"worker:convert-to-chai": "./scripts/start-worker.sh worker convert-to-chai-worker",
"sender:home": "./scripts/start-worker.sh sender home-sender",
"sender:foreign": "./scripts/start-worker.sh sender foreign-sender",
"dev": "concurrently -n 'watcher:signature-request,watcher:collected-signatures,watcher:affirmation-request,watcher:transfer,watcher:half-duplex-transfer, worker:swap-tokens, sender:home,sender:foreign' -c 'red,green,yellow,blue,white,gray,magenta,cyan' 'yarn watcher:signature-request' 'yarn watcher:collected-signatures' 'yarn watcher:affirmation-request' 'yarn watcher:transfer' 'yarn watcher:half-duplex-transfer' 'yarn worker:swap-tokens' 'yarn sender:home' 'yarn sender:foreign'",

View File

@@ -0,0 +1,13 @@
async function isChaiTokenEnabled(bridgeContract, logger) {
logger.debug('Checking Chai availability')
try {
return await bridgeContract.methods.isChaiTokenEnabled().call()
} catch (e) {
logger.debug('Method isChaiTokenEnabled is not supported')
return false
}
}
module.exports = {
isChaiTokenEnabled
}

View File

@@ -9,6 +9,7 @@ const rpcUrlsManager = require('./services/getRpcUrlsManager')
const { getRequiredBlockConfirmations, getEvents } = require('./tx/web3')
const { checkHTTPS, watchdog } = require('./utils/utils')
const { EXIT_CODES } = require('./utils/constants')
const { isChaiTokenEnabled } = require('./utils/chaiUtils')
if (process.argv.length < 3) {
logger.error('Please check the number of arguments, config file was not provided')
@@ -157,6 +158,15 @@ async function getLastBlockToProcess() {
return lastBlockNumber.sub(requiredBlockConfirmations)
}
async function isWorkerNeeded() {
switch (config.id) {
case 'erc-native-transfer':
return isChaiTokenEnabled(bridgeContract, logger)
default:
return true
}
}
async function main({ sendToQueue, sendToWorker }) {
try {
await checkConditions()
@@ -186,7 +196,7 @@ async function main({ sendToQueue, sendToWorker }) {
logger.info(`Found ${events.length} ${config.event} events`)
if (events.length) {
if (sendToWorker) {
if (sendToWorker && (await isWorkerNeeded())) {
await sendToWorker({ blockNumber: toBlock.toString() })
}

View File

@@ -8,6 +8,7 @@ const { connectWorkerToQueue } = require('./services/amqpClient')
const config = require(path.join('../config/', process.argv[2]))
const swapTokens = require('./workers/swapTokens')(config)
const convertToChai = require('./workers/convertToChai')(config)
async function initialize() {
try {
@@ -39,6 +40,8 @@ async function initialize() {
async function run(blockNumber) {
if (config.id === 'erc-native-swap-tokens') {
return swapTokens(blockNumber)
} else if (config.id === 'erc-native-convert-to-chai') {
return convertToChai(blockNumber)
} else {
return []
}

View File

@@ -0,0 +1,80 @@
require('../../env')
const { HttpListProviderError } = require('http-list-provider')
const rootLogger = require('../services/logger')
const { web3Foreign } = require('../services/web3')
const { BRIDGE_VALIDATORS_ABI } = require('../../../commons')
let validatorContract = null
function convertToChaiBuilder(config) {
const foreignBridge = new web3Foreign.eth.Contract(config.foreignBridgeAbi, config.foreignBridgeAddress)
return async function convertToChai(blockNumber) {
const txToSend = []
const logger = rootLogger.child({
blockNumber: blockNumber.toString()
})
logger.debug(`Starting convert to chai operation`)
if (validatorContract === null) {
logger.debug('Getting validator contract address')
const validatorContractAddress = await foreignBridge.methods.validatorContract().call()
logger.debug({ validatorContractAddress }, 'Validator contract address obtained')
validatorContract = new web3Foreign.eth.Contract(BRIDGE_VALIDATORS_ABI, validatorContractAddress)
}
logger.debug(`Checking if is validator duty`)
const validatorDuty = await validatorContract.methods.isValidatorDuty(config.validatorAddress).call()
if (!validatorDuty) {
logger.info(`Convert to chai discarded because is not validator duty`)
return txToSend
}
logger.debug(`Checking if dai token balance is above the threshold`)
const daiNeedsToBeInvested = await foreignBridge.methods.isDaiNeedsToBeInvested().call()
if (!daiNeedsToBeInvested) {
logger.info(`Convert to chai discarded because dai balance is below the threshold or chai token is not set`)
return txToSend
}
let gasEstimate
try {
logger.debug(`Estimate gas`)
gasEstimate = await foreignBridge.methods.convertDaiToChai().estimateGas({
from: config.validatorAddress
})
logger.debug({ gasEstimate }, 'Gas estimated')
} catch (e) {
if (e instanceof HttpListProviderError) {
const errorMsg = 'RPC Connection Error: convertToChai Gas Estimate cannot be obtained.'
logger.error(e, errorMsg)
throw new Error(errorMsg)
} else {
logger.error(e, 'Unknown error while processing transaction')
throw e
}
}
// generate data
const data = await foreignBridge.methods.convertDaiToChai().encodeABI()
// push to job
txToSend.push({
data,
gasEstimate,
transactionReference: `convert to chai operation for block number ${blockNumber.toString()}`,
to: config.foreignBridgeAddress
})
return txToSend
}
}
module.exports = convertToChaiBuilder

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