diff --git a/api/api/api.py b/api/api/api.py index aacb6d5..1f621d0 100644 --- a/api/api/api.py +++ b/api/api/api.py @@ -7,7 +7,7 @@ from .manage import (block_user_cmd, create_access_keys_cmd, create_enabled_token_cmd) from .routes import apiv1 -from .services import Web3Singleton +from .services import CSRF, Web3Singleton from .services.database import db @@ -46,6 +46,9 @@ def create_app(): db.init_app(app) Migrate(app, db) + # Initialize CSRF Library + CSRF(app.config['CSRF_PRIVATE_KEY'], app.config['CSRF_SECRET_SALT']) + # Initialize Web3 class for latter usage w3 = Web3Singleton(app.config['FAUCET_RPC_URL'], app.config['FAUCET_PRIVATE_KEY']) diff --git a/api/api/routes.py b/api/api/routes.py index aa1b1c8..ed8f566 100644 --- a/api/api/routes.py +++ b/api/api/routes.py @@ -2,7 +2,7 @@ from web3 import Web3 from .const import FaucetRequestType, TokenType -from .services import (AskEndpointValidator, Web3Singleton, claim_native, +from .services import (CSRF, AskEndpointValidator, Web3Singleton, claim_native, claim_token) from .services.database import AccessKey, Token, Transaction @@ -14,7 +14,7 @@ def status(): return jsonify(status='ok'), 200 -@apiv1.route("/info") +@apiv1.route("/info", methods=["GET"]) def info(): enabled_tokens = Token.enabled_tokens() rate_limit_days = current_app.config['FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS'] / (24*60*60) @@ -27,27 +27,37 @@ def info(): 'rateLimitDays': round(rate_limit_days, 2) } for t in enabled_tokens ] + + # it's a singleton, gets instantiated at app creation time + csrf = CSRF.instance + csrf_item = csrf.generate_token() + return jsonify( enabledTokens=enabled_tokens_json, chainId=current_app.config['FAUCET_CHAIN_ID'], chainName=current_app.config['FAUCET_CHAIN_NAME'], - faucetAddress=current_app.config['FAUCET_ADDRESS'] + faucetAddress=current_app.config['FAUCET_ADDRESS'], + csrfToken=csrf_item.token, + csrfRequestId=csrf_item.request_id ), 200 -def _ask(request_data, validate_captcha=True, access_key=None): +def _ask(request_data, request_headers, validate_captcha=True, validate_csrf=True, access_key=None): """Process /ask request Args: request_data (object): request object validate_captcha (bool, optional): True if captcha must be validated, False otherwise. Defaults to True. + validate_csrf (bool, optional): True if CSRF token must be validated, False otherwise. Defaults to True. access_key (object, optional): AccessKey instance. Defaults to None. Returns: tuple: json content, status code """ validator = AskEndpointValidator(request_data, + request_headers, validate_captcha, + validate_csrf, access_key=access_key) ok = validator.validate() if not ok: @@ -94,7 +104,7 @@ def _ask(request_data, validate_captcha=True, access_key=None): @apiv1.route("/ask", methods=["POST"]) def ask(): - data, status_code = _ask(request.get_json(), validate_captcha=True, access_key=None) + data, status_code = _ask(request.get_json(), request.headers, validate_captcha=True, access_key=None) return data, status_code @@ -116,5 +126,5 @@ def cli_ask(): validation_errors.append('Access denied') return jsonify(errors=validation_errors), 403 - data, status_code = _ask(request.get_json(), validate_captcha=False, access_key=access_key) + data, status_code = _ask(request.get_json(), request.headers, validate_captcha=False, validate_csrf=False, access_key=access_key) return data, status_code diff --git a/api/api/services/__init__.py b/api/api/services/__init__.py index ec2fec9..77abb75 100644 --- a/api/api/services/__init__.py +++ b/api/api/services/__init__.py @@ -1,3 +1,4 @@ +from .csrf import CSRF from .database import DatabaseSingleton from .rate_limit import RateLimitStrategy, Strategy from .token import Token diff --git a/api/api/services/captcha.py b/api/api/services/captcha.py index b9d51c2..5008eb6 100644 --- a/api/api/services/captcha.py +++ b/api/api/services/captcha.py @@ -1,6 +1,6 @@ -import requests import logging +import requests logging.basicConfig(level=logging.INFO) diff --git a/api/api/services/csrf.py b/api/api/services/csrf.py new file mode 100644 index 0000000..d6811f1 --- /dev/null +++ b/api/api/services/csrf.py @@ -0,0 +1,48 @@ + +import random + +from Crypto.Cipher import PKCS1_OAEP +from Crypto.PublicKey import RSA + + +class CSRFTokenItem: + def __init__(self, request_id, token): + self.request_id = request_id + self.token = token + + +class CSRFToken: + def __init__(self, privkey, salt): + self._privkey = RSA.import_key(privkey) + self._pubkey = self._privkey.publickey() + self._salt = salt + + def generate_token(self): + request_id = '%d' % random.randint(0, 1000) + data_to_encrypt = '%s%s' % (request_id, self._salt) + + cipher_rsa = PKCS1_OAEP.new(self._pubkey) + token = cipher_rsa.encrypt(data_to_encrypt.encode()) + + return CSRFTokenItem(request_id, token.hex()) + + def validate_token(self, request_id, token): + try: + cipher_rsa = PKCS1_OAEP.new(self._privkey) + decrypted_text = cipher_rsa.decrypt(bytes.fromhex(token)).decode() + + expected_text = '%s%s' % (request_id, self._salt) + if decrypted_text == expected_text: + return True + return False + except Exception: + return False + + +class CSRF: + _instance = None + + def __new__(cls, privatekey, salt): + if not hasattr(cls, 'instance'): + cls.instance = CSRFToken(privatekey, salt) + return cls.instance diff --git a/api/api/services/database.py b/api/api/services/database.py index 72350fd..9d8d688 100644 --- a/api/api/services/database.py +++ b/api/api/services/database.py @@ -1,11 +1,10 @@ import sqlite3 from datetime import datetime -from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import MetaData, func - from api.const import (DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, DEFAULT_NATIVE_MAX_AMOUNT_PER_DAY, FaucetRequestType) +from flask_sqlalchemy import SQLAlchemy +from sqlalchemy import MetaData, func flask_db_convention = { "ix": 'ix_%(column_0_label)s', diff --git a/api/api/services/validator.py b/api/api/services/validator.py index 842e8ad..9347a5b 100644 --- a/api/api/services/validator.py +++ b/api/api/services/validator.py @@ -1,14 +1,17 @@ import datetime +import logging +from api.const import TokenType from flask import current_app, request from web3 import Web3 -from api.const import TokenType - from .captcha import captcha_verify +from .csrf import CSRF from .database import AccessKeyConfig, BlockedUsers, Token, Transaction from .rate_limit import Strategy +logging.basicConfig(level=logging.INFO) + class AskEndpointValidator: errors = [] @@ -25,14 +28,22 @@ class AskEndpointValidator: 'RATE_LIMIT_EXCEEDED': 'recipient: you have exceeded the limit for today. Try again in %d hours' } - def __init__(self, request_data, validate_captcha, access_key=None, *args, **kwargs): + def __init__(self, request_data, request_headers, validate_captcha, validate_csrf, access_key=None, *args, **kwargs): self.request_data = request_data + self.request_headers = request_headers self.validate_captcha = validate_captcha + self.validate_csrf = validate_csrf self.access_key = access_key self.ip_address = request.environ.get('HTTP_X_FORWARDED_FOR', request.remote_addr) self.errors = [] + self.csrf = CSRF.instance def validate(self): + if self.validate_csrf: + self.csrf_validation() + if len(self.errors) > 0: + return False + self.blocked_user_validation() if len(self.errors) > 0: return False @@ -64,6 +75,22 @@ def validate(self): return False return True + def csrf_validation(self): + token = self.request_headers.get('X-CSRFToken', None) + if not token: + self.errors.append('Bad request') + self.http_return_code = 400 + + request_id = self.request_data.get('requestId', None) + if not request_id: + self.errors.append('Bad request') + self.http_return_code = 400 + + csrf_valid = self.csrf.validate_token(request_id, token) + if not csrf_valid: + self.errors.append('Bad request') + self.http_return_code = 400 + def blocked_user_validation(self): recipient = self.request_data.get('recipient', None) # Run validation on blocked users only if `recipient` is available. diff --git a/api/api/settings.py b/api/api/settings.py index 27c7549..8e5c029 100644 --- a/api/api/settings.py +++ b/api/api/settings.py @@ -27,3 +27,6 @@ CAPTCHA_VERIFY_ENDPOINT = os.getenv('CAPTCHA_VERIFY_ENDPOINT') CAPTCHA_SECRET_KEY = os.getenv('CAPTCHA_SECRET_KEY') CAPTCHA_SITE_KEY = os.getenv('CAPTCHA_SITE_KEY') + +CSRF_PRIVATE_KEY = os.getenv('CSRF_PRIVATE_KEY') +CSRF_SECRET_SALT = os.getenv('CSRF_SECRET_SALT') diff --git a/api/migrations/versions/4cacf36b2356_.py b/api/migrations/versions/4cacf36b2356_.py index c8c591b..71eb125 100644 --- a/api/migrations/versions/4cacf36b2356_.py +++ b/api/migrations/versions/4cacf36b2356_.py @@ -7,7 +7,6 @@ """ import sqlalchemy as sa from alembic import op - from api.services.database import flask_db_convention # revision identifiers, used by Alembic. diff --git a/api/requirements.txt b/api/requirements.txt index eabaf4b..990cd6d 100644 --- a/api/requirements.txt +++ b/api/requirements.txt @@ -7,4 +7,5 @@ python-dotenv==1.0.0 web3==6.11.3 pytest==7.4.3 pytest-mock==3.12.0 -gunicorn==21.2.0 \ No newline at end of file +gunicorn==21.2.0 +pycryptodome==3.20.0 \ No newline at end of file diff --git a/api/tests/conftest.py b/api/tests/conftest.py index f0be193..142a533 100644 --- a/api/tests/conftest.py +++ b/api/tests/conftest.py @@ -1,11 +1,11 @@ import os from unittest import TestCase, TestResult, mock +from api.services import CSRF, Strategy +from api.services.database import Token, db from flask.testing import FlaskClient from api import create_app -from api.services import Strategy -from api.services.database import Token, db from .temp_env_var import FAUCET_ENABLED_TOKENS, TEMP_ENV_VARS @@ -71,6 +71,10 @@ def setUp(self): with self.app_ctx: self._reset_db() + self.csrf = CSRF.instance + # use same token for the whole test + self.csrf_token = self.csrf.generate_token() + def tearDown(self): ''' Cleanup to do after running each test @@ -99,6 +103,10 @@ def setUp(self): with self.app_ctx: self._reset_db() + self.csrf = CSRF.instance + # use same token for the whole test + self.csrf_token = self.csrf.generate_token() + class RateLimitIPorAddressBaseTest(BaseTest): def setUp(self): @@ -117,3 +125,7 @@ def setUp(self): with self.app_ctx: self._reset_db() + + self.csrf = CSRF.instance + # use same token for the whole test + self.csrf_token = self.csrf.generate_token() \ No newline at end of file diff --git a/api/tests/temp_env_var.py b/api/tests/temp_env_var.py index 8d78740..ef369d2 100644 --- a/api/tests/temp_env_var.py +++ b/api/tests/temp_env_var.py @@ -3,6 +3,7 @@ from api.const import (DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, DEFAULT_NATIVE_MAX_AMOUNT_PER_DAY, NATIVE_TOKEN_ADDRESS, TokenType) +from Crypto.PublicKey import RSA ERC20_TOKEN_ADDRESS = "0x" + '1' * 40 @@ -28,6 +29,8 @@ } ] +privatekey = RSA.generate(1024) + TEMP_ENV_VARS = { 'FAUCET_RPC_URL': 'http://localhost:8545', 'FAUCET_CHAIN_ID': str(FAUCET_CHAIN_ID), @@ -35,7 +38,9 @@ 'FAUCET_RATE_LIMIT_TIME_LIMIT_SECONDS': '10', 'FAUCET_DATABASE_URI': 'sqlite://', # run in-memory # 'FAUCET_DATABASE_URI': 'sqlite:///test.db', - 'CAPTCHA_SECRET_KEY': CAPTCHA_TEST_SECRET_KEY + 'CAPTCHA_SECRET_KEY': CAPTCHA_TEST_SECRET_KEY, + 'CSRF_PRIVATE_KEY': privatekey.export_key().decode(), + 'CSRF_SECRET_SALT': 'testsalt' } # Mocked values diff --git a/api/tests/test_api.py b/api/tests/test_api.py index 83ceaff..4df9f36 100644 --- a/api/tests/test_api.py +++ b/api/tests/test_api.py @@ -1,6 +1,7 @@ import unittest from api.const import ZERO_ADDRESS +from api.services import CSRF from api.services.database import BlockedUsers, Transaction from .conftest import BaseTest, api_prefix @@ -33,7 +34,10 @@ def test_ask_route_parameters(self): 'chainId': -1, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': NATIVE_TOKEN_ADDRESS + 'tokenAddress': NATIVE_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -43,7 +47,10 @@ def test_ask_route_parameters(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1, 'recipient': ZERO_ADDRESS, - 'tokenAddress': NATIVE_TOKEN_ADDRESS + 'tokenAddress': NATIVE_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -52,7 +59,10 @@ def test_ask_route_parameters(self): 'captcha': CAPTCHA_TEST_RESPONSE_TOKEN, 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1, - 'tokenAddress': NATIVE_TOKEN_ADDRESS + 'tokenAddress': NATIVE_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -62,7 +72,10 @@ def test_ask_route_parameters(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1, 'recipient': 'not an address', - 'tokenAddress': NATIVE_TOKEN_ADDRESS + 'tokenAddress': NATIVE_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -71,7 +84,10 @@ def test_ask_route_parameters(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': '0x00000123', - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -80,7 +96,10 @@ def test_ask_route_parameters(self): 'captcha': CAPTCHA_TEST_RESPONSE_TOKEN, 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1, - 'recipient': ZERO_ADDRESS + 'recipient': ZERO_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -90,7 +109,10 @@ def test_ask_route_parameters(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY + 1, 'recipient': ZERO_ADDRESS, - 'tokenAddress': 'non existing token address' + 'tokenAddress': 'non existing token address', + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -100,7 +122,10 @@ def test_ask_route_native_transaction(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_NATIVE_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': NATIVE_TOKEN_ADDRESS + 'tokenAddress': NATIVE_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) transaction = Transaction.query.filter_by(recipient=ZERO_ADDRESS).first() self.assertEqual(response.status_code, 200) @@ -114,7 +139,10 @@ def test_ask_route_token_transaction(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': '0x' + '1234' * 10 + 'tokenAddress': '0x' + '1234' * 10, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 400) @@ -123,7 +151,10 @@ def test_ask_route_token_transaction(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) transaction = Transaction.query.filter_by(recipient=ZERO_ADDRESS).first() self.assertEqual(response.status_code, 200) @@ -136,7 +167,10 @@ def test_ask_route_blocked_users(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 200) @@ -149,7 +183,10 @@ def test_ask_route_blocked_users(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 403) diff --git a/api/tests/test_api_claim_rate_limit.py b/api/tests/test_api_claim_rate_limit.py index 6436013..df7bb5f 100644 --- a/api/tests/test_api_claim_rate_limit.py +++ b/api/tests/test_api_claim_rate_limit.py @@ -19,7 +19,10 @@ def test_ask_route_limit_by_ip(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 200) @@ -29,7 +32,10 @@ def test_ask_route_limit_by_ip(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 429) @@ -42,7 +48,10 @@ def test_ask_route_limit_by_ip_or_address(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 200) @@ -56,7 +65,10 @@ def test_ask_route_limit_by_ip_or_address(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 429) @@ -71,7 +83,10 @@ def test_ask_route_limit_by_ip_or_address(self): 'chainId': FAUCET_CHAIN_ID, 'amount': DEFAULT_ERC20_MAX_AMOUNT_PER_DAY, 'recipient': ZERO_ADDRESS, - 'tokenAddress': ERC20_TOKEN_ADDRESS + 'tokenAddress': ERC20_TOKEN_ADDRESS, + 'requestId': self.csrf_token.request_id + }, headers={ + 'X-CSRFToken': self.csrf_token.token }) self.assertEqual(response.status_code, 429) diff --git a/api/tests/test_csrf.py b/api/tests/test_csrf.py new file mode 100644 index 0000000..f40fea2 --- /dev/null +++ b/api/tests/test_csrf.py @@ -0,0 +1,19 @@ +from .conftest import BaseTest + + +class TestCSRF(BaseTest): + + def test_values(self): + token_obj = self.csrf.generate_token() + self.assertTrue( + self.csrf.validate_token(token_obj.request_id, token_obj.token) + ) + self.assertFalse( + self.csrf.validate_token('myfakeid', token_obj.token) + ) + self.assertFalse( + self.csrf.validate_token('myfakeid', 'myfaketoken') + ) + self.assertFalse( + self.csrf.validate_token(token_obj.request_id, 'myfaketoken') + ) diff --git a/api/tests/test_database.py b/api/tests/test_database.py index e436ca5..4740322 100644 --- a/api/tests/test_database.py +++ b/api/tests/test_database.py @@ -1,11 +1,10 @@ import unittest -from sqlalchemy.exc import IntegrityError - from api.const import ZERO_ADDRESS from api.services.database import (AccessKey, AccessKeyConfig, Token, Transaction) from api.utils import generate_access_key +from sqlalchemy.exc import IntegrityError from .conftest import BaseTest from .temp_env_var import NATIVE_TOKEN_ADDRESS, NATIVE_TRANSFER_TX_HASH diff --git a/app/src/App.tsx b/app/src/App.tsx index 1c6eddd..db02437 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -7,6 +7,11 @@ import "./css/App.css" import Loading from "./components/Loading/Loading" import Faucet from "./components/FaucetForm/Faucet" +interface CSRFInfo { + csrfToken: string + requestId: string +} + const chainName:{ [key: string]: string }= { 100: "Gnosis", 10200: "Chiado" @@ -17,6 +22,7 @@ function App(): JSX.Element { const [loading, setLoading] = useState(true) const [enabledTokens, setEnabledTokens] = useState([]) const [faucetLoading, setFaucetLoading] = useState(true) + const [csrfInfo, setCSRFInfo] = useState({csrfToken: '', requestId: ''}) const getFaucetInfo = async () => { return axios.get(`${process.env.REACT_APP_FAUCET_API_URL}/info`) @@ -31,6 +37,11 @@ function App(): JSX.Element { const chain = chainName[response.data.chainId] document.title = `${chain} Faucet` document.querySelector('meta[name="description"]')?.setAttribute("content", `Faucet for ${chain} chain`) + + csrfInfo.csrfToken = response.data.csrfToken, + csrfInfo.requestId = response.data.csrfRequestId + setCSRFInfo(csrfInfo) + }) .catch(() => { toast.error("Network error") @@ -67,6 +78,8 @@ function App(): JSX.Element { chainId={chainId} enabledTokens={enabledTokens} setLoading={setLoading} + csrfToken={csrfInfo.csrfToken} + requestId={csrfInfo.requestId} />

Want more{chainId === 100 ? '?' : ' on Gnosis Chain?'}