Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
78 changes: 66 additions & 12 deletions solidity/tasks/tokens.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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(),
Expand All @@ -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}`);
Expand All @@ -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;
});
Expand Down
24 changes: 22 additions & 2 deletions src/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]]:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully get the use case here, is this for local deployments with custom tokens?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes or even for the main deployments, when/if we decide to add new tokens, i dont see why we would require code changes for that?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this would still require restarts/down time.
Maybe we should think about a token config smart contract, and a endpoint which fetches them?

"""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)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a check if the value is an address with Web3.is_address() I think would be good

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:
Expand Down