Add bridge-ui sub-repository (#27)

* Introduced bridge-ui from branch 2.1.0-rc1

* Added submodule to root directory.

* Check out bridge-ui poa-bridge-contracts to tag 2.3.0-rc0

* Added bridge-ui workspace.

* Removed duplicate CoC, Contributing, Licence, updated links in readme.

* Pulling submodules.

* Use specified submodule commit instead of pulling latest.
This commit is contained in:
Przemyslaw Rzad 2019-05-09 12:03:18 +02:00 committed by GitHub
parent 052c1ba25d
commit 9746430fb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
218 changed files with 36723 additions and 123 deletions

@ -5,6 +5,7 @@ jobs:
- image: circleci/node:10.15
steps:
- checkout
- run: git submodule update --init
- run: yarn
- run: yarn run lint
test:
@ -15,6 +16,7 @@ jobs:
FOREIGN_RPC_URL: http://example.com
steps:
- checkout
- run: git submodule update --init
- run: yarn
- run: yarn run test
workflows:

3
.gitmodules vendored

@ -1,3 +1,6 @@
[submodule "oracle/submodules/poa-bridge-contracts"]
path = oracle/submodules/poa-bridge-contracts
url = https://github.com/poanetwork/poa-bridge-contracts.git
[submodule "bridge-ui/submodules/poa-bridge-contracts"]
path = bridge-ui/submodules/poa-bridge-contracts
url = https://github.com/poanetwork/poa-bridge-contracts.git

14
bridge-ui/.babelrc Normal file

@ -0,0 +1,14 @@
{
"env": {
"test": {
"plugins": [
[
"@babel/plugin-proposal-decorators",
{
"legacy": true
}
]
]
}
}
}

5
bridge-ui/.dockerignore Normal file

@ -0,0 +1,5 @@
node_modules
e2e-script
.git
.env
.dockerignore

38
bridge-ui/.env.example Normal file

@ -0,0 +1,38 @@
REACT_APP_HOME_BRIDGE_ADDRESS=0xABb4C1399DcC28FBa3Beb76CAE2b50Be3e087353
REACT_APP_FOREIGN_BRIDGE_ADDRESS=0xE405F6872cE38a7a4Ff63DcF946236D458c2ca3a
REACT_APP_FOREIGN_HTTP_PARITY_URL=https://kovan.infura.io/mew
REACT_APP_HOME_HTTP_PARITY_URL=https://sokol.poa.network
REACT_APP_HOME_NATIVE_NAME=POA
REACT_APP_HOME_NETWORK_NAME=POA Sokol
REACT_APP_FOREIGN_NETWORK_NAME=Kovan
# Set to true if network doesn't support events
REACT_APP_HOME_WITHOUT_EVENTS=false
REACT_APP_FOREIGN_WITHOUT_EVENTS=false
REACT_APP_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx/%s
REACT_APP_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
REACT_APP_HOME_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/poa/sokol/address/%s
REACT_APP_FOREIGN_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/eth/kovan/address/%s
REACT_APP_HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
REACT_APP_HOME_GAS_PRICE_SPEED_TYPE=standard
REACT_APP_HOME_GAS_PRICE_FALLBACK=5000000000
REACT_APP_HOME_GAS_PRICE_UPDATE_INTERVAL=15000
REACT_APP_FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
REACT_APP_FOREIGN_GAS_PRICE_SPEED_TYPE=standard
REACT_APP_FOREIGN_GAS_PRICE_FALLBACK=5000000000
REACT_APP_FOREIGN_GAS_PRICE_UPDATE_INTERVAL=15000
# Default
REACT_APP_TITLE=TokenBridge UI app - %c
REACT_APP_DESCRIPTION=The POA cross-chain bridge serves as a method of transferring POA native tokens from the POA Network to the Ethereum network in a quick and cost-efficient manner.
# RSK
#REACT_APP_DESCRIPTION=The TokenBridge serves as a method of transferring Bancor Network tokens between the Ethereum network and the Rootstock network in a quick and cost-efficient manner.
# To use Ethereum-classic styles
#APP_STYLES=classic

8
bridge-ui/.eslintrc Normal file

@ -0,0 +1,8 @@
{
"extends": "react-app",
"parserOptions": {
"ecmaFeatures": {
"legacyDecorators": true
}
}
}

29
bridge-ui/.gitignore vendored Normal file

@ -0,0 +1,29 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
.idea
# contracts build
/src/contracts
# compiled css
*.css
npm-debug.log*
yarn-debug.log*
yarn-error.log*

1
bridge-ui/.nvmrc Normal file

@ -0,0 +1 @@
v10.15

39
bridge-ui/.travis.yml Normal file

@ -0,0 +1,39 @@
language: node_js
node_js:
- "8"
sudo: required
services:
- docker
dist: trusty
addons:
apt:
sources:
- google-chrome
packages:
- google-chrome-stable
#cache:
# directories:
# - node_modules
install:
before_script:
- export DISPLAY=:99.0
- sh -e /etc/init.d/xvfb start
- sleep 3
- wget -N http://chromedriver.storage.googleapis.com/2.30/chromedriver_linux64.zip -P ~/
- unzip ~/chromedriver_linux64.zip -d ~/
- rm ~/chromedriver_linux64.zip
- sudo mv -f ~/chromedriver /usr/local/share/
- sudo chmod +x /usr/local/share/chromedriver
- sudo ln -s /usr/local/share/chromedriver /usr/local/bin/chromedriver
script:
- npm run coverage
- cd e2e-script && ./run-tests.sh
after_script:
- npm run coveralls

15
bridge-ui/.vscode/launch.json vendored Normal file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}"
}
]
}

8
bridge-ui/Dockerfile Normal file

@ -0,0 +1,8 @@
FROM node:10 as build-deps
WORKDIR /bridge
COPY package.json .
COPY package-lock.json .
COPY . .
RUN npm install
RUN npm run postinstall

213
bridge-ui/README.md Normal file

@ -0,0 +1,213 @@
# POA Bridge - User Interface (UI) Application
[![Build Status](https://travis-ci.org/poanetwork/bridge-ui.svg?branch=master)](https://travis-ci.org/poanetwork/bridge-ui)
[![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)
[![Coverage Status](https://coveralls.io/repos/github/poanetwork/bridge-ui/badge.svg?branch=master)](https://coveralls.io/github/poanetwork/bridge-ui?branch=master)
[![dependencies Status](https://david-dm.org/poanetwork/bridge-ui/status.svg)](https://david-dm.org/poanetwork/bridge-ui)
Welcome to the POA Bridge! Following is an overview of the POA Bridge and Bridge UI Application, as well as [basic instructions for getting started](#getting-started).
## POA Bridge Overview
The POA Bridge allows users to transfer assets between two chains in the Ethereum ecosystem. It is composed of several elements which are located in different POA Network repositories.
For a complete picture of the POA Bridge functionality, it is useful to explore each repository.
**Bridge Elements**
1. Bridge UI Application. A DApp interface to transfer tokens and coins between chains, located in this repository.
2. [Bridge Smart Contracts](https://github.com/poanetwork/poa-bridge-contracts). Solidity contracts used to manage bridge validators, collect signatures, and confirm asset relay and disposal.
3. [Token Bridge](https://github.com/poanetwork/token-bridge). The token bridge oracle written in NodeJS.
4. [Bridge Monitor](https://github.com/poanetwork/bridge-monitor). A tool for checking balances and unprocessed events in bridged networks.
5. [Bridge Deployment Playbooks](https://github.com/poanetwork/deployment-bridge). Manages configuration instructions for remote deployments and allows you to deploy separate bridge instances for validators.
## Bridge UI Application
The UI provides an intuitive interface for assets transfer between networks running the Bridge smart contracts. Users can connect to a web3 wallet such as [Nifty Wallet](https://github.com/poanetwork/nifty-wallet) or [MetaMask](https://metamask.io/) and complete the transfer through a web browser.
The current implementation allows for several bridge modes.
1. `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.
2. `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".
3. `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 to mint coins per the bridge contract request.
![Bridge UI](bridge-ui.png)
### UI Features
- Shows daily limits in both networks
- Displays all events in both networks
- Filter events from a specific block number on both sides of the bridge
- Find a corresponding event on different sides of the bridge
- Submit a transaction from Home to Foreign network
- Submit a transaction from Foreign to Home network
### User Transactions
- Connect to the network you want to transfer coins from using a web3 wallet such as Nifty Wallet or MetaMask. This can be the Home or Foreign network.
The wallet must be funded to cover gas costs related to the transfer. With the Native-to-ERC20 bridge, the wallet must contain the amount to transfer, and with the ERC20-to-ERC20 bridge, the wallet must contain tokens linked with the network you are transferring from.
**Process**
- Specify the amount to send.
- Click the `Transfer` button.
- Confirm the transaction via the web3 wallet.
The same address is used to send a coin from the Home network and receive a token on the Foreign Network. In order to send assets in the opposite direction, change the network in the web3 wallet. This changes the bridge interface to show the selected network on the left side of the bridge.
![Web3 Wallet Change Network](web3wallet_network.gif)
### Resources
Some of the following resources are outdated, but provide a general sense of the UI and transactional flow.
- [Deployed URL for POA -> Ethereum Network Bridge](https://bridge.poa.net/)
- [Testnet Bridge URL](https://bridge-testnet.poa.net/)
- [Bridge UI Tutorial Videos](https://www.youtube.com/playlist?list=PLS5SEs8ZftgUqR3hVFiEXQLqE9QI8sIGz)
- [Article on the POA Bridge](https://medium.com/poa-network/cross-chain-bridges-paving-the-way-to-internet-of-blockchains-422ac94bc2e5)
- Wallet Resources
- [MetaMask](https://consensys.zendesk.com/hc/en-us/categories/360001045692-Using-MetaMask)
- [Nifty Wallet](https://poanet.zendesk.com/hc/en-us/articles/360008957634-Nifty-Wallet)
- [AlphaWallet (iOS and Android)](https://alphawallet.github.io/AlphaWallet-Download-Page/)
## Getting Started
The following is an example setup using the POA Sokol testnet as the Home network, and the Ethereum Kovan testnet as the Foreign network. The instructions for the Bridge UI are identical for an `ERC20-to-ERC20` configuration, but the smart contract deployment steps will vary.
### Dependencies
- [poa-bridge-contracts](https://github.com/poanetwork/poa-bridge-contracts)
- [token-bridge](https://github.com/poanetwork/token-bridge)
- [node.js](https://nodejs.org/en/download/)
- [AlphaWallet](https://alphawallet.github.io/AlphaWallet-Download-Page/) or [Nifty Wallet](https://github.com/poanetwork/nifty-wallet) or [MetaMask](https://metamask.io/)
### Example Setup
1. Create an empty folder for setting up your bridge. In this example we call it `sokol-kovan-bridge`.
`mkdir sokol-kovan-bridge && cd sokol-kovan-bridge`
2. Prepare temporary ETH address(es) for deployment by creating new account(s) in Nifty Wallet or MetaMask. See the [wallet resources](#resources) if you need more information on this step. This account is used:
* for deploying bridge contracts to both networks
* as the bridge contracts management wallet
* as the validator's wallet address(es)
3. Fund the test account(s).
* Fund Home account(s) using the [POA Sokol Faucet](https://faucet.poa.network/)
* Get free Kovan Coins from the [gitter channel](https://gitter.im/kovan-testnet/faucet) or [Iracus faucet](https://github.com/kovan-testnet/faucet) for Foreign account(s). Get 5 Keth to 1 acc, and transfer from there to all other wallets if more than one account is used.
4. Deploy the Sokol <-> Kovan Bridge contracts.
* Go to the the `sokol-kovan-bridge` folder created in step 1 and `git clone https://github.com/poanetwork/poa-bridge-contracts`
* Follow instructions in the [POA Bridge contracts repo](https://github.com/poanetwork/poa-bridge-contracts).
* Set the parameters in the .env file.
* `DEPLOYMENT_ACCOUNT_PRIVATE_KEY`: Export the private key from step 2
* `HOME_RPC_URL`=https://sokol.poa.network
* Wallet address(es) for bridge contracts management. For testing, you can use the same address for all address values in the file. This includes:
* `HOME_OWNER_MULTISIG`
* `HOME_UPGRADEABLE_ADMIN_VALIDATORS`
* `HOME_UPGRADEABLE_ADMIN_BRIDGE`
* `FOREIGN_OWNER_MULTISIG`
* `FOREIGN_UPGRADEABLE_ADMIN_VALIDATORS`
* `FOREIGN_UPGRADEABLE_ADMIN_BRIDGE`
* `VALIDATORS` _Note: Wallet address(es) for validator(s) are separated by a space. For testing, you can use the same address that was used as the bridge contracts management account._
* `FOREIGN_RPC_URL`=https://kovan.infura.io/mew
* When deployment is finished, check that the `bridgeDeploymentResults.json` file exists in the `poa-bridge-contracts/deploy` directory and includes the bridge contract addresses.
5. Install and run the POA Token Bridge.
* Got to the `sokol-kovan-bridge` folder and `git clone https://github.com/poanetwork/token-bridge`
* Follow instructions in the [POA Token Bridge repo](https://github.com/poanetwork/token-bridge).
If successful, you will see bridge processes run when you issue a command. For example, run `npm run watcher:signature-request`
**Example NPM Output:**
```bash
[1539195000507] INFO (watcher-signature-request): Connected to redis
[1539195000545] INFO (watcher-signature-request): Connected to amqp Broker
[1539195006085] INFO (watcher-signature-request): Found 0 UserRequestForSignature events
[1539195011467] INFO (watcher-signature-request): Found 0 UserRequestForSignature events
```
**Example Docker Output:**
**Note:** The output will depend on your Docker configuration. You may need to access the container logs to view.
```bash
{"level":30,"time":1539366879816,"msg":"Connected to redis","validator":"0x..........","name":"watcher-signature-request","v":1}
{"level":30,"time":1539366879880,"msg":"Connected to amqp Broker","validator":"0x..........","name":"watcher-signature-request","v":1}
{"level":30,"time":1539366885587,"msg":"Found 0 UserRequestForSignature events","validator":"0x..........","name":"watcher-signature-request","v":1}
```
6. Keep the bridge processes running. Open a separate terminal window and go to the `sokol-kovan-bridge` folder to install and unpack this repository.
* `git clone https://github.com/poanetwork/bridge-ui.git`
* `cd bridge-ui`
* Update submodules
`git submodule update --init --recursive`
* Install dependencies
`npm install`
_**Note**: The bridge UI configuration should be performed with an unprivileged Linux account or with the following flag `npm install --unsafe-perm`. [More information](https://docs.npmjs.com/misc/scripts#user)_
* Create a .env file from the example file [.env.example](.env.example)
`cp .env.example .env`
* Insert the addresses from the bridgeDeploymentResults.json file (from step 4) into the .env file. No other changes are needed, see [Env Parameter Details](#env-parameter-details) for information about each parameter.
`cat ../poa-bridge-contracts/deploy/bridgeDeploymentResults.json`
```bash
# HomeBridge address in bridgeDeploymentResults.json
REACT_APP_HOME_BRIDGE_ADDRESS=0x..
# ForeignBridge address in bridgeDeploymentResults.json
REACT_APP_FOREIGN_BRIDGE_ADDRESS=0x..
# https public RPC node for Foreign network
REACT_APP_FOREIGN_HTTP_PARITY_URL=https://kovan.infura.io/mew
# public RPC node for Home network
REACT_APP_HOME_HTTP_PARITY_URL=https://sokol.poa.network
```
* Run `npm run start`
* Make sure your web3 wallet (Nifty Wallet, AlphaWallet or MetaMask) is funded and connected to the POA Sokol Network (see step 2)
* Specify an amount and click `Transfer` to complete a cross-chain transaction from Sokol to Kovan
### Env Parameter Details
Name | Description
--------- | -------
REACT_APP_HOME_BRIDGE_ADDRESS | address that you have deployed at step#3. Should also be recorded at `sokol-kovan-bridge/poa-bridge-contracts/deploy/bridgeDeploymentResults.json`
REACT_APP_FOREIGN_BRIDGE_ADDRESS | address that you have deployed at step#3.
REACT_APP_FOREIGN_HTTP_PARITY_URL | http public rpc node for Foreign Network
REACT_APP_HOME_HTTP_PARITY_URL | http public rpc node for Foreign Network
REACT_APP_HOME_NATIVE_NAME | name of the home native coin
REACT_APP_HOME_NETWORK_NAME | name to be displayed for home network
REACT_APP_FOREIGN_NETWORK_NAME | name to be displayed for foreign network
REACT_APP_HOME_WITHOUT_EVENTS | `true` if home network doesn't support events
REACT_APP_FOREIGN_WITHOUT_EVENTS | `true` if foreign network doesn't support events
REACT_APP_HOME_EXPLORER_TX_TEMPLATE | template link to transaction on home explorer. `%s` will be replaced by transaction hash
REACT_APP_FOREIGN_EXPLORER_TX_TEMPLATE | template link to transaction on foreign explorer. `%s` will be replaced by transaction hash
REACT_APP_HOME_EXPLORER_ADDRESS_TEMPLATE | template link to address on home explorer. `%s` will be replaced by address
REACT_APP_FOREIGN_EXPLORER_ADDRESS_TEMPLATE | template link to address on foreign explorer. `%s` will be replaced by address
REACT_APP_HOME_GAS_PRICE_ORACLE_URL | The URL used to get a JSON response from the gas price prediction oracle for Home network.
REACT_APP_HOME_GAS_PRICE_SPEED_TYPE | Gas Price speed (slow, standard, fast, instant)
REACT_APP_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.
REACT_APP_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.
REACT_APP_FOREIGN_GAS_PRICE_ORACLE_URL | The URL used to get a JSON response from the gas price prediction oracle for Foreign network.
REACT_APP_FOREIGN_GAS_PRICE_SPEED_TYPE | Gas Price speed (slow, standard, fast, instant)
REACT_APP_FOREIGN_GAS_PRICE_FALLBACK | The gas price (in Wei) that is used if both the oracle and the fall back gas price specified in the Foreign Bridge contract are not available.
REACT_APP_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.
REACT_APP_TITLE | The title for the bridge UI page. `%c` will be replaced by the name of the network.
REACT_APP_DESCRIPTION | The meta description for the deployed bridge page.
APP_STYLES | The set of styles to render the bridge UI page. Currently only `classic` is implemented
## Testing
To run tests
`npm run test`
To run tests with coverage
`npm run coverage`
## Contributing
See the [CONTRIBUTING](../CONTRIBUTING.md) document for contribution, testing and pull request protocol.
## License
[![License: LGPL v3.0](https://img.shields.io/badge/License-LGPL%20v3-blue.svg)](https://www.gnu.org/licenses/lgpl-3.0)
This project is licensed under the GNU Lesser General Public License v3.0. See the [LICENSE](../LICENSE) file for details.

BIN
bridge-ui/bridge-ui.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

@ -0,0 +1,10 @@
const {
addDecoratorsLegacy,
disableEsLint,
override
} = require("customize-cra");
module.exports = override(
addDecoratorsLegacy(),
disableEsLint()
);

Binary file not shown.

@ -0,0 +1,151 @@
const key = require('selenium-webdriver').Key;
const Page = require('./Page.js').Page;
const By = require('selenium-webdriver/lib/by').By;
const IDMetaMask = "nkbihfbeogaeaoehlefnkodbefgpgknn";
const URL = "chrome-extension://" + IDMetaMask + "//popup.html";
const buttonSubmit = By.className("confirm btn-green");
const buttonAccept = By.xpath('//*[@id="app-content"]/div/div[4]/div/button');
const agreement = By.xpath("//*[@id=\"app-content\"]/div/div[4]/div/div/div/p[1]/strong");
const fieldNewPass = By.xpath("//*[@id=\"password-box\"]");
const fieldConfirmPass = By.xpath("//*[@id=\"password-box-confirm\"]");
const buttonCreate = By.xpath("//*[@id=\"app-content\"]/div/div[4]/div/button");
const buttonIveCopied = By.xpath("//*[@id=\"app-content\"]/div/div[4]/div/button[1]");
const popupNetwork = By.className("network-name");
const popupAccount = By.xpath("//*[@id=\"app-content\"]/div/div[1]/div/div[2]/span/div");
const fieldPrivateKey = By.xpath("//*[@id=\"private-key-box\"]");
const pass = "qwerty12345";
const buttonImport = By.xpath("//*[@id=\"app-content\"]/div/div[4]/div/div[3]/button");
const fieldNewRPCURL = By.id("new_rpc");
const buttonSave = By.xpath("//*[@id=\"app-content\"]/div/div[4]/div/div[3]/div/div[2]/button");
const arrowBackRPCURL = By.xpath("//*[@id=\"app-content\"]/div/div[4]/div/div[1]/i");
const iconChangeAccount = By.className("cursor-pointer color-orange accounts-selector");
var accountOrderNumber = 1;
var networks = [0, 3, 43, 4, 8545];
class MetaMask extends Page {
constructor(driver) {
super(driver);
this.driver = driver;
this.URL = URL;
}
async clickButtonSubmitTransaction() {
return await this.clickWithWait(buttonSubmit);
}
async activate() {
return await this.switchToNextPage() &&
(await this.open(this.URL) === this.URL) &&
await this.clickWithWait(buttonAccept) &&
await this.clickWithWait(agreement) &&
await this.clickKey(key.TAB, 15) &&
await this.clickWithWait(buttonAccept) &&
await this.waitUntilLocated(fieldNewPass) &&
await this.clickWithWait(fieldNewPass) &&
await this.fillWithWait(fieldNewPass, pass) &&
await this.fillWithWait(fieldConfirmPass, pass) &&
await this.clickWithWait(buttonCreate) &&
await this.waitUntilDisplayed(buttonIveCopied) &&
await this.clickWithWait(buttonIveCopied) &&
await this.switchToNextPage();
}
async importAccount(user) {
user.accountOrderInMetamask = accountOrderNumber - 1;
return await this.switchToNextPage() &&
await this.setNetwork(user.networkID) &&
await this.clickImportAccount() &&
await this.fillWithWait(fieldPrivateKey, user.privateKey) &&
await this.waitUntilDisplayed(buttonImport) &&
await this.clickWithWait(buttonImport) &&
await this.switchToNextPage();
}
async selectAccount(user) {
try {
await this.switchToNextPage();
await this.setNetwork(user.networkID);
await super.clickWithWait(popupAccount);
await this.driver.executeScript("document.getElementsByClassName('dropdown-menu-item')[" +
user.accountOrderInMetamask + "].click();");
await this.switchToNextPage();
return true;
}
catch (err) {
return false;
}
}
async clickImportAccount() {
try {
await super.clickWithWait(popupAccount);
await this.driver.executeScript("document.getElementsByClassName('dropdown-menu-item')["
+ (accountOrderNumber + 1) + "].click();");
accountOrderNumber++;
return true;
}
catch (err) {
return false;
}
}
async signTransaction(refreshCount) {
await this.switchToNextPage();
let counter = 5;
if (refreshCount !== undefined) counter = refreshCount;
do {
await this.refresh();
await super.waitUntilLocated(iconChangeAccount);
if (await this.isElementDisplayed(buttonSubmit)) {
return await this.clickButtonSubmitTransaction() &&
await this.switchToNextPage();
}
await this.driver.sleep(3000);
} while (counter-- >= 0);
await this.switchToNextPage();
return false;
}
async setNetwork(provider) {
try {
await super.clickWithWait(popupNetwork);
let orderNumber = networks.indexOf(provider);
let script = "document.getElementsByClassName('dropdown-menu-item')[" + orderNumber + "].click();"
if (orderNumber < 0) await this.addNetwork(provider);
else await this.driver.executeScript(script);
return true;
}
catch (err) {
return false;
}
}
async addNetwork(provider) {
let url;
switch (provider) {
case 77: {
url = "http://10.1.0.102:8545";
networks.push(177);
break;
}
case 42: {
url = "http://10.1.0.103:8545";
networks.push(142);
break;
}
}
const index = networks.length > 8 ? 8 : networks.length;
await this.driver.executeScript("document.getElementsByClassName('dropdown-menu-item')[" +
(index - 1) + "].click();");
return await super.fillWithWait(fieldNewRPCURL, url) &&
await super.clickWithWait(buttonSave) &&
await super.clickWithWait(arrowBackRPCURL);
}
}
module.exports = {
MetaMask: MetaMask
};

@ -0,0 +1,166 @@
const webdriver = require('selenium-webdriver');
const Twait = 20000;
class Page {
constructor(driver) {
this.driver = driver;
}
async waitUntilDisplayed(element, Twaiting) {
let counter = Twaiting;
if (counter === undefined) counter = 180;
try {
do {
await this.driver.sleep(300);
if (await this.isElementDisplayed(element)) return true;
} while (counter-- > 0);
return false;
}
catch (err) {
return false;
}
}
async waitUntilDisappear(element, Twaiting) {
let counter = Twaiting;
if (counter === undefined) counter = 180;
try {
do {
await this.driver.sleep(300);
if (!await this.isElementDisplayed(element)) return true;
} while (counter-- > 0);
return false;
}
catch (err) {
return false;
}
}
async waitUntilLocated(element, Twaiting) {
let counter = Twaiting;
if (counter === undefined) counter = 180;
try {
do {
await this.driver.sleep(300);
if (await this.isElementLocated(element)) return true;
} while (counter-- > 0);
return false;
}
catch (err) {
return false;
}
}
async isElementDisplayed(element) {
try {
return await this.driver.findElement(element).isDisplayed();
}
catch (err) {
return false;
}
}
async isElementLocated(element) {
return (await this.driver.findElements(element)).length > 0;
}
async clickWithWait(element) {
try {
let field;
if (element.constructor.name !== "WebElement") {
field = await this.driver.wait(webdriver.until.elementLocated(element), Twait);
}
else field = element;
await field.click();
return true;
}
catch (err) {
return false;
}
}
async fillWithWait(element, text) {
try {
let field;
if (element.constructor.name !== "WebElement") {
field = await this.driver.wait(webdriver.until.elementLocated(element), Twait);
}
else field = element;
await field.sendKeys(text);
return true;
}
catch (err) {
return false;
}
}
async findWithWait(element) {
try {
await this.driver.wait(webdriver.until.elementLocated(element), Twait);
return await this.driver.findElements(element);
}
catch (err) {
return null;
}
}
async switchToNextPage() {
let allHandles = [];
let curHandle;
try {
allHandles = await this.driver.getAllWindowHandles();
curHandle = await this.driver.getWindowHandle();
if (allHandles.length > 2) {
let arr = [];
arr[0] = allHandles[0];
arr[1] = allHandles[1];
allHandles = arr;
}
let handle;
for (let i = 0; i < allHandles.length; i++) {
if (curHandle !== allHandles[i]) {
handle = allHandles[i];
break;
}
}
await this.driver.switchTo().window(handle);
await this.driver.sleep(500);
return true;
}
catch (err) {
return false;
}
}
async refresh() {
await this.driver.navigate().refresh();
}
async getUrl() {
return await this.driver.getCurrentUrl();
}
async open(url) {
await this.driver.get(url);
return this.getUrl();
}
async clickKey(key, times) {
try {
const action = this.driver.actions();
for (let i = 0; i < times; i++)
await action.sendKeys(key).perform();
return true;
}
catch (err) {
return false;
}
}
}
module.exports = {
Page: Page
};

@ -0,0 +1,26 @@
### e2e script for bridge-ui
Configure startURL, homeAccount, foreignAccount in ```config.json```
#### Tests
```
1. User is able to open main page of bridge-ui
2. Main page: foreign POA balance is displayed
3. Main page: home POA balance is displayed
4. User is able to send tokens from Home account to Foreign account
5. Home POA balance has correctly changed after transaction
6. Foreign account has received correct amount of tokens after transaction
7. User is able to send tokens from Foreign account to Home account
8. Foreign POA balance has correctly changed after transaction
9. Home account has received correct amount of tokens after transaction
```

@ -0,0 +1,53 @@
const fs = require('fs-extra');
const MetaMask = require('./MetaMask.js').MetaMask;
const MainPage = require('./mainPage.js').MainPage;
class User {
constructor(driver, file) {
try {
this.driver = driver;
let obj = JSON.parse(fs.readFileSync(file, "utf8"));
this.account = obj.account;
this.privateKey = obj.privateKey;
this.networkID = obj.networkID;
this.accountOrderInMetamask = "undefined";//for MetaMaskPage usage only
this.name = file;
}
catch (err) {
console.log("instance User was not created");
console.log(err);
}
}
async transferTokens(amount) {
let mainPage = new MainPage(this.driver);
let metaMask = new MetaMask(this.driver);
return await mainPage.fillFieldAmount(amount) &&
await mainPage.clickButtonTransfer() &&
await mainPage.waitUntilShowUpButtonTransferConfirm() &&
await mainPage.clickButtonTransferConfirm() &&
await metaMask.signTransaction() &&
await mainPage.waitUntilTransactionDone() &&
await mainPage.waitUntilShowUpButtonOk() &&
await mainPage.clickButtonOk()
}
async setMetaMaskNetwork() {
let metaMask = new MetaMask(this.driver);
return await metaMask.switchToNextPage() &&
await metaMask.setNetwork(this.networkID) &&
await metaMask.switchToNextPage();
}
async setMetaMaskAccount() {
let metaMask = new MetaMask(this.driver);
if (this.accountOrderInMetamask === "undefined") {
return await metaMask.importAccount(this);
} else
return await metaMask.selectAccount(this);
}
}
module.exports = {
User: User
};

@ -0,0 +1,68 @@
const webdriver = require('selenium-webdriver'),
chrome = require('selenium-webdriver/chrome');
const fs = require('fs-extra');
const configFile = './e2e-script/config.json';
class Utils {
static async getHomeAccount() {
try {
let obj = JSON.parse(fs.readFileSync(configFile), "utf8");
return obj.homeAccount;
} catch (err) {
return null;
}
}
static async getForeignAccount() {
try {
let obj = JSON.parse(fs.readFileSync(configFile), "utf8");
return obj.foreignAccount;
} catch (err) {
return null;
}
}
static async getStartURL() {
try {
let obj = JSON.parse(fs.readFileSync(configFile), "utf8");
return obj.startUrl;
} catch (err) {
return null;
}
}
static async getErc20StartURL() {
try {
let obj = JSON.parse(fs.readFileSync(configFile), "utf8");
return obj.erc20Url;
} catch (err) {
return null;
}
}
static async getErc20NativeStartURL() {
try {
let obj = JSON.parse(fs.readFileSync(configFile), "utf8");
return obj.erc20NativeUrl;
} catch (err) {
return null;
}
}
static async startBrowserWithMetamask() {
let source = './e2e-script/MetaMask.crx';
let options = new chrome.Options();
await options.addExtensions(source);
await options.addArguments('disable-popup-blocking');
let driver = await new webdriver.Builder().withCapabilities(options.toCapabilities()).build();
await driver.sleep(5000);
return driver;
}
}
module.exports = {
Utils: Utils
}

@ -0,0 +1,5 @@
{
"account": "0x7FC1442AB55Da569940Eb750AaD2BAA63DA4010E",
"privateKey": "460635eb4ac4287de2d2393985e19b4a9f948ac533453a1044ab8d50330b0df9",
"networkID" : 42
}

@ -0,0 +1,5 @@
{
"account": "0x7FC1442AB55Da569940Eb750AaD2BAA63DA4010E",
"privateKey": "460635eb4ac4287de2d2393985e19b4a9f948ac533453a1044ab8d50330b0df9",
"networkID" : 77
}

@ -0,0 +1,14 @@
FROM node:8
RUN apt-get update
RUN apt-get install -y build-essential
RUN apt-get install -y libc6-dev
RUN apt-get install -y libc6-dev-i386
RUN apt-get install -y wget
RUN apt-get clean
RUN git clone https://github.com/poanetwork/bridge-nodejs.git
WORKDIR /bridge-nodejs
RUN git fetch && git checkout develop
RUN cd submodules/poa-bridge-contracts && git submodule update --init --recursive
RUN npm install

@ -0,0 +1,7 @@
{
"startUrl" : "http://10.1.0.101:3000",
"erc20Url" : "http://10.1.0.104:3000",
"erc20NativeUrl" : "http://10.1.0.105:3000",
"homeAccount": "./e2e-script/accounts/user77_7FC1.json",
"foreignAccount": "./e2e-script/accounts/user42_7FC1.json"
}

@ -0,0 +1,24 @@
FROM node:8
RUN mkdir /stuff
WORKDIR /stuff
RUN git clone https://github.com/poanetwork/poa-bridge-contracts.git
RUN mkdir submodules && \
mv poa-bridge-contracts submodules && \
cd submodules/poa-bridge-contracts && \
git fetch && \
git checkout 2.1.0
RUN cd submodules/poa-bridge-contracts && \
npm install && \
./node_modules/.bin/truffle compile && \
cd deploy && \
npm install
COPY deploy.sh .
COPY contracts.env submodules/poa-bridge-contracts/deploy/
COPY erc-contracts.env submodules/poa-bridge-contracts/deploy/
COPY erc-native-contracts.env submodules/poa-bridge-contracts/deploy/
COPY deployERC20.js submodules/poa-bridge-contracts/deploy/
RUN cd submodules/poa-bridge-contracts/deploy && cp contracts.env .env

@ -0,0 +1,34 @@
BRIDGE_MODE=NATIVE_TO_ERC
DEPLOYMENT_ACCOUNT_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
DEPLOYMENT_ACCOUNT_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
DEPLOYMENT_GAS_LIMIT=4000000
HOME_DEPLOYMENT_GAS_PRICE=10000000000
FOREIGN_DEPLOYMENT_GAS_PRICE=10000000000
GET_RECEIPT_INTERVAL_IN_MILLISECONDS=50
BRIDGEABLE_TOKEN_NAME="Your New Bridged Token"
BRIDGEABLE_TOKEN_SYMBOL="TEST"
BRIDGEABLE_TOKEN_DECIMALS="18"
HOME_RPC_URL=http://parity1:8545
HOME_OWNER_MULTISIG=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_UPGRADEABLE_ADMIN_VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_UPGRADEABLE_ADMIN_BRIDGE=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_DAILY_LIMIT=30000000000000000000000000
HOME_MAX_AMOUNT_PER_TX=1500000000000000000000000
HOME_MIN_AMOUNT_PER_TX=10000000000000000
HOME_REQUIRED_BLOCK_CONFIRMATIONS=1
HOME_GAS_PRICE=1000000000
FOREIGN_RPC_URL=http://parity2:8545
FOREIGN_OWNER_MULTISIG=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_UPGRADEABLE_ADMIN_VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_UPGRADEABLE_ADMIN_BRIDGE=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_DAILY_LIMIT=15000000000000000000000000
FOREIGN_MAX_AMOUNT_PER_TX=750000000000000000000000
FOREIGN_MIN_AMOUNT_PER_TX=10000000000000000
FOREIGN_REQUIRED_BLOCK_CONFIRMATIONS=1
FOREIGN_GAS_PRICE=10000000000
REQUIRED_NUMBER_OF_VALIDATORS=1
VALIDATORS="0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"

@ -0,0 +1,14 @@
#!/usr/bin/env bash
cd submodules/poa-bridge-contracts/deploy
echo "Deploying Native-Erc contracts"
node deploy.js
echo "Deploying erc20 contract"
node deployERC20.js
cp erc-contracts.env .env
echo "Deploying erc20-erc20 contracts"
node deploy.js
cp erc-native-contracts.env .env
echo "Deploying Block Reward contract"
node src/utils/deployBlockReward.js
echo "Deploying erc20-native contracts"
node deploy.js

@ -0,0 +1,44 @@
/* eslint import/no-unresolved: 0 node/no-missing-require: 0 */
const path = require('path')
require('dotenv').config();
const {
deployContract,
sendRawTx
} = require('./src/deploymentUtils')
const {
web3Foreign,
deploymentPrivateKey
} = require('./src/web3')
const POA20 = require('../build/contracts/ERC677BridgeToken.json')
const user = '0x7FC1442AB55Da569940Eb750AaD2BAA63DA4010E'
const { DEPLOYMENT_ACCOUNT_ADDRESS } = process.env
async function deployErc20() {
try {
let foreignNonce = await web3Foreign.eth.getTransactionCount(DEPLOYMENT_ACCOUNT_ADDRESS)
console.log('\n[Foreign] Deploying POA20 Test token')
const poa20foreign = await deployContract(POA20, ['POA ERC20 Test', 'POA20', 18], {
from: DEPLOYMENT_ACCOUNT_ADDRESS,
network: 'foreign',
nonce: foreignNonce
})
foreignNonce++
console.log('[Foreign] POA20 Test: ', poa20foreign.options.address)
const mintData = await poa20foreign.methods
.mint(user, '500000000000000000000')
.encodeABI({ from: DEPLOYMENT_ACCOUNT_ADDRESS })
await sendRawTx({
data: mintData,
nonce: foreignNonce,
to: poa20foreign.options.address,
privateKey: deploymentPrivateKey,
url: process.env.FOREIGN_RPC_URL
})
} catch (e) {
console.log(e)
}
}
deployErc20()

@ -0,0 +1,35 @@
BRIDGE_MODE=ERC_TO_ERC
DEPLOYMENT_ACCOUNT_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
DEPLOYMENT_ACCOUNT_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
DEPLOYMENT_GAS_LIMIT=4000000
HOME_DEPLOYMENT_GAS_PRICE=10000000000
FOREIGN_DEPLOYMENT_GAS_PRICE=10000000000
GET_RECEIPT_INTERVAL_IN_MILLISECONDS=50
BRIDGEABLE_TOKEN_NAME="Your New Bridged Token"
BRIDGEABLE_TOKEN_SYMBOL="TEST"
BRIDGEABLE_TOKEN_DECIMALS="18"
HOME_RPC_URL=http://parity1:8545
HOME_OWNER_MULTISIG=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_UPGRADEABLE_ADMIN_VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_UPGRADEABLE_ADMIN_BRIDGE=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_DAILY_LIMIT=30000000000000000000000000
HOME_MAX_AMOUNT_PER_TX=1500000000000000000000000
HOME_MIN_AMOUNT_PER_TX=10000000000000000
HOME_REQUIRED_BLOCK_CONFIRMATIONS=1
HOME_GAS_PRICE=1000000000
FOREIGN_RPC_URL=http://parity2:8545
FOREIGN_OWNER_MULTISIG=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_UPGRADEABLE_ADMIN_VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_UPGRADEABLE_ADMIN_BRIDGE=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_DAILY_LIMIT=15000000000000000000000000
FOREIGN_MAX_AMOUNT_PER_TX=750000000000000000000000
FOREIGN_MIN_AMOUNT_PER_TX=10000000000000000
FOREIGN_REQUIRED_BLOCK_CONFIRMATIONS=1
FOREIGN_GAS_PRICE=10000000000
ERC20_TOKEN_ADDRESS=0x3C665A31199694Bf723fD08844AD290207B5797f
REQUIRED_NUMBER_OF_VALIDATORS=1
VALIDATORS="0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"

@ -0,0 +1,36 @@
BRIDGE_MODE=ERC_TO_NATIVE
DEPLOYMENT_ACCOUNT_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
DEPLOYMENT_GAS_LIMIT=4000000
HOME_DEPLOYMENT_GAS_PRICE=10000000000
FOREIGN_DEPLOYMENT_GAS_PRICE=10000000000
GET_RECEIPT_INTERVAL_IN_MILLISECONDS=50
BRIDGEABLE_TOKEN_NAME="Your New Bridged Token"
BRIDGEABLE_TOKEN_SYMBOL="TEST"
BRIDGEABLE_TOKEN_DECIMALS="18"
HOME_RPC_URL=http://parity1:8545
HOME_OWNER_MULTISIG=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_UPGRADEABLE_ADMIN_VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_UPGRADEABLE_ADMIN_BRIDGE=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
HOME_DAILY_LIMIT=30000000000000000000000000
HOME_MAX_AMOUNT_PER_TX=1500000000000000000000000
HOME_MIN_AMOUNT_PER_TX=10000000000000000
HOME_REQUIRED_BLOCK_CONFIRMATIONS=1
HOME_GAS_PRICE=1000000000
FOREIGN_RPC_URL=http://parity2:8545
FOREIGN_OWNER_MULTISIG=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_UPGRADEABLE_ADMIN_VALIDATORS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_UPGRADEABLE_ADMIN_BRIDGE=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
FOREIGN_DAILY_LIMIT=15000000000000000000000000
FOREIGN_MAX_AMOUNT_PER_TX=750000000000000000000000
FOREIGN_MIN_AMOUNT_PER_TX=10000000000000000
FOREIGN_REQUIRED_BLOCK_CONFIRMATIONS=1
FOREIGN_GAS_PRICE=10000000000
BLOCK_REWARD_ADDRESS=0xF9698Eb93702dfdd0e2d802088d4c21822a8A977
ERC20_TOKEN_ADDRESS=0x3C665A31199694Bf723fD08844AD290207B5797f
REQUIRED_NUMBER_OF_VALIDATORS=1
VALIDATORS="0xaaB52d66283F7A1D5978bcFcB55721ACB467384b"

@ -0,0 +1,213 @@
version: '3'
services:
parity1:
build: parity
networks:
testnet:
ipv4_address: 10.1.0.102
ports:
- "8541:8545"
parity2:
build:
context: parity
dockerfile: Dockerfile-foreign
networks:
testnet:
ipv4_address: 10.1.0.103
ports:
- "8542:8545"
contracts:
build: contracts
networks:
- testnet
command: "true"
redis:
image: "redis:4"
networks:
- testnet
rabbit:
image: "rabbitmq:3-management"
networks:
- testnet
ports:
- "15672:15672"
bridge:
build: bridge
environment:
- NODE_ENV=production
- BRIDGE_MODE=NATIVE_TO_ERC
- QUEUE_URL=amqp://rabbit
- REDIS_URL=redis://redis
- HOME_RPC_URL=http://parity1:8545
- FOREIGN_RPC_URL=http://parity2:8545
- HOME_BRIDGE_ADDRESS=0x32198D570fffC7033641F8A9094FFDCaAEF42624
- FOREIGN_BRIDGE_ADDRESS=0x2B6871b9B02F73fa24F4864322CdC78604207769
- ERC20_TOKEN_ADDRESS=0xdbeE25CbE97e4A5CC6c499875774dc7067E9426B
- VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
- VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
- REDIS_LOCK_TTL=1000
- HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- HOME_GAS_PRICE_SPEED_TYPE=standard
- HOME_GAS_PRICE_FALLBACK=1
- HOME_GAS_PRICE_UPDATE_INTERVAL=600000
- FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- FOREIGN_GAS_PRICE_SPEED_TYPE=standard
- FOREIGN_GAS_PRICE_FALLBACK=1
- FOREIGN_GAS_PRICE_UPDATE_INTERVAL=600000
- HOME_POLLING_INTERVAL=500
- FOREIGN_POLLING_INTERVAL=500
- ALLOW_HTTP=yes
networks:
- testnet
command: "true"
bridge-erc20:
build: bridge
environment:
- NODE_ENV=production
- BRIDGE_MODE=ERC_TO_ERC
- QUEUE_URL=amqp://rabbit
- REDIS_URL=redis://redis
- HOME_RPC_URL=http://parity1:8545
- FOREIGN_RPC_URL=http://parity2:8545
- HOME_BRIDGE_ADDRESS=0x1feB40aD9420b186F019A717c37f5546165d411E
- FOREIGN_BRIDGE_ADDRESS=0x4a58D6d8D416a5fBCAcf3dC52eb8bE8948E25127
- ERC20_TOKEN_ADDRESS=0x3C665A31199694Bf723fD08844AD290207B5797f
- BRIDGEABLE_TOKEN_ADDRESS=0x792455a6bCb62Ed4C4362D323E0590654CA4765c
- VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
- VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
- REDIS_LOCK_TTL=1000
- HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- HOME_GAS_PRICE_SPEED_TYPE=standard
- HOME_GAS_PRICE_FALLBACK=1
- HOME_GAS_PRICE_UPDATE_INTERVAL=600000
- FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- FOREIGN_GAS_PRICE_SPEED_TYPE=standard
- FOREIGN_GAS_PRICE_FALLBACK=1
- FOREIGN_GAS_PRICE_UPDATE_INTERVAL=600000
- HOME_POLLING_INTERVAL=500
- FOREIGN_POLLING_INTERVAL=500
- ALLOW_HTTP=yes
networks:
- testnet
command: "true"
bridge-erc20-native:
build: bridge
environment:
- NODE_ENV=production
- BRIDGE_MODE=ERC_TO_NATIVE
- QUEUE_URL=amqp://rabbit
- REDIS_URL=redis://redis
- HOME_RPC_URL=http://parity1:8545
- FOREIGN_RPC_URL=http://parity2:8545
- HOME_BRIDGE_ADDRESS=0x488Af810997eD1730cB3a3918cD83b3216E6eAda
- FOREIGN_BRIDGE_ADDRESS=0x488Af810997eD1730cB3a3918cD83b3216E6eAda
- ERC20_TOKEN_ADDRESS=0x3C665A31199694Bf723fD08844AD290207B5797f
- BRIDGEABLE_TOKEN_ADDRESS=0x792455a6bCb62Ed4C4362D323E0590654CA4765c
- VALIDATOR_ADDRESS=0xaaB52d66283F7A1D5978bcFcB55721ACB467384b
- VALIDATOR_ADDRESS_PRIVATE_KEY=8e829f695aed89a154550f30262f1529582cc49dc30eff74a6b491359e0230f9
- REDIS_LOCK_TTL=1000
- HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- HOME_GAS_PRICE_SPEED_TYPE=standard
- HOME_GAS_PRICE_FALLBACK=1
- HOME_GAS_PRICE_UPDATE_INTERVAL=600000
- FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- FOREIGN_GAS_PRICE_SPEED_TYPE=standard
- FOREIGN_GAS_PRICE_FALLBACK=1
- FOREIGN_GAS_PRICE_UPDATE_INTERVAL=600000
- HOME_POLLING_INTERVAL=500
- FOREIGN_POLLING_INTERVAL=500
- ALLOW_HTTP=yes
networks:
- testnet
command: "true"
ui:
build: ..
environment:
- REACT_APP_HOME_BRIDGE_ADDRESS=0x32198D570fffC7033641F8A9094FFDCaAEF42624
- REACT_APP_FOREIGN_BRIDGE_ADDRESS=0x2B6871b9B02F73fa24F4864322CdC78604207769
- REACT_APP_FOREIGN_HTTP_PARITY_URL=http://10.1.0.103:8545
- REACT_APP_HOME_HTTP_PARITY_URL=http://10.1.0.102:8545
- REACT_APP_HOME_NATIVE_NAME=POA
- REACT_APP_HOME_NETWORK_NAME=Sokol
- REACT_APP_FOREIGN_NETWORK_NAME=Kovan
- REACT_APP_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx//%s
- REACT_APP_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
- REACT_APP_HOME_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/poa/sokol/address/%s
- REACT_APP_FOREIGN_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/eth/kovan/address/%s
- REACT_APP_HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- REACT_APP_HOME_GAS_PRICE_SPEED_TYPE=standard
- REACT_APP_HOME_GAS_PRICE_FALLBACK=5000000000
- REACT_APP_HOME_GAS_PRICE_UPDATE_INTERVAL=15000
- REACT_APP_FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- REACT_APP_FOREIGN_GAS_PRICE_SPEED_TYPE=standard
- REACT_APP_FOREIGN_GAS_PRICE_FALLBACK=5000000000
- REACT_APP_FOREIGN_GAS_PRICE_UPDATE_INTERVAL=15000
ports:
- "3000:3000"
networks:
testnet:
ipv4_address: 10.1.0.101
command: "true"
ui-erc20:
build: ..
environment:
- REACT_APP_HOME_BRIDGE_ADDRESS=0x1feB40aD9420b186F019A717c37f5546165d411E
- REACT_APP_FOREIGN_BRIDGE_ADDRESS=0x4a58D6d8D416a5fBCAcf3dC52eb8bE8948E25127
- REACT_APP_FOREIGN_HTTP_PARITY_URL=http://10.1.0.103:8545
- REACT_APP_HOME_HTTP_PARITY_URL=http://10.1.0.102:8545
- REACT_APP_HOME_NATIVE_NAME=POA
- REACT_APP_HOME_NETWORK_NAME=Sokol
- REACT_APP_FOREIGN_NETWORK_NAME=Kovan
- REACT_APP_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx//%s
- REACT_APP_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
- REACT_APP_HOME_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/poa/sokol/address/%s
- REACT_APP_FOREIGN_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/eth/kovan/address/%s
- REACT_APP_HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- REACT_APP_HOME_GAS_PRICE_SPEED_TYPE=standard
- REACT_APP_HOME_GAS_PRICE_FALLBACK=5000000000
- REACT_APP_HOME_GAS_PRICE_UPDATE_INTERVAL=15000
- REACT_APP_FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- REACT_APP_FOREIGN_GAS_PRICE_SPEED_TYPE=standard
- REACT_APP_FOREIGN_GAS_PRICE_FALLBACK=5000000000
- REACT_APP_FOREIGN_GAS_PRICE_UPDATE_INTERVAL=15000
ports:
- "3001:3000"
networks:
testnet:
ipv4_address: 10.1.0.104
command: "true"
ui-erc20-native:
build: ..
environment:
- REACT_APP_HOME_BRIDGE_ADDRESS=0x488Af810997eD1730cB3a3918cD83b3216E6eAda
- REACT_APP_FOREIGN_BRIDGE_ADDRESS=0x488Af810997eD1730cB3a3918cD83b3216E6eAda
- REACT_APP_FOREIGN_HTTP_PARITY_URL=http://10.1.0.103:8545
- REACT_APP_HOME_HTTP_PARITY_URL=http://10.1.0.102:8545
- REACT_APP_HOME_NATIVE_NAME=POA
- REACT_APP_HOME_NETWORK_NAME=Sokol
- REACT_APP_FOREIGN_NETWORK_NAME=Kovan
- REACT_APP_HOME_EXPLORER_TX_TEMPLATE=https://blockscout.com/poa/sokol/tx//%s
- REACT_APP_FOREIGN_EXPLORER_TX_TEMPLATE=https://blockscout.com/eth/kovan/tx/%s
- REACT_APP_HOME_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/poa/sokol/address/%s
- REACT_APP_FOREIGN_EXPLORER_ADDRESS_TEMPLATE=https://blockscout.com/eth/kovan/address/%s
- REACT_APP_HOME_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- REACT_APP_HOME_GAS_PRICE_SPEED_TYPE=standard
- REACT_APP_HOME_GAS_PRICE_FALLBACK=5000000000
- REACT_APP_HOME_GAS_PRICE_UPDATE_INTERVAL=15000
- REACT_APP_FOREIGN_GAS_PRICE_ORACLE_URL=https://gasprice.poa.network/
- REACT_APP_FOREIGN_GAS_PRICE_SPEED_TYPE=standard
- REACT_APP_FOREIGN_GAS_PRICE_FALLBACK=5000000000
- REACT_APP_FOREIGN_GAS_PRICE_UPDATE_INTERVAL=15000
ports:
- "3002:3000"
networks:
testnet:
ipv4_address: 10.1.0.105
command: "true"
networks:
testnet:
driver: bridge
ipam:
driver: default
config:
- subnet: 10.1.0.0/24

@ -0,0 +1,133 @@
const Page = require('./Page.js').Page;
const By = require('selenium-webdriver/lib/by').By;
const fieldAmount = By.id("amount");
const buttonTransfer = By.className("bridge-form-button ");
const buttonOk = By.className("swal-button swal-button--confirm");
const fieldsBalance = By.className("network-balance");
const classWeb3Loaded = By.className("web3-loaded");
const classPendingTransaction = By.className("pending-transaction");
const loadingContainer = By.className("loading-container");
const buttonTransferConfirm = By.className("transfer-confirm");
const buttonDisclaimerConfirm = By.className("disclaimer-confirm");
const checkboxDisclaimer = By.className("disclaimer-checkbox");
const disclaimer = By.className("disclaimer-title");
class MainPage extends Page {
constructor(driver) {
super(driver);
this.url;
this.fieldHomePOABalance;
this.fieldForeignPOABalance;
}
async initFieldsBalance() {
if (!(await this.waitUntilWeb3Loaded())) return null;
try {
let array;
array = await super.findWithWait(fieldsBalance);
this.fieldHomePOABalance = array[0];
this.fieldForeignPOABalance = array[1];
return array;
}
catch (err) {
return null;
}
}
async getHomePOABalance() {
await this.initFieldsBalance();
return parseFloat(await this.fieldHomePOABalance.getText());
}
async getForeignPOABalance() {
await this.initFieldsBalance();
return parseFloat(await this.fieldForeignPOABalance.getText());
}
async fillFieldAmount(amount) {
try {
await this.clickWithWait(fieldAmount);
await this.fillWithWait(fieldAmount, amount);
return true;
}
catch (err) {
return false;
}
}
async clickButtonTransfer() {
return await this.clickWithWait(buttonTransfer);
}
async clickButtonOk() {
return await super.clickWithWait(buttonOk);
}
async clickButtonTransferConfirm() {
return await super.clickWithWait(buttonTransferConfirm);
}
async isPresentButtonOk() {
return await super.isElementDisplayed(buttonOk, 180);
}
async waitUntilWeb3Loaded() {
return await this.waitUntilLocated(classWeb3Loaded, 180);
}
async isPendingTransaction() {
return await super.isElementLocated(classPendingTransaction);
}
async waitUntilTransactionDone() {
return await this.waitUntilDisappear(classPendingTransaction, 360);
}
async waitUntilShowUpButtonOk() {
return await super.waitUntilDisplayed(buttonOk, 360);
}
async waitUntilShowUpButtonTransferConfirm() {
return await super.waitUntilDisplayed(buttonTransferConfirm, 360);
}
async waitUntilShowUpButtonOk() {
return await super.waitUntilDisplayed(buttonOk, 360);
}
async waitUntilShowUpLoadingContainer() {
return await super.waitUntilDisplayed(loadingContainer, 180);
}
async isDisplayedLoadingContainer() {
return await super.isElementDisplayed(loadingContainer);
}
async confirmDisclaimer() {
return await super.waitUntilDisplayed(disclaimer, 180) &&
//await this.clickCheckboxDisclaimer() &&
await this.clickButtonDisclaimerConfirm();
}
async clickButtonDisclaimerConfirm() {
return await super.clickWithWait(buttonDisclaimerConfirm);
}
async clickCheckboxDisclaimer() {
return await super.clickWithWait(checkboxDisclaimer);
}
async open(url) {
let counter = 60;
do {
await this.driver.sleep(1000);
await super.open(url);
} while (counter-- >= 0 && !await this.isElementDisplayed(disclaimer))
return (counter >= 0);
}
}
module.exports = {
MainPage: MainPage
};

@ -0,0 +1,7 @@
FROM parity/parity:v2.3.3
WORKDIR /stuff
COPY . .
CMD ["--chain", "chain.json", "--network-id", "77", "--jsonrpc-apis", "all", "--jsonrpc-interface", "all", "--jsonrpc-cors", "all", "--jsonrpc-hosts", "all"]

@ -0,0 +1,7 @@
FROM parity/parity:v2.3.3
WORKDIR /stuff
COPY . .
CMD ["--chain", "chain-foreign.json", "--network-id", "42", "--jsonrpc-apis", "all", "--jsonrpc-interface", "all", "--jsonrpc-cors", "all", "--jsonrpc-hosts", "all"]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1,27 @@
#!/usr/bin/env bash
docker-compose up -d --build --force-recreate
cd ..
npm run start:blocks &
cd e2e-script
docker-compose run contracts ./deploy.sh
docker-compose run -d bridge npm run watcher:signature-request
docker-compose run -d bridge npm run watcher:collected-signatures
docker-compose run -d bridge npm run watcher:affirmation-request
docker-compose run -d bridge-erc20 npm run watcher:signature-request
docker-compose run -d bridge-erc20 npm run watcher:collected-signatures
docker-compose run -d bridge-erc20 npm run watcher:affirmation-request
docker-compose run -d bridge-erc20-native npm run watcher:signature-request
docker-compose run -d bridge-erc20-native npm run watcher:collected-signatures
docker-compose run -d bridge-erc20-native npm run watcher:affirmation-request
docker-compose run -d bridge npm run sender:home
docker-compose run -d bridge npm run sender:foreign
docker-compose run -d ui npm start
docker-compose run -d ui-erc20 npm start
docker-compose run -d ui-erc20-native npm start
cd ..
npm run startE2e
rc=$?
cd e2e-script
ps | grep node | grep -v grep | awk '{print "kill " $1}' | sh
docker-compose down
exit $rc

@ -0,0 +1,33 @@
const Web3 = require('web3')
const homeWeb3 = new Web3(new Web3.providers.HttpProvider('http://10.1.0.102:8545'))
const foreignWeb3 = new Web3(new Web3.providers.HttpProvider('http://10.1.0.103:8545'))
const account = '0x7FC1442AB55Da569940Eb750AaD2BAA63DA4010E'
const privateKey = '0x460635eb4ac4287de2d2393985e19b4a9f948ac533453a1044ab8d50330b0df9'
homeWeb3.eth.accounts.wallet.add(privateKey)
foreignWeb3.eth.accounts.wallet.add(privateKey)
function generateNewBlock(web3, address) {
return web3.eth.sendTransaction({
from: address,
to: '0x0000000000000000000000000000000000000000',
gasPrice: '1',
gas: '21000',
value: '1'
})
}
function main() {
setTimeout(async () => {
generateNewBlock(homeWeb3, account)
generateNewBlock(foreignWeb3, account)
main()
}, 5000)
}
main()
process.on('SIGTERM', function () {
console.log('Finishing sending blocks...')
process.exit(0);
});

@ -0,0 +1,309 @@
let test = require('selenium-webdriver/testing');
let assert = require('assert');
const Utils = require('./Utils.js').Utils;
const MetaMask = require('./MetaMask.js').MetaMask;
const MainPage = require('./mainPage.js').MainPage;
const User = require("./User.js").User;
test.describe('e2e-test for bridge.poa, version 1.5.0', async function () {
this.timeout(5 * 60000);
this.slow(1 * 60000);
const maxAmountPerTransactionLimit = 1;
let startURL;
let driver;
let mainPage;
let homeAccount;
let foreignAccount;
let metaMask;
let foreignBalanceBefore;
let homeBalanceBefore;
test.before(async function () {
try {
driver = await Utils.startBrowserWithMetamask();
mainPage = new MainPage(driver);
homeAccount = new User(driver, await Utils.getHomeAccount());
foreignAccount = new User(driver, await Utils.getForeignAccount());
metaMask = new MetaMask(driver);
await metaMask.activate();
await homeAccount.setMetaMaskAccount();
} catch (e) {
console.log(e)
}
});
test.after(async function () {
try {
await driver.quit();
} catch (e) {
console.log(e)
}
});
test.it('User is able to open main page of bridge-ui ',
async function () {
startURL = await Utils.getStartURL();
let result = await mainPage.open(startURL);
console.log("Test URL: " + startURL);
return await assert.equal(result, true, "Test FAILED. Build failed.");
});
test.it('Home page: disclaimer is displayed ',
async function () {
let result = await mainPage.confirmDisclaimer();
return await assert.equal(result, true, "Test FAILED. Disclaimer is not displayed");
});
test.it('Main page: foreign POA balance is displayed ',
async function () {
foreignBalanceBefore = await mainPage.getForeignPOABalance();
console.log("foreignBalanceBefore = "+foreignBalanceBefore);
let result = foreignBalanceBefore === 0;
return await assert.equal(result, true, "Test FAILED.Foreign POA balance is zero or not displayed ");
});
test.it('Main page: home POA balance is displayed ',
async function () {
homeBalanceBefore = await mainPage.getHomePOABalance();
console.log("homeBalanceBefore = "+homeBalanceBefore);
let result = homeBalanceBefore !== 0;
return await assert.equal(result, true, "Test FAILED.Home POA balance is zero or not displayed ");
});
test.it('User is able to send tokens from Home account to Foreign account ',
async function () {
let result = await homeAccount.transferTokens(maxAmountPerTransactionLimit);
return await assert.equal(result, true, "Test FAILED. User is able send tokens from Home account to Foreign account");
});
test.it('Home POA balance has correctly changed after transaction',
async function () {
let newHomeBalance = await mainPage.getHomePOABalance();
let shouldBe = homeBalanceBefore - maxAmountPerTransactionLimit;
console.log("newHomeBalance = " + newHomeBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newHomeBalance)) < (maxAmountPerTransactionLimit / 100);
homeBalanceBefore = newHomeBalance;
return await assert.equal(result, true, "Test FAILED.Home POA balance is not correct after transaction");
});
test.it('Foreign account has received correct amount of tokens after transaction ',
async function () {
let newForeignBalance = await mainPage.getForeignPOABalance();
let shouldBe = foreignBalanceBefore + maxAmountPerTransactionLimit;
console.log("newForeignBalance = " + newForeignBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newForeignBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED. Foreign POA balance is not correct after transaction");
});
test.it('User is able to send tokens from Foreign account to Home account ',
async function () {
await foreignAccount.setMetaMaskNetwork();
foreignBalanceBefore = await mainPage.getHomePOABalance();
let result = await foreignAccount.transferTokens(maxAmountPerTransactionLimit);
return await assert.equal(result, true, "Test FAILED. User is able send tokens from Home account to Foreign account");
});
test.it('Foreign POA balance has correctly changed after transaction',
async function () {
let newForeignBalance = await mainPage.getHomePOABalance();
let shouldBe = foreignBalanceBefore - maxAmountPerTransactionLimit;
console.log("newForeignBalance = " + newForeignBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newForeignBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED.Foreign POA balance is not correct after transaction");
});
test.it('Home account has received correct amount of tokens after transaction ',
async function () {
let newHomeBalance = await mainPage.getForeignPOABalance();
let shouldBe = homeBalanceBefore + maxAmountPerTransactionLimit;
console.log("newHomeBalance = " + newHomeBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newHomeBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED.Home POA balance is not correct after transaction");
});
test.it('ERC20-ERC20 - User is able to open main page of bridge-ui ',
async function () {
startURL = await Utils.getErc20StartURL();
let result = await mainPage.open(startURL);
console.log("Test URL: " + startURL);
return await assert.equal(result, true, "Test FAILED. Build failed.");
});
test.it('ERC20-ERC20 - Home page: disclaimer is displayed ',
async function () {
let result = await mainPage.confirmDisclaimer();
return await assert.equal(result, true, "Test FAILED. Disclaimer is not displayed");
});
test.it('ERC20-ERC20 - Main page: foreign erc20 balance is displayed ',
async function () {
foreignBalanceBefore = await mainPage.getForeignPOABalance();
console.log("foreignBalanceBefore = "+foreignBalanceBefore);
let result = foreignBalanceBefore === 0;
return await assert.equal(result, true, "Test FAILED. Foreign erc20 balance is not zero");
});
test.it('ERC20-ERC20 - Main page: home erc20 balance is displayed ',
async function () {
homeBalanceBefore = await mainPage.getHomePOABalance();
console.log("homeBalanceBefore = "+homeBalanceBefore);
let result = homeBalanceBefore !== 0;
return await assert.equal(result, true, "Test FAILED. Home erc20 balance is zero or not displayed ");
});
test.it('ERC20-ERC20 - User is able to send tokens from Foreign account to Home account ',
async function () {
homeBalanceBefore = await mainPage.getForeignPOABalance();
foreignBalanceBefore = await mainPage.getHomePOABalance();
let result = await foreignAccount.transferTokens(maxAmountPerTransactionLimit);
return await assert.equal(result, true, "Test FAILED. User is able send tokens from Foreign account to Home account");
});
test.it('ERC20-ERC20 - Foreign POA balance has correctly changed after transaction',
async function () {
let newForeignBalance = await mainPage.getHomePOABalance();
let shouldBe = foreignBalanceBefore - maxAmountPerTransactionLimit;
console.log("newForeignBalance = " + newForeignBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newForeignBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED.Foreign POA balance is not correct after transaction");
});
test.it('ERC20-ERC20 - Home account has received correct amount of tokens after transaction ',
async function () {
let newHomeBalance = await mainPage.getForeignPOABalance();
let shouldBe = homeBalanceBefore + maxAmountPerTransactionLimit;
console.log("newHomeBalance = " + newHomeBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newHomeBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED.Home POA balance is not correct after transaction");
});
test.it('ERC20-ERC20 - User is able to send tokens from Home account to Foreign account ',
async function () {
await homeAccount.setMetaMaskNetwork();
homeBalanceBefore = await mainPage.getHomePOABalance();
foreignBalanceBefore = await mainPage.getForeignPOABalance();
let result = await homeAccount.transferTokens(maxAmountPerTransactionLimit);
return await assert.equal(result, true, "Test FAILED. User is able send tokens from Home account to Foreign account");
});
test.it('ERC20-ERC20 - Home POA balance has correctly changed after transaction',
async function () {
let newHomeBalance = await mainPage.getHomePOABalance();
let shouldBe = homeBalanceBefore - maxAmountPerTransactionLimit;
console.log("newHomeBalance = " + newHomeBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newHomeBalance)) < (maxAmountPerTransactionLimit / 100);
homeBalanceBefore = newHomeBalance;
return await assert.equal(result, true, "Test FAILED.Home POA balance is not correct after transaction");
});
test.it('ERC20-ERC20 - Foreign account has received correct amount of tokens after transaction ',
async function () {
let newForeignBalance = await mainPage.getForeignPOABalance();
let shouldBe = foreignBalanceBefore + maxAmountPerTransactionLimit;
console.log("newForeignBalance = " + newForeignBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newForeignBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED. Foreign POA balance is not correct after transaction");
});
test.it('ERC20-Native - User is able to open main page of bridge-ui ',
async function () {
startURL = await Utils.getErc20NativeStartURL();
let result = await mainPage.open(startURL);
console.log("Test URL: " + startURL);
return await assert.equal(result, true, "Test FAILED. Build failed.");
});
test.it('ERC20-Native - Home page: disclaimer is displayed ',
async function () {
let result = await mainPage.confirmDisclaimer();
return await assert.equal(result, true, "Test FAILED. Disclaimer is not displayed");
});
test.it('ERC20-Native - Main page: foreign erc20 balance is displayed ',
async function () {
await foreignAccount.setMetaMaskNetwork();
foreignBalanceBefore = await mainPage.getForeignPOABalance();
console.log("foreignBalanceBefore = "+foreignBalanceBefore);
let result = foreignBalanceBefore !== 0;
return await assert.equal(result, true, "Test FAILED. Foreign erc20 balance is zero");
});
test.it('ERC20-Native - Main page: home erc20 balance is displayed ',
async function () {
homeBalanceBefore = await mainPage.getHomePOABalance();
console.log("homeBalanceBefore = "+homeBalanceBefore);
let result = homeBalanceBefore !== 0;
return await assert.equal(result, true, "Test FAILED. Home erc20 balance is zero or not displayed ");
});
test.it('ERC20-Native - User is able to send tokens from Foreign account to Home account ',
async function () {
homeBalanceBefore = await mainPage.getForeignPOABalance();
foreignBalanceBefore = await mainPage.getHomePOABalance();
let result = await foreignAccount.transferTokens(maxAmountPerTransactionLimit);
return await assert.equal(result, true, "Test FAILED. User is able send tokens from Foreign account to Home account");
});
test.it('ERC20-Native - Foreign POA balance has correctly changed after transaction',
async function () {
let newForeignBalance = await mainPage.getHomePOABalance();
let shouldBe = foreignBalanceBefore - maxAmountPerTransactionLimit;
console.log("newForeignBalance = " + newForeignBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newForeignBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED.Foreign POA balance is not correct after transaction");
});
test.it('ERC20-Native - Home account has received correct amount of tokens after transaction ',
async function () {
let newHomeBalance = await mainPage.getForeignPOABalance();
let shouldBe = homeBalanceBefore + maxAmountPerTransactionLimit;
console.log("newHomeBalance = " + newHomeBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newHomeBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED.Home POA balance is not correct after transaction");
});
test.it('ERC20-Native - User is able to send tokens from Home account to Foreign account ',
async function () {
await homeAccount.setMetaMaskNetwork();
homeBalanceBefore = await mainPage.getHomePOABalance();
foreignBalanceBefore = await mainPage.getForeignPOABalance();
let result = await homeAccount.transferTokens(maxAmountPerTransactionLimit);
return await assert.equal(result, true, "Test FAILED. User is able send tokens from Home account to Foreign account");
});
test.it('ERC20-Native - Home POA balance has correctly changed after transaction',
async function () {
let newHomeBalance = await mainPage.getHomePOABalance();
let shouldBe = homeBalanceBefore - maxAmountPerTransactionLimit;
console.log("newHomeBalance = " + newHomeBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newHomeBalance)) < (maxAmountPerTransactionLimit / 100);
homeBalanceBefore = newHomeBalance;
return await assert.equal(result, true, "Test FAILED.Home POA balance is not correct after transaction");
});
test.it('ERC20-Native - Foreign account has received correct amount of tokens after transaction ',
async function () {
let newForeignBalance = await mainPage.getForeignPOABalance();
let shouldBe = foreignBalanceBefore + maxAmountPerTransactionLimit;
console.log("newForeignBalance = " + newForeignBalance);
console.log("shouldBe = " + shouldBe);
let result = (Math.abs(shouldBe - newForeignBalance)) < (maxAmountPerTransactionLimit / 100);
return await assert.equal(result, true, "Test FAILED. Foreign POA balance is not correct after transaction");
});
});

@ -0,0 +1,472 @@
/*
This file is part of web3.js.
web3.js is free software: you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
web3.js is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License
along with web3.js. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* @file index.js
* @author Fabian Vogelsteller <fabian@ethereum.org>
* @date 2017
*/
"use strict";
var _ = require('underscore');
var core = require('web3-core');
var helpers = require('web3-core-helpers');
var Subscriptions = require('web3-core-subscriptions').subscriptions;
var Method = require('web3-core-method');
var utils = require('web3-utils');
var Net = require('web3-net');
var Personal = require('web3-eth-personal');
var BaseContract = require('web3-eth-contract');
var Iban = require('web3-eth-iban');
var Accounts = require('web3-eth-accounts');
var abi = require('web3-eth-abi');
var getNetworkType = require('./getNetworkType.js');
var formatter = helpers.formatters;
var blockCall = function (args) {
return (_.isString(args[0]) && args[0].indexOf('0x') === 0) ? "eth_getBlockByHash" : "eth_getBlockByNumber";
};
var transactionFromBlockCall = function (args) {
return (_.isString(args[0]) && args[0].indexOf('0x') === 0) ? 'eth_getTransactionByBlockHashAndIndex' : 'eth_getTransactionByBlockNumberAndIndex';
};
var uncleCall = function (args) {
return (_.isString(args[0]) && args[0].indexOf('0x') === 0) ? 'eth_getUncleByBlockHashAndIndex' : 'eth_getUncleByBlockNumberAndIndex';
};
var getBlockTransactionCountCall = function (args) {
return (_.isString(args[0]) && args[0].indexOf('0x') === 0) ? 'eth_getBlockTransactionCountByHash' : 'eth_getBlockTransactionCountByNumber';
};
var uncleCountCall = function (args) {
return (_.isString(args[0]) && args[0].indexOf('0x') === 0) ? 'eth_getUncleCountByBlockHash' : 'eth_getUncleCountByBlockNumber';
};
var Eth = function Eth() {
var _this = this;
// sets _requestmanager
core.packageInit(this, arguments);
// overwrite setProvider
var setProvider = this.setProvider;
this.setProvider = function () {
setProvider.apply(_this, arguments);
_this.net.setProvider.apply(_this, arguments);
_this.personal.setProvider.apply(_this, arguments);
_this.accounts.setProvider.apply(_this, arguments);
_this.Contract.setProvider(_this.currentProvider, _this.accounts);
};
var defaultAccount = null;
var defaultBlock = 'latest';
Object.defineProperty(this, 'defaultAccount', {
get: function () {
return defaultAccount;
},
set: function (val) {
if(val) {
defaultAccount = utils.toChecksumAddress(formatter.inputAddressFormatter(val));
}
// also set on the Contract object
_this.Contract.defaultAccount = defaultAccount;
_this.personal.defaultAccount = defaultAccount;
// update defaultBlock
methods.forEach(function(method) {
method.defaultAccount = defaultAccount;
});
return val;
},
enumerable: true
});
Object.defineProperty(this, 'defaultBlock', {
get: function () {
return defaultBlock;
},
set: function (val) {
defaultBlock = val;
// also set on the Contract object
_this.Contract.defaultBlock = defaultBlock;
_this.personal.defaultBlock = defaultBlock;
// update defaultBlock
methods.forEach(function(method) {
method.defaultBlock = defaultBlock;
});
return val;
},
enumerable: true
});
this.clearSubscriptions = _this._requestManager.clearSubscriptions;
// add net
this.net = new Net(this.currentProvider);
// add chain detection
this.net.getNetworkType = getNetworkType.bind(this);
// add accounts
this.accounts = new Accounts(this.currentProvider);
// add personal
this.personal = new Personal(this.currentProvider);
this.personal.defaultAccount = this.defaultAccount;
// create a proxy Contract type for this instance, as a Contract's provider
// is stored as a class member rather than an instance variable. If we do
// not create this proxy type, changing the provider in one instance of
// web3-eth would subsequently change the provider for _all_ contract
// instances!
var Contract = function Contract() {
BaseContract.apply(this, arguments);
};
Contract.setProvider = function() {
BaseContract.setProvider.apply(this, arguments);
};
// make our proxy Contract inherit from web3-eth-contract so that it has all
// the right functionality and so that instanceof and friends work properly
Contract.prototype = Object.create(BaseContract.prototype);
Contract.prototype.constructor = Contract;
// add contract
this.Contract = Contract;
this.Contract.defaultAccount = this.defaultAccount;
this.Contract.defaultBlock = this.defaultBlock;
this.Contract.setProvider(this.currentProvider, this.accounts);
// add IBAN
this.Iban = Iban;
// add ABI
this.abi = abi;
var methods = [
new Method({
name: 'getProtocolVersion',
call: 'eth_protocolVersion',
params: 0
}),
new Method({
name: 'getCoinbase',
call: 'eth_coinbase',
params: 0
}),
new Method({
name: 'isMining',
call: 'eth_mining',
params: 0
}),
new Method({
name: 'getHashrate',
call: 'eth_hashrate',
params: 0,
outputFormatter: utils.hexToNumber
}),
new Method({
name: 'getChainId',
call: 'eth_chainId',
params: 0,
outputFormatter: utils.hexToNumber
}),
new Method({
name: 'isSyncing',
call: 'eth_syncing',
params: 0,
outputFormatter: formatter.outputSyncingFormatter
}),
new Method({
name: 'getGasPrice',
call: 'eth_gasPrice',
params: 0,
outputFormatter: formatter.outputBigNumberFormatter
}),
new Method({
name: 'getAccounts',
call: 'eth_accounts',
params: 0,
outputFormatter: utils.toChecksumAddress
}),
new Method({
name: 'getBlockNumber',
call: 'eth_blockNumber',
params: 0,
outputFormatter: utils.hexToNumber
}),
new Method({
name: 'getBalance',
call: 'eth_getBalance',
params: 2,
inputFormatter: [formatter.inputAddressFormatter, formatter.inputDefaultBlockNumberFormatter],
outputFormatter: formatter.outputBigNumberFormatter
}),
new Method({
name: 'getStorageAt',
call: 'eth_getStorageAt',
params: 3,
inputFormatter: [formatter.inputAddressFormatter, utils.numberToHex, formatter.inputDefaultBlockNumberFormatter]
}),
new Method({
name: 'getCode',
call: 'eth_getCode',
params: 2,
inputFormatter: [formatter.inputAddressFormatter, formatter.inputDefaultBlockNumberFormatter]
}),
new Method({
name: 'getBlock',
call: blockCall,
params: 2,
inputFormatter: [formatter.inputBlockNumberFormatter, function (val) { return !!val; }],
outputFormatter: formatter.outputBlockFormatter
}),
new Method({
name: 'getUncle',
call: uncleCall,
params: 2,
inputFormatter: [formatter.inputBlockNumberFormatter, utils.numberToHex],
outputFormatter: formatter.outputBlockFormatter,
}),
new Method({
name: 'getBlockTransactionCount',
call: getBlockTransactionCountCall,
params: 1,
inputFormatter: [formatter.inputBlockNumberFormatter],
outputFormatter: utils.hexToNumber
}),
new Method({
name: 'getBlockUncleCount',
call: uncleCountCall,
params: 1,
inputFormatter: [formatter.inputBlockNumberFormatter],
outputFormatter: utils.hexToNumber
}),
new Method({
name: 'getTransaction',
call: 'eth_getTransactionByHash',
params: 1,
inputFormatter: [null],
outputFormatter: formatter.outputTransactionFormatter
}),
new Method({
name: 'getTransactionFromBlock',
call: transactionFromBlockCall,
params: 2,
inputFormatter: [formatter.inputBlockNumberFormatter, utils.numberToHex],
outputFormatter: formatter.outputTransactionFormatter
}),
new Method({
name: 'getTransactionReceipt',
call: 'eth_getTransactionReceipt',
params: 1,
inputFormatter: [null],
outputFormatter: formatter.outputTransactionReceiptFormatter
}),
new Method({
name: 'getTransactionCount',
call: 'eth_getTransactionCount',
params: 2,
inputFormatter: [formatter.inputAddressFormatter, formatter.inputDefaultBlockNumberFormatter],
outputFormatter: utils.hexToNumber
}),
new Method({
name: 'sendSignedTransaction',
call: 'eth_sendRawTransaction',
params: 1,
inputFormatter: [null]
}),
new Method({
name: 'signTransaction',
call: 'eth_signTransaction',
params: 1,
inputFormatter: [formatter.inputTransactionFormatter]
}),
new Method({
name: 'sendTransaction',
call: 'eth_sendTransaction',
params: 1,
inputFormatter: [formatter.inputTransactionFormatter]
}),
new Method({
name: 'sign',
call: 'eth_sign',
params: 2,
inputFormatter: [formatter.inputSignFormatter, formatter.inputAddressFormatter],
transformPayload: function (payload) {
payload.params.reverse();
return payload;
}
}),
new Method({
name: 'call',
call: 'eth_call',
params: 2,
inputFormatter: [formatter.inputCallFormatter, formatter.inputDefaultBlockNumberFormatter]
}),
new Method({
name: 'estimateGas',
call: 'eth_estimateGas',
params: 1,
inputFormatter: [formatter.inputCallFormatter],
outputFormatter: utils.hexToNumber
}),
new Method({
name: 'getCompilers',
call: 'eth_getCompilers',
params: 0
}),
new Method({
name: 'compile.solidity',
call: 'eth_compileSolidity',
params: 1
}),
new Method({
name: 'compile.lll',
call: 'eth_compileLLL',
params: 1
}),
new Method({
name: 'compile.serpent',
call: 'eth_compileSerpent',
params: 1
}),
new Method({
name: 'submitWork',
call: 'eth_submitWork',
params: 3
}),
new Method({
name: 'getWork',
call: 'eth_getWork',
params: 0
}),
new Method({
name: 'getPastLogs',
call: 'eth_getLogs',
params: 1,
inputFormatter: [formatter.inputLogFormatter],
outputFormatter: formatter.outputLogFormatter
}),
// subscriptions
new Subscriptions({
name: 'subscribe',
type: 'eth',
subscriptions: {
'newBlockHeaders': {
// TODO rename on RPC side?
subscriptionName: 'newHeads', // replace subscription with this name
params: 0,
outputFormatter: formatter.outputBlockFormatter
},
'pendingTransactions': {
subscriptionName: 'newPendingTransactions', // replace subscription with this name
params: 0
},
'logs': {
params: 1,
inputFormatter: [formatter.inputLogFormatter],
outputFormatter: formatter.outputLogFormatter,
// DUBLICATE, also in web3-eth-contract
subscriptionHandler: function (output) {
if(output.removed) {
this.emit('changed', output);
} else {
this.emit('data', output);
}
if (_.isFunction(this.callback)) {
this.callback(null, output, this);
}
}
},
'syncing': {
params: 0,
outputFormatter: formatter.outputSyncingFormatter,
subscriptionHandler: function (output) {
var _this = this;
// fire TRUE at start
if(this._isSyncing !== true) {
this._isSyncing = true;
this.emit('changed', _this._isSyncing);
if (_.isFunction(this.callback)) {
this.callback(null, _this._isSyncing, this);
}
setTimeout(function () {
_this.emit('data', output);
if (_.isFunction(_this.callback)) {
_this.callback(null, output, _this);
}
}, 0);
// fire sync status
} else {
this.emit('data', output);
if (_.isFunction(_this.callback)) {
this.callback(null, output, this);
}
// wait for some time before fireing the FALSE
clearTimeout(this._isSyncingTimeout);
this._isSyncingTimeout = setTimeout(function () {
if(output.currentBlock > output.highestBlock - 200) {
_this._isSyncing = false;
_this.emit('changed', _this._isSyncing);
if (_.isFunction(_this.callback)) {
_this.callback(null, _this._isSyncing, _this);
}
}
}, 500);
}
}
}
}
})
];
methods.forEach(function(method) {
method.attachToObject(_this);
method.setRequestManager(_this._requestManager, _this.accounts); // second param means is eth.accounts (necessary for wallet signing)
method.defaultBlock = _this.defaultBlock;
method.defaultAccount = _this.defaultAccount;
});
};
core.addProviders(Eth);
module.exports = Eth;

18159
bridge-ui/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

62
bridge-ui/package.json Normal file

@ -0,0 +1,62 @@
{
"name": "bridge-ui",
"version": "0.1.0",
"private": true,
"homepage": "https://poanetwork.github.io/",
"dependencies": {
"@babel/plugin-proposal-decorators": "^7.4.0",
"bignumber.js": "^6.0.0",
"chromedriver": "^2.35.0",
"coveralls": "^3.0.0",
"customize-cra": "^0.2.12",
"dotenv": "^7.0.0",
"fs-extra": "^5.0.0",
"gh-pages": "^1.1.0",
"mobx": "^4.0.2",
"mobx-react": "^5.0.0",
"mocha": "^5.1.1",
"node-sass-chokidar": "^1.0.1",
"numeral": "^2.0.6",
"react": "^16.2.0",
"react-app-rewire-mobx": "^1.0.7",
"react-app-rewired": "^2.0.3",
"react-copy-to-clipboard": "^5.0.1",
"react-dom": "^16.2.0",
"react-router": "^4.3.1",
"react-router-dom": "^4.2.2",
"react-scripts": "2.1.8",
"react-transition-group": "^2.2.1",
"selenium-webdriver": "^3.6.0",
"sweetalert": "^2.1.0",
"web3": "1.0.0-beta.30",
"web3-utils": "1.0.0-beta.30"
},
"scripts": {
"select-css-theme": "node scripts/selectTheme.js",
"build-css": "node-sass-chokidar src/assets/stylesheets -o src/assets/stylesheets --output-style=compressed -m application*.css",
"watch-css": "nodemon -e scss -x \"npm run build-css\"",
"start": "npm run build-css && npm run select-css-theme && react-app-rewired start",
"build": "npm run compile:contracts && npm run build-css && npm run select-css-theme && react-app-rewired build",
"test": "react-app-rewired test --env=jsdom",
"startE2e": "mocha -b ./e2e-script/test.js",
"start:blocks": "node ./e2e-script/scripts/blocks.js",
"coverage": "react-app-rewired test --env=jsdom --coverage",
"coveralls": "cat ./coverage/lcov.info | node node_modules/.bin/coveralls",
"eject": "react-app-rewired eject",
"predeploy": "npm run build",
"deploy": "gh-pages -d build -o origin",
"compile:contracts": "cd submodules/poa-bridge-contracts && npm install && npm run compile && cd ../../ && rm -r -f src/contracts && cp -r submodules/poa-bridge-contracts/build/contracts src/contracts",
"postinstall": "cp lib/web3-eth/index.js ../node_modules/web3-eth/src && npm run compile:contracts"
},
"devDependencies": {
"jest-dom": "^3.0.1",
"nodemon": "^1.18.11",
"react-testing-library": "^5.4.4"
},
"browserslist": [
">0.2%",
"not dead",
"not ie <= 11",
"not op_mini all"
]
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 921 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

@ -0,0 +1,18 @@
{
"name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-256x256.png",
"sizes": "256x256",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta property="og:title" content="POA Bridge UI" />
<meta property="og:description" content="%REACT_APP_DESCRIPTION%" />
<meta property="og:url" content="https://poanetwork.github.io/bridge-ui" />
<meta property="og:type" content="website" />
<meta property="og:image" content="/images/bridgeogimage.jpg">
<link href="https://fonts.googleapis.com/css?family=Nunito:300,400,700" rel="stylesheet">
<link rel="manifest" href="%PUBLIC_URL%/favicons/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicons/favicon.ico">
<title>TokenBridge UI app</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
</body>
</html>

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

@ -0,0 +1,17 @@
const path = require('path');
require('dotenv').config({
path: path.resolve(__dirname, '..', '.env')
});
const fs = require('fs');
const stylePath = path.resolve(__dirname, '..', 'src', 'assets', 'stylesheets');
const destinationFilename = 'application.css';
let filename;
if (process.env.APP_STYLES === 'classic') {
filename = 'application.classic.css'
} else {
filename = 'application.core.css'
}
fs.copyFileSync(path.resolve(stylePath, filename), path.resolve(stylePath, destinationFilename));

62
bridge-ui/src/App.js Normal file

@ -0,0 +1,62 @@
import React from 'react';
import { Header, Bridge, RelayEvents, Footer, SweetAlert, Loading, StatusPage, StatisticsPage } from './components';
import { Route } from 'react-router-dom'
import './assets/stylesheets/application.css';
import { Disclaimer } from './components'
import { ModalContainer } from './components'
import { NoWallet } from './components'
import { setItem, getItem, DISCLAIMER_KEY } from './components/utils/localstorage'
export class App extends React.Component {
state = {
showDisclaimer: false,
showMobileMenu: false
}
componentDidMount() {
const disclaimerDisplayed = getItem(DISCLAIMER_KEY)
if(!disclaimerDisplayed) {
this.setState({ showDisclaimer: true })
}
}
closeDisclaimer = () => {
setItem(DISCLAIMER_KEY, true)
this.setState({showDisclaimer: false})
}
toggleMobileMenu = () => {
this.setState(prevState => ({ showMobileMenu: !prevState.showMobileMenu}))
}
render() {
const { showDisclaimer, showMobileMenu } = this.state
return (
<div className={showMobileMenu ? 'mobile-menu-is-open' : ''}>
<Route component={Loading}/>
<Route component={SweetAlert}/>
<Route render={() =>
<Header
onMenuToggle={this.toggleMobileMenu}
showMobileMenu={showMobileMenu}
/>
}/>
<div className="app-container">
{showMobileMenu && <Route render={() => <div className="mobile-menu-open"/>}/>}
<Route exact path="/" component={Bridge}/>
<Route exact path="/events" component={RelayEvents}/>
<Route exact path="/status" component={StatusPage}/>
<Route exact path="/statistics" component={StatisticsPage}/>
</div>
<Route component={Footer}/>
<ModalContainer
showModal={showDisclaimer}
>
<Disclaimer onConfirmation={this.closeDisclaimer} />
</ModalContainer>
<NoWallet showModal={!showDisclaimer} />
</div>
)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -0,0 +1,27 @@
<svg
style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:1.41421"
version="1.1"
viewBox="0 0 330 71"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<style type="text/css">
@import url('https://fonts.googleapis.com/css?family=Nunito:400');
text {
font-family: "Nunito", sans-serif;
}
</style>
</defs>
<text
fill="#fff"
font-family="Nunito"
font-size="26.923px"
font-weight="500"
x="128.126px"
y="44.219px"
>Bridge Loading</text>
<path
d="M25.076,44.801l0,0.001l-1.563,0l0,4.625l-0.003,0l0.003,0.015c0,0.854 -0.713,1.547 -1.593,1.547l-12.625,0c-0.88,0 -1.594,-0.693 -1.594,-1.547l0.003,-0.015l-0.003,0l0,-27.876l0.003,0l-0.003,-0.015c0,-0.854 0.714,-1.547 1.594,-1.547l15.781,0l0,0.001c6.974,0.008 12.625,5.559 12.625,12.405c0,6.847 -5.651,12.397 -12.625,12.406Zm31.125,-24.812c8.56,0 15.5,6.94 15.5,15.5c0,8.56 -6.94,15.5 -15.5,15.5c-8.56,0 -15.5,-6.94 -15.5,-15.5c0,-8.56 6.94,-15.5 15.5,-15.5Zm50.506,30.028c0,0 0,0.001 0,0.002c0,0.523 -0.43,0.954 -0.954,0.955l-35.039,0c-0.026,0.002 -0.049,0.015 -0.076,0.015c-0.518,-0.005 -0.941,-0.435 -0.937,-0.953c0.004,-0.202 0.073,-0.398 0.197,-0.557l-0.018,-0.035l17.364,-28.683l0.039,0c0.084,-0.439 0.469,-0.76 0.915,-0.764c0.459,0 0.825,0.331 0.916,0.764l0.038,0l17.555,28.874l-0.06,0.084c0.036,0.095 0.057,0.196 0.06,0.298Z"
fill="#fff"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="8">
<path fill="#FFF" fill-rule="evenodd" d="M3.781 6.375h-.344v1.031a.333.333 0 0 1-.312.337v.007H.469v-.007c-.006.001-.01.007-.016.007a.336.336 0 0 1-.328-.344V1.219c0-.19.147-.344.328-.344.006 0 .01.006.016.007V.875h3.312c1.467 0 2.656 1.231 2.656 2.75s-1.189 2.75-2.656 2.75zM10.422.875c1.838 0 3.328 1.539 3.328 3.437 0 1.899-1.49 3.438-3.328 3.438-1.838 0-3.328-1.539-3.328-3.438 0-1.898 1.49-3.437 3.328-3.437zM21.369 7.532a.209.209 0 0 1-.205.212h-7.542c-.005.001-.008.006-.013.006-.112 0-.203-.098-.203-.219 0-.042.023-.076.042-.109l-.008-.017 3.739-6.363h.017c.021-.094.091-.17.188-.17.098 0 .167.076.189.17h.017l3.779 6.405-.021.032c.005.019.021.032.021.053zM33.562 7.75h-4.374a.313.313 0 0 1-.313-.313V1.188c0-.173.14-.313.313-.313h4.374c.173 0 .313.14.313.313v6.249c0 .173-.14.313-.313.313zM29.5 1.5v5.625h3.75V1.5H29.5zm-2.188 5.625a.313.313 0 1 1 0 .625h-4.374a.313.313 0 0 1-.313-.313V4.312c0-.172.14-.312.313-.312H27V1.5h-4.062a.313.313 0 1 1 0-.625h4.374c.173 0 .313.14.313.313v3.124c0 .173-.14.313-.313.313H23.25v2.5h4.062z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="15">
<path fill="#FFF" fill-rule="evenodd" d="M8.941 7.468l5.781 5.781a1.019 1.019 0 1 1-1.441 1.441L7.5 8.909 1.719 14.69a1.019 1.019 0 1 1-1.441-1.441l5.781-5.781L.31 1.719A1.019 1.019 0 1 1 1.751.278L7.5 6.027 13.249.278a1.019 1.019 0 1 1 1.441 1.441L8.941 7.468z"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="360">
<path fill="#90E1D9" fill-rule="evenodd" d="M120 300h60v60h-60v-60z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M120 360c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M60 240h60v60H60v-60z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M120 240H60v-60c33.137 0 60 26.863 60 60z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M60 240c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M0 180c0-33.137 26.863-60 60-60v60H0z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M60 180v-60h60c0 33.137-26.863 60-60 60z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M60 60h60v60H60V60z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M60 60c0-33.137 26.863-60 60-60v60H60z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M120 0h60v60h-60V0z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M300 300h60v60h-60v-60z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M300 360c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M240 240h60v60h-60v-60z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M300 240h-60v-60c33.137 0 60 26.863 60 60z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M240 240c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M180 180c0-33.137 26.863-60 60-60v60h-60z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M240 180v-60h60c0 33.137-26.863 60-60 60z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M240 60h60v60h-60V60z"/>
<path fill="#5B33A2" fill-rule="evenodd" d="M240 60c0-33.137 26.863-60 60-60v60h-60z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M300 0h60v60h-60V0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="203" height="276">
<path fill="#FFF" fill-rule="evenodd" d="M203 0h-51l-50 138 50 138h51l-50-138L203 0z" opacity=".039"/>
<path fill="#FFF" fill-rule="evenodd" d="M152 0h-51L51 138l50 138h51l-50-138L152 0z" opacity=".078"/>
<path fill="#FFF" fill-rule="evenodd" d="M101 0H50L0 138l50 138h51L51 138 101 0z" opacity=".122"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="203" height="276">
<path fill="#FFF" fill-rule="evenodd" d="M0 0h51l50 138-50 138H0l50-138L0 0z" opacity=".039"/>
<path fill="#FFF" fill-rule="evenodd" d="M51 0h51l50 138-50 138H51l50-138L51 0z" opacity=".078"/>
<path fill="#FFF" fill-rule="evenodd" d="M102 0h51l50 138-50 138h-51l50-138L102 0z" opacity=".122"/>
</svg>

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="8">
<path fill="#5c34a2" fill-rule="evenodd" d="M3.781 6.375h-.344v1.031a.333.333 0 0 1-.312.337v.007H.469v-.007c-.006.001-.01.007-.016.007a.336.336 0 0 1-.328-.344V1.219c0-.19.147-.344.328-.344.006 0 .01.006.016.007V.875h3.312c1.467 0 2.656 1.231 2.656 2.75s-1.189 2.75-2.656 2.75zM10.422.875c1.838 0 3.328 1.539 3.328 3.437 0 1.899-1.49 3.438-3.328 3.438-1.838 0-3.328-1.539-3.328-3.438 0-1.898 1.49-3.437 3.328-3.437zM21.369 7.532a.209.209 0 0 1-.205.212h-7.542c-.005.001-.008.006-.013.006-.112 0-.203-.098-.203-.219 0-.042.023-.076.042-.109l-.008-.017 3.739-6.363h.017c.021-.094.091-.17.188-.17.098 0 .167.076.189.17h.017l3.779 6.405-.021.032c.005.019.021.032.021.053zM33.562 7.75h-4.374a.313.313 0 0 1-.313-.313V1.188c0-.173.14-.313.313-.313h4.374c.173 0 .313.14.313.313v6.249c0 .173-.14.313-.313.313zM29.5 1.5v5.625h3.75V1.5H29.5zm-2.188 5.625a.313.313 0 1 1 0 .625h-4.374a.313.313 0 0 1-.313-.313V4.312c0-.172.14-.312.313-.312H27V1.5h-4.062a.313.313 0 1 1 0-.625h4.374c.173 0 .313.14.313.313v3.124c0 .173-.14.313-.313.313H23.25v2.5h4.062z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="34" height="8">
<path fill="#FFF" fill-rule="evenodd" d="M3.781 6.375h-.344v1.031a.333.333 0 0 1-.312.337v.007H.469v-.007c-.006.001-.01.007-.016.007a.336.336 0 0 1-.328-.344V1.219c0-.19.147-.344.328-.344.006 0 .01.006.016.007V.875h3.312c1.467 0 2.656 1.231 2.656 2.75s-1.189 2.75-2.656 2.75zM10.422.875c1.838 0 3.328 1.539 3.328 3.437 0 1.899-1.49 3.438-3.328 3.438-1.838 0-3.328-1.539-3.328-3.438 0-1.898 1.49-3.437 3.328-3.437zM21.369 7.532a.209.209 0 0 1-.205.212h-7.542c-.005.001-.008.006-.013.006-.112 0-.203-.098-.203-.219 0-.042.023-.076.042-.109l-.008-.017 3.739-6.363h.017c.021-.094.091-.17.188-.17.098 0 .167.076.189.17h.017l3.779 6.405-.021.032c.005.019.021.032.021.053zM33.562 7.75h-4.374a.313.313 0 0 1-.313-.313V1.188c0-.173.14-.313.313-.313h4.374c.173 0 .313.14.313.313v6.249c0 .173-.14.313-.313.313zM29.5 1.5v5.625h3.75V1.5H29.5zm-2.188 5.625a.313.313 0 1 1 0 .625h-4.374a.313.313 0 0 1-.313-.313V4.312c0-.172.14-.312.313-.312H27V1.5h-4.062a.313.313 0 1 1 0-.625h4.374c.173 0 .313.14.313.313v3.124c0 .173-.14.313-.313.313H23.25v2.5h4.062z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

@ -0,0 +1,27 @@
<svg
height="26"
width="188"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<style type="text/css">
@import url('https://fonts.googleapis.com/css?family=Nunito:400');
text {
font-family: "Nunito", sans-serif;
}
</style>
</defs>
<path
d="M8.938 17.812h-.813v2.376h-.003l.003.015c0 .44-.364.797-.812.797h-6.5A.805.805 0 0 1 0 20.203l.003-.015H0V5.812h.003L0 5.797C0 5.357.364 5 .813 5h8.125c3.589 0 6.499 2.868 6.499 6.406s-2.91 6.406-6.499 6.406zM25.188 5c4.487 0 8.125 3.582 8.125 8s-3.638 8-8.125 8c-4.488 0-8.126-3.582-8.126-8s3.638-8 8.126-8zM52 20.493a.498.498 0 0 1-.503.494H33.064c-.022.003-.041.013-.064.013a.5.5 0 0 1-.5-.5c0-.109.042-.204.101-.286l-.009-.017 9.151-14.809h.02a.493.493 0 0 1 .965 0h.021L52 20.296l-.032.043c.017.05.032.1.032.154z"
fill-rule="evenodd"
fill="#FFF"
/>
<text
fill="#fff"
font-family="Nunito"
font-size="14"
font-weight="300"
x="70"
y="17"
>Bridge&#32;UI&#32;App</text>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1,32 @@
<svg
height="26"
width="188"
xmlns="http://www.w3.org/2000/svg"
>
<defs>
<style type="text/css">
@import url('https://fonts.googleapis.com/css?family=Nunito:400');
text {
font-family: "Nunito", sans-serif;
}
</style>
</defs>
<path
d="M8.938 17.812h-.813v2.376h-.003l.003.015c0 .44-.364.797-.812.797h-6.5A.805.805 0 0 1 0 20.203l.003-.015H0V5.812h.003L0 5.797C0 5.357.364 5 .813 5h8.125c3.589 0 6.499 2.868 6.499 6.406s-2.91 6.406-6.499 6.406zM25.188 5c4.487 0 8.125 3.582 8.125 8s-3.638 8-8.125 8c-4.488 0-8.126-3.582-8.126-8s3.638-8 8.126-8zM52 20.493a.498.498 0 0 1-.503.494H33.064c-.022.003-.041.013-.064.013a.5.5 0 0 1-.5-.5c0-.109.042-.204.101-.286l-.009-.017 9.151-14.809h.02a.493.493 0 0 1 .965 0h.021L52 20.296l-.032.043c.017.05.032.1.032.154z"
fill-rule="evenodd"
fill="#FFF"
/>
<text
fill="#fff"
font-family="Nunito"
font-size="14"
font-weight="300"
x="70"
y="17"
>Bridge&#32;UI&#32;App</text>
<path
d="M179 0a1 1 0 0 1 1 1v24a1 1 0 0 1-2 0V1a1 1 0 0 1 1-1z"
fill-rule="evenodd"
fill="#60DB97"
/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="59" height="59">
<path fill="#5C34A2" fill-rule="evenodd" d="M6 0h47a6 6 0 0 1 6 6v47a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M19 12h6v6h-6v-6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M13 18a6 6 0 0 1 6-6v6h-6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M13 18h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M13 30v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M7 30a6 6 0 0 1 6-6v6H7z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M13 36a6 6 0 0 1-6-6h6v6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M19 36h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M13 36h6v6h-6v-6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M19 48a6 6 0 0 1-6-6h6v6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M19 42h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M35 42h6v6h-6v-6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M41 48v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M41 36h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M41 36a6 6 0 0 1 6-6v6h-6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M47 36v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M53 30h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M47 30a6 6 0 0 1-6-6h6v6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M41 18h6v6h-6v-6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M47 18h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M35 12h6v6h-6v-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="59" height="59">
<path fill="#61DC97" fill-rule="evenodd" d="M6 0h47a6 6 0 0 1 6 6v47a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M19 12h6v6h-6v-6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M13 18a6 6 0 0 1 6-6v6h-6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M13 18h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M13 30v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M7 30a6 6 0 0 1 6-6v6H7z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M13 36a6 6 0 0 1-6-6h6v6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M19 36h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M13 36h6v6h-6v-6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M19 48a6 6 0 0 1-6-6h6v6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M19 42h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M35 42h6v6h-6v-6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M41 48v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M41 36h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M41 36a6 6 0 0 1 6-6v6h-6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M47 36v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M53 30h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M47 30a6 6 0 0 1-6-6h6v6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M41 18h6v6h-6v-6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M47 18h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#9987FC" fill-rule="evenodd" d="M35 12h6v6h-6v-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,23 @@
<svg xmlns="http://www.w3.org/2000/svg" width="59" height="59">
<path fill="#9987FC" fill-rule="evenodd" d="M6 0h47a6 6 0 0 1 6 6v47a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M19 12h6v6h-6v-6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M13 18a6 6 0 0 1 6-6v6h-6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M13 18h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M13 30v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M7 30a6 6 0 0 1 6-6v6H7z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M13 36a6 6 0 0 1-6-6h6v6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M19 36h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M13 36h6v6h-6v-6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M19 48a6 6 0 0 1-6-6h6v6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M19 42h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M35 42h6v6h-6v-6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M41 48v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M41 36h6v6h-6v-6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M41 36a6 6 0 0 1 6-6v6h-6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M47 36v-6h6a6 6 0 0 1-6 6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M53 30h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#90E1D9" fill-rule="evenodd" d="M47 30a6 6 0 0 1-6-6h6v6z"/>
<path fill="#FFF" fill-rule="evenodd" d="M41 18h6v6h-6v-6z"/>
<path fill="#61DC97" fill-rule="evenodd" d="M47 18h-6v-6a6 6 0 0 1 6 6z"/>
<path fill="#5C34A2" fill-rule="evenodd" d="M35 12h6v6h-6v-6z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

@ -0,0 +1,22 @@
<svg xmlns="http://www.w3.org/2000/svg" width="360" height="360">
<path fill="#343053" fill-rule="evenodd" d="M120 300h60v60h-60v-60z"/>
<path fill="#453E6A" fill-rule="evenodd" d="M120 360c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M60 240h60v60H60v-60z"/>
<path fill="#453E6A" fill-rule="evenodd" d="M120 240H60v-60c33.137 0 60 26.863 60 60z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M60 240c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#343053" fill-rule="evenodd" d="M0 180c0-33.137 26.863-60 60-60v60H0z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M60 180v-60h60c0 33.137-26.863 60-60 60z"/>
<path fill="#453E6A" fill-rule="evenodd" d="M60 60h60v60H60V60z"/>
<path fill="#343053" fill-rule="evenodd" d="M60 60c0-33.137 26.863-60 60-60v60H60z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M120 0h60v60h-60V0z"/>
<path fill="#343053" fill-rule="evenodd" d="M300 300h60v60h-60v-60z"/>
<path fill="#453E6A" fill-rule="evenodd" d="M300 360c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M240 240h60v60h-60v-60z"/>
<path fill="#453E6A" fill-rule="evenodd" d="M300 240h-60v-60c33.137 0 60 26.863 60 60z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M240 240c-33.137 0-60-26.863-60-60h60v60z"/>
<path fill="#343053" fill-rule="evenodd" d="M180 180c0-33.137 26.863-60 60-60v60h-60z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M240 180v-60h60c0 33.137-26.863 60-60 60z"/>
<path fill="#453E6A" fill-rule="evenodd" d="M240 60h60v60h-60V60z"/>
<path fill="#6D649B" fill-rule="evenodd" d="M240 60c0-33.137 26.863-60 60-60v60h-60z"/>
<path fill="#343053" fill-rule="evenodd" d="M300 0h60v60h-60V0z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="203" height="276">
<path fill="#FFF" fill-rule="evenodd" d="M203 0h-51l-50 138 50 138h51l-50-138L203 0z" opacity=".039"/>
<path fill="#FFF" fill-rule="evenodd" d="M152 0h-51L51 138l50 138h51l-50-138L152 0z" opacity=".078"/>
<path fill="#FFF" fill-rule="evenodd" d="M101 0H50L0 138l50 138h51L51 138 101 0z" opacity=".122"/>
</svg>

After

Width:  |  Height:  |  Size: 389 B

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="203" height="276">
<path fill="#FFF" fill-rule="evenodd" d="M0 0h51l50 138-50 138H0l50-138L0 0z" opacity=".039"/>
<path fill="#FFF" fill-rule="evenodd" d="M51 0h51l50 138-50 138H51l50-138L51 0z" opacity=".078"/>
<path fill="#FFF" fill-rule="evenodd" d="M102 0h51l50 138-50 138h-51l50-138L102 0z" opacity=".122"/>
</svg>

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@ -0,0 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="38">
<path fill="#807E8E" fill-rule="evenodd" d="M12 14L0 19.5 12 0v14z"/>
<path fill="#3C3264" fill-rule="evenodd" d="M12 14L0 19.5 12 26V14z"/>
<path fill="#807E8E" fill-rule="evenodd" d="M12 29L0 22l12 16v-9z"/>
<path fill="#352D59" fill-rule="evenodd" d="M12 14l12 5.5L12 0v14z"/>
<path fill="#1D1830" fill-rule="evenodd" d="M12 14l12 5.5L12 26V14z"/>
<path fill="#352D59" fill-rule="evenodd" d="M12 29l12-7-12 16v-9z"/>
</svg>

After

Width:  |  Height:  |  Size: 515 B

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="25" height="39">
<path fill="#352D59" fill-rule="evenodd" d="M24.987 19.514l-.004.02c.012.467-.173.909-.516 1.223l-10.56 17.45a1.622 1.622 0 0 1-.535.521c-.042.03-.08.062-.126.088-.043.023-.092.023-.136.042a1.455 1.455 0 0 1-.61.124 1.455 1.455 0 0 1-.61-.124c-.044-.019-.093-.018-.136-.042-.046-.026-.084-.059-.126-.088a1.622 1.622 0 0 1-.535-.521L.533 20.757a1.592 1.592 0 0 1-.516-1.223l-.004-.02c.001-.007-.003-.014-.003-.021l.003-.018c.003-.056.008-.11.017-.166.028-.402.195-.777.491-1.055L11.093.786c.136-.225.327-.39.537-.523.042-.029.079-.062.124-.087.043-.023.091-.022.135-.041.105-.043.207-.078.318-.099a1.596 1.596 0 0 1 .904.099c.044.019.092.018.135.041.045.025.082.058.124.087.209.133.401.298.537.523l10.572 17.468c.296.278.462.653.491 1.055.009.056.014.109.017.166.001.005.003.011.003.017 0 .008-.004.015-.003.022zM12.5 34.636l6.202-10.247-5.242 3.243a1.394 1.394 0 0 1-.729.328c-.078.015-.153.015-.231.017-.078-.002-.153-.002-.231-.017a1.394 1.394 0 0 1-.729-.328l-5.242-3.243L12.5 34.636zm0-30.28l-6.21 10.26 5.25-3.248c.211-.186.462-.288.729-.328.078-.015.153-.015.231-.017.078.002.153.002.231.017.267.04.518.142.729.328l5.25 3.248-6.21-10.26zm0 10.065L4.289 19.5l8.211 5.079 8.211-5.079-8.211-5.079z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 12 KiB

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