diff --git a/Cargo.lock b/Cargo.lock index a074abf36ea..1e7b47b967e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10709,6 +10709,28 @@ dependencies = [ "strum 0.27.2", ] +[[package]] +name = "reth-sdk" +version = "1.9.1" +dependencies = [ + "alloy-dyn-abi", + "alloy-genesis", + "alloy-json-abi", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-signer-local", + "alloy-sol-types", + "alloy-transport-http", + "anyhow", + "clap", + "eyre", + "tokio", + "tracing", + "tracing-subscriber 0.3.20", +] + [[package]] name = "reth-stages" version = "1.9.1" @@ -11603,7 +11625,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn 2.0.108", + "syn 2.0.110", "unicode-ident", ] @@ -14596,15 +14618,9 @@ dependencies = [ "rustix 1.1.2", ] -[[package]] -name = "xsum" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" - [[package]] name = "xlayer-e2e-test" -version = "1.8.3" +version = "1.9.1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14631,6 +14647,12 @@ dependencies = [ "url", ] +[[package]] +name = "xsum" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 581d72b249e..aaef8b9b977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,6 +179,7 @@ members = [ "testing/runner", "tests/", "crates/tracing-otlp", + "crates/sdk", ] default-members = ["bin/reth"] exclude = ["docs/cli"] @@ -500,6 +501,7 @@ alloy-contract = { version = "1.1.0", default-features = false } alloy-eips = { version = "1.1.0", default-features = false } alloy-genesis = { version = "1.1.0", default-features = false } alloy-json-rpc = { version = "1.1.0", default-features = false } +alloy-json-abi = { version = "1.4.1", default-features = false } alloy-network = { version = "1.1.0", default-features = false } alloy-network-primitives = { version = "1.1.0", default-features = false } alloy-provider = { version = "1.1.0", features = ["reqwest"], default-features = false } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml new file mode 100644 index 00000000000..6cdcf99139d --- /dev/null +++ b/crates/sdk/Cargo.toml @@ -0,0 +1,36 @@ +[package] +name = "reth-sdk" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +# reth +alloy-genesis.workspace = true +alloy-provider.workspace = true +alloy-rpc-types-eth.workspace = true +alloy-network.workspace = true +alloy-signer-local.workspace = true +alloy-primitives.workspace = true +alloy-transport-http.workspace = true +alloy-sol-types.workspace = true +alloy-json-abi.workspace = true +alloy-dyn-abi.workspace = true + +# async +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + +# tracing +tracing.workspace = true +tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] } + +# misc +clap = { workspace = true, features = ["derive"] } +anyhow.workspace=true +eyre.workspace=true diff --git a/crates/sdk/README.md b/crates/sdk/README.md new file mode 100644 index 00000000000..86affd7c697 --- /dev/null +++ b/crates/sdk/README.md @@ -0,0 +1,33 @@ +# run +``` +export RPC_URL=https://testrpc.xlayer.tech/unlimited/abcd + +## OKB transfer +cargo run -p reth-sdk --bin xlayer_cli -- transfer \ + --rpc-url $RPC_URL$ \ + --private-key $PRIVATE_KEY \ + --to X44667e638246762d7ba3dfcedbd753d336e8bc81 \ + --amount 1 + +## USDC transfer +cargo run -p reth-sdk --bin xlayer_cli -- token-transfer \ + --rpc-url $RPC_URL$ \ + --private-key $PRIVATE_KEY \ + --to X44667e638246762d7ba3dfcedbd753d336e8bc81 \ + --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d \ + --amount 1 + +## OKB balance +cargo run -p reth-sdk --bin xlayer_cli -- balance --rpc-url $RPC_URL$ --address X33f34D8b20696780Ba07b1ea89F209B4Dc51723A + +## USDC balance +cargo run -p reth-sdk --bin xlayer_cli -- token-balance --rpc-url $RPC_URL$ --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d --account X33f34D8b20696780Ba07b1ea89F209B4Dc51723A + +## General contract call, use uniswap v3 quoteExactInputSingle as example +cargo run -p reth-sdk --bin xlayer_cli -- eth-call --rpc-url https://maximum-yolo-seed.quiknode.pro/e4a602e14006c812850883f288b1574b36c48ef6 --to 0x61fFE014bA17989E743c5F6cB21bF9697530B21e --sig "quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32,uint256)" --args "(XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7,1000000000000000000,3000,0)" +``` + +# troubleshooting +``` +cargo expand -p reth-sdk --bin xlayer_cli > expanded.rs +``` diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs new file mode 100644 index 00000000000..bc1b663ecc5 --- /dev/null +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -0,0 +1,187 @@ +//! X Layer CLI - Command-line interface for interacting with X Layer blockchain. +//! +//! This binary provides commands for sending transactions and interacting +//! with the Reth chain via RPC. +use alloy_dyn_abi::{FunctionExt, JsonAbiExt}; +use alloy_json_abi::Function; +use alloy_primitives::{Address, Bytes, U256}; +use alloy_provider::Provider; +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use reth_sdk::{ + create_provider, encode_args, get_balance, get_token_balance, transfer_native_asset, + transfer_token, XAddress, +}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +/// Common fields shared across transfer commands +#[derive(Parser, Debug)] +pub struct CommonTransferArgs { + /// RPC URL + #[arg(long)] + rpc_url: String, + /// Private key (hex string, with or without 0x prefix) + #[arg(long)] + private_key: String, +} + +#[derive(Parser)] +#[command(version, about, long_about = None)] +struct XlayerCli { + #[command(subcommand)] + command: XlayerCommands, +} + +/// CLI commands for X Layer operations. +#[derive(Subcommand, Debug)] +pub enum XlayerCommands { + /// Transfer native assets (OKB) to a specified address. + Transfer { + /// Common transfer arguments (RPC URL and private key) + #[command(flatten)] + common: CommonTransferArgs, + /// Recipient address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") + #[arg(long)] + to: XAddress, + /// Amount to send in wei (optional, defaults to 0) + #[arg(long)] + amount: Option, + }, + /// Transfer ERC20 tokens using transfer(address,uint256) function. + TokenTransfer { + /// Common transfer arguments (RPC URL and private key) + #[command(flatten)] + common: CommonTransferArgs, + /// Token contract address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") + #[arg(long)] + token: XAddress, + /// Recipient address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") + #[arg(long)] + to: XAddress, + /// Amount to transfer (in token's smallest unit, e.g., wei for 18 decimals) + #[arg(long)] + amount: U256, + }, + /// Get the balance of an address (equivalent to `eth_getBalance`). + Balance { + /// RPC URL + #[arg(long)] + rpc_url: String, + /// Address to query balance for (supports optional "X" prefix, e.g., "X1234..." or + /// "0x1234...") + #[arg(long)] + address: XAddress, + }, + /// Get the ERC20 token balance of an address (equivalent to calling balanceOf(address)). + TokenBalance { + /// RPC URL + #[arg(long)] + rpc_url: String, + /// Token contract address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") + #[arg(long)] + token: XAddress, + /// Account address to query balance for (supports optional "X" prefix, e.g., "X1234..." or + /// "0x1234...") + #[arg(long)] + account: XAddress, + }, + /// Call a contract function using `eth_call` RPC method. + EthCall { + /// RPC URL + #[arg(long)] + rpc_url: String, + /// Token contract address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") + #[arg(long)] + to: XAddress, + /// The signature of the function. + #[arg(long)] + sig: Option, + + /// The arguments of the function. + #[arg(allow_negative_numbers = true)] + #[arg(long, num_args = 1..)] + args: Vec, + }, +} + +#[tokio::main] +async fn main() -> Result<()> { + // RUST_LOG=alloy_provider=debug,alloy_transport_http=debug,alloy_json_rpc=debug + let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info")); + + tracing_subscriber::registry().with(fmt::layer().with_target(false)).with(filter).init(); + + let cli = XlayerCli::parse(); + + match cli.command { + XlayerCommands::Transfer { common, to, amount } => { + let to_address: Address = to.into(); + let tx_hash = + transfer_native_asset(&common.rpc_url, &common.private_key, to_address, amount) + .await?; + println!("✅ Transaction sent! Hash: {tx_hash:?}"); + } + XlayerCommands::TokenTransfer { common, token, to, amount } => { + let token_address: Address = token.into(); + let to_address: Address = to.into(); + let tx_hash = transfer_token( + &common.rpc_url, + &common.private_key, + token_address, + to_address, + amount, + ) + .await?; + println!("✅ Token transfer transaction sent! Hash: {tx_hash:?}"); + } + XlayerCommands::Balance { rpc_url, address } => { + let address: Address = address.into(); + let balance = get_balance(&rpc_url, address).await?; + println!("Balance: {balance} wei"); + } + XlayerCommands::TokenBalance { rpc_url, token, account } => { + let token_address: Address = token.into(); + let account_address: Address = account.into(); + let balance = get_token_balance(&rpc_url, token_address, account_address).await?; + println!("Balance: {balance} wei"); + } + XlayerCommands::EthCall { rpc_url, to, sig, args } => { + let sig = sig.unwrap(); + + if sig.contains('(') { + let func = Function::parse(&sig).unwrap(); + let values = encode_args(&func.inputs, args).unwrap(); + let data = func.abi_encode_input(&values).unwrap(); + + let to_address: Address = to.into(); + + let provider = create_provider(&rpc_url).await?; + + let result = provider + .call(alloy_rpc_types_eth::TransactionRequest { + to: Some(alloy_primitives::TxKind::Call(to_address)), + input: alloy_rpc_types_eth::TransactionInput { + input: None, + data: Some(Bytes::from(data)), + }, + ..Default::default() + }) + .await + .context("Failed to execute eth_call")?; + + match func.abi_decode_output(result.as_ref()) { + Ok(decoded) => { + println!("Result: decoded {decoded:?}"); + } + Err(err) => { + eprintln!("error in decode: {err:?}"); + } + }; + } else { + panic!("signature needs contains ()"); + } + } + } + + Ok(()) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs new file mode 100644 index 00000000000..6b45d28cb1a --- /dev/null +++ b/crates/sdk/src/lib.rs @@ -0,0 +1,574 @@ +//! Reth SDK for interacting with X Layer blockchain. +//! +//! This crate provides utilities and functions for sending transactions +//! and interacting with the Reth chain via RPC. + +use alloy_dyn_abi::{DynSolType, DynSolValue}; +use alloy_json_abi::Param; +use alloy_network::EthereumWallet; +use alloy_primitives::{Address, Bytes, B256, U256}; +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_rpc_types_eth::TransactionRequest; +use alloy_signer_local::PrivateKeySigner; +use alloy_sol_types::{sol, SolCall}; +use anyhow::{Context, Result}; +use std::str::FromStr; + +/// Prefix character for X Layer addresses +const X_ADDRESS_PREFIX: char = 'X'; + +// Define ERC20 interface using sol! macro +sol! { + interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); + } +} + +/// Address type that handles addresses with an optional "X" prefix +/// The "X" prefix is stripped when converting to Address for chain operations +/// Examples: "X1234..." or "0x1234..." or "1234..." (all valid) +#[derive(Debug, Clone)] +pub struct XAddress { + address: Address, +} + +impl FromStr for XAddress { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let addr_str = s.strip_prefix(X_ADDRESS_PREFIX).unwrap_or(s); + let addr_with_prefix = + if addr_str.starts_with("0x") { addr_str.to_string() } else { format!("0x{addr_str}") }; + + let address = addr_with_prefix.parse::
().context("Failed to parse address")?; + Ok(Self { address }) + } +} + +impl From for Address { + fn from(x_addr: XAddress) -> Self { + x_addr.address + } +} + +impl std::fmt::Display for XAddress { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.address) + } +} + +/// Create a provider with wallet from RPC URL and private key +pub async fn create_provider_with_wallet( + rpc_url: &str, + private_key: &str, +) -> Result + Clone> { + let private_key = private_key.strip_prefix("0x").unwrap_or(private_key); + let signer: PrivateKeySigner = private_key.parse().context("Failed to parse private key")?; + let wallet = EthereumWallet::from(signer); + + let provider = ProviderBuilder::new() + .wallet(wallet) + .connect_http(rpc_url.parse().context("Invalid RPC URL")?); + + Ok(provider) +} + +/// Create a provider without wallet (for read-only operations) +pub async fn create_provider( + rpc_url: &str, +) -> Result + Clone> { + let provider = ProviderBuilder::new().connect_http(rpc_url.parse().context("Invalid RPC URL")?); + Ok(provider) +} + +/// Transfer native assets (OKB) to an address +pub async fn transfer_native_asset( + rpc_url: &str, + private_key: &str, + to_address: Address, + amount: Option, +) -> Result { + let provider = create_provider_with_wallet(rpc_url, private_key).await?; + + let chain_id = provider.get_chain_id().await.context("Failed to get chain ID")?; + let gas_price = provider.get_gas_price().await.context("Failed to get gas price")?; + + let tx_request = TransactionRequest { + to: Some(alloy_primitives::TxKind::Call(to_address)), + value: amount, + gas: Some(21000u64), + gas_price: Some(gas_price), + chain_id: Some(chain_id), + ..Default::default() + }; + + let pending_tx = + provider.send_transaction(tx_request).await.context("Failed to send transaction")?; + + let receipt = pending_tx.get_receipt().await?; + + Ok(receipt.transaction_hash) +} + +/// Transfer ERC20 tokens using transfer(address,uint256) function +pub async fn transfer_token( + rpc_url: &str, + private_key: &str, + token_address: Address, + recipient: Address, + amount: U256, +) -> Result { + let provider = create_provider_with_wallet(rpc_url, private_key).await?; + + let chain_id = provider.get_chain_id().await.context("Failed to get chain ID")?; + let gas_price = provider.get_gas_price().await.context("Failed to get gas price")?; + + let call = IERC20::transferCall { to: recipient, amount }; + + // This produces the calldata automatically: + let calldata = call.abi_encode(); + + let tx_request = TransactionRequest { + to: Some(alloy_primitives::TxKind::Call(token_address)), + input: alloy_rpc_types_eth::TransactionInput { + input: None, + data: Some(Bytes::from(calldata)), + }, + gas: Some(100000u64), // ERC20 transfers typically need more gas than native transfers + gas_price: Some(gas_price), + chain_id: Some(chain_id), + ..Default::default() + }; + + let pending_tx = + provider.send_transaction(tx_request).await.context("Failed to send transaction")?; + + let receipt = pending_tx.get_receipt().await?; + + Ok(receipt.transaction_hash) +} + +/// Get the balance of an address +pub async fn get_balance(rpc_url: &str, address: Address) -> Result { + let provider = create_provider(rpc_url).await?; + let balance = provider.get_balance(address).await.context("Failed to get balance")?; + Ok(balance) +} + +/// Get the ERC20 token balance of an address +pub async fn get_token_balance( + rpc_url: &str, + token_address: Address, + account_address: Address, +) -> Result { + let provider = create_provider(rpc_url).await?; + + let call = IERC20::balanceOfCall { account: account_address }; + let calldata = call.abi_encode(); + let result = provider + .call(alloy_rpc_types_eth::TransactionRequest { + to: Some(alloy_primitives::TxKind::Call(token_address)), + input: alloy_rpc_types_eth::TransactionInput { + input: None, + data: Some(Bytes::from(calldata)), + }, + ..Default::default() + }) + .await + .context("Failed to call balanceOf")?; + + let balance = IERC20::balanceOfCall::abi_decode_returns(&result.0) + .context("Failed to decode balanceOf return value")?; + + Ok(balance) +} + +/// Encode function arguments from string inputs +pub fn encode_args(inputs: &[Param], args: I) -> Result> +where + I: IntoIterator, + S: AsRef, +{ + let args: Vec = args.into_iter().collect(); + + if inputs.len() != args.len() { + anyhow::bail!( + "encode length mismatch: expected {} types, got {}", + inputs.len(), + args.len() + ); + } + + std::iter::zip(inputs, args) + .map(|(input, arg)| coerce_value(&input.selector_type(), arg.as_ref())) + .collect() +} + +/// Helper function to coerce a value to a [`DynSolValue`] given a type string +pub fn coerce_value(ty: &str, arg: &str) -> Result { + let parsed_ty = DynSolType::parse(ty)?; + coerce_value_recursive(&parsed_ty, arg) +} + +/// Recursive helper to process types and strip X prefix from addresses +fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { + match ty { + DynSolType::Tuple(tuple_types) => { + let args = parse_tuple_args(arg)?; + + if tuple_types.len() != args.len() { + anyhow::bail!( + "Tuple length mismatch: expected {} elements, got {}", + tuple_types.len(), + args.len() + ); + } + let values: Result> = tuple_types + .iter() + .zip(args.iter()) + .map(|(ty, arg_str)| coerce_value_recursive(ty, arg_str)) + .collect(); + + Ok(DynSolValue::Tuple(values?)) + } + DynSolType::Array(inner_ty) => { + let args = parse_array_args(arg)?; + let values: Result> = + args.iter().map(|arg_str| coerce_value_recursive(inner_ty, arg_str)).collect(); + + Ok(DynSolValue::Array(values?)) + } + DynSolType::FixedArray(inner_ty, size) => { + let args = parse_array_args(arg)?; + if args.len() != *size { + anyhow::bail!( + "Fixed array length mismatch: expected {} elements, got {}", + size, + args.len() + ); + } + let values: Result> = + args.iter().map(|arg_str| coerce_value_recursive(inner_ty, arg_str)).collect(); + + Ok(DynSolValue::FixedArray(values?)) + } + DynSolType::Address => { + // Strip X prefix from address + let addr_str = arg.strip_prefix(X_ADDRESS_PREFIX).unwrap_or(arg); + let addr_with_prefix = if addr_str.starts_with("0x") { + addr_str.to_string() + } else { + format!("0x{addr_str}") + }; + DynSolType::coerce_str(&DynSolType::Address, &addr_with_prefix) + .context("Failed to coerce address") + } + _ => { + // For non-tuple, non-address types, use normal coercion + DynSolType::coerce_str(ty, arg).context("Failed to coerce value") + } + } +} + +/// Parse tuple arguments from a string, handling nested tuples +fn parse_tuple_args(s: &str) -> Result> { + let s = s.trim(); + if !s.starts_with('(') || !s.ends_with(')') { + anyhow::bail!("Tuple argument must start with '(' and end with ')'"); + } + + let inner = &s[1..s.len() - 1]; // Remove outer parentheses + let mut args = Vec::new(); + let mut current = String::new(); + let mut depth = 0; + + for ch in inner.chars() { + match ch { + '(' => { + depth += 1; + current.push(ch); + } + ')' => { + depth -= 1; + current.push(ch); + } + ',' if depth == 0 => { + args.push(current.trim().to_string()); + current.clear(); + } + _ => { + current.push(ch); + } + } + } + + if !current.is_empty() { + args.push(current.trim().to_string()); + } + + Ok(args) +} + +/// Parse array arguments from a string (comma-separated, optionally wrapped in brackets) +fn parse_array_args(s: &str) -> Result> { + let s = s.trim(); + let inner = if s.starts_with('[') && s.ends_with(']') { &s[1..s.len() - 1] } else { s }; + + if inner.is_empty() { + return Ok(Vec::new()); + } + + let mut args = Vec::new(); + let mut current = String::new(); + let mut depth = 0; + + for ch in inner.chars() { + match ch { + '(' | '[' => { + depth += 1; + current.push(ch); + } + ')' | ']' => { + depth -= 1; + current.push(ch); + } + ',' if depth == 0 => { + args.push(current.trim().to_string()); + current.clear(); + } + _ => { + current.push(ch); + } + } + } + + if !current.is_empty() { + args.push(current.trim().to_string()); + } + + Ok(args) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloy_json_abi::Function; + + #[test] + fn test_encode_args_with_xaddress_in_tuple() { + // Test the example from the CLI command: + // struct QuoteExactInputSingleParams { + // address tokenIn; + // address tokenOut; + // uint24 fee; + // uint256 amountIn; + // uint160 sqrtPriceLimitX96; + // } + // quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32, + // uint256) with args: + // (XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7, + // 1000000000000000000,3000,0) + + let sig = "quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32,uint256)"; + let func = Function::parse(sig).unwrap(); + + let args = vec!["(XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7,1000000000000000000,3000,0)"]; + + let result = encode_args(&func.inputs, args).unwrap(); + + assert_eq!(result.len(), 1); + + if let DynSolValue::Tuple(tuple_values) = &result[0] { + assert_eq!(tuple_values.len(), 5); + + // Check first address (should have X prefix stripped) + if let DynSolValue::Address(addr1) = &tuple_values[0] { + assert_eq!(addr1.to_string(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + } else { + panic!("First element should be an address"); + } + + // Check second address (should have X prefix stripped) + if let DynSolValue::Address(addr2) = &tuple_values[1] { + assert_eq!(addr2.to_string(), "0xdAC17F958D2ee523a2206206994597C13D831ec7"); + } else { + panic!("Second element should be an address"); + } + + // Check uint256 + if let DynSolValue::Uint(amount, 256) = &tuple_values[2] { + assert_eq!(amount.to_string(), "1000000000000000000"); + } else { + panic!("Third element should be uint256"); + } + + // Check uint24 + if let DynSolValue::Uint(fee, 24) = &tuple_values[3] { + assert_eq!(fee.to_string(), "3000"); + } else { + panic!("Fourth element should be uint24"); + } + + // Check uint160 + if let DynSolValue::Uint(sqrt_price, 160) = &tuple_values[4] { + assert_eq!(sqrt_price.to_string(), "0"); + } else { + panic!("Fifth element should be uint160"); + } + } else { + panic!("Result should be a tuple"); + } + } + + #[test] + fn test_encode_args_with_nested_tuple() { + // Test nested tuple: (address,(address,uint256)) + // Function signature: testFunction((address,(address,uint256)))(uint256) + // Args: (XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2, + // (XdAC17F958D2ee523a2206206994597C13D831ec7,1000)) + + let sig = "testFunction((address,(address,uint256)))(uint256)"; + let func = Function::parse(sig).unwrap(); + + let args = vec!["(XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,(XdAC17F958D2ee523a2206206994597C13D831ec7,1000))"]; + + let result = encode_args(&func.inputs, args).unwrap(); + + assert_eq!(result.len(), 1); + + if let DynSolValue::Tuple(outer_tuple) = &result[0] { + assert_eq!(outer_tuple.len(), 2); + + // Check first element (address) + if let DynSolValue::Address(addr1) = &outer_tuple[0] { + assert_eq!(addr1.to_string(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + } else { + panic!("First element should be an address"); + } + + // Check second element (nested tuple) + if let DynSolValue::Tuple(inner_tuple) = &outer_tuple[1] { + assert_eq!(inner_tuple.len(), 2); + + // Check inner tuple's first element (address) + if let DynSolValue::Address(addr2) = &inner_tuple[0] { + assert_eq!(addr2.to_string(), "0xdAC17F958D2ee523a2206206994597C13D831ec7"); + } else { + panic!("Inner tuple's first element should be an address"); + } + + // Check inner tuple's second element (uint256) + if let DynSolValue::Uint(amount, 256) = &inner_tuple[1] { + assert_eq!(amount.to_string(), "1000"); + } else { + panic!("Inner tuple's second element should be uint256"); + } + } else { + panic!("Second element should be a tuple"); + } + } else { + panic!("Result should be a tuple"); + } + } + #[test] + fn test_encode_args_with_dynamic_array() { + // Test dynamic array without brackets: address[] + // Function signature: testFunction(address[])(uint256) + // Args: XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7 + // (comma-separated without brackets) + + let sig = "testFunction(address[])(uint256)"; + let func = Function::parse(sig).unwrap(); + + let args = vec![ + "XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7", + ]; + + let result = encode_args(&func.inputs, args).unwrap(); + + assert_eq!(result.len(), 1); + + if let DynSolValue::Array(array_values) = &result[0] { + assert_eq!(array_values.len(), 2); + + // Check first address (should have X prefix stripped) + if let DynSolValue::Address(addr1) = &array_values[0] { + assert_eq!(addr1.to_string(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + } else { + panic!("First element should be an address"); + } + + // Check second address (should have X prefix stripped) + if let DynSolValue::Address(addr2) = &array_values[1] { + assert_eq!(addr2.to_string(), "0xdAC17F958D2ee523a2206206994597C13D831ec7"); + } else { + panic!("Second element should be an address"); + } + } else { + panic!("Result should be an array"); + } + } + + #[test] + fn test_encode_args_with_fixed_array() { + // Test fixed-size array: address[3] + // Function signature: testFunction(address[3])(uint256) + // Args: [XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2, + // XdAC17F958D2ee523a2206206994597C13D831ec7,0x1234567890123456789012345678901234567890] + + let sig = "testFunction(address[3])(uint256)"; + let func = Function::parse(sig).unwrap(); + + let args = vec!["[XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7,0x1234567890123456789012345678901234567890]"]; + + let result = encode_args(&func.inputs, args).unwrap(); + + assert_eq!(result.len(), 1); + + if let DynSolValue::FixedArray(array_values) = &result[0] { + assert_eq!(array_values.len(), 3); + + // Check first address (should have X prefix stripped) + if let DynSolValue::Address(addr1) = &array_values[0] { + assert_eq!(addr1.to_string(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); + } else { + panic!("First element should be an address"); + } + + // Check second address (should have X prefix stripped) + if let DynSolValue::Address(addr2) = &array_values[1] { + assert_eq!(addr2.to_string(), "0xdAC17F958D2ee523a2206206994597C13D831ec7"); + } else { + panic!("Second element should be an address"); + } + + // Check third address (without X prefix, should still work) + if let DynSolValue::Address(addr3) = &array_values[2] { + assert_eq!(addr3.to_string(), "0x1234567890123456789012345678901234567890"); + } else { + panic!("Third element should be an address"); + } + } else { + panic!("Result should be a fixed array"); + } + } + + #[test] + fn test_encode_args_with_fixed_array_wrong_length() { + // Test fixed-size array with wrong length: address[3] but only 2 elements provided + // This should fail with a length mismatch error + + let sig = "testFunction(address[3])(uint256)"; + let func = Function::parse(sig).unwrap(); + + let args = vec![ + "[XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7]", + ]; + + let result = encode_args(&func.inputs, args); + + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("Fixed array length mismatch")); + } +}