From 19531c68c6e532ac8c980f29b52c6cdcc218b3b3 Mon Sep 17 00:00:00 2001 From: def Date: Mon, 16 Mar 2026 03:25:26 +0000 Subject: [PATCH] Add ETHO and ETI wallet payment support --- backend/app.py | 41 +++++++++++++++++++++++++--- templates/portal_invoice_detail.html | 36 ++++++++++++++++++++---- 2 files changed, 67 insertions(+), 10 deletions(-) diff --git a/backend/app.py b/backend/app.py index 3394beb..bd115e4 100644 --- a/backend/app.py +++ b/backend/app.py @@ -60,6 +60,11 @@ RPC_ARBITRUM_URL = os.getenv("OTB_BILLING_RPC_ARBITRUM", "https://arbitrum-one-r 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") +RPC_ETICA_URL = os.getenv("OTB_BILLING_RPC_ETICA", "https://rpc.etica-stats.org") +RPC_ETICA_URL_2 = os.getenv("OTB_BILLING_RPC_ETICA_2", "https://eticamainnet.eticaprotocol.org") +RPC_ETHO_URL = os.getenv("OTB_BILLING_RPC_ETHO", "https://rpc.ethoprotocol.com") +RPC_ETHO_URL_2 = os.getenv("OTB_BILLING_RPC_ETHO_2", "https://rpc4.ethoprotocol.com") + CRYPTO_PROCESSING_TIMEOUT_SECONDS = int(os.getenv("OTB_BILLING_CRYPTO_PROCESSING_TIMEOUT_SECONDS", "180")) CRYPTO_WATCH_INTERVAL_SECONDS = int(os.getenv("OTB_BILLING_CRYPTO_WATCH_INTERVAL_SECONDS", "30")) CRYPTO_WATCHER_STARTED = False @@ -230,11 +235,23 @@ def get_invoice_crypto_options(invoice): "label": "ETHO (Etho)", "payment_currency": "ETHO", "wallet_address": CRYPTO_EVM_PAYMENT_ADDRESS, - "wallet_capable": False, + "wallet_capable": True, "asset_type": "native", - "chain_id": None, + "chain_id": 1313114, "decimals": 18, "token_contract": None, + "rpc_urls": [RPC_ETHO_URL, RPC_ETHO_URL_2], + "chain_add_params": { + "chainId": "0x14095a", + "chainName": "Etho Protocol", + "nativeCurrency": { + "name": "Etho Protocol", + "symbol": "ETHO", + "decimals": 18 + }, + "rpcUrls": [RPC_ETHO_URL, RPC_ETHO_URL_2], + "blockExplorerUrls": ["https://explorer.ethoprotocol.com"] + }, }, "ETI": { "symbol": "ETI", @@ -242,11 +259,23 @@ def get_invoice_crypto_options(invoice): "label": "ETI (Etica)", "payment_currency": "ETI", "wallet_address": CRYPTO_EVM_PAYMENT_ADDRESS, - "wallet_capable": False, + "wallet_capable": True, "asset_type": "token", - "chain_id": None, + "chain_id": 61803, "decimals": 18, "token_contract": "0x34c61EA91bAcdA647269d4e310A86b875c09946f", + "rpc_urls": [RPC_ETICA_URL, RPC_ETICA_URL_2], + "chain_add_params": { + "chainId": "0xf16b", + "chainName": "Etica", + "nativeCurrency": { + "name": "Etica Gas", + "symbol": "EGAZ", + "decimals": 18 + }, + "rpcUrls": [RPC_ETICA_URL, RPC_ETICA_URL_2], + "blockExplorerUrls": ["https://explorer.etica-stats.org"] + }, }, } @@ -276,6 +305,10 @@ def get_rpc_urls_for_chain(chain_name): return [u for u in [RPC_ETHEREUM_URL, RPC_ETHEREUM_URL_2, RPC_ETHEREUM_URL_3] if u] if chain == "arbitrum": return [u for u in [RPC_ARBITRUM_URL, RPC_ARBITRUM_URL_2, RPC_ARBITRUM_URL_3] if u] + if chain == "etica": + return [u for u in [RPC_ETICA_URL, RPC_ETICA_URL_2] if u] + if chain == "etho": + return [u for u in [RPC_ETHO_URL, RPC_ETHO_URL_2] if u] return [] def rpc_call_any(rpc_urls, method, params): diff --git a/templates/portal_invoice_detail.html b/templates/portal_invoice_detail.html index b12e22d..7e22afa 100644 --- a/templates/portal_invoice_detail.html +++ b/templates/portal_invoice_detail.html @@ -262,6 +262,7 @@ data-amount="{{ pending_crypto_payment.payment_amount }}" data-decimals="{{ selected_crypto_option.decimals }}" data-token-contract="{{ selected_crypto_option.token_contract or '' }}" + data-chain-add='{{ (selected_crypto_option.chain_add_params or {})|tojson|safe }}' > Open MetaMask / Rabby @@ -441,12 +442,29 @@ return "0x" + method + addr + amtHex; } - async function switchChain(chainId) { + async function switchChain(chainId, chainAddParams) { const hexChainId = "0x" + Number(chainId).toString(16); - await window.ethereum.request({ - method: "wallet_switchEthereumChain", - params: [{ chainId: hexChainId }] - }); + try { + await window.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: hexChainId }] + }); + return; + } catch (err) { + const code = err && (err.code ?? err?.data?.originalError?.code); + if ((code === 4902 || String(err).includes("4902")) && chainAddParams) { + await window.ethereum.request({ + method: "wallet_addEthereumChain", + params: [chainAddParams] + }); + await window.ethereum.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: hexChainId }] + }); + return; + } + throw err; + } } function buildMetaMaskMobileLink() { @@ -571,6 +589,12 @@ Reference: ${invoiceRef}`; const amount = this.dataset.amount; const decimals = Number(this.dataset.decimals || "18"); const tokenContract = this.dataset.tokenContract || ""; + let chainAddParams = null; + try { + chainAddParams = this.dataset.chainAdd ? JSON.parse(this.dataset.chainAdd) : null; + } catch (err) { + chainAddParams = null; + } const setStatus = (msg) => { if (walletStatus) walletStatus.textContent = msg; @@ -589,7 +613,7 @@ Reference: ${invoiceRef}`; if (chainId && chainId !== "None" && chainId !== "") { try { - await switchChain(Number(chainId)); + await switchChain(Number(chainId), chainAddParams); } catch (err) { setStatus(`Chain switch failed: ${err.message || err}`); this.disabled = false;