From 76cf7551c3b83f44f72c91667da54a743d9f6d2a Mon Sep 17 00:00:00 2001 From: ptrus Date: Fri, 27 Feb 2026 07:45:20 +0100 Subject: [PATCH] feat: add configurable ERC20 token addresses --- .env.example | 4 +++ compose.yaml | 1 + solidity/tasks/tokens.ts | 78 +++++++++++++++++++++++++++++++++------- src/config/__init__.py | 24 +++++++++++-- 4 files changed, 93 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index fea867d8..d10baba5 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,10 @@ ACCOUNTING_CONTRACT_ADDRESS=0xYourContractAddressHere # Get your API key from: https://dashboard.alchemy.com/ ALCHEMY_API_KEY=your-alchemy-api-key-here +# Optional ERC20 token address overrides +# Format: ERC20_{SYMBOL}_{CHAIN_ID} +# ERC20_USDC_84532=0xYourUsdcAddressOnBaseSepolia + # Sapphire Testnet configuration SAPPHIRE_CHAIN_ID=23295 SAPPHIRE_RPC_URL=https://testnet.sapphire.oasis.io diff --git a/compose.yaml b/compose.yaml index 37fb3dff..faaf84c7 100644 --- a/compose.yaml +++ b/compose.yaml @@ -13,6 +13,7 @@ services: - SAPPHIRE_CHAIN_ID=${SAPPHIRE_CHAIN_ID:-23295} - SAPPHIRE_RPC_URL=${SAPPHIRE_RPC_URL:-https://testnet.sapphire.oasis.io} - ALCHEMY_API_KEY=${ALCHEMY_API_KEY} + - ERC20_USDC_84532=${ERC20_USDC_84532:-} - ACCOUNTING_GAS_LIMIT=${ACCOUNTING_GAS_LIMIT:-500000} - DEPOSIT_POLL_INTERVAL=${DEPOSIT_POLL_INTERVAL:-1} - WITHDRAWAL_POLL_INTERVAL=${WITHDRAWAL_POLL_INTERVAL:-12} diff --git a/solidity/tasks/tokens.ts b/solidity/tasks/tokens.ts index 2e1a42d1..4a8aa462 100644 --- a/solidity/tasks/tokens.ts +++ b/solidity/tasks/tokens.ts @@ -1,10 +1,22 @@ import { task } from "hardhat/config"; +import { AbiCoder, keccak256, solidityPacked } from "ethers"; + +// Compute token ID: keccak256(abi.encode(tokenType, abi.encodePacked(chainId, tokenAddress))) +function computeErc20TokenId(chainId: bigint, tokenAddress: string): string { + const abiCoder = new AbiCoder(); + const tokenType = 1; // ERC20 + // abi.encodePacked(chainId, tokenAddress) = 32 bytes + 20 bytes = 52 bytes + const data = solidityPacked(["uint256", "address"], [chainId, tokenAddress]); + return keccak256(abiCoder.encode(["uint8", "bytes"], [tokenType, data])); +} task("transferERC20") - .addOptionalParam("token", "ERC20 token address", "0x12084e1a0fe92b5ab803a81a0ae54d91040f89ca") - .addOptionalParam("recipient", "Recipient address", "0x284a3Fe2939a4e4859e6321537d4264533E3D549") + .addOptionalParam("token", "ERC20 token address", "0x036CbD53842c5426634e7929541eC2318f3dCF7e") + .addOptionalParam("recipient", "Recipient address", "0xb420ab8484cf72ddf73e014c7800984c85604ef4") .addOptionalParam("amount", "Amount in token units", "1") - .addOptionalParam("decimals", "Token decimals", "18") + .addOptionalParam("decimals", "Token decimals", "6") + .addOptionalParam("apiurl", "Accounting API base URL", "https://p8000.m1356.opf-testnet-rofl-25.rofl.app") + .addOptionalParam("timeout", "Timeout in seconds to wait for deposit", "120") .setAction(async (args, hre) => { const tokenAddress = args.token; const recipient = args.recipient; @@ -13,6 +25,11 @@ task("transferERC20") const [signer] = await hre.ethers.getSigners(); console.log("Sender address:", signer.address); + // Compute token ID from chain ID and token address + const network = await hre.ethers.provider.getNetwork(); + const tokenId = computeErc20TokenId(network.chainId, tokenAddress); + console.log("Token ID:", tokenId); + const feeData = await hre.ethers.provider.getFeeData(); console.log("Fee data:", { maxFeePerGas: feeData.maxFeePerGas?.toString(), @@ -36,6 +53,24 @@ task("transferERC20") throw new Error("Insufficient token balance!"); } + // Get accounting balance before transfer + let balanceBefore = BigInt(0); + const balanceUrl = `${args.apiurl}/v1/accounting/balances/${signer.address}/${tokenId}`; + console.log("Balance URL:", balanceUrl); + try { + const resp = await fetch(balanceUrl); + if (!resp.ok) { + const text = await resp.text(); + console.log(`API error ${resp.status}: ${text}`); + } else { + const data = await resp.json(); + balanceBefore = BigInt(data.balance || "0"); + console.log("Accounting balance before:", balanceBefore.toString()); + } + } catch (e) { + console.log("Could not fetch accounting balance:", e); + } + console.log("\nSending EIP-1559 (type 2) transaction..."); console.log(`Token: ${tokenAddress}`); console.log(`Recipient: ${recipient}`); @@ -56,17 +91,36 @@ task("transferERC20") console.log("Block number:", receipt?.blockNumber); console.log("Transaction index:", receipt?.index); console.log("Gas used:", receipt?.gasUsed.toString()); - console.log("Type:", receipt?.type, "(should be 2 for EIP-1559)"); - const rawReceipt = await hre.ethers.provider.send("eth_getTransactionReceipt", [tx.hash]); - console.log("\nRaw receipt status:", rawReceipt.status); - console.log("Receipt type field:", rawReceipt.type); + // Wait for deposit to be credited + console.log("\n=== Waiting for deposit to be credited ==="); + const timeoutMs = parseInt(args.timeout) * 1000; + const startTime = Date.now(); + + while (Date.now() - startTime < timeoutMs) { + try { + const resp = await fetch(balanceUrl); + const data = await resp.json(); + const currentBalance = BigInt(data.balance || "0"); + + if (currentBalance > balanceBefore) { + const credited = currentBalance - balanceBefore; + console.log("\n=== Deposit Credited ==="); + console.log("Balance before:", balanceBefore.toString()); + console.log("Balance after:", currentBalance.toString()); + console.log("Credited:", credited.toString()); + return tx.hash; + } + } catch { + // API error, keep polling + } + + process.stdout.write("."); + await new Promise((r) => setTimeout(r, 3000)); + } - console.log("\n=== Use these values for creditEVMDeposit ==="); - console.log("Block number:", receipt?.blockNumber); - console.log("Transaction index:", receipt?.index); - console.log("User address:", signer.address); - console.log("Transaction hash:", tx.hash); + console.log("\n\nTimeout waiting for deposit to be credited."); + console.log("The deposit may still be processing in the background."); return tx.hash; }); diff --git a/src/config/__init__.py b/src/config/__init__.py index 455f7172..bdce64f1 100644 --- a/src/config/__init__.py +++ b/src/config/__init__.py @@ -27,13 +27,33 @@ 84532: "ETH", } -ERC20_TOKENS: Dict[int, Dict[str, str]] = { +# Default ERC20 token addresses (can be overridden via env) +_DEFAULT_ERC20_TOKENS: Dict[int, Dict[str, str]] = { 84532: { - "0x12084E1A0fe92b5ab803a81A0Ae54D91040F89ca": "USDC", + "0x036CbD53842c5426634e7929541eC2318f3dCF7e": "USDC", } } +def _build_erc20_tokens() -> Dict[int, Dict[str, str]]: + """Build ERC20 tokens config, allowing env overrides.""" + tokens: Dict[int, Dict[str, str]] = {} + for chain_id, chain_tokens in _DEFAULT_ERC20_TOKENS.items(): + tokens[chain_id] = {} + for addr, symbol in chain_tokens.items(): + # Check for env override: ERC20_{SYMBOL}_{CHAIN_ID} + env_key = f"ERC20_{symbol}_{chain_id}" + override_addr = os.getenv(env_key) + if override_addr: + tokens[chain_id][override_addr] = symbol + else: + tokens[chain_id][addr] = symbol + return tokens + + +ERC20_TOKENS: Dict[int, Dict[str, str]] = _build_erc20_tokens() + + def _get_int(name: str, default: int) -> int: value = os.getenv(name) if value is None: