Browse Source

Add 3-RPC polling pools for Ethereum and Arbitrum

main
def 6 days ago
parent
commit
309209c5c0
  1. 151
      backend/app.py

151
backend/app.py

@ -50,8 +50,13 @@ SQUARE_API_VERSION = "2026-01-22"
SQUARE_WEBHOOK_LOG = str(BASE_DIR / "logs" / "square_webhook_events.log") SQUARE_WEBHOOK_LOG = str(BASE_DIR / "logs" / "square_webhook_events.log")
ORACLE_BASE_URL = os.getenv("ORACLE_BASE_URL", "https://monitor.outsidethebox.top") ORACLE_BASE_URL = os.getenv("ORACLE_BASE_URL", "https://monitor.outsidethebox.top")
CRYPTO_EVM_PAYMENT_ADDRESS = os.getenv("OTB_BILLING_CRYPTO_EVM_ADDRESS", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5") CRYPTO_EVM_PAYMENT_ADDRESS = os.getenv("OTB_BILLING_CRYPTO_EVM_ADDRESS", "0x44f6c44C42e6ae0392E7289F032384C0d37F56D5")
RPC_ETHEREUM_URL = os.getenv("OTB_BILLING_RPC_ETHEREUM", "https://cloudflare-eth.com") RPC_ETHEREUM_URL = os.getenv("OTB_BILLING_RPC_ETHEREUM", "https://ethereum-rpc.publicnode.com")
RPC_ARBITRUM_URL = os.getenv("OTB_BILLING_RPC_ARBITRUM", "https://arb1.arbitrum.io/rpc") RPC_ETHEREUM_URL_2 = os.getenv("OTB_BILLING_RPC_ETHEREUM_2", "https://rpc.ankr.com/eth")
RPC_ETHEREUM_URL_3 = os.getenv("OTB_BILLING_RPC_ETHEREUM_3", "https://eth.drpc.org")
RPC_ARBITRUM_URL = os.getenv("OTB_BILLING_RPC_ARBITRUM", "https://arbitrum-one-rpc.publicnode.com")
RPC_ARBITRUM_URL_2 = os.getenv("OTB_BILLING_RPC_ARBITRUM_2", "https://rpc.ankr.com/arbitrum")
RPC_ARBITRUM_URL_3 = os.getenv("OTB_BILLING_RPC_ARBITRUM_3", "https://arb1.arbitrum.io/rpc")
@ -258,40 +263,53 @@ def get_invoice_crypto_options(invoice):
options.sort(key=lambda x: (0 if x.get("recommended") else 1, x.get("symbol"))) options.sort(key=lambda x: (0 if x.get("recommended") else 1, x.get("symbol")))
return options return options
def get_rpc_url_for_chain(chain_name): def get_rpc_urls_for_chain(chain_name):
chain = str(chain_name or "").lower() chain = str(chain_name or "").lower()
if chain == "ethereum": if chain == "ethereum":
return RPC_ETHEREUM_URL return [u for u in [RPC_ETHEREUM_URL, RPC_ETHEREUM_URL_2, RPC_ETHEREUM_URL_3] if u]
if chain == "arbitrum": if chain == "arbitrum":
return RPC_ARBITRUM_URL return [u for u in [RPC_ARBITRUM_URL, RPC_ARBITRUM_URL_2, RPC_ARBITRUM_URL_3] if u]
return None return []
def rpc_call(rpc_url, method, params): def rpc_call_any(rpc_urls, method, params):
payload = json.dumps({ last_error = None
"jsonrpc": "2.0",
"id": 1,
"method": method,
"params": params,
}).encode("utf-8")
req = urllib.request.Request( for rpc_url in rpc_urls:
rpc_url, try:
data=payload, payload = json.dumps({
headers={ "jsonrpc": "2.0",
"Content-Type": "application/json", "id": 1,
"Accept": "application/json", "method": method,
"User-Agent": "otb-billing-rpc/0.1", "params": params,
}, }).encode("utf-8")
method="POST"
) req = urllib.request.Request(
rpc_url,
data=payload,
headers={
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "otb-billing-rpc/0.1",
},
method="POST"
)
with urllib.request.urlopen(req, timeout=20) as resp: with urllib.request.urlopen(req, timeout=20) as resp:
data = json.loads(resp.read().decode("utf-8")) data = json.loads(resp.read().decode("utf-8"))
if isinstance(data, dict) and data.get("error"): if isinstance(data, dict) and data.get("error"):
raise RuntimeError(str(data["error"])) raise RuntimeError(str(data["error"]))
return {
"rpc_url": rpc_url,
"result": (data or {}).get("result"),
}
except Exception as err:
last_error = err
return (data or {}).get("result") if last_error:
raise last_error
raise RuntimeError("No RPC URLs configured")
def _to_base_units(amount_text, decimals): def _to_base_units(amount_text, decimals):
amount_dec = Decimal(str(amount_text)) amount_dec = Decimal(str(amount_text))
@ -317,41 +335,58 @@ def parse_erc20_transfer_input(input_data):
} }
def verify_wallet_transaction(option, tx_hash): def verify_wallet_transaction(option, tx_hash):
rpc_url = get_rpc_url_for_chain(option.get("chain")) rpc_urls = get_rpc_urls_for_chain(option.get("chain"))
if not rpc_url: if not rpc_urls:
raise RuntimeError("No RPC configured for chain") raise RuntimeError("No RPC configured for chain")
tx = rpc_call(rpc_url, "eth_getTransactionByHash", [tx_hash]) seen_result = None
if not tx: last_not_found = False
raise RuntimeError("Transaction hash not found on RPC")
wallet_to = str(option.get("wallet_address") or "").lower() for rpc_url in rpc_urls:
expected_units = _to_base_units(option.get("display_amount"), option.get("decimals") or 18) try:
rpc_resp = rpc_call_any([rpc_url], "eth_getTransactionByHash", [tx_hash])
tx = rpc_resp.get("result")
if not tx:
last_not_found = True
continue
if option.get("asset_type") == "native": wallet_to = str(option.get("wallet_address") or "").lower()
tx_to = str(tx.get("to") or "").lower() expected_units = _to_base_units(option.get("display_amount"), option.get("decimals") or 18)
tx_value = int(tx.get("value") or "0x0", 16)
if tx_to != wallet_to:
raise RuntimeError("Transaction destination does not match payment wallet")
if tx_value != expected_units:
raise RuntimeError("Transaction value does not match frozen quote amount")
else:
tx_to = str(tx.get("to") or "").lower()
contract = str(option.get("token_contract") or "").lower()
if tx_to != contract:
raise RuntimeError("Token contract does not match expected asset contract")
parsed = parse_erc20_transfer_input(tx.get("input") or "")
if not parsed:
raise RuntimeError("Transaction input is not a supported ERC20 transfer")
if str(parsed["to"]).lower() != wallet_to:
raise RuntimeError("Token transfer recipient does not match payment wallet")
if int(parsed["amount"]) != expected_units:
raise RuntimeError("Token transfer amount does not match frozen quote amount")
return { if option.get("asset_type") == "native":
"rpc_url": rpc_url, tx_to = str(tx.get("to") or "").lower()
"tx": tx, tx_value = int(tx.get("value") or "0x0", 16)
} if tx_to != wallet_to:
raise RuntimeError("Transaction destination does not match payment wallet")
if tx_value != expected_units:
raise RuntimeError("Transaction value does not match frozen quote amount")
else:
tx_to = str(tx.get("to") or "").lower()
contract = str(option.get("token_contract") or "").lower()
if tx_to != contract:
raise RuntimeError("Token contract does not match expected asset contract")
parsed = parse_erc20_transfer_input(tx.get("input") or "")
if not parsed:
raise RuntimeError("Transaction input is not a supported ERC20 transfer")
if str(parsed["to"]).lower() != wallet_to:
raise RuntimeError("Token transfer recipient does not match payment wallet")
if int(parsed["amount"]) != expected_units:
raise RuntimeError("Token transfer amount does not match frozen quote amount")
seen_result = {
"rpc_url": rpc_url,
"tx": tx,
}
break
except Exception:
continue
if not seen_result:
if last_not_found:
raise RuntimeError("Transaction hash not found on any configured RPC")
raise RuntimeError("Unable to verify transaction on configured RPC pool")
return seen_result
def square_amount_to_cents(value): def square_amount_to_cents(value):
return int((to_decimal(value) * 100).quantize(Decimal("1"))) return int((to_decimal(value) * 100).quantize(Decimal("1")))

Loading…
Cancel
Save