Skip to content

Commit

Permalink
Merge pull request #5 from gnosischain/improvements
Browse files Browse the repository at this point in the history
Add capacity to handle maximum amount per token
  • Loading branch information
giacomolicari authored Nov 30, 2023
2 parents edd3bd3 + 381214d commit dc4eb12
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 77 deletions.
21 changes: 21 additions & 0 deletions .github/workflows/publish-api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,29 @@ env:

# There is a single job in this workflow. It's configured to run on the latest available version of Ubuntu.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8"]
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
cache: 'pip'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Test with pytest
run: |
python3 -m pytest -s
build-and-push-image:
runs-on: ubuntu-latest
needs: test
# Sets the permissions granted to the `GITHUB_TOKEN` for the actions in this job.
permissions:
contents: read
Expand Down
8 changes: 8 additions & 0 deletions api/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
FAUCET_AMOUNT=0.1
FAUCET_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000000
FAUCET_RPC_URL=https://rpc.chiadochain.net
FAUCET_CHAIN_ID=10200
FAUCET_ENABLED_TOKENS="[{\"address\": \"0x19C653Da7c37c66208fbfbE8908A5051B57b4C70\", \"name\":\"GNO\", \"maximumAmount\": 0.5}]"
# FAUCET_ENABLED_TOKENS=
CAPTCHA_VERIFY_ENDPOINT=https://api.hcaptcha.com/siteverify
CAPTCHA_SECRET_KEY=0x0000000000000000000000000000000000000000
85 changes: 61 additions & 24 deletions api/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from web3.middleware import construct_sign_and_send_raw_middleware

from .services import Token, Cache, Strategy, claim_native, claim_token, captcha_verify
from .const import NATIVE_TOKEN_ADDRESS


def is_token_enabled(address, tokens_list):
Expand All @@ -14,18 +15,62 @@ def is_token_enabled(address, tokens_list):

is_enabled = False
checksum_address = Web3.to_checksum_address(address)
for enabled_tokens in tokens_list:
if checksum_address == enabled_tokens['address']:
for enabled_token in tokens_list:
if checksum_address == enabled_token['address']:
is_enabled = True
break
return is_enabled

def is_amount_valid(amount, token_address, tokens_list):

if not token_address:
raise ValueError(
'Token address not supported',
str(token_address),
'supported tokens',
" ".join(list(map(lambda x: x['address'], tokens_list)))
)

token_address_to_check = None
if token_address.lower() == NATIVE_TOKEN_ADDRESS:
token_address_to_check = NATIVE_TOKEN_ADDRESS
else:
token_address_to_check = Web3.to_checksum_address(token_address)

for enabled_token in tokens_list:
if token_address_to_check == enabled_token['address']:
return (
amount <= enabled_token['maximumAmount'],
enabled_token['maximumAmount']
)

raise ValueError(
'Token address not supported',
token_address,
'supported tokens',
" ".join(list(map(lambda x: x['address'], tokens_list)))
)


def get_balance(w3, address, format='ether'):
balance = w3.eth.get_balance(address)
return w3.from_wei(balance, format)


def setup_logger(log_level):
# Set logger
logging.basicConfig(level=log_level)


def print_info(w3, config):
faucet_native_balance = get_balance(w3, config['FAUCET_ADDRESS'])
logging.info("="*60)
logging.info("RPC_URL = " + config['FAUCET_RPC_URL'])
logging.info("FAUCET ADDRESS = " + config['FAUCET_ADDRESS'])
logging.info("FAUCET BALANCE = %d %s" % (faucet_native_balance, config['FAUCET_CHAIN_NAME']))
logging.info("="*60)


def create_app():
# Init Flask app
app = Flask(__name__)
Expand All @@ -38,16 +83,9 @@ def create_app():
w3.middleware_onion.add(construct_sign_and_send_raw_middleware(app.config['FAUCET_PRIVATE_KEY']))

cache = Cache(app.config['FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS'])
faucet_balance = get_balance(w3, app.config['FAUCET_ADDRESS'])

# Set logger
logging.basicConfig(level=logging.INFO)
logging.info("="*60)
logging.info("RPC_URL = " + app.config['FAUCET_RPC_URL'])
logging.info("FAUCET ADDRESS = " + app.config['FAUCET_ADDRESS'])
logging.info("FAUCET BALANCE = %d %s" % (faucet_balance, app.config['FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL']))
logging.info("="*60)

setup_logger(logging.INFO)
print_info(w3, app.config)

@apiv1.route("/status")
def status():
Expand All @@ -56,18 +94,11 @@ def status():

@apiv1.route("/info")
def info():
enabled_tokens = []
enabled_tokens.extend(app.config['FAUCET_ENABLED_TOKENS'])
enabled_tokens.append(
{
'name': app.config['FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL'],
'address': 'native'
}
)
return jsonify(
enabledTokens=enabled_tokens,
maximumAmount=app.config['FAUCET_AMOUNT'],
chainId=app.config['FAUCET_CHAIN_ID']
enabledTokens=app.config['FAUCET_ENABLED_TOKENS'],
chainId=app.config['FAUCET_CHAIN_ID'],
chainName=app.config['FAUCET_CHAIN_NAME'],
faucetAddress=app.config['FAUCET_ADDRESS']
), 200


Expand Down Expand Up @@ -103,8 +134,14 @@ def ask():
validation_errors.append('tokenAddress: invalid token address'), 400

amount = request_data.get('amount', None)
if not amount or float(amount) > app.config['FAUCET_AMOUNT']:
validation_errors.append('amount: a valid amount must be specified and must be less or equals to %s' % app.config['FAUCET_AMOUNT'])

try:
amount_valid, amount_limit = is_amount_valid(amount, token_address, app.config['FAUCET_ENABLED_TOKENS'])
if not amount_valid:
validation_errors.append('amount: a valid amount must be specified and must be less or equals to %s' % amount_limit)
except ValueError as e:
message = "".join([arg for arg in e.args])
validation_errors.append(message)

if len(validation_errors) > 0:
return jsonify(errors=validation_errors), 400
Expand Down
1 change: 1 addition & 0 deletions api/api/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
NATIVE_TOKEN_ADDRESS='native'
14 changes: 12 additions & 2 deletions api/api/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import json

from .services import RateLimitStrategy
from .utils import get_chain_name

from dotenv import load_dotenv
from eth_account import Account
Expand All @@ -16,9 +17,18 @@
FAUCET_RPC_URL = os.getenv("FAUCET_RPC_URL")
FAUCET_PRIVATE_KEY = os.environ.get("FAUCET_PRIVATE_KEY")
FAUCET_CHAIN_ID=os.getenv('FAUCET_CHAIN_ID')
FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL=os.getenv('FAUCET_CHAIN_NATIVE_TOKEN_SYMBOL', default='xDAI')
FAUCET_CHAIN_NAME=get_chain_name(os.getenv('FAUCET_CHAIN_ID'))

# env FAUCET_ENABLED_TOKENS
# sample JSON string:
# [
# {
# "address": "0x19C653Da7c37c66208fbfbE8908A5051B57b4C70"
# "name": "GNO",
# "maximumAmount": 0.5
# }
# ]
FAUCET_ENABLED_TOKENS=json.loads(os.getenv('FAUCET_ENABLED_TOKENS', default='[]'))
FAUCET_AMOUNT=float(os.getenv('FAUCET_AMOUNT'))
FAUCET_ADDRESS: LocalAccount = Account.from_key(FAUCET_PRIVATE_KEY).address
FAUCET_RATE_LIMIT_STRATEGY=rate_limit_strategy
FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS=seconds=os.getenv('FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS', 86400) # 86400 = 24h
Expand Down
8 changes: 8 additions & 0 deletions api/api/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def get_chain_name(chain_id):
chains = {
1: 'Ethereum',
100: 'Gnosis Chain',
10200: 'Chiado Chain Testnet'
}

return chains.get(int(chain_id), 'Undefined')
15 changes: 12 additions & 3 deletions api/tests/temp_env_var.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
from secrets import token_bytes
import json

from api.const import NATIVE_TOKEN_ADDRESS

ZERO_ADDRESS = "0x" + '0'*40

NATIVE_TOKEN_AMOUNT = 0.1
ERC20_TOKEN_AMOUNT = 0.5
ERC20_TOKEN_ADDRESS = ZERO_ADDRESS

CAPTCHA_TEST_SECRET_KEY = '0x0000000000000000000000000000000000000000'
CAPTCHA_TEST_RESPONSE_TOKEN = '10000000-aaaa-bbbb-cccc-000000000001'

FAUCET_ENABLED_TOKENS = [
{"address": NATIVE_TOKEN_ADDRESS, "name": "Native", "maximumAmount": NATIVE_TOKEN_AMOUNT},
{"address": ERC20_TOKEN_ADDRESS, "name": "TestToken", "maximumAmount": ERC20_TOKEN_AMOUNT}
]

TEMP_ENV_VARS = {
'FAUCET_RPC_URL': 'http://localhost:8545',
'FAUCET_CHAIN_ID': '100000',
'FAUCET_PRIVATE_KEY': token_bytes(32).hex(),
'FAUCET_AMOUNT': 0.1,
'FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS': '1',
'FAUCET_ENABLED_TOKENS': json.loads('[{"address":"' + ZERO_ADDRESS + '", "name": "TestToken"}]'),
'FAUCET_ENABLED_TOKENS': FAUCET_ENABLED_TOKENS,
'CAPTCHA_SECRET_KEY': CAPTCHA_TEST_SECRET_KEY
}

Expand Down
40 changes: 20 additions & 20 deletions api/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import pytest
from conftest import api_prefix
# from mock import patch
from temp_env_var import TEMP_ENV_VARS, NATIVE_TRANSFER_TX_HASH, TOKEN_TRANSFER_TX_HASH, ZERO_ADDRESS, CAPTCHA_TEST_RESPONSE_TOKEN
from temp_env_var import TEMP_ENV_VARS, NATIVE_TRANSFER_TX_HASH, TOKEN_TRANSFER_TX_HASH, ZERO_ADDRESS, CAPTCHA_TEST_RESPONSE_TOKEN, NATIVE_TOKEN_AMOUNT, NATIVE_TOKEN_ADDRESS, ERC20_TOKEN_ADDRESS


class BaseTest:
Expand Down Expand Up @@ -47,46 +47,46 @@ def test_ask_route_parameters(self, client):
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': -1,
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'],
'amount': NATIVE_TOKEN_AMOUNT,
'recipient': ZERO_ADDRESS,
'tokenAddress': 'native'
'tokenAddress': NATIVE_TOKEN_ADDRESS
})
assert response.status_code == 400

# wrong amount, should return 400
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'] + 1,
'amount': NATIVE_TOKEN_AMOUNT + 1,
'recipient': ZERO_ADDRESS,
'tokenAddress': 'native'
'tokenAddress': NATIVE_TOKEN_ADDRESS
})
assert response.status_code == 400

# missing recipient, should return 400
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'] + 1,
'tokenAddress': 'native'
'amount': NATIVE_TOKEN_AMOUNT + 1,
'tokenAddress': NATIVE_TOKEN_ADDRESS
})
assert response.status_code == 400

# wrong recipient recipient, should return 400
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'] + 1,
'amount': NATIVE_TOKEN_AMOUNT + 1,
'recipient': 'not an address',
'tokenAddress': 'native'
'tokenAddress': NATIVE_TOKEN_ADDRESS
})
assert response.status_code == 400

# missing token address, should return 400
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'] + 1,
'amount': NATIVE_TOKEN_AMOUNT + 1,
'recipient': ZERO_ADDRESS
})
assert response.status_code == 400
Expand All @@ -95,7 +95,7 @@ def test_ask_route_parameters(self, client):
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'] + 1,
'amount': NATIVE_TOKEN_AMOUNT + 1,
'recipient': ZERO_ADDRESS,
'tokenAddress': 'non existing token address'
})
Expand All @@ -105,9 +105,9 @@ def test_ask_route_native_transaction(self, client):
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'],
'amount': NATIVE_TOKEN_AMOUNT,
'recipient': ZERO_ADDRESS,
'tokenAddress': 'native'
'tokenAddress': NATIVE_TOKEN_ADDRESS
})
assert response.status_code == 200
assert response.get_json().get('transactionHash') == NATIVE_TRANSFER_TX_HASH
Expand All @@ -117,7 +117,7 @@ def test_ask_route_token_transaction(self, client):
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'],
'amount': NATIVE_TOKEN_AMOUNT,
'recipient': ZERO_ADDRESS,
'tokenAddress': '0x' + '1'*40
})
Expand All @@ -126,9 +126,9 @@ def test_ask_route_token_transaction(self, client):
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'],
'amount': NATIVE_TOKEN_AMOUNT,
'recipient': ZERO_ADDRESS,
'tokenAddress': TEMP_ENV_VARS['FAUCET_ENABLED_TOKENS'][0]['address']
'tokenAddress': ERC20_TOKEN_ADDRESS
})
assert response.status_code == 200
assert response.get_json().get('transactionHash') == TOKEN_TRANSFER_TX_HASH
Expand All @@ -154,18 +154,18 @@ def test_ask_route_limit_by_ip(self, client):
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'],
'amount': NATIVE_TOKEN_AMOUNT,
'recipient': ZERO_ADDRESS,
'tokenAddress': TEMP_ENV_VARS['FAUCET_ENABLED_TOKENS'][0]['address']
'tokenAddress': ERC20_TOKEN_ADDRESS
})
assert response.status_code == 200

# Second request should return 429
response = client.post(api_prefix + '/ask', json={
'captcha': CAPTCHA_TEST_RESPONSE_TOKEN,
'chainId': TEMP_ENV_VARS['FAUCET_CHAIN_ID'],
'amount': TEMP_ENV_VARS['FAUCET_AMOUNT'],
'amount': NATIVE_TOKEN_AMOUNT,
'recipient': ZERO_ADDRESS,
'tokenAddress': TEMP_ENV_VARS['FAUCET_ENABLED_TOKENS'][0]['address']
'tokenAddress': ERC20_TOKEN_ADDRESS
})
assert response.status_code == 429
Loading

0 comments on commit dc4eb12

Please sign in to comment.