Compare commits

...

250 Commits

Author SHA1 Message Date
ce515a8635
Improve contract fetching
Some checks failed
tokenbridge / initialize (push) Has been cancelled
tokenbridge / build-e2e-images (push) Has been cancelled
tokenbridge / build-molecule-runner (push) Has been cancelled
tokenbridge / validate (build) (push) Has been cancelled
tokenbridge / validate (lint) (push) Has been cancelled
tokenbridge / validate (test) (push) Has been cancelled
tokenbridge / e2e (alm-e2e, true) (push) Has been cancelled
tokenbridge / e2e (monitor-e2e) (push) Has been cancelled
tokenbridge / e2e (oracle-e2e) (push) Has been cancelled
tokenbridge / deployment (monitor) (push) Has been cancelled
tokenbridge / deployment (multiple) (push) Has been cancelled
tokenbridge / deployment (oracle) (push) Has been cancelled
tokenbridge / deployment (repo) (push) Has been cancelled
tokenbridge / ultimate (amb) (push) Has been cancelled
tokenbridge / ultimate (erc-to-native) (push) Has been cancelled
2024-05-08 23:45:31 +00:00
Alexander Kolotov
961b12b9f3
Naming convention for oracle (#661) 2022-10-28 14:07:02 +03:00
Alexander Kolotov
ff9f3fb7d6
Merge the develop branch to the master branch, preparation to v3.7.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Periodic check for RPC sync state (#656)
2022-05-25 14:09:36 +03:00
Kirill Fedoseev
297cb67895
Periodic check for RPC sync state (#656) 2022-05-25 13:54:47 +03:00
Alexander Kolotov
bcf16144c1
Merge the develop branch to the master branch, preparation to v3.6.0
This merge contains the following set of changes:
  * [Oracle, Fix] Add missing Ganache nonce error message (#651)
  * [ALM, Improvement] ALM: show manually added signatures (#653)
2022-04-29 19:13:41 +03:00
Kirill Fedoseev
16f3e9add6
ALM: show manually added signatures (#653) 2022-04-29 16:38:08 +03:00
Ho Xuan Cuong
4af882b0ff
Add missing Ganache nonce error message (#651) 2022-04-08 13:26:55 +04:00
Alexander Kolotov
dbaf7feca7
Merge the develop branch to the master branch, preparation to v3.5.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Helpers for overriding AMB signatures (#640)
  * [Oracle, Improvement] Support manual signatures in erc to native mode (#646)
  * [ALM, Improvement] ALM: Do not show unneeded failed confirmations (#641)
  * [ALM, Improvement] ALM: warning for auto-relayed messages (#644)
  * [ALM, Fix] ALM: reorder warnings
2022-03-18 17:07:42 +03:00
Kirill Fedoseev
735aa75f81 ALM: reorder warnings 2022-03-17 17:41:33 +04:00
Kirill Fedoseev
910c3759c1
ALM: warning for auto-relayed messages (#644) 2022-03-17 14:11:14 +03:00
Kirill Fedoseev
9c2d2f404c
ALM: Do not show unneeded failed confirmations (#641) 2022-03-13 13:54:33 +03:00
Kirill Fedoseev
2b51d4c209
Support manual signatures in erc to native mode (#646) 2022-03-10 14:18:19 +03:00
Kirill Fedoseev
981231fb47
Helpers for overriding AMB signatures (#640) 2022-02-18 17:35:12 +03:00
Alexander Kolotov
5bc562e810
Merge the develop branch to the master branch, preparation to v3.4.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Refetch old logs ranges to see if there are missed events (#627)
  * [Oracle, Improvement] Add support for EIP1559 gas price oracle (#631)
  * [Oracle, Improvement] CollectedSignatures AMB watcher for MEV bundling (#634)
  * [Oracle, Fix] Fix eip1559 transaction sending problems (#632)
2022-02-11 10:24:38 +03:00
Kirill Fedoseev
72f0d30b52
CollectedSignatures AMB watcher for MEV bundling (#634) 2022-02-07 23:32:41 +03:00
Kirill Fedoseev
8ec11d0476
Fix eip1559 transaction sending problems (#632) 2022-01-17 16:51:29 +03:00
Kirill Fedoseev
8d732adba1
Add support for EIP1559 gas price oracle (#631) 2022-01-03 17:58:36 +03:00
Kirill Fedoseev
296e5c5a22
Refetch old logs ranges to see if there are missed events (#627) 2022-01-03 15:32:38 +03:00
Alexander Kolotov
b17fff2b56
Merge the develop branch to the master branch, preparation to v3.3.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Add oracle helper script for fetching interest amounts via async calls (#615)
  * [Monitor, Fix] Include cDAI balance in balanceDiff (#613)
  * [Monitor, Fix] Added logging prior the investedAmount call (#614)
  * [ALM, Fix] Fetch AMB signatures a bit earlier (#620)
2021-11-05 20:22:46 +03:00
Kirill Fedoseev
4eba91ef7e
Fetch AMB signatures a bit earlier (#620) 2021-11-03 19:21:35 +03:00
Kirill Fedoseev
1e3aa53ab3
Add oracle helper script for fetching interest amounts via async calls (#615) 2021-10-21 13:33:12 +03:00
Alexander Kolotov
a05ff51555
Added logging prior the investedAmount call (#614) 2021-10-12 18:06:50 +03:00
Kirill Fedoseev
52e1c88b58
Include cDAI balance in balanceDiff (#613) 2021-10-11 12:14:55 +03:00
Alexander Kolotov
d36dcadd34
Merge the develop branch to the master branch, preparation to v3.2.0
This merge contains the following set of changes:
  * [Oracle, Fix] Use more accurate gas estimates for very high message gas limits (#611)
  * [Common, Other] Final version of AMB and OB contracts security audit report by ChainSecurity (#608)
2021-10-07 21:45:51 +03:00
Kirill Fedoseev
d543dbb339
Use more accurate gas estimates for very high message gas limits (#611) 2021-10-07 20:44:14 +03:00
Kirill Fedoseev
bd21cc163e
Merge pull request #608 from poanetwork/reports/chainsecurity-amb-6.0.0-ob-1.1.0-contracts
AMB and OB contracts security audit report by ChainSecurity
2021-09-28 21:12:37 +03:00
Alexander Kolotov
4a0fc936a1
Final version of AMB and OB contracts security audit report by ChainSecurity 2021-09-28 20:39:04 +03:00
Alexander Kolotov
78564afabd
Merge the develop branch to the master branch, preparation to v3.1.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Try to detect unsynced node state (#592)
  * [Oracle, Improvement] Allow to override JSON RPC error codes (#603)
  * [Oracle, Fix] Fix handling of Compound related Transfer events (#595)
  * [Oracle, Fix] Add new nonce-related error messages (#599)
  * [Deployment, Fix] .env template includes latest changes related to the oracle configuration (#601)
  * [Deployment, Fix] Improvements for the local logs configuration (#602)
  * [Common, Other] Update the contract's submodule to the release 6.0.0 (#600)
2021-09-20 23:56:37 +03:00
Kirill Fedoseev
70a2c30b4c Fix imported ABI 2021-09-20 13:31:37 +03:00
Kirill Fedoseev
06a9586148 Update yarn.lock 2021-09-20 12:50:07 +03:00
Kirill Fedoseev
7379fe4190
Allow to override JSON RPC error codes (#603) 2021-09-20 12:07:49 +03:00
Alexander Kolotov
e59766c5df
Update the contract's submodule to the release 6.0.0 (#600) 2021-09-18 13:44:48 +03:00
Alexander Kolotov
fdb18a1a17
Improvements for the local logs configuration (#602) 2021-09-18 13:42:29 +03:00
Alexander Kolotov
4412046f66
.env template includes latest changes related to the oracle configuration (#601) 2021-09-18 13:41:56 +03:00
Kirill Fedoseev
0ff224ccd3
Add new nonce-related error messages (#599) 2021-09-15 23:42:32 +03:00
Kirill Fedoseev
5cedacafe5
Fix handling of Compound related Transfer events (#595) 2021-08-24 11:31:15 +03:00
Kirill Fedoseev
2e6179f974
Try to detect unsynced node state (#592) 2021-08-03 23:36:50 +03:00
Alexander Kolotov
4f5e3c47be
Merge the develop branch to the master branch, preparation to v3.0.0
This merge contains the following set of changes:
  * [Oracle, Improvement] Async calls error codes (#587)
  * [Oracle, Improvement] Refactor/async call serializers (#588)
2021-07-16 17:58:21 +03:00
Kirill Fedoseev
4c06329153
Refactor/async call serializers (#588) 2021-07-13 12:25:05 +03:00
Kirill Fedoseev
d53452675e
Async calls error codes (#587) 2021-07-13 11:57:08 +03:00
Alexander Kolotov
c92f80c484
Merge the develop branch to the master branch, preparation to v3.0.0-rc1
This merge contains the following set of changes:
  * [Monitor, Improvement] Add statistics about used AMB information requests (#577)
  * [ALM, Improvement] Add safe-execute button in ALM (#580), closes #573, closes #551
  * [Oracle, Improvement] Improve confirm-relay feature (#582), closes #569
  * [Monitor, Fix] Prune print of long error messages about missing file (#579), closes #578
  * [Oracle, Fix] Use safe approach for eth_getLogs requests (#581)
  * [Oracle, Fix] Fix logging in gas price service (#583), closes #552
  * [Oracle, Fix] Fix oracle error patterns and oracle e2e tests (#585)
  * [Common, Other] Fix dependencies versions and update example.yml (#566)
  * [Common, Other] Upload services logs in e2e and ultimate tests (#568)
  * [Oracle, Other] added example of emergency shutdown controller contract (#572)
  * [Oracle, Other] Refactor oracle configuration (#584)
2021-07-10 10:10:22 +03:00
Kirill Fedoseev
5e95b5d8c5
Fix oracle error patterns and oracle e2e tests (#585) 2021-07-08 16:42:56 +03:00
Kirill Fedoseev
6e1f57493a
Refactor oracle configuration (#584) 2021-07-08 09:38:55 +03:00
Kirill Fedoseev
8ed6550635
Improve confirm-relay feature (#582) 2021-07-07 16:21:01 +03:00
Kirill Fedoseev
c8eb0f1ed8
Fix logging in gas price service (#583) 2021-07-05 13:50:18 +03:00
Kirill Fedoseev
3e5e50c06e
Use safe approach for eth_getLogs requests (#581) 2021-06-30 12:28:03 +03:00
Kirill Fedoseev
92e1b597c4
Add safe-execute button in ALM (#580) 2021-06-18 09:43:18 +03:00
Kirill Fedoseev
8b1a97e673
Prune print of long error messages about missing file (#579) 2021-06-12 00:24:20 +03:00
Kirill Fedoseev
3cf184c391
Add statistics about used AMB information requests (#577) 2021-05-26 08:58:20 -06:00
Alexander Kolotov
8f72516374
added example of emergency shutdown controller contract (#572) 2021-05-16 15:42:36 -06:00
Kirill Fedoseev
98155e3075
Upload services logs in e2e and ultimate tests (#568) 2021-05-15 10:29:20 -06:00
Kirill Fedoseev
0d724147bd
Fix dependencies versions and update example.yml (#566) 2021-05-10 14:36:57 -06:00
Alexander Kolotov
3b959776f3
Merge the develop branch to the master branch, preparation to v3.0.0-rc0
This merge contains the following set of changes:
  * [Oracle, Improvement] Oracle watcher for AMB async calls (#509), 
  * [Common, Other] Update the contract's submodule to the release 6.0.0-rc0 (#562)
2021-05-09 07:59:36 -06:00
Kirill Fedoseev
ffbca8b941
Oracle watcher for AMB async calls (#509) 2021-05-09 07:34:19 -06:00
Alexander Kolotov
38f1bae8f5
Update the contract's submodule to the release 6.0.0-rc0 (#562) 2021-05-08 09:50:46 -06:00
Alexander Kolotov
9da1d7ab0a
Merge the develop branch to the master branch, preparation to v2.7.0-rc2
This merge contains the following set of changes:
  * [Monitor, Improvement] Include stats about failed and pending messages in mediators monitor endpoint (#544), closes #522
  * [Oracle, Fix] Fix remote shutdown sender behaviour (#550)
  * [Oracle, Fix] Force update of gas price when getting underprice tx error (#554), closes #545
  * [Oracle, Fix] Add oracle configuration option for max block range length (#557)
  * [Monitor, Fix] Limit block range of logs request (#543), closes #537
  * [Monitor, Fix] Add missing cache file save (#549)
  * [UI, Other] Remove UI from monorepo (#553)
  * [Common, Other] Update the contract's submodule to the release 5.7.0-rc0 (#548)
  * [Common, Other] Final version of the FT OB contract security audit report by ChainSecurity (#559)
2021-05-04 07:42:06 -06:00
Alexander Kolotov
78bcd7568b
Final version of the FT OB contract security audit report by ChainSecurity (#559) 2021-05-04 07:25:02 -06:00
Kirill Fedoseev
429312500a
Add oracle configuration option for max block range length (#557) 2021-04-26 10:47:38 -06:00
Kirill Fedoseev
59e0bf7565
Force update of gas price when getting underprice tx error (#554) 2021-04-18 12:30:40 -06:00
Kirill Fedoseev
ab51370d5a
Remove UI from monorepo (#553) 2021-04-16 03:31:12 -06:00
Kirill Fedoseev
9dfb0510c4
Fix remote shutdown sender behaviour (#550) 2021-04-14 14:35:43 -06:00
Kirill Fedoseev
f65e8f9244
Add missing cache file save (#549) 2021-04-14 08:55:13 -06:00
Alexander Kolotov
5c8b595382
Update the contract's submodule to the release 5.7.0-rc0 (#548) 2021-04-13 14:50:52 -06:00
Kirill Fedoseev
71bf9d5583
Limit block range of logs request (#543) 2021-04-13 08:05:01 -06:00
Kirill Fedoseev
329ded4beb
Include stats about failed and pending messages in mediators monitor endpoint (#544) 2021-04-13 07:59:49 -06:00
Alexander Kolotov
dc70247e2c
Merge the develop branch to the master branch, preparation to v2.7.0-rc1 2021-04-13 04:09:38 -06:00
Kirill Fedoseev
f95beee5dc
Add oracle service for remote shutdown (#531) 2021-04-12 14:38:42 -06:00
Leonid Tyurin
ae83c76be9
Fix monitor metrics (#533) 2021-04-02 03:59:49 -06:00
Kirill Fedoseev
dc3026e584
Fix undefined is not an object exception (#535) 2021-03-26 12:16:23 -06:00
Kirill Fedoseev
b6ba0744b9
Fix ALM react subscriptions leading to duplicated RPC requests (#534) 2021-03-18 00:04:33 -06:00
Kirill Fedoseev
4dba9a50e8
Extract tx resend intervals in env variables (#532) 2021-03-17 21:48:40 -06:00
Kirill Fedoseev
818bc4675d
Fallback eth_getLogs to explorer API for BSC (#530) 2021-03-14 07:39:23 -06:00
Alexander Kolotov
f93ab330cc
Merge the develop branch to the master branch, preparation to v2.7.0-rc0 (#515)
This merge contains the following set of changes:
  * [Improvement] Add /metrics endpoint for prometheus support (#512)
  * [Improvement] Add monitor script for detecting failed AMB messages (#513)
  * [Improvement] Improve performance of BS requests in ALM (#516)
  * [Fix] Add pretty error messages for manual execution transaction reverts (#511)
  * [Fix] Fix resend of stuck pending transactions (#514)
2021-02-25 20:42:51 -06:00
Leonid Tyurin
f64f8b1c91
Add /metrics endpoint for prometheus support (#512) 2021-02-25 20:39:48 -06:00
Kirill Fedoseev
9fd3f6ab82
Improve performance of BS requests in ALM (#516) 2021-02-25 20:38:13 -06:00
Kirill Fedoseev
626f9376b2
Fix resend of stuck pending transactions (#514) 2021-02-24 16:11:28 -06:00
Kirill Fedoseev
894134ba26
Add monitor script for detecting failed AMB messages (#513) 2021-02-18 19:26:07 -06:00
Kirill Fedoseev
e1536755f4
Add pretty error messages for manual execution transaction reverts (#511) 2021-02-18 19:25:01 -06:00
Alexander Kolotov
0451d6e373
Merge the develop branch to the master branch, preparation to v2.6.0 2021-01-11 20:17:53 -06:00
Kirill Fedoseev
409044b8a5
Add env variables for selective validator balance checks (#507) 2021-01-10 20:18:06 -06:00
Kirill Fedoseev
5fc52f42d7
ALM: fix blocking Blockscout/Etherscan requests (#505) 2021-01-07 13:35:23 -06:00
Alexander Kolotov
8a0d9f38b0
Added mediator endpoint in the monitor web service (#503) 2020-12-24 19:15:51 +03:00
Alexander Kolotov
1aee0a84ef
Merge the develop branch to the master branch, preparation to v2.6.0-rc3 2020-12-22 10:58:25 +03:00
Alexander Kolotov
811b1a27f1
setLogger added to the RedundantHttpListProvider (#501) 2020-12-21 22:16:37 +03:00
Kirill Fedoseev
4d468ae107
Detect all AMB mediators in monitor (#493) 2020-12-20 01:19:49 +03:00
Kirill Fedoseev
4497a024b1
Fix RPC urls in the ultimate tests (#498) 2020-12-20 01:15:46 +03:00
Kirill Fedoseev
6ce98ff3dd
Fetch signatures from RPC endpoint immediatly after enough signatures are collected (#499) 2020-12-20 01:13:49 +03:00
Kirill Fedoseev
04f66b243c
Handle RPC error about ancient blocks (#500) 2020-12-20 01:10:47 +03:00
Kirill Fedoseev
21581b3c01
Change message status when signatures are manually submitted (#497) 2020-12-07 23:12:30 +03:00
Kirill Fedoseev
bbc68f9fa2
ALM manual signatures execution (#471) 2020-11-26 00:22:21 +03:00
Kirill Fedoseev
5327688a20
Handle JSONRPC error codes in web3 providers (#491) 2020-11-12 02:20:03 +03:00
Alexander Kolotov
1122daf9a1
Merge the develop branch to the master branch, preparation to v2.6.0-rc2 2020-11-08 15:35:47 +03:00
Kirill Fedoseev
12269d7426
Remove access lists tests from ultimate test suites (#488) 2020-11-08 01:11:21 +03:00
Alexander Kolotov
683fa0728d
Update the contract's submodule to the release 5.5.0 (#486) 2020-11-07 16:22:53 +03:00
Alexander Kolotov
dd2075c351
Security Audit report by Quantstamp added (#485) 2020-11-07 13:15:35 +03:00
Kirill Fedoseev
ce29b95729
Support AMB manual lane and allowance/block lists in the monitor (#484) 2020-11-04 22:16:43 +03:00
Kirill Fedoseev
eb1069497a
Cache fetched events in monitor (#482) 2020-11-04 14:24:42 +03:00
Kirill Fedoseev
0228fc7d5f
Support of manual lane in the AMB oracle (#483) 2020-10-31 21:02:56 +03:00
Kirill Fedoseev
f8d85b14de
Add allowance/block lists support to monitor (#477) 2020-10-29 11:25:43 +03:00
Kirill Fedoseev
5fa9d21246
Fix set-env deprecation warnings (#478) 2020-10-28 15:35:11 +03:00
Kirill Fedoseev
611b8c539d
Improve workflow with several RPC URLs in oracle (#476) 2020-10-28 14:20:50 +03:00
Kirill Fedoseev
389cea3c39
Reduce time complexity of events comparison in monitor (#475) 2020-10-23 13:48:37 +03:00
Alexander Kolotov
fbce0fc035
Merge the develop branch to the master branch, preparation to v2.6.0-rc1 2020-10-14 22:29:18 +03:00
Max Alekseenko
621b20d070
Allow cors in monitor server (#472) 2020-10-12 21:16:41 +03:00
Alexander Kolotov
48752e8575
Remove nonce update for the re-send case (#470) 2020-10-08 18:51:01 +03:00
Kirill Fedoseev
4efda98f2b
Do not reset nonce, if nothing was resent (#469) 2020-10-07 15:28:57 +03:00
Kirill Fedoseev
aff8b777c5
Add requiredBlockConfirmations ABI to the NATIVE_TO_ERC_V1 bridge mode (#468) 2020-10-06 21:35:30 +03:00
Kirill Fedoseev
74293959f3
Fixed resent job for failed transactions (#466) 2020-10-06 15:15:35 +03:00
Kirill Fedoseev
46daeb6815
Respect requiredBlockConfirmations parameter in the monitor (#464) 2020-10-05 15:48:36 +03:00
Alexander Kolotov
44ca0d71ce
Merge the develop branch to the master branch, preparation to v2.6.0-rc0 2020-10-02 20:34:31 +03:00
Kirill Fedoseev
fbeb878cdb
Fix insufficient funds error handler (#459) 2020-10-02 19:46:07 +03:00
Alexander Kolotov
d17ea2ad2b
One point of handling all collected signatures requests (#457) 2020-10-02 19:44:05 +03:00
Kirill Fedoseev
4cc87ef61a
Use totalExecutedPerDay instead of totalSpentPerDay for calculating quota (#460) 2020-10-02 19:07:40 +03:00
Alexander Kolotov
125b66b86d
Update requirements for higher bound of gas price (#458) 2020-10-01 10:53:11 +03:00
Alexander Kolotov
7a0ed3f699
Small update of the execution waiting status description in ALM (#456) 2020-10-01 10:52:22 +03:00
Kirill Fedoseev
4e04f2ae1f
Update the condition for checking if a tx was stuck (#444) 2020-09-30 19:30:51 +03:00
Kirill Fedoseev
dc377aeb9b
Possibility to allow/block specific addresses in erc-to-native mode (#442) 2020-09-28 14:54:03 +03:00
Alexander Kolotov
48dd53622c
Merge the develop branch to the master branch, preparation to v2.5.0 2020-09-13 11:11:29 +03:00
Kirill Fedoseev
6fe63ae9f4
Possibility to resend old pending transactions (#425) 2020-09-12 17:01:37 +03:00
Kirill Fedoseev
4954c859c3
Migrate to GitHub actions (#423) 2020-09-02 17:43:48 +03:00
Kirill Fedoseev
27f059db94
Changed used RPC URLs delimiter from comma to space (#424) 2020-09-01 23:02:33 +03:00
Andrew Gross
686c415a5c
Update README.md (#422) 2020-09-01 22:56:54 +03:00
Alexander Kolotov
2e1b022512
Merge the develop branch to the master branch, preparation to v2.4.0 2020-08-24 00:28:20 +03:00
Kirill Fedoseev
f252ed2618
Add a possibility to fetch gas price from multiple sources (#420) 2020-08-23 22:56:59 +03:00
Gerardo Nardelli
bea91c0e6e
Remove half duplex erc20 token support from oracle (#413) 2020-08-11 23:37:47 +03:00
Alexander Kolotov
c2f6b5e8ba
Merge the develop branch to the master branch, preparation to v2.3.1 2020-08-11 20:13:58 +03:00
Alexander Kolotov
f3f226afdf
Correction of the message for a transaction without the bridge requests (#416) 2020-08-06 23:43:13 +03:00
Kirill Fedoseev
1eb8a8b1dc
Use requestGasLimit parameter in AMB oracle in estimateGas (#415) 2020-08-06 16:15:51 +03:00
Alexander Kolotov
8a42cfbe2b
Merge the develop branch to the master branch, preparation to v2.3.0 2020-07-29 16:13:24 +03:00
Gerardo Nardelli
ab406bc1db
Add alm basic e2e tests (#407) 2020-07-29 13:30:36 +03:00
Gerardo Nardelli
8cf73d572c
Correctly set a validator status as not required (#410) 2020-07-27 22:23:08 +03:00
Jongchan J
fa6b37db1c
Change the remote RPC url for Ethereum Classic (#408) 2020-07-26 00:16:10 +03:00
Alexander Kolotov
64cd258354
Merge the develop branch to the master branch, preparation to v2.3.0-rc1 2020-07-22 23:34:22 +03:00
Alexander Kolotov
7a48495118
Update the contract's submodule to the release 5.2.0 (#406) 2020-07-22 23:24:47 +03:00
Gerardo Nardelli
fdfa5cd7af
Add new status for when there is no registered activity by validators (#401) 2020-07-21 00:44:29 +03:00
Gerardo Nardelli
77bc6c662a
Add info alert in ALM (#402) 2020-07-20 18:29:16 +03:00
Gerardo Nardelli
dc060387bc
Update ALM logic to continue displaying failed signatures from validators (#399) 2020-07-17 23:59:25 +03:00
Gerardo Nardelli
efc433e9e0
Set descriptions for every state in ALM (#398) 2020-07-14 22:55:22 +03:00
Kirill Fedoseev
ebd97dce5c
Speed up e2e building process (#395) 2020-07-14 17:53:05 +03:00
Gerardo Nardelli
42953ffe30
Properly handle not found transaction and disable auto refresh (#397) 2020-07-13 21:11:43 +03:00
Kirill Fedoseev
4f6d53964f
Possibility to send a tx through all provided RPC endpoints (#394) 2020-07-13 15:09:07 +03:00
Kirill Fedoseev
9e6833eb40
Update bw plugin README and version (#389) 2020-07-10 11:28:03 +03:00
Gerardo Nardelli
4c44aa5fcd
Add Alm unit tests (#388) 2020-07-09 13:20:49 +03:00
Kirill Fedoseev
2edd8f2783
Add bw plugin assets (#387) 2020-07-07 17:26:14 +03:00
Alexander Kolotov
861c755b09 Merge branch 'master' into develop 2020-07-06 21:34:40 +03:00
Gerardo Nardelli
8c268d6f06
Add ALM snapshots (#382) 2020-07-06 21:33:23 +03:00
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
603 changed files with 46025 additions and 18252 deletions

View File

@ -1,289 +0,0 @@
version: 2.1
orbs:
tokenbridge-orb:
commands:
install-chrome:
steps:
- run:
name: Update dpkg
command: |
sudo apt-get clean
sudo apt-get update
sudo apt-get install dpkg
- run:
name: Install Chrome
command: |
wget -O chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb
sudo dpkg -i chrome.deb
install-node:
steps:
- run:
name: Install Node
command: |
export NVM_DIR="/opt/circleci/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install 10.16.3 && nvm alias default 10.16.3
echo 'export NVM_DIR="/opt/circleci/.nvm"' >> $BASH_ENV
echo ' [ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"' >> $BASH_ENV
install-yarn:
steps:
- run:
name: Install Yarn
command: |
curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get -y install yarn
yarn-install-cached-on-machine:
steps:
- restore_cache:
name: Restore Machine Yarn Package Cache
keys:
- yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
- run:
name: Install npm dependencies using Yarn
command: nvm use default; yarn install --frozen-lockfile
- save_cache:
name: Save Machine Yarn Package Cache
key: yarn-machine-{{ checksum "package.json" }}-{{ checksum "yarn.lock" }}
paths:
- ~/.cache/yarn
wait-for-oracle:
parameters:
redis-key:
type: string
steps:
- run:
name: Install redis tools
command: sudo apt-get install -y redis-tools
- run:
name: Wait for the Oracle to start
command: |
set +e
i=0
while [[ $(redis-cli GET << parameters.redis-key >> ) ]]; do
((i++))
if [ "$i" -gt 30 ]
then
exit -1
fi
echo "Sleeping..."
sleep 3
done
executors:
docker-node:
docker:
- image: circleci/node:10.15
machine-with-docker-caching:
machine:
image: circleci/classic:latest
docker_layer_caching: true
jobs:
initialize:
executor: tokenbridge-orb/docker-node
steps:
- 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
- restore_cache:
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
- 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
- save_cache:
name: Save initialized project for subsequent jobs
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
paths:
- ~/project
initialize-root:
executor: tokenbridge-orb/docker-node
steps:
- checkout
- run: sudo su - -c 'export CI=true && cd /home/circleci/project && yarn initialize && yarn test'
build:
executor: tokenbridge-orb/docker-node
steps:
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn run build
lint:
executor: tokenbridge-orb/docker-node
steps:
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn run lint
test:
executor: tokenbridge-orb/docker-node
steps:
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn run test
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
ui-e2e:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- tokenbridge-orb/install-node
- tokenbridge-orb/install-yarn
- tokenbridge-orb/install-chrome
- run: git submodule update --init
- tokenbridge-orb/yarn-install-cached-on-machine
- run: yarn run ui-e2e
monitor-e2e:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- run: git submodule update --init
- run: ./monitor-e2e/run-tests.sh
cover:
executor: tokenbridge-orb/docker-node
steps:
- restore_cache:
key: initialize-{{ .Environment.CIRCLE_SHA1 }}
- run: yarn workspace ui run coverage
- run: yarn workspace ui run coveralls
deployment-oracle:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- run: git submodule update --init
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh oracle
no_output_timeout: 40m
deployment-ui:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- run: git submodule update --init
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh ui
no_output_timeout: 40m
deployment-monitor:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- run: git submodule update --init
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh monitor
no_output_timeout: 40m
deployment-repo:
executor: tokenbridge-orb/machine-with-docker-caching
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/install-node
- tokenbridge-orb/install-yarn
- tokenbridge-orb/yarn-install-cached-on-machine
- run:
name: Run the scenario
command: deployment-e2e/molecule.sh repo
no_output_timeout: 40m
ultimate:
executor: tokenbridge-orb/machine-with-docker-caching
parameters:
scenario-name:
description: "Molecule scenario name used to create the infrastructure"
type: string
redis-key:
description: "Redis key checked for non-emptiness to assert if Oracle is running"
type: string
ui-e2e-grep:
description: "Mocha grep string used to run ui-e2e tests specific to given type of bridge"
type: string
steps:
- checkout
- run: git submodule update --init
- tokenbridge-orb/install-node
- tokenbridge-orb/install-chrome
- tokenbridge-orb/install-yarn
- tokenbridge-orb/yarn-install-cached-on-machine
- run:
name: Prepare the infrastructure
command: e2e-commons/up.sh deploy << parameters.scenario-name >>
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
workflows:
tokenbridge:
jobs:
- initialize
- initialize-root:
filters:
branches:
only: master
- build:
requires:
- initialize
- lint:
requires:
- initialize
- test:
requires:
- initialize
- cover:
requires:
- initialize
filters:
branches:
only: master
- oracle-e2e
- ui-e2e
- monitor-e2e
- deployment-oracle
- deployment-ui
- deployment-monitor
- deployment-repo
- ultimate:
name: "ultimate: native to erc"
scenario-name: native-to-erc
redis-key: native-erc-collected-signatures:lastProcessedBlock
ui-e2e-grep: "NATIVE TO ERC"
- ultimate:
name: "ultimate: erc to native"
scenario-name: erc-to-native
redis-key: erc-native-collected-signatures:lastProcessedBlock
ui-e2e-grep: "ERC TO NATIVE"
- ultimate:
name: "ultimate: erc to erc"
scenario-name: erc-to-erc
redis-key: erc-erc-collected-signatures:lastProcessedBlock
ui-e2e-grep: "ERC TO ERC"

View File

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

View File

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

227
.github/workflows/main.yml vendored Normal file
View File

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

15
.gitignore vendored
View File

@ -6,14 +6,12 @@ coverage
# production
build
dist
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
*.env*
!.env.example
.idea
.nyc_output
logs/
@ -48,4 +46,9 @@ __pycache__
#monitor
monitor/responses/*
!monitor/.gitkeep
monitor/cache/*
!monitor/cache/.gitkeep
!monitor/.gitkeep
# Local Netlify folder
.netlify

2
.nvmrc
View File

@ -1 +1 @@
10.16
12.22

View File

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

View File

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

View File

@ -1,4 +1,4 @@
[![CircleCI](https://circleci.com/gh/poanetwork/tokenbridge.svg?style=svg)](https://circleci.com/gh/poanetwork/tokenbridge)
![tokenbridge](https://github.com/poanetwork/tokenbridge/workflows/tokenbridge/badge.svg?branch=master)
[![Gitter](https://badges.gitter.im/poanetwork/poa-bridge.svg)](https://gitter.im/poanetwork/poa-bridge?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![License: LGPL v3.0](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
@ -19,16 +19,16 @@ Sub-repositories maintained within this monorepo are listed below.
| Sub-repository | Description |
| --- | --- |
| [Oracle](oracle/README.md) | Oracle responsible for listening to bridge related events and authorizing asset transfers. |
| [UI](ui/README.md) | DApp interface to transfer tokens and coins between chains. |
| [Oracle](oracle/README.md) | Responsible for listening to bridge related events and authorizing asset transfers. |
| [Monitor](monitor/README.md) | Tool for checking balances and unprocessed events in bridged networks. |
| [Deployment](deployment/README.md) | Ansible playbooks for deploying cross-chain bridges. |
| [Oracle-E2E](oracle-e2e/README.md) | End to end tests for the Oracle |
| [Monitor-E2E](monitor-e2e/README.md) | End to end tests for the Monitor |
| [UI-E2E](ui-e2e/README.md) | End to end tests for the UI |
| [Deployment-E2E](deployment-e2e/README.md) | End to end tests for the Deployment |
| [Commons](commons/README.md) | Interfaces, constants and utilities shared between the sub-repositories |
| [E2E-Commons](e2e-commons/README.md) | Common utilities and configuration used in end to end tests |
| [ALM](alm/README.md) | DApp interface tool for AMB Live Monitoring |
| [Burner-wallet-plugin](burner-wallet-plugin/README.md) | TokenBridge Burner Wallet 2 Plugin |
Additionally there are [Smart Contracts](https://github.com/poanetwork/tokenbridge-contracts) used to manage bridge validators, collect signatures, and confirm asset relay and disposal.
@ -54,8 +54,6 @@ Additionally there are [Smart Contracts](https://github.com/poanetwork/tokenbrid
The POA TokenBridge provides four operational modes:
- [x] `Native-to-ERC20` **Coins** on a Home network can be converted to ERC20-compatible **tokens** on a Foreign network. Coins are locked on the Home side and the corresponding amount of ERC20 tokens are minted on the Foreign side. When the operation is reversed, tokens are burnt on the Foreign side and unlocked in the Home network. **More Information: [POA-to-POA20 Bridge](https://medium.com/poa-network/introducing-poa-bridge-and-poa20-55d8b78058ac)**
- [x] `ERC20-to-ERC20` ERC20-compatible tokens on the Foreign network are locked and minted as ERC20-compatible tokens (ERC677 tokens) on the Home network. When transferred from Home to Foreign, they are burnt on the Home side and unlocked in the Foreign network. This can be considered a form of atomic swap when a user swaps the token "X" in network "A" to the token "Y" in network "B". **More Information: [ERC20-to-ERC20](https://medium.com/poa-network/introducing-the-erc20-to-erc20-tokenbridge-ce266cc1a2d0)**
- [x] `ERC20-to-Native`: Pre-existing **tokens** in the Foreign network are locked and **coins** are minted in the `Home` network. In this mode, the Home network consensus engine invokes [Parity's Block Reward contract](https://wiki.parity.io/Block-Reward-Contract.html) to mint coins per the bridge contract request. **More Information: [xDai Chain](https://medium.com/poa-network/poa-network-partners-with-makerdao-on-xdai-chain-the-first-ever-usd-stable-blockchain-65a078c41e6a)**
- [x] `Arbitrary-Message`: Transfer arbitrary data between two networks as so the data could be interpreted as an arbitrary contract method invocation.
@ -66,11 +64,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), 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:
@ -87,7 +87,7 @@ Running tests for all projects:
yarn test
```
Additionally there are end-to-end tests for [Oracle](oracle-e2e/README.md) and [UI](ui-e2e/README.md).
Additionally there are end-to-end tests for [Oracle](oracle-e2e/README.md) and [Monitor](monitor-e2e/README.md).
For details on building, running and developing please refer to respective READMEs in sub-repositories.
@ -106,5 +106,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](https://docs.tokenbridge.net/)

15
alm-e2e/.eslintrc Normal file
View File

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

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

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

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

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

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

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

View File

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

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"
]
}

25
alm/.gitignore vendored Normal file
View File

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

38
alm/Dockerfile Normal file
View File

@ -0,0 +1,38 @@
FROM node:12 as contracts
WORKDIR /mono
COPY contracts/package.json contracts/package-lock.json ./contracts/
WORKDIR /mono/contracts
RUN npm install --only=prod
COPY ./contracts/truffle-config.js ./
COPY ./contracts/contracts ./contracts
RUN npm run compile
FROM node:12 as alm-builder
WORKDIR /mono
COPY package.json .
COPY --from=contracts /mono/contracts/build ./contracts/build
COPY commons/package.json ./commons/
COPY alm/package.json ./alm/
COPY yarn.lock .
RUN NOYARNPOSTINSTALL=1 yarn install --frozen-lockfile
COPY ./commons ./commons
COPY ./alm ./alm
ARG DOT_ENV_PATH=./alm/.env
COPY ${DOT_ENV_PATH} ./alm/.env
WORKDIR /mono/alm
RUN yarn run build
FROM node:12 as alm-production
RUN yarn global add serve
WORKDIR /app
COPY --from=alm-builder /mono/alm/build .
CMD serve -p $PORT -s .

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/).

View File

@ -1,4 +1,4 @@
const { addDecoratorsLegacy, disableEsLint, override } = require('customize-cra')
const { override, disableEsLint } = require('customize-cra')
const ModuleScopePlugin = require('react-dev-utils/ModuleScopePlugin')
const disableModuleScopePlugin = () => config => {
@ -6,4 +6,4 @@ const disableModuleScopePlugin = () => config => {
return config
}
module.exports = override(addDecoratorsLegacy(), disableEsLint(), disableModuleScopePlugin())
module.exports = override(disableEsLint(), disableModuleScopePlugin())

View File

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

64
alm/package.json Normal file
View File

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

1
alm/public/_redirects Normal file
View File

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

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>

View File

@ -1,6 +1,6 @@
{
"short_name": "POA Bridge UI",
"name": "POA Bridge UI",
"short_name": "ALM",
"name": "AMB Live Monitoring",
"icons": [
{
"src": "favicon.ico",
@ -8,7 +8,7 @@
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"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:

View File

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

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

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

View File

@ -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

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

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

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

View File

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

View File

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

View File

@ -0,0 +1,148 @@
import React, { useEffect, useState, useCallback } from 'react'
import styled from 'styled-components'
import { Route, useHistory } from 'react-router-dom'
import { Form } from './Form'
import { StatusContainer } from './StatusContainer'
import { useStateProvider } from '../state/StateProvider'
import { TransactionReceipt } from 'web3-eth'
import { InfoAlert } from './commons/InfoAlert'
import { ExplorerTxLink } from './commons/ExplorerTxLink'
import { FOREIGN_NETWORK_NAME, HOME_NETWORK_NAME } from '../config/constants'
const StyledMainPage = styled.div`
text-align: center;
min-height: 100vh;
`
const Header = styled.header`
background-color: #001529;
color: #ffffff;
margin-bottom: 50px;
`
const HeaderContainer = styled.header`
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
font-size: 16px;
height: 64px;
line-height: 64px;
padding: 0 50px;
@media (max-width: 600px) {
padding: 0 20px;
}
`
const AlertP = styled.p`
align-items: start;
margin-bottom: 0;
@media (max-width: 600px) {
flex-direction: column;
}
`
export interface FormSubmitParams {
chainId: number
txHash: string
receipt: TransactionReceipt
}
export const MainPage = () => {
const history = useHistory()
const { home, foreign } = useStateProvider()
const [networkName, setNetworkName] = useState('')
const [receipt, setReceipt] = useState<Maybe<TransactionReceipt>>(null)
const [showInfoAlert, setShowInfoAlert] = useState(false)
const loadFromStorage = useCallback(() => {
const hideAlert = window.localStorage.getItem('hideInfoAlert')
setShowInfoAlert(!hideAlert)
}, [])
useEffect(
() => {
loadFromStorage()
},
[loadFromStorage]
)
const onAlertClose = useCallback(
() => {
window.localStorage.setItem('hideInfoAlert', 'true')
loadFromStorage()
},
[loadFromStorage]
)
const setNetworkData = (chainId: number) => {
const network = chainId === home.chainId ? home.name : foreign.name
setNetworkName(network)
}
const onFormSubmit = ({ chainId, txHash, receipt }: FormSubmitParams) => {
setNetworkData(chainId)
setReceipt(receipt)
history.push(`/${chainId}/${txHash}`)
}
const resetNetworkHeader = () => {
setNetworkName('')
}
const setNetworkFromParams = (chainId: number) => {
setNetworkData(chainId)
}
useEffect(() => {
const w = window as any
if (w.ethereum) {
w.ethereum.autoRefreshOnNetworkChange = false
}
}, [])
return (
<StyledMainPage>
<Header>
<HeaderContainer>
<span>AMB Live Monitoring</span>
<span>{networkName}</span>
</HeaderContainer>
</Header>
<div className="container">
{showInfoAlert && (
<InfoAlert onClick={onAlertClose}>
<p className="is-left text-left">
The Arbitrary Message Bridge Live Monitoring application provides real-time status updates for messages
bridged between {HOME_NETWORK_NAME} and {FOREIGN_NETWORK_NAME}. You can check current tx status, view
validator info, and troubleshoot potential issues with bridge transfers.
</p>
<AlertP className="is-left text-left">
For more information refer to&nbsp;
<ExplorerTxLink
href="https://docs.tokenbridge.net/about-tokenbridge/components/amb-live-monitoring-application"
target="_blank"
rel="noopener noreferrer"
>
the ALM documentation
</ExplorerTxLink>
</AlertP>
</InfoAlert>
)}
<Route exact path={['/']} children={<Form onSubmit={onFormSubmit} />} />
<Route
path={['/:chainId/:txHash/:messageIdParam', '/:chainId/:txHash']}
children={
<StatusContainer
onBackToMain={resetNetworkHeader}
setNetworkFromParams={setNetworkFromParams}
receiptParam={receipt}
/>
}
/>
</div>
</StyledMainPage>
)
}

View File

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

View File

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

View File

@ -0,0 +1,67 @@
import React, { useEffect } from 'react'
import { useTransactionFinder } from '../hooks/useTransactionFinder'
import { useStateProvider } from '../state/StateProvider'
import { TRANSACTION_STATUS } from '../config/constants'
import { TransactionReceipt } from 'web3-eth'
import { Loading } from './commons/Loading'
import { NetworkTransactionSelector } from './NetworkTransactionSelector'
import { BackButton } from './commons/BackButton'
import { TRANSACTION_STATUS_DESCRIPTION } from '../config/descriptions'
import { MultiLine } from './commons/MultiLine'
import styled from 'styled-components'
const StyledMultiLine = styled(MultiLine)`
margin-bottom: 40px;
`
export const TransactionSelector = ({
txHash,
onSelected,
onBack
}: {
txHash: string
onSelected: (chainId: number, receipt: TransactionReceipt) => void
onBack: () => void
}) => {
const { home, foreign } = useStateProvider()
const { receipt: homeReceipt, status: homeStatus } = useTransactionFinder({ txHash, web3: home.web3 })
const { receipt: foreignReceipt, status: foreignStatus } = useTransactionFinder({ txHash, web3: foreign.web3 })
useEffect(
() => {
if (!home.chainId || !foreign.chainId) return
if (homeStatus === TRANSACTION_STATUS.FOUND && foreignStatus === TRANSACTION_STATUS.NOT_FOUND) {
if (!homeReceipt) return
onSelected(home.chainId, homeReceipt)
} else if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
if (!foreignReceipt) return
onSelected(foreign.chainId, foreignReceipt)
}
},
[homeReceipt, homeStatus, foreignReceipt, foreignStatus, home.chainId, foreign.chainId, onSelected]
)
const onSelectedNetwork = (chainId: number) => {
const chain = chainId === home.chainId ? home.chainId : foreign.chainId
const receipt = chainId === home.chainId ? homeReceipt : foreignReceipt
if (!receipt) return
onSelected(chain, receipt)
}
if (foreignStatus === TRANSACTION_STATUS.FOUND && homeStatus === TRANSACTION_STATUS.FOUND) {
return <NetworkTransactionSelector onNetworkSelected={onSelectedNetwork} />
}
if (foreignStatus === TRANSACTION_STATUS.NOT_FOUND && homeStatus === TRANSACTION_STATUS.NOT_FOUND) {
const message = TRANSACTION_STATUS_DESCRIPTION[TRANSACTION_STATUS.NOT_FOUND]
return (
<div>
<StyledMultiLine>{message}</StyledMultiLine>
<BackButton onBackToMain={onBack} />
</div>
)
}
return <Loading />
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,5 @@
import styled from 'styled-components'
export const MultiLine = styled.div`
white-space: pre-wrap;
`

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,13 @@
import styled from 'styled-components'
export const Thead = styled.thead`
border-bottom: 2px solid #9e9e9e;
`
export const StatusTd = styled.td`
width: 150px;
`
export const AgeTd = styled.td`
width: 180px;
`

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

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

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

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

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

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.

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