Compare commits

..

104 Commits

Author SHA1 Message Date
Alexander Kolotov
ffd88f6cd0 Merge the develop branch to the master branch, preparation to v2.3.0-rc0 2020-07-06 21:31:12 +03:00
Alexander Kolotov
caf2e2b4d3 Update the contract's submodule to the release 5.1.0 (#381) 2020-07-05 00:01:42 +03:00
Kirill Fedoseev
d5d0c8f56a Extend burner wallet plugin with support of erc-to-native mediator (#378) 2020-07-02 00:37:19 +03:00
Gerardo Nardelli
dc27bd6caa ALM docker improvements (#380) 2020-07-02 00:34:27 +03:00
Gerardo Nardelli
ab814f831c Get required block confirmation at the moment of the transaction (#379) 2020-06-30 23:41:19 +03:00
Gerardo Nardelli
691e4294ae Automatically detect network by searching the transaction in both chains (#377) 2020-06-29 15:39:31 +03:00
Gerardo Nardelli
4a727dc159 Add ALM new styles (#373) 2020-06-26 18:47:45 +03:00
Gerardo Nardelli
2ca07e998a Display Age field for validator signatures in ALM (#371) 2020-06-26 00:18:54 +03:00
Gerardo Nardelli
0eb7c41278 Build the list of validators and required signatures at the moment of the transaction (#367) 2020-06-24 20:47:12 +03:00
Gerardo Nardelli
9e9e891db8 Add ALM Pending validator transactions detection (#363) 2020-06-23 17:24:10 +03:00
Gerardo Nardelli
d228bb7ea9 Add button to search another transaction in ALM (#364) 2020-06-23 16:49:59 +03:00
Gerardo Nardelli
d2606997a3 Add ALM failed validator transactions detection (#357) 2020-06-23 16:48:58 +03:00
Gerardo Nardelli
3c956ab9ec Add ALM transaction verification detection (#356) 2020-06-22 21:52:10 +03:00
Alexander Kolotov
9b3e6a51a9 Merge branch 'master' into develop 2020-06-12 17:40:18 +03:00
Kirill Fedoseev
d512b9850f Add old events processing in AMB monitor (#354) 2020-06-10 16:22:01 +03:00
Gerardo Nardelli
bcdf691000 Add Alm project structure and transaction form (#353) 2020-06-09 16:37:43 +03:00
Alexander Kolotov
8e10a5d609 Merge the develop branch to the master branch, preparation to v2.2.0 2020-06-08 16:48:28 +03:00
Alexander Kolotov
da60edae4b More straightforward approach to configure logging for oracle services (#345) 2020-06-08 15:02:03 +03:00
Alexander Kolotov
e7eb8ec758 Update the contract's submodule to the release 5.0.1 (#351) 2020-06-08 14:43:36 +03:00
Gerardo Nardelli
571dec8022 Add ALM project (#348) 2020-06-04 19:15:39 +03:00
Gerardo Nardelli
4db62d721d Add UI e2e and ultimate tests for stake mediators and fix transfer finalization detection (#347) 2020-06-01 20:58:03 +03:00
Gerardo Nardelli
8d6acd0339 Update UI transfer loading to wait block confirmations from contracts (#346) 2020-06-01 17:27:30 +03:00
Gerardo Nardelli
c013cc7378 Bridge UI stake styles fixes (#343) 2020-06-01 17:16:40 +03:00
Gerardo Nardelli
d6e39f34af Prepare for metamask breaking changes (#341) 2020-05-26 22:36:22 +03:00
Alexander Kolotov
3b368ce644 Update the contract's submodule to the release 5.0.0 (#342) 2020-05-26 16:32:59 +03:00
Gerardo Nardelli
c1d58c2908 Add stake theme for Bridge UI (#340) 2020-05-25 23:30:41 +03:00
Kirill Fedoseev
d17e9e0eea Support of multiple AMB requests in one transaction (#315) 2020-05-21 23:29:56 +03:00
Kirill Fedoseev
a2c678d0a2 Add confirmRelay script (#333) 2020-05-21 20:23:00 +03:00
Gerardo Nardelli
4bd3576691 Display latest operations in UI status page for mediators (#332) 2020-05-19 08:53:36 +03:00
Gerardo Nardelli
d3576f5a79 Add network list dropdown in UI (#330) 2020-05-18 23:39:39 +03:00
Gerardo Nardelli
62f9a080c9 Add support for STAKE token ERC677-to-ERC677 through AMB mode in Bridge UI (#328) 2020-05-18 23:36:52 +03:00
Alexander Kolotov
10f67168a7 Health field in the monitor reports (#334) 2020-05-18 21:24:16 +03:00
Kirill Fedoseev
f90f888ae4 Return back filter for tx with TokensSwapped (#335) 2020-05-17 17:57:49 +03:00
Kirill Fedoseev
84508e2b84 Update SAI handling in monitor after ES (#331) 2020-05-16 14:08:08 +03:00
Kirill Fedoseev
2369e876aa Upgrade minimist and kind-of dependencies (#325) 2020-05-08 20:14:06 +03:00
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
Alexander Kolotov
bc6dd13193 Merge the develop branch to the master branch, preparation to v2.0.0-rc0 2020-02-04 14:33:16 +03:00
Gerardo Nardelli
bce1e6509e Use monitor docker hub image for monitor deployment (#284) 2020-02-04 08:16:27 +03:00
Gerardo Nardelli
52358d477b Use oracle docker hub image for oracle deployment (#280) 2020-02-04 00:48:28 +03:00
Gerardo Nardelli
232f807e9d Add ultimate e2e tests for AMB (#285) 2020-02-03 19:39:21 +03:00
Gerardo Nardelli
fe4a569e34 Update monitor check-all script to generate stuckTransfers statistics for v1 bridges (#279) 2020-02-03 15:50:18 +03:00
Gerardo Nardelli
c408d57716 Add custom response for favicon resource in monitor (#278) 2020-02-03 15:48:28 +03:00
Gerardo Nardelli
7fcb118c8c Add deployment test scenario 3 components on 1 host (#277) 2020-02-03 15:47:44 +03:00
Alexander Kolotov
1eaf774e33 Update the contract's submodule to the release 4.0.0-rc0 (#276) 2020-01-22 22:51:28 +03:00
Kirill Fedoseev
8650ba4d21 Updated oracle watchers to use blob version of execute signatures (#269) 2020-01-21 00:11:26 +03:00
Gerardo Nardelli
edc51c78e2 Use 3 validators in oracle e2e tests (#264) 2020-01-20 23:00:04 +03:00
Gerardo Nardelli
612f130544 Fix remote server loop var name in deployment (#275) 2020-01-20 22:53:20 +03:00
Alexander Kolotov
73d5002105 Merge the develop branch to the master branch, preparation to v1.3.0-rc0 2020-01-13 18:09:19 +03:00
Gerardo Nardelli
65dd131107 Support multiple bridges in one monitor (#262) 2020-01-10 15:55:34 +03:00
Alexander Kolotov
4de24efc01 Security audit report provided by Quantstamp for TokenBridge contracts (#263) 2020-01-10 15:48:41 +03:00
Alexander Kolotov
727371f251 Merge the develop branch to the master branch, preparation to v1.2.0 2020-01-06 22:09:12 +03:00
Gerardo Nardelli
9cb1a2041d Add support to disable validator balances check in monitor (#259)
* Add support to disable validator balance check in monitor
* Convert validator address to checksum address
2020-01-04 15:44:57 +04:00
Igor Barinov
b6d96d7f62 Update README.md (#260) 2020-01-03 01:04:39 +04:00
Alexander Kolotov
dc06ee8ceb Update the contract's submodule to the release 3.3.0 (#258) 2019-12-30 23:45:27 +04:00
Alexander Kolotov
8c2f58b06f Oracle upgrade script to migrate from 1.1.1 to 1.2.0-rc0 (#257) 2019-12-30 17:15:49 +04:00
dependabot[bot]
3ad62d6a7f Bump handlebars from 4.1.2 to 4.5.3 (#255) 2019-12-30 17:14:56 +04:00
Alexander Kolotov
9f9638970a Add handling of error case with RPC links in getTokensState (#252)
* add handling for error case and extend logging
2019-12-23 18:59:03 +03:00
Gerardo Nardelli
7054ff26a0 Add rabbit and redis networks to new workers (#249) 2019-12-22 15:44:06 +03:00
Alexander Kolotov
afb601b7f5 Merge the develop branch to the master branch, preparation to v1.2.0-rc0 2019-12-19 19:30:30 +03:00
Alexander Kolotov
1736fd615d Update the contract's submodule to the release 3.3.0-rc0 (#247) 2019-12-19 18:53:00 +03:00
Gerardo Nardelli
ef0a734650 Support two tokens deposits in monitor (#245)
* Support two tokens deposits in monitor
* update chrome version
2019-12-19 12:39:41 +03:00
Alexander Kolotov
6e2238fc9b Merge branch 'master' into develop 2019-12-05 23:46:13 +03:00
Alexander Kolotov
0f3bea5a41 Merge pull request #244 from poanetwork/support-two-tokens-oracle
Support two tokens deposit requests in Oracle
2019-12-05 23:42:30 +03:00
Gerardo Nardelli
0829c95561 Move tokenState file to utils 2019-12-05 13:50:55 -03:00
Gerardo Nardelli
5bb99a7e95 Update token used in erc-native monitor-e2e 2019-12-05 09:48:55 -03:00
Gerardo Nardelli
3cd53f7bda Update watcher to be able to skip events 2019-12-04 17:18:14 -03:00
Gerardo Nardelli
0eeae74ffa Update log
Co-Authored-By: Alexander Kolotov <alexandr.kolotov@gmail.com>
2019-12-04 09:59:34 -03:00
Gerardo Nardelli
8fa715089b Add watcher idle option 2019-12-04 09:58:27 -03:00
Gerardo Nardelli
ab2c0ea120 Log block timestamp 2019-12-03 17:17:27 -03:00
Gerardo Nardelli
5583ea8b6b Catch zero balance error in swap tokens worker 2019-12-03 17:17:24 -03:00
Gerardo Nardelli
a4eb446f7b Rename syslog logging cofig file 2019-12-03 17:17:16 -03:00
Gerardo Nardelli
2d526a1454 Fix wording
Co-Authored-By: Alexander Kolotov <alexandr.kolotov@gmail.com>
2019-12-03 11:38:37 -03:00
Gerardo Nardelli
9811c13a04 Typo fix 2019-12-03 10:16:23 -03:00
Gerardo Nardelli
406ede9352 Add e2e tests two tokens support 2019-12-02 17:20:53 -03:00
Gerardo Nardelli
1360c79e69 Fix half duplex transfer watcher 2019-12-02 17:19:28 -03:00
Gerardo Nardelli
12229e5e0b Use pre-deployed token for erc to native e2e tests 2019-11-29 15:32:53 -03:00
Gerardo Nardelli
588b289bb9 remove comment attribute in chain spec 2019-11-29 13:16:31 -03:00
Gerardo Nardelli
b3419ccca6 Add parity hardcoded addresses for erc to native 2019-11-29 11:57:45 -03:00
Gerardo Nardelli
ecd20890c8 Add sai contract bytecode in foreign parity chain config 2019-11-29 09:56:48 -03:00
Gerardo Nardelli
b6588ff3c5 Add erc to native docker config in deployment playbooks 2019-11-29 09:28:50 -03:00
Gerardo Nardelli
ed2de112a2 fixes 2019-11-28 16:56:18 -03:00
Gerardo Nardelli
c19f48ef3f Add swap-tokens worker 2019-11-28 16:31:27 -03:00
Gerardo Nardelli
eb8de323ee Add half duplex transfer watcher 2019-11-26 17:00:56 -03:00
Gerardo Nardelli
c42b2f03b7 Update submodule to phase 2 contracts 2019-11-26 16:59:46 -03:00
380 changed files with 33513 additions and 2634 deletions

View File

@@ -11,10 +11,10 @@ orbs:
sudo apt-get clean
sudo apt-get update
sudo apt-get install dpkg
- run:
- run:
name: Install Chrome
command: |
wget -O chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
wget -O chrome.deb https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_77.0.3865.120-1_amd64.deb
sudo dpkg -i chrome.deb
install-node:
steps:
@@ -89,29 +89,29 @@ jobs:
- checkout
- run: git submodule update --init
- restore_cache:
name: Restore Yarn Package Cache
keys:
- yarn-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- run: git submodule status > submodule.status
name: Restore Yarn Package Cache
keys:
- yarn-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- run: git submodule status > submodule.status
- restore_cache:
name: Restore contracts submodule with compiled contracts
keys:
- contracts-{{ checksum "submodule.status" }}
name: Restore contracts submodule with compiled contracts
keys:
- contracts-{{ checksum "submodule.status" }}
- run: yarn install --frozen-lockfile
- save_cache:
name: Save Yarn Package Cache
key: yarn-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
name: Save Yarn Package Cache
key: yarn-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
- run: touch install_deploy.log; test -d contracts/build/contracts || yarn install:deploy &> install_deploy.log
- store_artifacts:
path: install_deploy.log
- run: test -d contracts/build/contracts || yarn compile:contracts
- save_cache:
name: Save contracts submodule with compiled contracts
key: contracts-{{ checksum "submodule.status" }}
paths:
- contracts
name: Save contracts submodule with compiled contracts
key: contracts-{{ checksum "submodule.status" }}
paths:
- contracts
- save_cache:
name: Save initialized project for subsequent jobs
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
@@ -143,11 +143,11 @@ jobs:
oracle-e2e:
executor: tokenbridge-orb/docker-node
steps:
- checkout
- run: git submodule update --init
- setup_remote_docker:
docker_layer_caching: true
- run: yarn run oracle-e2e
- checkout
- run: git submodule update --init
- setup_remote_docker:
docker_layer_caching: true
- run: yarn run oracle-e2e
ui-e2e:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
@@ -161,9 +161,9 @@ jobs:
monitor-e2e:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- run: git submodule update --init
- run: ./monitor-e2e/run-tests.sh
- checkout
- run: git submodule update --init
- run: ./monitor-e2e/run-tests.sh
cover:
executor: tokenbridge-orb/docker-node
steps:
@@ -194,7 +194,7 @@ jobs:
steps:
- checkout
- run: git submodule update --init
- run:
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh monitor
no_output_timeout: 40m
@@ -210,7 +210,15 @@ jobs:
name: Run the scenario
command: deployment-e2e/molecule.sh repo
no_output_timeout: 40m
deployment-multiple:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- run: git submodule update --init
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh multiple
no_output_timeout: 40m
ultimate:
executor: tokenbridge-orb/machine-with-docker-caching
parameters:
@@ -222,6 +230,11 @@ jobs:
type: string
ui-e2e-grep:
description: "Mocha grep string used to run ui-e2e tests specific to given type of bridge"
default: ''
type: string
oracle-e2e-script:
description: "Yarn script string used to run oracle-e2e tests specific to given type of bridge"
default: ''
type: string
steps:
- checkout
@@ -232,16 +245,24 @@ 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 >>
- run:
name: Run the ui-e2e tests
command: |
nvm use default;
node ./e2e-commons/scripts/blocks.js &
cd ui-e2e; yarn mocha -g "<< parameters.ui-e2e-grep >>" -b ./test.js
- when:
condition: << parameters.ui-e2e-grep >>
steps:
- run:
name: Run the ui-e2e tests
command: |
nvm use default;
cd ui-e2e; yarn mocha -g "<< parameters.ui-e2e-grep >>" -b ./test.js
- when:
condition: << parameters.oracle-e2e-script >>
steps:
- run:
name: Run the oracle-e2e tests
command: cd e2e-commons && docker-compose run e2e yarn workspace oracle-e2e run << parameters.oracle-e2e-script >>
workflows:
tokenbridge:
jobs:
@@ -272,18 +293,29 @@ workflows:
- deployment-ui
- deployment-monitor
- deployment-repo
- deployment-multiple
- ultimate:
name: "ultimate: native to erc"
name: "ultimate: native to erc"
scenario-name: native-to-erc
redis-key: native-erc-collected-signatures:lastProcessedBlock
ui-e2e-grep: "NATIVE TO ERC"
- ultimate:
name: "ultimate: erc to native"
name: "ultimate: erc to native"
scenario-name: erc-to-native
redis-key: erc-native-collected-signatures:lastProcessedBlock
ui-e2e-grep: "ERC TO NATIVE"
- ultimate:
name: "ultimate: erc to erc"
name: "ultimate: erc to erc"
scenario-name: erc-to-erc
redis-key: erc-erc-collected-signatures:lastProcessedBlock
ui-e2e-grep: "ERC TO ERC"
- ultimate:
name: "ultimate: amb"
scenario-name: amb
redis-key: amb-collected-signatures:lastProcessedBlock
oracle-e2e-script: "amb"
- ultimate:
name: "ultimate: amb stake erc to erc"
scenario-name: ultimate-amb-stake-erc-to-erc
redis-key: amb-collected-signatures:lastProcessedBlock
ui-e2e-grep: "AMB-STAKE-ERC-TO-ERC"

View File

@@ -9,6 +9,8 @@
**/*.md
contracts/test
contracts/build
oracle/test
oracle/**/*.png
oracle/**/*.jpg
audit

View File

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

4
.gitignore vendored
View File

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

View File

@@ -43,6 +43,7 @@ ORACLE_VALIDATOR_ADDRESS | The public address of the bridge validator | hexideci
name | description | value
--- | --- | ---
UI_TITLE | The title for the bridge UI page. `%c` will be replaced by the name of the network. | string
UI_OG_TITLE | The meta title for the deployed bridge page. | string
UI_DESCRIPTION | The meta description for the deployed bridge page. | string
UI_NATIVE_TOKEN_DISPLAY_NAME | name of the home native coin | string
UI_HOME_NETWORK_DISPLAY_NAME | name to be displayed for home network | string
@@ -56,7 +57,8 @@ UI_FOREIGN_EXPLORER_ADDRESS_TEMPLATE | template link to address on foreign explo
UI_HOME_GAS_PRICE_UPDATE_INTERVAL | An interval in milliseconds used to get the updated gas price value either from the oracle or from the Home Bridge contract. | integer
UI_FOREIGN_GAS_PRICE_UPDATE_INTERVAL | An interval in milliseconds used to get the updated gas price value either from the oracle or from the Foreign Bridge contract. | integer
UI_PORT | The port for the UI app. | integer
UI_STYLES | The set of styles to render the bridge UI page. Currently only `classic` is implemented | classic
UI_STYLES | The set of styles to render the bridge UI page. | core/classic/stake
UI_PUBLIC_URL | The public url for the deployed bridge page | string
## Monitor configuration
@@ -69,3 +71,5 @@ MONITOR_VALIDATOR_HOME_TX_LIMIT | Average gas usage of a transaction sent by a v
MONITOR_VALIDATOR_FOREIGN_TX_LIMIT | Average gas usage of a transaction sent by a validator, it is used to estimate the number of transaction that can be paid by the validator. | integer
MONITOR_TX_NUMBER_THRESHOLD | If estimated number of transaction is equal to or below this value, the monitor will report that the validator has less funds than it is required. | integer
MONITOR_PORT | The port for the Monitor. | integer
MONITOR_BRIDGE_NAME | The name to be used in the url path for the bridge | string
MONITOR_CACHE_EVENTS | If set to true, monitor will cache obtained events for other workers runs

View File

@@ -29,6 +29,8 @@ Sub-repositories maintained within this monorepo are listed below.
| [Deployment-E2E](deployment-e2e/README.md) | End to end tests for the Deployment |
| [Commons](commons/README.md) | Interfaces, constants and utilities shared between the sub-repositories |
| [E2E-Commons](e2e-commons/README.md) | Common utilities and configuration used in end to end tests |
| [ALM](alm/README.md) | DApp interface tool for AMB Live Monitoring |
| [Burner-wallet-plugin](burner-wallet-plugin/README.md) | TokenBridge Burner Wallet 2 Plugin |
Additionally there are [Smart Contracts](https://github.com/poanetwork/tokenbridge-contracts) used to manage bridge validators, collect signatures, and confirm asset relay and disposal.
@@ -66,11 +68,13 @@ Clone the repository:
git clone https://github.com/poanetwork/tokenbridge
```
Initialize submodules, install dependencies, compile the Smart Contracts:
If there is no need to build docker images for the TokenBridge components (oracle, monitor, UI), initialize submodules, install dependencies, compile the Smart Contracts:
```
yarn initialize
```
Then refer to the corresponding README files to get information about particular TokenBridge component.
## Linting
Running linter for all JS projects:
@@ -106,5 +110,4 @@ This project is licensed under the GNU Lesser General Public License v3.0. See t
## References
* [Additional Documentation](https://forum.poa.network/c/tokenbridge)
* [POA20 Bridge FAQ](https://forum.poa.network/c/tokenbridge/poa20-bridge)
* [TokenBridge Documentation](http://www.tokenbridge.net/)

15
alm/.env.example Normal file
View File

@@ -0,0 +1,15 @@
COMMON_HOME_BRIDGE_ADDRESS=0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560
COMMON_FOREIGN_BRIDGE_ADDRESS=0xFe446bEF1DbF7AFE24E81e05BC8B271C1BA9a560
COMMON_HOME_RPC_URL=https://sokol.poa.network
COMMON_FOREIGN_RPC_URL=https://kovan.infura.io/v3/
ALM_HOME_NETWORK_NAME=Sokol Testnet
ALM_FOREIGN_NETWORK_NAME=Kovan Testnet
ALM_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx/%s
ALM_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
ALM_HOME_EXPLORER_API=https://blockscout.com/poa/sokol/api
ALM_FOREIGN_EXPLORER_API=https://kovan.etherscan.io/api?apikey=YourApiKeyToken
PORT=8080

6
alm/.eslintrc.js Normal file
View File

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

23
alm/.gitignore vendored Normal file
View File

@@ -0,0 +1,23 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

29
alm/Dockerfile Normal file
View File

@@ -0,0 +1,29 @@
FROM node:12 as alm-builder
WORKDIR /mono
COPY package.json .
COPY contracts/package.json ./contracts/
COPY commons/package.json ./commons/
COPY alm/package.json ./alm/
COPY yarn.lock .
RUN yarn install --production --frozen-lockfile
COPY ./contracts ./contracts
RUN yarn run compile:contracts
RUN mv ./contracts/build ./ && rm -rf ./contracts/* ./contracts/.[!.]* && mv ./build ./contracts/
COPY ./commons ./commons
COPY ./alm ./alm
ARG DOT_ENV_PATH=./alm/.env
COPY ${DOT_ENV_PATH} ./alm/.env
WORKDIR /mono/alm
RUN yarn run build
FROM node:12 as alm-production
RUN yarn global add serve
WORKDIR /app
COPY --from=alm-builder /mono/alm/build .
CMD serve -p $PORT -s .

46
alm/README.md Normal file
View File

@@ -0,0 +1,46 @@
# AMB Live Monitoring
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `yarn start`
Runs the app in the development mode.<br />
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br />
You will also see any lint errors in the console.
### `yarn test`
Launches the test runner in the interactive watch mode.<br />
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `yarn build`
Builds the app for production to the `build` folder.<br />
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br />
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `yarn eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

9
alm/config-overrides.js Normal file
View File

@@ -0,0 +1,9 @@
const { override, disableEsLint } = require('customize-cra')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
const disableModuleScopePlugin = () => config => {
config.resolve.plugins = config.resolve.plugins.filter(plugin => !(plugin instanceof ModuleScopePlugin))
return config
}
module.exports = override(disableEsLint(), disableModuleScopePlugin())

14
alm/docker-compose.yml Normal file
View File

@@ -0,0 +1,14 @@
---
version: '2.4'
services:
alm:
build:
context: ..
dockerfile: alm/Dockerfile
ports:
- "${PORT}:${PORT}"
env_file: ./.env
environment:
- NODE_ENV=production
restart: unless-stopped
entrypoint: serve -p ${PORT} -s .

16
alm/load-env.sh Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
while read line; do
if [ "$line" = "" ]; then
: # Skip empty lines
elif [[ "$line" =~ \#.* ]]; then
: # Skip comment lines
elif [[ "$line" =~ "UI_PORT"* ]]; then
eval $line
export PORT="$UI_PORT"
else
export "REACT_APP_$line"
fi
done < '.env'
$*

57
alm/package.json Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "alm",
"version": "0.1.0",
"private": true,
"dependencies": {
"@react-hook/window-size": "^3.0.6",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/promise-retry": "^1.1.3",
"@types/react": "^16.9.0",
"@types/react-dom": "^16.9.0",
"@types/react-router-dom": "^5.1.5",
"@types/styled-components": "^5.1.0",
"@use-it/interval": "^0.1.3",
"customize-cra": "^1.0.0",
"date-fns": "^2.14.0",
"fast-memoize": "^2.5.2",
"promise-retry": "^2.0.1",
"react": "^16.13.1",
"react-app-rewired": "^2.1.6",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.0.1",
"styled-components": "^5.1.1",
"typescript": "^3.5.2",
"web3": "1.2.7",
"web3-eth-contract": "1.2.7"
},
"scripts": {
"start": "./load-env.sh react-app-rewired start",
"build": "./load-env.sh react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject",
"lint": "eslint '*/**/*.{js,ts,tsx}' --ignore-path ../.eslintignore"
},
"eslintConfig": {
"extends": "react-app"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"eslint-plugin-prettier": "^3.1.3"
}
}

BIN
alm/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

45
alm/public/index.html Normal file
View File

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

15
alm/public/manifest.json Normal file
View File

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

3
alm/public/robots.txt Normal file
View File

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

5
alm/src/App.test.tsx Normal file
View File

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

16
alm/src/App.tsx Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react'
import { BrowserRouter } from 'react-router-dom'
import { MainPage } from './components/MainPage'
import { StateProvider } from './state/StateProvider'
function App() {
return (
<BrowserRouter>
<StateProvider>
<MainPage />
</StateProvider>
</BrowserRouter>
)
}
export default App

View File

@@ -0,0 +1,319 @@
import { AbiItem } from 'web3-utils'
const abi: AbiItem[] = [
{
constant: true,
inputs: [],
name: 'validatorCount',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getBridgeValidatorsInterfacesVersion',
outputs: [
{
name: 'major',
type: 'uint64'
},
{
name: 'minor',
type: 'uint64'
},
{
name: 'patch',
type: 'uint64'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'isInitialized',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'validatorList',
outputs: [
{
name: '',
type: 'address[]'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_requiredSignatures',
type: 'uint256'
}
],
name: 'setRequiredSignatures',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'requiredSignatures',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_address',
type: 'address'
}
],
name: 'getNextValidator',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'owner',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_validator',
type: 'address'
}
],
name: 'isValidatorDuty',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'deployedAtBlock',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'F_ADDR',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'newOwner',
type: 'address'
}
],
name: 'transferOwnership',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_validator',
type: 'address'
}
],
name: 'isValidator',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'validator',
type: 'address'
}
],
name: 'ValidatorAdded',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'validator',
type: 'address'
}
],
name: 'ValidatorRemoved',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'requiredSignatures',
type: 'uint256'
}
],
name: 'RequiredSignaturesChanged',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'previousOwner',
type: 'address'
},
{
indexed: false,
name: 'newOwner',
type: 'address'
}
],
name: 'OwnershipTransferred',
type: 'event'
},
{
constant: false,
inputs: [
{
name: '_requiredSignatures',
type: 'uint256'
},
{
name: '_initialValidators',
type: 'address[]'
},
{
name: '_owner',
type: 'address'
}
],
name: 'initialize',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_validator',
type: 'address'
}
],
name: 'addValidator',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_validator',
type: 'address'
}
],
name: 'removeValidator',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
}
]
export default abi

589
alm/src/abis/ForeignAMB.ts Normal file
View File

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

777
alm/src/abis/HomeAMB.ts Normal file
View File

@@ -0,0 +1,777 @@
import { AbiItem } from 'web3-utils'
const abi: AbiItem[] = [
{
constant: true,
inputs: [],
name: 'transactionHash',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_message',
type: 'bytes32'
}
],
name: 'numMessagesSigned',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_hash',
type: 'bytes32'
},
{
name: '_index',
type: 'uint256'
}
],
name: 'signature',
outputs: [
{
name: '',
type: 'bytes'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_sourceChainId',
type: 'uint256'
},
{
name: '_destinationChainId',
type: 'uint256'
},
{
name: '_validatorContract',
type: 'address'
},
{
name: '_maxGasPerTx',
type: 'uint256'
},
{
name: '_gasPrice',
type: 'uint256'
},
{
name: '_requiredBlockConfirmations',
type: 'uint256'
},
{
name: '_owner',
type: 'address'
}
],
name: 'initialize',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'isInitialized',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'requiredBlockConfirmations',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_data',
type: 'bytes'
}
],
name: 'getMinimumGasUsage',
outputs: [
{
name: 'gas',
type: 'uint256'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'failedMessageReceiver',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getBridgeMode',
outputs: [
{
name: '_data',
type: 'bytes4'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_sourceChainId',
type: 'uint256'
},
{
name: '_destinationChainId',
type: 'uint256'
}
],
name: 'setChainIds',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_hash',
type: 'bytes32'
}
],
name: 'message',
outputs: [
{
name: '',
type: 'bytes'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'failedMessageSender',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'signature',
type: 'bytes'
},
{
name: 'message',
type: 'bytes'
}
],
name: 'submitSignature',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'messageId',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_token',
type: 'address'
},
{
name: '_to',
type: 'address'
}
],
name: 'claimTokens',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_hash',
type: 'bytes32'
}
],
name: 'numAffirmationsSigned',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_hash',
type: 'bytes32'
}
],
name: 'affirmationsSigned',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_maxGasPerTx',
type: 'uint256'
}
],
name: 'setMaxGasPerTx',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'requiredSignatures',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'owner',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_message',
type: 'bytes32'
}
],
name: 'messagesSigned',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'validatorContract',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'deployedAtBlock',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'getBridgeInterfacesVersion',
outputs: [
{
name: 'major',
type: 'uint64'
},
{
name: 'minor',
type: 'uint64'
},
{
name: 'patch',
type: 'uint64'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'messageSourceChainId',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_blockConfirmations',
type: 'uint256'
}
],
name: 'setRequiredBlockConfirmations',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_gasPrice',
type: 'uint256'
}
],
name: 'setGasPrice',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'messageCallStatus',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'messageSender',
outputs: [
{
name: '',
type: 'address'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: '_contract',
type: 'address'
},
{
name: '_data',
type: 'bytes'
},
{
name: '_gas',
type: 'uint256'
}
],
name: 'requireToPassMessage',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_messageId',
type: 'bytes32'
}
],
name: 'failedMessageDataHash',
outputs: [
{
name: '',
type: 'bytes32'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'maxGasPerTx',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'message',
type: 'bytes'
}
],
name: 'executeAffirmation',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: false,
inputs: [
{
name: 'newOwner',
type: 'address'
}
],
name: 'transferOwnership',
outputs: [],
payable: false,
stateMutability: 'nonpayable',
type: 'function'
},
{
constant: true,
inputs: [],
name: 'gasPrice',
outputs: [
{
name: '',
type: 'uint256'
}
],
payable: false,
stateMutability: 'view',
type: 'function'
},
{
constant: true,
inputs: [
{
name: '_number',
type: 'uint256'
}
],
name: 'isAlreadyProcessed',
outputs: [
{
name: '',
type: 'bool'
}
],
payable: false,
stateMutability: 'pure',
type: 'function'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'messageId',
type: 'bytes32'
},
{
indexed: false,
name: 'encodedData',
type: 'bytes'
}
],
name: 'UserRequestForSignature',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'sender',
type: 'address'
},
{
indexed: true,
name: 'executor',
type: 'address'
},
{
indexed: true,
name: 'messageId',
type: 'bytes32'
},
{
indexed: false,
name: 'status',
type: 'bool'
}
],
name: 'AffirmationCompleted',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'signer',
type: 'address'
},
{
indexed: false,
name: 'messageHash',
type: 'bytes32'
}
],
name: 'SignedForUserRequest',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: true,
name: 'signer',
type: 'address'
},
{
indexed: false,
name: 'messageHash',
type: 'bytes32'
}
],
name: 'SignedForAffirmation',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'authorityResponsibleForRelay',
type: 'address'
},
{
indexed: false,
name: 'messageHash',
type: 'bytes32'
},
{
indexed: false,
name: 'NumberOfCollectedSignatures',
type: 'uint256'
}
],
name: 'CollectedSignatures',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'gasPrice',
type: 'uint256'
}
],
name: 'GasPriceChanged',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'requiredBlockConfirmations',
type: 'uint256'
}
],
name: 'RequiredBlockConfirmationChanged',
type: 'event'
},
{
anonymous: false,
inputs: [
{
indexed: false,
name: 'previousOwner',
type: 'address'
},
{
indexed: false,
name: 'newOwner',
type: 'address'
}
],
name: 'OwnershipTransferred',
type: 'event'
}
]
export default abi

3
alm/src/abis/index.ts Normal file
View File

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

View File

@@ -0,0 +1,85 @@
import React from 'react'
import { TransactionReceipt } from 'web3-eth'
import { useMessageConfirmations } from '../hooks/useMessageConfirmations'
import { MessageObject } from '../utils/web3'
import styled from 'styled-components'
import { CONFIRMATIONS_STATUS } from '../config/constants'
import { CONFIRMATIONS_STATUS_LABEL } from '../config/descriptions'
import { SimpleLoading } from './commons/Loading'
import { ValidatorsConfirmations } from './ValidatorsConfirmations'
import { getConfirmationsStatusDescription } from '../utils/networks'
import { useStateProvider } from '../state/StateProvider'
import { ExecutionConfirmation } from './ExecutionConfirmation'
import { useValidatorContract } from '../hooks/useValidatorContract'
import { useBlockConfirmations } from '../hooks/useBlockConfirmations'
const StatusLabel = styled.label`
font-weight: bold;
font-size: 18px;
`
const StatusResultLabel = styled.label`
font-size: 18px;
padding-left: 10px;
`
const StyledConfirmationContainer = styled.div`
background-color: var(--bg-color);
padding: 10px;
border-radius: 4px;
`
const StatusDescription = styled.div`
padding-top: 10px;
`
export interface ConfirmationsContainerParams {
message: MessageObject
receipt: Maybe<TransactionReceipt>
fromHome: boolean
timestamp: number
}
export const ConfirmationsContainer = ({ message, receipt, fromHome, timestamp }: ConfirmationsContainerParams) => {
const {
home: { name: homeName },
foreign: { name: foreignName }
} = useStateProvider()
const { requiredSignatures, validatorList } = useValidatorContract({ fromHome, receipt })
const { blockConfirmations } = useBlockConfirmations({ fromHome, receipt })
const { confirmations, status, executionData, signatureCollected } = useMessageConfirmations({
message,
receipt,
fromHome,
timestamp,
requiredSignatures,
validatorList,
blockConfirmations
})
return (
<div className="row is-center">
<StyledConfirmationContainer className="col-9">
<div className="row is-center">
<StatusLabel>Status:</StatusLabel>
<StatusResultLabel>
{status !== CONFIRMATIONS_STATUS.UNDEFINED ? CONFIRMATIONS_STATUS_LABEL[status] : <SimpleLoading />}
</StatusResultLabel>
</div>
<StatusDescription className="row is-center">
<p className="col-10">
{status !== CONFIRMATIONS_STATUS.UNDEFINED
? getConfirmationsStatusDescription(status, homeName, foreignName)
: ''}
</p>
</StatusDescription>
<ValidatorsConfirmations
confirmations={confirmations}
requiredSignatures={requiredSignatures}
validatorList={validatorList}
/>
{signatureCollected && <ExecutionConfirmation executionData={executionData} isHome={!fromHome} />}
</StyledConfirmationContainer>
</div>
)
}

View File

@@ -0,0 +1,69 @@
import React from 'react'
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
import { useWindowWidth } from '@react-hook/window-size'
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { SimpleLoading } from './commons/Loading'
import styled from 'styled-components'
import { ExecutionData } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
const StyledExecutionConfirmation = styled.div`
margin-top: 30px;
`
export interface ExecutionConfirmationParams {
executionData: ExecutionData
isHome: boolean
}
export const ExecutionConfirmation = ({ executionData, isHome }: ExecutionConfirmationParams) => {
const windowWidth = useWindowWidth()
const txExplorerLink = getExplorerTxUrl(executionData.txHash, isHome)
const formattedValidator =
windowWidth < 850 && executionData.validator ? formatTxHash(executionData.validator) : executionData.validator
const getExecutionStatusElement = (validatorStatus = '') => {
switch (validatorStatus) {
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
return <SuccessLabel>{validatorStatus}</SuccessLabel>
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
return <RedLabel>{validatorStatus}</RedLabel>
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
return <GreyLabel>{validatorStatus}</GreyLabel>
default:
return <SimpleLoading />
}
}
return (
<StyledExecutionConfirmation>
<table>
<Thead>
<tr>
<th>Executed by</th>
<th className="text-center">Status</th>
<th className="text-center">Age</th>
</tr>
</Thead>
<tbody>
<tr>
<td>{formattedValidator ? formattedValidator : <SimpleLoading />}</td>
<td className="text-center">{getExecutionStatusElement(executionData.status)}</td>
<td className="text-center">
<ExplorerTxLink href={txExplorerLink} target="_blank">
{executionData.timestamp > 0 ? formatTimestamp(executionData.timestamp) : ''}
</ExplorerTxLink>
</td>
</tr>
</tbody>
</table>
</StyledExecutionConfirmation>
)
}

View File

@@ -0,0 +1,64 @@
import React, { useState, FormEvent } from 'react'
import styled from 'styled-components'
import { FormSubmitParams } from './MainPage'
import { Button } from './commons/Button'
import { TransactionSelector } from './TransactionSelector'
import { TransactionReceipt } from 'web3-eth'
const LabelText = styled.label`
line-height: 36px;
max-width: 140px;
`
const Input = styled.input`
background-color: var(--bg-color);
color: var(--font-color);
max-width: 100%;
border-color: var(--color-primary) !important;
&:hover,
&:active,
&:focus {
border-color: var(--button-color) !important;
}
`
export const Form = ({ onSubmit }: { onSubmit: ({ chainId, txHash, receipt }: FormSubmitParams) => void }) => {
const [txHash, setTxHash] = useState('')
const [searchTx, setSearchTx] = useState(false)
const formSubmit = (e: FormEvent) => {
e.preventDefault()
setSearchTx(true)
}
const onSelected = (chainId: number, receipt: TransactionReceipt) => {
onSubmit({ chainId, txHash, receipt })
}
if (searchTx) {
return <TransactionSelector txHash={txHash} onSelected={onSelected} />
}
return (
<form onSubmit={formSubmit}>
<div className="row is-center">
<LabelText className="col-2">Bridgeable tx hash:</LabelText>
<div className="col-7">
<Input
placeholder="Enter transaction hash"
type="text"
onChange={e => setTxHash(e.target.value)}
required
pattern="^0x[a-fA-F0-9]{64}$"
value={txHash}
/>
</div>
<div className="col-1">
<Button className="button outline" type="submit">
Check
</Button>
</div>
</div>
</form>
)
}

View File

@@ -0,0 +1,90 @@
import React, { useState } from 'react'
import styled from 'styled-components'
import { Route, useHistory } from 'react-router-dom'
import { Form } from './Form'
import { StatusContainer } from './StatusContainer'
import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth'
const StyledMainPage = styled.div`
text-align: center;
min-height: 100vh;
`
const Header = styled.header`
background-color: #001529;
color: #ffffff;
margin-bottom: 50px;
`
const HeaderContainer = styled.header`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 16px;
height: 64px;
line-height: 64px;
padding: 0 50px;
@media (max-width: 600px) {
padding: 0 20px;
}
`
export interface FormSubmitParams {
chainId: number
txHash: string
receipt: TransactionReceipt
}
export const MainPage = () => {
const history = useHistory()
const { home, foreign } = useStateProvider()
const [networkName, setNetworkName] = useState('')
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
const setNetworkData = (chainId: number) => {
const network = chainId === home.chainId ? home.name : foreign.name
setNetworkName(network)
}
const onFormSubmit = ({ chainId, txHash, receipt }: FormSubmitParams) => {
setNetworkData(chainId)
setReceipt(receipt)
history.push(`/${chainId}/${txHash}`)
}
const resetNetworkHeader = () => {
setNetworkName('')
}
const setNetworkFromParams = (chainId: number) => {
setNetworkData(chainId)
}
return (
<StyledMainPage>
<Header>
<HeaderContainer>
<span>AMB Live Monitoring</span>
<span>{networkName}</span>
</HeaderContainer>
</Header>
<div className="container">
<Route exact path={['/']} children={<Form onSubmit={onFormSubmit} />} />
<Route
path={['/:chainId/:txHash/:messageIdParam', '/:chainId/:txHash']}
children={
<StatusContainer
onBackToMain={resetNetworkHeader}
setNetworkFromParams={setNetworkFromParams}
receiptParam={receipt}
/>
}
/>
</div>
</StyledMainPage>
)
}

View File

@@ -0,0 +1,47 @@
import React, { useState } from 'react'
import { Button } from './commons/Button'
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
import { useWindowWidth } from '@react-hook/window-size'
import { formatTxHashExtended } from '../utils/networks'
import { MessageObject } from '../utils/web3'
export interface MessageSelectorParams {
messages: Array<MessageObject>
onMessageSelected: (index: number) => void
}
export const MessageSelector = ({ messages, onMessageSelected }: MessageSelectorParams) => {
const [messageIndex, setMessageIndex] = useState(0)
const windowWidth = useWindowWidth()
const onSelect = () => {
onMessageSelected(messageIndex)
}
return (
<div className="row is-center">
<div className="col-7-lg col-12 is-marginless">
{messages.map((message, i) => (
<RadioButtonContainer className="row is-center is-vertical-align" key={i} onClick={() => setMessageIndex(i)}>
<input
className="is-marginless"
type="radio"
name="message"
value={i}
checked={i === messageIndex}
onChange={() => setMessageIndex(i)}
/>
<RadioButtonLabel htmlFor={i.toString()}>
{windowWidth < 700 ? formatTxHashExtended(message.id) : message.id}
</RadioButtonLabel>
</RadioButtonContainer>
))}
</div>
<div className="col-1-lg col-12 is-marginless">
<Button className="button outline" onClick={onSelect}>
Select
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
import React, { useState } from 'react'
import { Button } from './commons/Button'
import { RadioButtonLabel, RadioButtonContainer } from './commons/RadioButton'
import { useStateProvider } from '../state/StateProvider'
export const NetworkTransactionSelector = ({ onNetworkSelected }: { onNetworkSelected: (chainId: number) => void }) => {
const { home, foreign } = useStateProvider()
const [chainId, setChainId] = useState(home.chainId)
const networks = [home, foreign]
const onSelect = () => {
onNetworkSelected(chainId)
}
return (
<div>
<p>The transaction was found in both networks, please select one:</p>
<div className="row is-center">
<div className="col-3-lg col-12 is-marginless">
{networks.map((network, i) => (
<RadioButtonContainer
className="row is-center is-vertical-align"
key={i}
onClick={() => setChainId(network.chainId)}
>
<input
className="is-marginless"
type="radio"
name="message"
value={network.chainId}
checked={network.chainId === chainId}
onChange={() => setChainId(network.chainId)}
/>
<RadioButtonLabel htmlFor={i.toString()}>{network.name}</RadioButtonLabel>
</RadioButtonContainer>
))}
</div>
<div className="col-3-lg col-12 is-marginless">
<Button className="button outline" onClick={onSelect}>
Select
</Button>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,122 @@
import React, { useEffect } from 'react'
import { Link, useHistory, useParams } from 'react-router-dom'
import { useTransactionStatus } from '../hooks/useTransactionStatus'
import { formatTxHash, getExplorerTxUrl, getTransactionStatusDescription, validTxHash } from '../utils/networks'
import { TRANSACTION_STATUS } from '../config/constants'
import { MessageSelector } from './MessageSelector'
import { Loading } from './commons/Loading'
import { useStateProvider } from '../state/StateProvider'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { ConfirmationsContainer } from './ConfirmationsContainer'
import { LeftArrow } from './commons/LeftArrow'
import styled from 'styled-components'
import { TransactionReceipt } from 'web3-eth'
const BackButton = styled.button`
color: var(--button-color);
border-color: var(--font-color);
margin-top: 10px;
&:focus {
outline: var(--button-color);
}
`
const BackLabel = styled.label`
margin-left: 5px;
cursor: pointer;
`
export interface StatusContainerParam {
onBackToMain: () => void
setNetworkFromParams: (chainId: number) => void
receiptParam: Maybe<TransactionReceipt>
}
export const StatusContainer = ({ onBackToMain, setNetworkFromParams, receiptParam }: StatusContainerParam) => {
const { home, foreign } = useStateProvider()
const history = useHistory()
const { chainId, txHash, messageIdParam } = useParams()
const validChainId = chainId === home.chainId.toString() || chainId === foreign.chainId.toString()
const validParameters = validChainId && validTxHash(txHash)
const { messages, receipt, status, description, timestamp, loading } = useTransactionStatus({
txHash: validParameters ? txHash : '',
chainId: validParameters ? parseInt(chainId) : 0,
receiptParam
})
const selectedMessageId = messageIdParam === undefined || messages[messageIdParam] === undefined ? -1 : messageIdParam
useEffect(
() => {
if (validChainId) {
setNetworkFromParams(parseInt(chainId))
}
},
[validChainId, chainId, setNetworkFromParams]
)
if (!validParameters && home.chainId && foreign.chainId) {
return (
<div>
<p>
Chain Id: {chainId} and/or Transaction Hash: {txHash} are not valid
</p>
</div>
)
}
if (loading) {
return <Loading />
}
const onMessageSelected = (messageId: number) => {
history.push(`/${chainId}/${txHash}/${messageId}`)
}
const displayMessageSelector = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId === -1
const multiMessageSelected = status === TRANSACTION_STATUS.SUCCESS_MULTIPLE_MESSAGES && selectedMessageId !== -1
const displayReference = multiMessageSelected ? messages[selectedMessageId].id : txHash
const formattedMessageId = formatTxHash(displayReference)
const displayedDescription = multiMessageSelected
? getTransactionStatusDescription(TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE, timestamp)
: description
const isHome = chainId === home.chainId.toString()
const txExplorerLink = getExplorerTxUrl(txHash, isHome)
const displayExplorerLink = status !== TRANSACTION_STATUS.NOT_FOUND
const displayConfirmations = status === TRANSACTION_STATUS.SUCCESS_ONE_MESSAGE || multiMessageSelected
const messageToConfirm =
messages.length > 1 ? messages[selectedMessageId] : messages.length > 0 ? messages[0] : { id: '', data: '' }
return (
<div>
{status && (
<p>
The request{' '}
{displayExplorerLink && (
<ExplorerTxLink href={txExplorerLink} target="blank">
{formattedMessageId}
</ExplorerTxLink>
)}
{!displayExplorerLink && <label>{formattedMessageId}</label>} {displayedDescription}
</p>
)}
{displayMessageSelector && <MessageSelector messages={messages} onMessageSelected={onMessageSelected} />}
{displayConfirmations && (
<ConfirmationsContainer message={messageToConfirm} receipt={receipt} fromHome={isHome} timestamp={timestamp} />
)}
<div className="row is-center">
<div className="col-9">
<Link to="/" onClick={onBackToMain}>
<BackButton className="button outline is-left">
<LeftArrow />
<BackLabel>Search another transaction</BackLabel>
</BackButton>
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,47 @@
import React, { useEffect } from 'react'
import { useTransactionFinder } from '../hooks/useTransactionFinder'
import { useStateProvider } from '../state/StateProvider'
import { TRANSACTION_STATUS } from '../config/constants'
import { TransactionReceipt } from 'web3-eth'
import { Loading } from './commons/Loading'
import { NetworkTransactionSelector } from './NetworkTransactionSelector'
export const TransactionSelector = ({
txHash,
onSelected
}: {
txHash: string
onSelected: (chainId: number, receipt: TransactionReceipt) => void
}) => {
const { home, foreign } = useStateProvider()
const { receipt: homeReceipt, status: homeStatus } = useTransactionFinder({ txHash, web3: home.web3 })
const { receipt: foreignReceipt, status: foreignStatus } = useTransactionFinder({ txHash, web3: foreign.web3 })
useEffect(
() => {
if (!home.chainId || !foreign.chainId) return
if (homeStatus === TRANSACTION_STATUS.FOUND && foreignStatus === TRANSACTION_STATUS.NOT_FOUND) {
if (!homeReceipt) return
onSelected(home.chainId, homeReceipt)
} else if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
if (!foreignReceipt) return
onSelected(foreign.chainId, foreignReceipt)
}
},
[homeReceipt, homeStatus, foreignReceipt, foreignStatus, home.chainId, foreign.chainId, onSelected]
)
const onSelectedNetwork = (chainId: number) => {
const chain = chainId === home.chainId ? home.chainId : foreign.chainId
const receipt = chainId === home.chainId ? homeReceipt : foreignReceipt
if (!receipt) return
onSelected(chain, receipt)
}
if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.FOUND) {
return <NetworkTransactionSelector onNetworkSelected={onSelectedNetwork} />
}
return <Loading />
}

View File

@@ -0,0 +1,91 @@
import React from 'react'
import { formatTimestamp, formatTxHash, getExplorerTxUrl } from '../utils/networks'
import { useWindowWidth } from '@react-hook/window-size'
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { SimpleLoading } from './commons/Loading'
import styled from 'styled-components'
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
import { GreyLabel, RedLabel, SuccessLabel } from './commons/Labels'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
const RequiredConfirmations = styled.label`
font-size: 14px;
`
export interface ValidatorsConfirmationsParams {
confirmations: Array<ConfirmationParam>
requiredSignatures: number
validatorList: string[]
}
export const ValidatorsConfirmations = ({
confirmations,
requiredSignatures,
validatorList
}: ValidatorsConfirmationsParams) => {
const windowWidth = useWindowWidth()
const getValidatorStatusElement = (validatorStatus = '') => {
switch (validatorStatus) {
case VALIDATOR_CONFIRMATION_STATUS.SUCCESS:
return <SuccessLabel>{validatorStatus}</SuccessLabel>
case VALIDATOR_CONFIRMATION_STATUS.FAILED:
return <RedLabel>{validatorStatus}</RedLabel>
case VALIDATOR_CONFIRMATION_STATUS.PENDING:
case VALIDATOR_CONFIRMATION_STATUS.WAITING:
case VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED:
return <GreyLabel>{validatorStatus}</GreyLabel>
default:
return <SimpleLoading />
}
}
return (
<div>
<table>
<Thead>
<tr>
<th>Validator</th>
<th className="text-center">Status</th>
<th className="text-center">Age</th>
</tr>
</Thead>
<tbody>
{validatorList.map((validator, i) => {
const filteredConfirmation = confirmations.filter(c => c.validator === validator)
const confirmation = filteredConfirmation.length > 0 ? filteredConfirmation[0] : null
const displayedStatus = confirmation && confirmation.status ? confirmation.status : ''
const explorerLink = confirmation && confirmation.txHash ? getExplorerTxUrl(confirmation.txHash, true) : ''
const elementIfNoTimestamp =
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.WAITING &&
displayedStatus !== VALIDATOR_CONFIRMATION_STATUS.NOT_REQUIRED ? (
<SimpleLoading />
) : (
''
)
return (
<tr key={i}>
<td>{windowWidth < 850 ? formatTxHash(validator) : validator}</td>
<td className="text-center">{getValidatorStatusElement(displayedStatus)}</td>
<td className="text-center">
<ExplorerTxLink href={explorerLink} target="_blank">
{confirmation && confirmation.timestamp > 0
? formatTimestamp(confirmation.timestamp)
: elementIfNoTimestamp}
</ExplorerTxLink>
</td>
</tr>
)
})}
</tbody>
</table>
<RequiredConfirmations>
{requiredSignatures} of {validatorList.length} confirmations required
</RequiredConfirmations>
</div>
)
}

View File

@@ -0,0 +1,10 @@
import styled from 'styled-components'
export const Button = styled.button`
height: 36px;
color: var(--button-color);
border-color: var(--button-color);
&:focus {
outline: var(--button-color);
}
`

View File

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

View File

@@ -0,0 +1,22 @@
import styled from 'styled-components'
export const SuccessLabel = styled.label`
color: var(--success-color);
background-color: var(--success-bg-color);
padding: 0.4rem 0.7rem;
border-radius: 4px;
`
export const GreyLabel = styled.label`
color: var(--not-required-color);
background-color: var(--not-required-bg-color);
padding: 0.4rem 0.7rem;
border-radius: 4px;
`
export const RedLabel = styled.label`
color: var(--failed-color);
background-color: var(--failed-bg-color);
padding: 0.4rem 0.7rem;
border-radius: 4px;
`

View File

@@ -0,0 +1,20 @@
import React from 'react'
import { useContext } from 'react'
import { ThemeContext } from 'styled-components'
export const LeftArrow = () => {
const themeContext = useContext(ThemeContext)
return (
<svg
xmlns="http://www.w3.org/2000/svg"
version="1.1"
id="mdi-arrow-left"
width="24"
height="24"
viewBox="0 0 24 24"
fill={themeContext.buttonColor}
>
<path d="M20,11V13H8L13.5,18.5L12.08,19.92L4.16,12L12.08,4.08L13.5,5.5L8,11H20Z" />
</svg>
)
}

View File

@@ -0,0 +1,165 @@
import React, { useContext } from 'react'
import { ThemeContext } from 'styled-components'
export interface LoadingParams {
width?: string
height?: string
displayMessage?: boolean
}
export const Loading = ({ width = '50px', height = '50px', displayMessage = true }: LoadingParams) => {
const themeContext = useContext(ThemeContext)
return (
<div className="row is-center">
<svg
xmlns="http://www.w3.org/2000/svg"
style={{ background: 'none', display: 'block', shapeRendering: 'auto' }}
width={width}
height={height}
viewBox="0 0 100 100"
preserveAspectRatio="xMidYMid"
>
<g transform="rotate(0 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.9166666666666666s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(30 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.8333333333333334s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(60 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.75s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(90 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.6666666666666666s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(120 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.5833333333333334s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(150 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.5s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(180 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.4166666666666667s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(210 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.3333333333333333s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(240 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.25s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(270 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.16666666666666666s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(300 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate
attributeName="opacity"
values="1;0"
keyTimes="0;1"
dur="1s"
begin="-0.08333333333333333s"
repeatCount="indefinite"
/>
</rect>
</g>
<g transform="rotate(330 50 50)">
<rect x="47" y="24" rx="3" ry="6" width="6" height="12" fill={themeContext.buttonColor}>
<animate attributeName="opacity" values="1;0" keyTimes="0;1" dur="1s" begin="0s" repeatCount="indefinite" />
</rect>
</g>
</svg>
{displayMessage && <label style={{ color: themeContext.buttonColor }}>Loading...</label>}
</div>
)
}
export const SimpleLoading = () => <Loading width="30px" height="30px" displayMessage={false} />

View File

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

View File

@@ -0,0 +1,59 @@
export const HOME_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_HOME_BRIDGE_ADDRESS || ''
export const FOREIGN_BRIDGE_ADDRESS: string = process.env.REACT_APP_COMMON_FOREIGN_BRIDGE_ADDRESS || ''
export const HOME_RPC_URL: string = process.env.REACT_APP_COMMON_HOME_RPC_URL || ''
export const FOREIGN_RPC_URL: string = process.env.REACT_APP_COMMON_FOREIGN_RPC_URL || ''
export const HOME_NETWORK_NAME: string = process.env.REACT_APP_ALM_HOME_NETWORK_NAME || ''
export const FOREIGN_NETWORK_NAME: string = process.env.REACT_APP_ALM_FOREIGN_NETWORK_NAME || ''
export const HOME_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_HOME_EXPLORER_TX_TEMPLATE || ''
export const FOREIGN_EXPLORER_TX_TEMPLATE: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_TX_TEMPLATE || ''
export const HOME_EXPLORER_API: string = process.env.REACT_APP_ALM_HOME_EXPLORER_API || ''
export const FOREIGN_EXPLORER_API: string = process.env.REACT_APP_ALM_FOREIGN_EXPLORER_API || ''
export const HOME_RPC_POLLING_INTERVAL: number = 5000
export const FOREIGN_RPC_POLLING_INTERVAL: number = 5000
export const BLOCK_RANGE: number = 50
export const ONE_DAY_TIMESTAMP: number = 86400
export const THREE_DAYS_TIMESTAMP: number = 259200
export const EXECUTE_AFFIRMATION_HASH = 'e7a2c01f'
export const SUBMIT_SIGNATURE_HASH = '630cea8e'
export const EXECUTE_SIGNATURES_HASH = '3f7658fd'
export const CACHE_KEY_SUCCESS = 'success-confirmation-validator-'
export const CACHE_KEY_FAILED = 'failed-confirmation-validator-'
export const CACHE_KEY_EXECUTION_FAILED = 'failed-execution-validator-'
export const TRANSACTION_STATUS = {
SUCCESS_MULTIPLE_MESSAGES: 'SUCCESS_MULTIPLE_MESSAGES',
SUCCESS_ONE_MESSAGE: 'SUCCESS_ONE_MESSAGE',
SUCCESS_NO_MESSAGES: 'SUCCESS_NO_MESSAGES',
FAILED: 'FAILED',
FOUND: 'FOUND',
NOT_FOUND: 'NOT_FOUND',
UNDEFINED: 'UNDEFINED'
}
export const CONFIRMATIONS_STATUS = {
SUCCESS: 'SUCCESS',
SUCCESS_MESSAGE_FAILED: 'SUCCESS_MESSAGE_FAILED',
EXECUTION_FAILED: 'EXECUTION_FAILED',
EXECUTION_PENDING: 'EXECUTION_PENDING',
EXECUTION_WAITING: 'EXECUTION_WAITING',
FAILED: 'FAILED',
PENDING: 'PENDING',
WAITING: 'WAITING',
UNDEFINED: 'UNDEFINED'
}
export const VALIDATOR_CONFIRMATION_STATUS = {
SUCCESS: 'Success',
FAILED: 'Failed',
PENDING: 'Pending',
WAITING: 'Waiting',
NOT_REQUIRED: 'Not required',
UNDEFINED: 'UNDEFINED'
}

View File

@@ -0,0 +1,36 @@
// %t will be replaced by the time -> x minutes/hours/days ago
export const TRANSACTION_STATUS_DESCRIPTION: { [key: string]: string } = {
SUCCESS_MULTIPLE_MESSAGES: 'was initiated %t and contains several bridge messages. Specify one of them:',
SUCCESS_ONE_MESSAGE: 'was initiated %t',
SUCCESS_NO_MESSAGES: 'execution succeeded %t but it does not contain any bridge messages',
FAILED: 'failed %t',
NOT_FOUND: 'was not found'
}
export const CONFIRMATIONS_STATUS_LABEL: { [key: string]: string } = {
SUCCESS: 'Success',
SUCCESS_MESSAGE_FAILED: 'Success',
EXECUTION_FAILED: 'Execution failed',
EXECUTION_PENDING: 'Execution pending',
EXECUTION_WAITING: 'Execution waiting',
FAILED: 'Failed',
PENDING: 'Pending',
WAITING: 'Waiting'
}
// %homeChain will be replaced by the home network name
// %foreignChain will be replaced by the foreign network name
export const CONFIRMATIONS_STATUS_DESCRIPTION: { [key: string]: string } = {
SUCCESS: '',
SUCCESS_MESSAGE_FAILED:
'Signatures have been collected in the %homeChain and they were successfully sent to the %foreignChain but the contained message execution failed.',
EXECUTION_FAILED:
'Signatures have been collected in the %homeChain and they were sent to the %foreignChain but the transaction with signatures failed',
EXECUTION_PENDING:
'Signatures have been collected in the %homeChain and they were sent to the %foreignChain but the transaction is in the pending state (transactions congestion or low gas price)',
EXECUTION_WAITING: 'Execution waiting',
FAILED:
'Some validators sent improper transactions as so they were failed, collected confirmations are not enough to execute the relay request',
PENDING: 'Some confirmations are in pending state',
WAITING: 'Validators are waiting for the chain finalization'
}

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

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

View File

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

View File

@@ -0,0 +1,38 @@
import { useEffect, useState } from 'react'
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
import { FOREIGN_BRIDGE_ADDRESS, HOME_BRIDGE_ADDRESS } from '../config/constants'
import { Contract } from 'web3-eth-contract'
import Web3 from 'web3'
export interface useBridgeContractsParams {
homeWeb3: Web3
foreignWeb3: Web3
}
export const useBridgeContracts = ({ homeWeb3, foreignWeb3 }: useBridgeContractsParams) => {
const [homeBridge, setHomeBridge] = useState<Maybe<Contract>>(null)
const [foreignBridge, setForeignBridge] = useState<Maybe<Contract>>(null)
useEffect(
() => {
if (!homeWeb3) return
const homeContract = new homeWeb3.eth.Contract(HOME_AMB_ABI, HOME_BRIDGE_ADDRESS)
setHomeBridge(homeContract)
},
[homeWeb3]
)
useEffect(
() => {
if (!foreignWeb3) return
const foreignContract = new foreignWeb3.eth.Contract(FOREIGN_AMB_ABI, FOREIGN_BRIDGE_ADDRESS)
setForeignBridge(foreignContract)
},
[foreignWeb3]
)
return {
homeBridge,
foreignBridge
}
}

View File

@@ -0,0 +1,357 @@
import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth'
import { MessageObject } from '../utils/web3'
import { useEffect, useState } from 'react'
import { EventData } from 'web3-eth-contract'
import { getAffirmationsSigned, getMessagesSigned } from '../utils/contract'
import {
BLOCK_RANGE,
CONFIRMATIONS_STATUS,
FOREIGN_RPC_POLLING_INTERVAL,
HOME_RPC_POLLING_INTERVAL,
VALIDATOR_CONFIRMATION_STATUS
} from '../config/constants'
import { homeBlockNumberProvider, foreignBlockNumberProvider } from '../services/BlockNumberProvider'
import { checkSignaturesWaitingForBLocks } from '../utils/signatureWaitingForBlocks'
import { getCollectedSignaturesEvent } from '../utils/getCollectedSignaturesEvent'
import { checkWaitingBlocksForExecution } from '../utils/executionWaitingForBlocks'
import { getConfirmationsForTx } from '../utils/getConfirmationsForTx'
import { getFinalizationEvent } from '../utils/getFinalizationEvent'
import {
getValidatorFailedTransactionsForMessage,
getExecutionFailedTransactionForMessage,
getValidatorPendingTransactionsForMessage,
getExecutionPendingTransactionsForMessage,
getValidatorSuccessTransactionsForMessage
} from '../utils/explorer'
export interface useMessageConfirmationsParams {
message: MessageObject
receipt: Maybe<TransactionReceipt>
fromHome: boolean
timestamp: number
requiredSignatures: number
validatorList: string[]
blockConfirmations: number
}
export interface BasicConfirmationParam {
validator: string
status: string
}
export interface ConfirmationParam extends BasicConfirmationParam {
txHash: string
timestamp: number
}
export interface ExecutionData {
status: string
validator: string
txHash: string
timestamp: number
executionResult: boolean
}
export const useMessageConfirmations = ({
message,
receipt,
fromHome,
timestamp,
requiredSignatures,
validatorList,
blockConfirmations
}: useMessageConfirmationsParams) => {
const { home, foreign } = useStateProvider()
const [confirmations, setConfirmations] = useState<Array<ConfirmationParam>>([])
const [status, setStatus] = useState(CONFIRMATIONS_STATUS.UNDEFINED)
const [waitingBlocks, setWaitingBlocks] = useState(false)
const [waitingBlocksResolved, setWaitingBlocksResolved] = useState(false)
const [signatureCollected, setSignatureCollected] = useState(false)
const [collectedSignaturesEvent, setCollectedSignaturesEvent] = useState<Maybe<EventData>>(null)
const [executionData, setExecutionData] = useState<ExecutionData>({
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
validator: '',
txHash: '',
timestamp: 0,
executionResult: false
})
const [waitingBlocksForExecution, setWaitingBlocksForExecution] = useState(false)
const [waitingBlocksForExecutionResolved, setWaitingBlocksForExecutionResolved] = useState(false)
const [failedConfirmations, setFailedConfirmations] = useState(false)
const [failedExecution, setFailedExecution] = useState(false)
const [pendingConfirmations, setPendingConfirmations] = useState(false)
const [pendingExecution, setPendingExecution] = useState(false)
// Check if the validators are waiting for block confirmations to verify the message
useEffect(
() => {
if (!receipt || !blockConfirmations) return
const subscriptions: Array<number> = []
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
const blockProvider = fromHome ? homeBlockNumberProvider : foreignBlockNumberProvider
const interval = fromHome ? HOME_RPC_POLLING_INTERVAL : FOREIGN_RPC_POLLING_INTERVAL
const web3 = fromHome ? home.web3 : foreign.web3
blockProvider.start(web3)
const targetBlock = receipt.blockNumber + blockConfirmations
checkSignaturesWaitingForBLocks(
targetBlock,
setWaitingBlocks,
setWaitingBlocksResolved,
validatorList,
setConfirmations,
blockProvider,
interval,
subscriptions
)
return () => {
unsubscribe()
blockProvider.stop()
}
},
[blockConfirmations, foreign.web3, fromHome, validatorList, home.web3, receipt]
)
// The collected signature event is only fetched once the signatures are collected on tx from home to foreign, to calculate if
// the execution tx on the foreign network is waiting for block confirmations
// This is executed if the message is in Home to Foreign direction only
useEffect(
() => {
if (!fromHome || !receipt || !home.web3 || !signatureCollected) return
const subscriptions: Array<number> = []
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
homeBlockNumberProvider.start(home.web3)
const fromBlock = receipt.blockNumber
const toBlock = fromBlock + BLOCK_RANGE
const messageHash = home.web3.utils.soliditySha3Raw(message.data)
getCollectedSignaturesEvent(
home.web3,
home.bridgeContract,
fromBlock,
toBlock,
messageHash,
setCollectedSignaturesEvent,
subscriptions
)
return () => {
unsubscribe()
homeBlockNumberProvider.stop()
}
},
[fromHome, home.bridgeContract, home.web3, message.data, receipt, signatureCollected]
)
// Check if the responsible validator is waiting for block confirmations to execute the message on foreign network
// This is executed if the message is in Home to Foreign direction only
useEffect(
() => {
if (!fromHome || !home.web3 || !receipt || !collectedSignaturesEvent || !blockConfirmations) return
const subscriptions: Array<number> = []
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
homeBlockNumberProvider.start(home.web3)
const targetBlock = collectedSignaturesEvent.blockNumber + blockConfirmations
checkWaitingBlocksForExecution(
homeBlockNumberProvider,
HOME_RPC_POLLING_INTERVAL,
targetBlock,
collectedSignaturesEvent,
setWaitingBlocksForExecution,
setWaitingBlocksForExecutionResolved,
setExecutionData,
subscriptions
)
return () => {
unsubscribe()
homeBlockNumberProvider.stop()
}
},
[collectedSignaturesEvent, fromHome, blockConfirmations, home.web3, receipt]
)
// Checks if validators verified the message
// To avoid making extra requests, this is only executed when validators finished waiting for blocks confirmations
useEffect(
() => {
if (!waitingBlocksResolved || !timestamp || !requiredSignatures) return
const subscriptions: Array<number> = []
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
const confirmationContractMethod = fromHome ? getMessagesSigned : getAffirmationsSigned
getConfirmationsForTx(
message.data,
home.web3,
validatorList,
home.bridgeContract,
confirmationContractMethod,
setConfirmations,
requiredSignatures,
setSignatureCollected,
waitingBlocksResolved,
subscriptions,
timestamp,
getValidatorFailedTransactionsForMessage,
setFailedConfirmations,
getValidatorPendingTransactionsForMessage,
setPendingConfirmations,
getValidatorSuccessTransactionsForMessage
)
return () => {
unsubscribe()
}
},
[
fromHome,
message.data,
home.web3,
validatorList,
home.bridgeContract,
requiredSignatures,
waitingBlocksResolved,
timestamp
]
)
// Gets finalization event to display the information about the execution of the message
// In a message from Home to Foreign it will be executed after finishing waiting for block confirmations for the execution transaction on Foreign
// In a message from Foreign to Home it will be executed after finishing waiting for block confirmations of the message request
useEffect(
() => {
if ((fromHome && !waitingBlocksForExecutionResolved) || (!fromHome && !waitingBlocksResolved)) return
const subscriptions: Array<number> = []
const unsubscribe = () => {
subscriptions.forEach(s => {
clearTimeout(s)
})
}
const contractEvent = fromHome ? 'RelayedMessage' : 'AffirmationCompleted'
const bridgeContract = fromHome ? foreign.bridgeContract : home.bridgeContract
const providedWeb3 = fromHome ? foreign.web3 : home.web3
const interval = fromHome ? FOREIGN_RPC_POLLING_INTERVAL : HOME_RPC_POLLING_INTERVAL
getFinalizationEvent(
bridgeContract,
contractEvent,
providedWeb3,
setExecutionData,
waitingBlocksResolved,
message,
interval,
subscriptions,
timestamp,
collectedSignaturesEvent,
getExecutionFailedTransactionForMessage,
setFailedExecution,
getExecutionPendingTransactionsForMessage,
setPendingExecution
)
return () => {
unsubscribe()
}
},
[
fromHome,
foreign.bridgeContract,
home.bridgeContract,
message,
foreign.web3,
home.web3,
waitingBlocksResolved,
waitingBlocksForExecutionResolved,
timestamp,
collectedSignaturesEvent
]
)
// Sets the message status based in the collected information
useEffect(
() => {
if (executionData.status === VALIDATOR_CONFIRMATION_STATUS.SUCCESS) {
const newStatus = executionData.executionResult
? CONFIRMATIONS_STATUS.SUCCESS
: CONFIRMATIONS_STATUS.SUCCESS_MESSAGE_FAILED
setStatus(newStatus)
} else if (signatureCollected) {
if (fromHome) {
if (waitingBlocksForExecution) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_WAITING)
} else if (failedExecution) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_FAILED)
} else if (pendingExecution) {
setStatus(CONFIRMATIONS_STATUS.EXECUTION_PENDING)
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
}
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
}
} else if (waitingBlocks) {
setStatus(CONFIRMATIONS_STATUS.WAITING)
} else if (failedConfirmations) {
setStatus(CONFIRMATIONS_STATUS.FAILED)
} else if (pendingConfirmations) {
setStatus(CONFIRMATIONS_STATUS.PENDING)
} else {
setStatus(CONFIRMATIONS_STATUS.UNDEFINED)
}
},
[
executionData,
fromHome,
signatureCollected,
waitingBlocks,
waitingBlocksForExecution,
failedConfirmations,
failedExecution,
pendingConfirmations,
pendingExecution
]
)
return {
confirmations,
status,
signatureCollected,
executionData
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,68 @@
import { useEffect, useState } from 'react'
import { Contract } from 'web3-eth-contract'
import Web3 from 'web3'
import { getRequiredSignatures, getValidatorAddress, getValidatorList } from '../utils/contract'
import { BRIDGE_VALIDATORS_ABI } from '../abis'
import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth'
export interface useValidatorContractParams {
fromHome: boolean
receipt: Maybe<TransactionReceipt>
}
export const useValidatorContract = ({ receipt, fromHome }: useValidatorContractParams) => {
const [validatorContract, setValidatorContract] = useState<Maybe<Contract>>(null)
const [requiredSignatures, setRequiredSignatures] = useState(0)
const [validatorList, setValidatorList] = useState([])
const { home, foreign } = useStateProvider()
const callValidatorContract = async (bridgeContract: Maybe<Contract>, web3: Web3, setValidatorContract: Function) => {
if (!web3 || !bridgeContract) return
const address = await getValidatorAddress(bridgeContract)
const contract = new web3.eth.Contract(BRIDGE_VALIDATORS_ABI, address)
setValidatorContract(contract)
}
const callRequiredSignatures = async (
contract: Maybe<Contract>,
receipt: TransactionReceipt,
setResult: Function
) => {
if (!contract) return
const result = await getRequiredSignatures(contract, receipt.blockNumber)
setResult(result)
}
const callValidatorList = async (contract: Maybe<Contract>, receipt: TransactionReceipt, setResult: Function) => {
if (!contract) return
const result = await getValidatorList(contract, receipt.blockNumber)
setResult(result)
}
useEffect(
() => {
const web3 = fromHome ? home.web3 : foreign.web3
const bridgeContract = fromHome ? home.bridgeContract : foreign.bridgeContract
if (!web3 || !bridgeContract) return
callValidatorContract(bridgeContract, web3, setValidatorContract)
},
[home.web3, foreign.web3, home.bridgeContract, foreign.bridgeContract, fromHome]
)
useEffect(
() => {
if (!receipt) return
callRequiredSignatures(validatorContract, receipt, setRequiredSignatures)
callValidatorList(validatorContract, receipt, setValidatorList)
},
[validatorContract, receipt]
)
return {
requiredSignatures,
validatorList
}
}

16
alm/src/index.tsx Normal file
View File

@@ -0,0 +1,16 @@
import React from 'react'
import ReactDOM from 'react-dom'
import { ThemeProvider } from 'styled-components'
import { GlobalStyle } from './themes/GlobalStyle'
import App from './App'
import Light from './themes/Light'
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={Light}>
<GlobalStyle />
<App />
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
)

1
alm/src/react-app-env.d.ts vendored Normal file
View File

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

View File

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

View File

@@ -0,0 +1,29 @@
import { ConfirmationParam } from '../hooks/useMessageConfirmations'
class ValidatorsCache {
private readonly store: { [key: string]: boolean }
private readonly dataStore: { [key: string]: ConfirmationParam }
constructor() {
this.store = {}
this.dataStore = {}
}
get(key: string) {
return this.store[key]
}
set(key: string, value: boolean) {
this.store[key] = value
}
getData(key: string) {
return this.dataStore[key]
}
setData(key: string, value: ConfirmationParam) {
this.dataStore[key] = value
}
}
export default new ValidatorsCache()

5
alm/src/setupTests.ts Normal file
View File

@@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom/extend-expect'

View File

@@ -0,0 +1,78 @@
import React, { createContext, ReactNode } from 'react'
import { useNetwork } from '../hooks/useNetwork'
import {
HOME_RPC_URL,
FOREIGN_RPC_URL,
HOME_BRIDGE_ADDRESS,
FOREIGN_BRIDGE_ADDRESS,
HOME_NETWORK_NAME,
FOREIGN_NETWORK_NAME
} from '../config/constants'
import Web3 from 'web3'
import { useBridgeContracts } from '../hooks/useBridgeContracts'
import { Contract } from 'web3-eth-contract'
export interface BaseNetworkParams {
chainId: number
name: string
web3: Maybe<Web3>
bridgeAddress: string
bridgeContract: Maybe<Contract>
}
export interface StateContext {
home: BaseNetworkParams
foreign: BaseNetworkParams
loading: boolean
}
const initialState = {
home: {
chainId: 0,
name: '',
web3: null,
bridgeAddress: HOME_BRIDGE_ADDRESS,
bridgeContract: null
},
foreign: {
chainId: 0,
name: '',
web3: null,
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
bridgeContract: null
},
loading: true
}
const StateContext = createContext<StateContext>(initialState)
export const StateProvider = ({ children }: { children: ReactNode }) => {
const homeNetwork = useNetwork(HOME_RPC_URL)
const foreignNetwork = useNetwork(FOREIGN_RPC_URL)
const { homeBridge, foreignBridge } = useBridgeContracts({
homeWeb3: homeNetwork.web3,
foreignWeb3: foreignNetwork.web3
})
const value = {
home: {
bridgeAddress: HOME_BRIDGE_ADDRESS,
name: HOME_NETWORK_NAME,
bridgeContract: homeBridge,
...homeNetwork
},
foreign: {
bridgeAddress: FOREIGN_BRIDGE_ADDRESS,
name: FOREIGN_NETWORK_NAME,
bridgeContract: foreignBridge,
...foreignNetwork
},
loading: homeNetwork.loading || foreignNetwork.loading
}
return <StateContext.Provider value={value}>{children}</StateContext.Provider>
}
export const useStateProvider = (): StateContext => {
return React.useContext(StateContext)
}

22
alm/src/themes/Dark.tsx Normal file
View File

@@ -0,0 +1,22 @@
const theme = {
backgroundColor: '#121212',
fontColor: '#f5f5f5',
buttonColor: '#f5f5f5',
colorPrimary: '#272727',
colorGrey: '#272727',
colorLightGrey: '#272727',
linkColor: '#ffffff',
success: {
textColor: '#00c9a7',
backgroundColor: '#004d40'
},
notRequired: {
textColor: '#bdbdbd',
backgroundColor: '#424242'
},
failed: {
textColor: '#EF5350',
backgroundColor: '#4E342E'
}
}
export default theme

View File

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

22
alm/src/themes/Light.ts Normal file
View File

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

68
alm/src/utils/contract.ts Normal file
View File

@@ -0,0 +1,68 @@
import { Contract } from 'web3-eth-contract'
export const getRequiredBlockConfirmations = async (contract: Contract, blockNumber: number) => {
const events = await contract.getPastEvents('RequiredBlockConfirmationChanged', {
fromBlock: 0,
toBlock: blockNumber
})
let blockConfirmations
if (events.length > 0) {
// Use the value from last event before the transaction
const event = events[events.length - 1]
blockConfirmations = event.returnValues.requiredBlockConfirmations
} else {
// This is a special case where RequiredBlockConfirmationChanged was not emitted during initialization in early versions of AMB
// of Sokol - Kovan. In this case the current value is used.
blockConfirmations = await contract.methods.requiredBlockConfirmations().call()
}
return parseInt(blockConfirmations)
}
export const getValidatorAddress = (contract: Contract) => contract.methods.validatorContract().call()
export const getRequiredSignatures = async (contract: Contract, blockNumber: number) => {
const events = await contract.getPastEvents('RequiredSignaturesChanged', {
fromBlock: 0,
toBlock: blockNumber
})
// Use the value form last event before the transaction
const event = events[events.length - 1]
const { requiredSignatures } = event.returnValues
return parseInt(requiredSignatures)
}
export const getValidatorList = async (contract: Contract, blockNumber: number) => {
let currentList: string[] = await contract.methods.validatorList().call()
const [added, removed] = await Promise.all([
contract.getPastEvents('ValidatorAdded', {
fromBlock: blockNumber
}),
contract.getPastEvents('ValidatorRemoved', {
fromBlock: blockNumber
})
])
// Ordered desc
const orderedEvents = [...added, ...removed].sort(({ blockNumber: prev }, { blockNumber: next }) => next - prev)
// Stored as a Set to avoid duplicates
const validatorList = new Set(currentList)
orderedEvents.forEach(e => {
const { validator } = e.returnValues
if (e.event === 'ValidatorRemoved') {
validatorList.add(validator)
} else if (e.event === 'ValidatorAdded') {
validatorList.delete(validator)
}
})
return Array.from(validatorList)
}
export const getMessagesSigned = (contract: Contract, hash: string) => contract.methods.messagesSigned(hash).call()
export const getAffirmationsSigned = (contract: Contract, hash: string) =>
contract.methods.affirmationsSigned(hash).call()

View File

@@ -0,0 +1,58 @@
import { BlockNumberProvider } from '../services/BlockNumberProvider'
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { EventData } from 'web3-eth-contract'
export const checkWaitingBlocksForExecution = async (
blockProvider: BlockNumberProvider,
interval: number,
targetBlock: number,
collectedSignaturesEvent: EventData,
setWaitingBlocksForExecution: Function,
setWaitingBlocksForExecutionResolved: Function,
setExecutionData: Function,
subscriptions: number[]
) => {
const currentBlock = blockProvider.get()
if (currentBlock && currentBlock >= targetBlock) {
setExecutionData({
status: VALIDATOR_CONFIRMATION_STATUS.UNDEFINED,
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
txHash: '',
timestamp: 0,
executionResult: false
})
setWaitingBlocksForExecution(false)
setWaitingBlocksForExecutionResolved(true)
blockProvider.stop()
} else {
let nextInterval = interval
if (!currentBlock) {
nextInterval = 500
} else {
setWaitingBlocksForExecution(true)
setExecutionData({
status: VALIDATOR_CONFIRMATION_STATUS.WAITING,
validator: collectedSignaturesEvent.returnValues.authorityResponsibleForRelay,
txHash: '',
timestamp: 0,
executionResult: false
})
}
const timeoutId = setTimeout(
() =>
checkWaitingBlocksForExecution(
blockProvider,
interval,
targetBlock,
collectedSignaturesEvent,
setWaitingBlocksForExecution,
setWaitingBlocksForExecutionResolved,
setExecutionData,
subscriptions
),
nextInterval
)
subscriptions.push(timeoutId)
}
}

280
alm/src/utils/explorer.ts Normal file
View File

@@ -0,0 +1,280 @@
import {
EXECUTE_AFFIRMATION_HASH,
EXECUTE_SIGNATURES_HASH,
FOREIGN_EXPLORER_API,
HOME_EXPLORER_API,
SUBMIT_SIGNATURE_HASH
} from '../config/constants'
export interface APITransaction {
timeStamp: string
isError: string
input: string
to: string
hash: string
}
export interface APIPendingTransaction {
input: string
to: string
hash: string
}
export interface PendingTransactionsParams {
account: string
api: string
}
export interface AccountTransactionsParams {
account: string
to: string
startTimestamp: number
endTimestamp: number
api: string
}
export interface GetFailedTransactionParams {
account: string
to: string
messageData: string
startTimestamp: number
endTimestamp: number
}
export interface GetPendingTransactionParams {
account: string
to: string
messageData: string
}
export const fetchAccountTransactionsFromBlockscout = async ({
account,
to,
startTimestamp,
endTimestamp,
api
}: AccountTransactionsParams): Promise<APITransaction[]> => {
const url = `${api}?module=account&action=txlist&address=${account}&filterby=from=${account}&to=${to}&starttimestamp=${startTimestamp}&endtimestamp=${endTimestamp}`
try {
const result = await fetch(url).then(res => res.json())
if (result.status === '0') {
return []
}
return result.result
} catch (e) {
console.log(e)
return []
}
}
export const getBlockByTimestampUrl = (api: string, timestamp: number) =>
`${api}&module=block&action=getblocknobytime&timestamp=${timestamp}&closest=before`
export const fetchAccountTransactionsFromEtherscan = async ({
account,
to,
startTimestamp,
endTimestamp,
api
}: AccountTransactionsParams): Promise<APITransaction[]> => {
const startBlockUrl = getBlockByTimestampUrl(api, startTimestamp)
const endBlockUrl = getBlockByTimestampUrl(api, endTimestamp)
let fromBlock = 0
let toBlock = 9999999999999
try {
const [fromBlockResult, toBlockResult] = await Promise.all([
fetch(startBlockUrl).then(res => res.json()),
fetch(endBlockUrl).then(res => res.json())
])
if (fromBlockResult.status !== '0') {
fromBlock = parseInt(fromBlockResult.result)
}
if (toBlockResult.status !== '0') {
toBlock = parseInt(toBlockResult.result)
}
} catch (e) {
console.log(e)
return []
}
const url = `${api}&module=account&action=txlist&address=${account}&startblock=${fromBlock}&endblock=${toBlock}`
try {
const result = await fetch(url).then(res => res.json())
if (result.status === '0') {
return []
}
const toAddressLowerCase = to.toLowerCase()
const transactions: APITransaction[] = result.result
return transactions.filter(t => t.to.toLowerCase() === toAddressLowerCase)
} catch (e) {
console.log(e)
return []
}
}
export const fetchAccountTransactions = (api: string) => {
return api.includes('blockscout') ? fetchAccountTransactionsFromBlockscout : fetchAccountTransactionsFromEtherscan
}
export const fetchPendingTransactions = async ({
account,
api
}: PendingTransactionsParams): Promise<APIPendingTransaction[]> => {
const url = `${api}?module=account&action=pendingtxlist&address=${account}`
try {
const result = await fetch(url).then(res => res.json())
if (result.status === '0') {
return []
}
return result.result
} catch (e) {
return []
}
}
export const getFailedTransactions = async (
account: string,
to: string,
startTimestamp: number,
endTimestamp: number,
api: string,
fetchAccountTransactions: (args: AccountTransactionsParams) => Promise<APITransaction[]>
): Promise<APITransaction[]> => {
const transactions = await fetchAccountTransactions({ account, to, startTimestamp, endTimestamp, api })
return transactions.filter(t => t.isError !== '0')
}
export const getSuccessTransactions = async (
account: string,
to: string,
startTimestamp: number,
endTimestamp: number,
api: string,
fetchAccountTransactions: (args: AccountTransactionsParams) => Promise<APITransaction[]>
): Promise<APITransaction[]> => {
const transactions = await fetchAccountTransactions({ account, to, startTimestamp, endTimestamp, api })
return transactions.filter(t => t.isError === '0')
}
export const filterValidatorSignatureTransaction = (
transactions: APITransaction[],
messageData: string
): APITransaction[] => {
const messageDataValue = messageData.replace('0x', '')
return transactions.filter(
t =>
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
t.input.includes(messageDataValue)
)
}
export const getValidatorFailedTransactionsForMessage = async ({
account,
to,
messageData,
startTimestamp,
endTimestamp
}: GetFailedTransactionParams): Promise<APITransaction[]> => {
const failedTransactions = await getFailedTransactions(
account,
to,
startTimestamp,
endTimestamp,
HOME_EXPLORER_API,
fetchAccountTransactionsFromBlockscout
)
return filterValidatorSignatureTransaction(failedTransactions, messageData)
}
export const getValidatorSuccessTransactionsForMessage = async ({
account,
to,
messageData,
startTimestamp,
endTimestamp
}: GetFailedTransactionParams): Promise<APITransaction[]> => {
const transactions = await getSuccessTransactions(
account,
to,
startTimestamp,
endTimestamp,
HOME_EXPLORER_API,
fetchAccountTransactionsFromBlockscout
)
return filterValidatorSignatureTransaction(transactions, messageData)
}
export const getExecutionFailedTransactionForMessage = async ({
account,
to,
messageData,
startTimestamp,
endTimestamp
}: GetFailedTransactionParams): Promise<APITransaction[]> => {
const failedTransactions = await getFailedTransactions(
account,
to,
startTimestamp,
endTimestamp,
FOREIGN_EXPLORER_API,
fetchAccountTransactions(FOREIGN_EXPLORER_API)
)
const messageDataValue = messageData.replace('0x', '')
return failedTransactions.filter(t => t.input.includes(EXECUTE_SIGNATURES_HASH) && t.input.includes(messageDataValue))
}
export const getValidatorPendingTransactionsForMessage = async ({
account,
to,
messageData
}: GetPendingTransactionParams): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactions({
account,
api: HOME_EXPLORER_API
})
const toAddressLowerCase = to.toLowerCase()
const messageDataValue = messageData.replace('0x', '')
return pendingTransactions.filter(
t =>
t.to.toLowerCase() === toAddressLowerCase &&
(t.input.includes(SUBMIT_SIGNATURE_HASH) || t.input.includes(EXECUTE_AFFIRMATION_HASH)) &&
t.input.includes(messageDataValue)
)
}
export const getExecutionPendingTransactionsForMessage = async ({
account,
to,
messageData
}: GetPendingTransactionParams): Promise<APIPendingTransaction[]> => {
const pendingTransactions = await fetchPendingTransactions({
account,
api: FOREIGN_EXPLORER_API
})
const toAddressLowerCase = to.toLowerCase()
const messageDataValue = messageData.replace('0x', '')
return pendingTransactions.filter(
t =>
t.to.toLowerCase() === toAddressLowerCase &&
t.input.includes(EXECUTE_SIGNATURES_HASH) &&
t.input.includes(messageDataValue)
)
}

View File

@@ -0,0 +1,53 @@
import Web3 from 'web3'
import { Contract, EventData } from 'web3-eth-contract'
import { homeBlockNumberProvider } from '../services/BlockNumberProvider'
import { BLOCK_RANGE } from '../config/constants'
export const getCollectedSignaturesEvent = async (
web3: Maybe<Web3>,
contract: Maybe<Contract>,
fromBlock: number,
toBlock: number,
messageHash: string,
setCollectedSignaturesEvent: Function,
subscriptions: number[]
) => {
if (!web3 || !contract) return
const currentBlock = homeBlockNumberProvider.get()
let events: EventData[] = []
let securedToBlock = toBlock
if (currentBlock) {
// prevent errors if the toBlock parameter is bigger than the latest
securedToBlock = toBlock >= currentBlock ? currentBlock : toBlock
events = await contract.getPastEvents('CollectedSignatures', {
fromBlock,
toBlock: securedToBlock
})
}
const filteredEvents = events.filter(e => e.returnValues.messageHash === messageHash)
if (filteredEvents.length) {
const event = filteredEvents[0]
setCollectedSignaturesEvent(event)
homeBlockNumberProvider.stop()
} else {
const newFromBlock = currentBlock ? securedToBlock : fromBlock
const newToBlock = currentBlock ? toBlock + BLOCK_RANGE : toBlock
const timeoutId = setTimeout(
() =>
getCollectedSignaturesEvent(
web3,
contract,
newFromBlock,
newToBlock,
messageHash,
setCollectedSignaturesEvent,
subscriptions
),
500
)
subscriptions.push(timeoutId)
}
}

View File

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

View File

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

41
alm/src/utils/networks.ts Normal file
View File

@@ -0,0 +1,41 @@
import { formatDistance } from 'date-fns'
import { CONFIRMATIONS_STATUS_DESCRIPTION, TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
import { FOREIGN_EXPLORER_TX_TEMPLATE, HOME_EXPLORER_TX_TEMPLATE } from '../config/constants'
export const validTxHash = (txHash: string) => /^0x[a-fA-F0-9]{64}$/.test(txHash)
export const formatTxHash = (txHash: string) => `${txHash.substring(0, 6)}...${txHash.substring(txHash.length - 4)}`
export const getExplorerTxUrl = (txHash: string, isHome: boolean) => {
const template = isHome ? HOME_EXPLORER_TX_TEMPLATE : FOREIGN_EXPLORER_TX_TEMPLATE
return template.replace('%s', txHash)
}
export const formatTxHashExtended = (txHash: string) =>
`${txHash.substring(0, 10)}...${txHash.substring(txHash.length - 8)}`
export const formatTimestamp = (timestamp: number): string => {
const txDate = new Date(0).setUTCSeconds(timestamp)
return formatDistance(txDate, new Date(), {
addSuffix: true
})
}
export const getTransactionStatusDescription = (status: string, timestamp: Maybe<number> = null) => {
let description = TRANSACTION_STATUS_DESCRIPTION[status]
if (timestamp) {
description = description.replace('%t', formatTimestamp(timestamp))
}
return description
}
export const getConfirmationsStatusDescription = (status: string, home: string, foreign: string) => {
let description = CONFIRMATIONS_STATUS_DESCRIPTION[status]
description = description.replace('%homeChain', home)
description = description.replace('%foreignChain', foreign)
return description
}

View File

@@ -0,0 +1,50 @@
import { VALIDATOR_CONFIRMATION_STATUS } from '../config/constants'
import { BlockNumberProvider } from '../services/BlockNumberProvider'
export const checkSignaturesWaitingForBLocks = async (
targetBlock: number,
setWaitingStatus: Function,
setWaitingBlocksResolved: Function,
validatorList: string[],
setConfirmations: Function,
blockProvider: BlockNumberProvider,
interval: number,
subscriptions: number[]
) => {
const currentBlock = blockProvider.get()
if (currentBlock && currentBlock >= targetBlock) {
setWaitingStatus(false)
setWaitingBlocksResolved(true)
blockProvider.stop()
} else {
let nextInterval = interval
if (!currentBlock) {
nextInterval = 500
} else {
const validatorsWaiting = validatorList.map(validator => {
return {
validator,
status: VALIDATOR_CONFIRMATION_STATUS.WAITING
}
})
setWaitingStatus(true)
setConfirmations(validatorsWaiting)
}
const timeoutId = setTimeout(
() =>
checkSignaturesWaitingForBLocks(
targetBlock,
setWaitingStatus,
setWaitingBlocksResolved,
validatorList,
setConfirmations,
blockProvider,
interval,
subscriptions
),
nextInterval
)
subscriptions.push(timeoutId)
}
}

63
alm/src/utils/web3.ts Normal file
View File

@@ -0,0 +1,63 @@
import Web3 from 'web3'
import { BlockTransactionString } from 'web3-eth'
import { TransactionReceipt } from 'web3-eth'
import { AbiItem } from 'web3-utils'
import memoize from 'fast-memoize'
import promiseRetry from 'promise-retry'
import { HOME_AMB_ABI, FOREIGN_AMB_ABI } from '../abis'
export interface MessageObject {
id: string
data: string
}
const rawGetWeb3 = (url: string) => new Web3(new Web3.providers.HttpProvider(url))
const memoized = memoize(rawGetWeb3)
export const getWeb3 = (url: string) => memoized(url)
export const filterEventsByAbi = (
txReceipt: TransactionReceipt,
web3: Web3,
bridgeAddress: string,
eventAbi: AbiItem
): MessageObject[] => {
const eventHash = web3.eth.abi.encodeEventSignature(eventAbi)
const events = txReceipt.logs.filter(e => e.address === bridgeAddress && e.topics[0] === eventHash)
return events.map(e => {
let decodedLogs: { [p: string]: string } = {
messageId: '',
encodedData: ''
}
if (eventAbi && eventAbi.inputs && eventAbi.inputs.length) {
decodedLogs = web3.eth.abi.decodeLog(eventAbi.inputs, e.data, [e.topics[1]])
}
return { id: decodedLogs.messageId, data: decodedLogs.encodedData }
})
}
export const getHomeMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
const UserRequestForSignatureAbi: AbiItem = HOME_AMB_ABI.filter(
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForSignature'
)[0]
return filterEventsByAbi(txReceipt, web3, bridgeAddress, UserRequestForSignatureAbi)
}
export const getForeignMessagesFromReceipt = (txReceipt: TransactionReceipt, web3: Web3, bridgeAddress: string) => {
const userRequestForAffirmationAbi: AbiItem = FOREIGN_AMB_ABI.filter(
(e: AbiItem) => e.type === 'event' && e.name === 'UserRequestForAffirmation'
)[0]
return filterEventsByAbi(txReceipt, web3, bridgeAddress, userRequestForAffirmationAbi)
}
// In some rare cases the block data is not available yet for the block of a new event detected
// so this logic retry to get the block in case it fails
export const getBlock = async (web3: Web3, blockNumber: number): Promise<BlockTransactionString> =>
promiseRetry(async retry => {
const result = await web3.eth.getBlock(blockNumber)
if (!result) {
return retry('Error getting block data')
}
return result
})

25
alm/tsconfig.json Normal file
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,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": {}
}

View File

Before

Width:  |  Height:  |  Size: 3.8 KiB

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,42 @@
{
"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"
},
"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,53 @@
import { Gateway } from '@burner-wallet/core/gateways'
import Web3 from 'web3'
export default class LocalhostGateway extends Gateway {
private readonly providers: object
private readonly providerStrings: { [id: string]: string }
constructor() {
super()
this.providerStrings = {
'111': 'http://localhost:8545',
'1337': 'http://localhost:8546'
}
this.providers = {}
}
isAvailable() {
return true
}
getNetworks() {
return ['111', '1337']
}
_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 LocalhostGateway`)
}
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('LocalhostGateway 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,125 @@
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,
NativeMediatorAsset,
MediatorErcToNative,
BridgeableERC20Asset
} from '@poanet/tokenbridge-bw-exchange'
import MetamaskPlugin from '@burner-wallet/metamask-plugin'
import LocalhostGateway from './LocalhostGateway'
let assetIdAtHome = 'assetAtHome'
const assetIdAtForeign = 'assetAtForeign'
let assetAtHome: Asset
let assetAtForeign: Asset
let testBridge: Mediator
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
})
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
})
} else if (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
})
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
})
} else {
// process.env.REACT_APP_MODE === 'AMB_ERC20_TO_NATIVE'
assetAtHome = new NativeMediatorAsset({
id: assetIdAtHome,
name: 'qDAI',
network: process.env.REACT_APP_HOME_NETWORK as string,
mediatorAddress: process.env.REACT_APP_HOME_MEDIATOR_ADDRESS
})
assetAtForeign = new BridgeableERC20Asset({
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,
// @ts-ignore
bridgeAddress: process.env.REACT_APP_FOREIGN_MEDIATOR_ADDRESS
})
testBridge = new MediatorErcToNative({
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 LocalhostGateway(),
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,26 @@
{
"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,
"noImplicitAny": false,
"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,51 @@
import { ERC20Asset } from '@burner-wallet/assets'
import { MEDIATOR_ABI, constants } from '../../utils'
import { toBN } from 'web3-utils'
interface BridgeableERC20Constructor {
abi?: object
address: string
id: string
name: string
network: string
bridgeAddress: string
}
export default class BridgeableERC20Asset extends ERC20Asset {
protected bridgeAddress: string
private _bridge
constructor({ bridgeAddress, ...params }: BridgeableERC20Constructor) {
super({ ...params })
this.bridgeAddress = bridgeAddress.toLowerCase()
}
getBridgeContract() {
if (!this._bridge) {
const Contract = this.getWeb3().eth.Contract
this._bridge = new Contract(MEDIATOR_ABI, this.bridgeAddress)
}
return this._bridge
}
async _send({ from, to, value }) {
if (to.toLowerCase() === this.bridgeAddress) {
const allowance = await this.allowance(from, to)
if (toBN(allowance).lt(toBN(value))) {
await this.approve(from, to, value)
}
const receipt = await this.getBridgeContract()
.methods.relayTokens(from, value)
.send({ from })
const transferLog = Object.values(receipt.events as object).find(
e => e.raw.topics[0] === constants.TRANSFER_TOPIC
)
return {
...receipt,
txHash: receipt.transactionHash,
id: `${receipt.transactionHash}-${transferLog.logIndex}`
}
}
return super._send({ from, to, value })
}
}

View File

@@ -0,0 +1,78 @@
import { ERC20Asset } from '@burner-wallet/assets'
import { ERC677_ABI, constants } from '../../utils'
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: [constants.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
}
}

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