Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
akolotov committed May 30, 2021
0 parents commit 2211adf
Show file tree
Hide file tree
Showing 7 changed files with 362 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
db
.env
docker-compose.yml
.git
.ipynb_checkpoints
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
db
.env
docker-compose.yml
.ipynb_checkpoints
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
FROM python:3.9-alpine

RUN apk update && apk upgrade
# GCC.
RUN apk add --no-cache --virtual .build-deps gcc musl-dev

RUN python -m pip install --upgrade pip
COPY requirements.txt .
RUN python -m pip install -r requirements.txt

# Remove gcc.
RUN apk del .build-deps
# Remove cache.
RUN python -m pip cache purge

WORKDIR /faucet

COPY bridge-faucet.py .

ENTRYPOINT python bridge-faucet.py
80 changes: 80 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
OmniBridge faucet service
====

This service is monitoring transfers executed through the OmniBridge on the xDai chain and reward the tokens recipients with small amount of xdai.

## Run by docker CLI

1. Prepare `.env` file with the following (at least) variables definitons:

```bash
FAUCET_PRIVKEY=cafe...cafe
JSON_DB_DIR=/db
INITIAL_START_BLOCK=123
```

See below with the variables explanation.

2. Create the directory where the faucet service will store its data.

```bash
mkdir ./db
```

3. Run the service

```bash
docker run -ti --rm -v $(pwd)/db:/db --env-file .env omnibridge/bridge-faucet:latest
```

_Note:_ the source mount point after the key `-v` is the directory created on the step 2. The destination mount point is the directory specified in the variable `JSON_DB_DIR`.

## Run by docker-compose

1. Create the directory where the faucet service will store its data.

```bash
mkdir ./db
```

2. Initialize the `docker-compose.yml` file based on `docker-compose.yml.example`. Set proper values for the following variables (at least) there: `FAUCET_PRIVKEY`, `JSON_DB_DIR` and `INITIAL_START_BLOCK`.

Make sure that the source mount point in the `volumes` section is the directory created on the step 1.

See below with the variables explanation.

3. Run the service

```bash
docker-compose up -d
```

## Faucet configuration

The following environment variables may be used to configure the faucet behavior:

1. `XDAI_RPC` -- JSON RPC endpoint the faucet uses to monitor OB events and get data. **Default:** `https://xdai.poanetwork.dev`.

2. `BSC_OB` -- an address of BSC-xDai OB mediator on the xDai side. **Default:** `0x59447362798334d3485c64D1e4870Fde2DDC0d75`.

3. `ETH_OB` -- an address of ETH-xDai OB mediator on the xDai side. **Default:** `0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d`.

4. `FAUCET_PRIVKEY` -- a private key of an account holding xdai to reward. **No default value!**.

5. `GAS_PRICE` -- the gas price (in gwei) the faucet uses for reward transactions. **Default:** `1`.

6. `GAS_LIMIT` -- the gas limit the faucet uses for reward transactions. **Default:** `30000`.

7. `REWARD` -- amount of xdai used as reward. **Default:** `0.005`.

8. `POLLING_INTERVAL` -- amount of time (in seconds) between two subsequent cycles to discover OB transfers and send rewards. **Default:** `60`.

9. `INITIAL_START_BLOCK` -- a block the first faucet's attempt to discover OB transfers starts from. **No default value!**.
10. `FINALIZATION_INTERVAL` -- a number of blocks starting from the chain head to consider the chain as finalized. **Default:** `12`.
11. `JSON_DB_DIR` -- a directory where the faucet service keeps its data. **No default value!**.
12. `JSON_START_BLOCK` -- a name of JSON file where the last observed block is stored. **Default:** `faucet_start_block.json`.
13. `JSON_CONTRACTS` -- a name of JSON file where addresses of recipient-contracts are stored. **Default:** `xdai-contracts.json`.
231 changes: 231 additions & 0 deletions bridge-faucet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
#!/usr/bin/env python3

from json import load, dump
from web3 import Web3, HTTPProvider
from eth_account import Account
from time import sleep
from os import getenv
from dotenv import load_dotenv
from logging import basicConfig, info, INFO

basicConfig(level=INFO)

STOP_FILE = 'stop.tmp'

dotenv_read = False

while True:
XDAI_RPC = getenv('XDAI_RPC', 'https://xdai.poanetwork.dev')

BSC_OB = getenv('BSC_OB', '0x59447362798334d3485c64D1e4870Fde2DDC0d75')
ETH_OB = getenv('ETH_OB', '0xf6A78083ca3e2a662D6dd1703c939c8aCE2e268d')

FAUCET_PRIVKEY = getenv('FAUCET_PRIVKEY', None)

GAS_PRICE = int(getenv('GAS_PRICE', 1))
GAS_LIMIT = int(getenv('GAS_LIMIT', 30000))
REWARD = float(getenv('REWARD', 0.005))
POLLING_INTERVAL = getenv('POLLING_INTERVAL', 60)

#INITIAL_START_BLOCK = 16173518
INITIAL_START_BLOCK = int(getenv('INITIAL_START_BLOCK', 16190379))
FINALIZATION_INTERVAL = int(getenv('FINALIZATION_INTERVAL', 12)) # blocks

JSON_DB_DIR = getenv('JSON_DB_DIR', '.')
JSON_START_BLOCK = getenv('JSON_START_BLOCK', 'faucet_start_block.json')
JSON_CONTRACTS = getenv('JSON_CONTRACTS', 'xdai-contracts.json')

if not FAUCET_PRIVKEY:
if dotenv_read:
break

info('Environment is not configured')
load_dotenv('./.env')
dotenv_read = True
else:
break

if not FAUCET_PRIVKEY:
raise BaseException("Faucet's privkey is not provided. Check the configuration")

info(f'XDAI_RPC = {XDAI_RPC}')
info(f'BSC_OB = {BSC_OB}')
info(f'ETH_OB = {ETH_OB}')
info(f'FAUCET_PRIVKEY = ...')
info(f'GAS_PRICE = {GAS_PRICE}')
info(f'GAS_LIMIT = {GAS_LIMIT}')
info(f'REWARD = {REWARD}')
info(f'POLLING_INTERVAL = {POLLING_INTERVAL}')
info(f'INITIAL_START_BLOCK = {INITIAL_START_BLOCK}')
info(f'FINALIZATION_INTERVAL = {FINALIZATION_INTERVAL}')
info(f'JSON_DB_DIR = {JSON_DB_DIR}')
info(f'JSON_START_BLOCK = {JSON_START_BLOCK}')
info(f'JSON_CONTRACTS = {JSON_CONTRACTS}')

# event
# TokensBridged(address token, address recipient, uint256 value, bytes32 messageId)
ABI = """
[
{
"type":"event",
"name":"TokensBridged",
"inputs":[
{
"type":"address",
"name":"token",
"internalType":"address",
"indexed":true
},
{
"type":"address",
"name":"recipient",
"internalType":"address",
"indexed":true
},
{
"type":"uint256",
"name":"value",
"internalType":"uint256",
"indexed":false
},
{
"type":"bytes32",
"name":"messageId",
"internalType":"bytes32",
"indexed":true
}
],
"anonymous":false
}
]
"""

xdai_w3 = Web3(HTTPProvider(XDAI_RPC))
bsc_ob = xdai_w3.eth.contract(abi = ABI, address = BSC_OB)
eth_ob = xdai_w3.eth.contract(abi = ABI, address = ETH_OB)

faucet = Account.privateKeyToAccount(FAUCET_PRIVKEY)

try:
with open(f'{JSON_DB_DIR}/{JSON_START_BLOCK}') as f:
tmp = load(f)
start_block = int(tmp['start_block'])
except IOError:
info("no start block stored previously")
start_block = INITIAL_START_BLOCK
info(f'start block: {start_block}')

while True:
try:
with open(f'{JSON_DB_DIR}/{STOP_FILE}') as f:
info("Stopping faucet")
break
except IOError:
pass

try:
last_block = xdai_w3.eth.getBlock('latest').number
except:
raise BaseException('Cannot get the latest block number')
info(f'current last block: {last_block}')
last_block = last_block - FINALIZATION_INTERVAL

filter = bsc_ob.events.TokensBridged.build_filter()
info(f'Looking for TokensBridged events on BSC-xDAI OB from {start_block} to {last_block}')
try:
bsc_logs = xdai_w3.eth.getLogs({'fromBlock': start_block,
'toBlock': last_block,
'address': filter.address,
'topics': filter.topics})
except:
raise BaseException('Cannot get BSC-xDAI OB OB logs')
info(f'Found {len(bsc_logs)} TokensBridged events on BSC-xDAI OB')

filter = eth_ob.events.TokensBridged.build_filter()
info(f'Looking for TokensBridged events on ETH-xDAI OB from {start_block} to {last_block}')
try:
eth_logs = xdai_w3.eth.getLogs({'fromBlock': start_block,
'toBlock': last_block,
'address': filter.address,
'topics': filter.topics})
except:
raise BaseException('Cannot get ETH-xDAI OB OB logs')
info(f'Found {len(eth_logs)} TokensBridged events on ETH-xDAI OB')

recipients = set()

for log in bsc_logs:
recipient = bsc_ob.events.TokensBridged().processLog(log).args.recipient
recipients.add(recipient)
info(f'Identified {len(recipients)} tokens recipients from BSC-xDAI OB events')

tmp = len(recipients)
for log in eth_logs:
recipient = eth_ob.events.TokensBridged().processLog(log).args.recipient
recipients.add(recipient)
info(f'Identified {len(recipients) - tmp} tokens recipients from ETH-xDAI OB events')

try:
with open(f'{JSON_DB_DIR}/{JSON_CONTRACTS}') as f:
contracts = load(f)
except IOError:
info("no contracts identified previously")
contracts = {}

endowing = []
for recipient in recipients:
if recipient in contracts:
continue
code = xdai_w3.eth.getCode(recipient)
if code != b'':
contracts[recipient] = True
continue
balance = xdai_w3.eth.getBalance(recipient)
if balance == 0:
info(f'{recipient} balance is zero')
endowing.append(recipient)
info(f'found {len(endowing)} accounts for reward')

with open(f'{JSON_DB_DIR}/{JSON_CONTRACTS}', 'w') as json_file:
dump(contracts, json_file)

balance_error = False

if len(endowing) > 0:
try:
faucet_balance = xdai_w3.eth.getBalance(faucet.address)
except:
raise BaseException("Cannot get faucet balance")
info(f'faucet balance: {faucet_balance}')

if faucet_balance > len(endowing) * GAS_LIMIT * Web3.toWei(GAS_PRICE, 'gwei'):
try:
nonce = xdai_w3.eth.getTransactionCount(faucet.address)
except:
raise BaseException("Cannot get transactions count of faucet's account")
info(f'starting nonce: {nonce}')
for recipient in endowing:
tx = {
'nonce': nonce,
'gas': GAS_LIMIT,
'gasPrice': Web3.toWei(GAS_PRICE, 'gwei'),
'data': b'Rewarded for OmniBridge transaction',
'chainId': 100,
'value': Web3.toWei(REWARD, 'ether'),
'to': recipient,
}
rawtx = faucet.signTransaction(tx)
sent_tx_hash = xdai_w3.eth.sendRawTransaction(rawtx.rawTransaction)
info(f'{recipient} rewarded by {Web3.toHex(sent_tx_hash)}')
nonce += 1
sleep(0.1)
else:
info(f'not enough balance on the faucet {faucet.address}')
balance_error = True

if not balance_error:
start_block = last_block + 1
with open(f'{JSON_DB_DIR}/{JSON_START_BLOCK}', 'w') as json_file:
dump({'start_block': start_block}, json_file)

sleep(POLLING_INTERVAL)
19 changes: 19 additions & 0 deletions docker-compose.yml.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
version: "3.9"

services:
faucet:
image: omnibridge/bridge-faucet:latest
container_name: bridge-faucet
environment:
# faucet account's private key must be here (without 0x)
- FAUCET_PRIVKEY=cafe...cafe
- JSON_DB_DIR=/db
- INITIAL_START_BLOCK=123
volumes:
- ./db:/db
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "1"
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
eth-account==0.5.4
web3==5.17.0
python-dotenv==0.17.1

0 comments on commit 2211adf

Please sign in to comment.