From a43b41b83de5ef5eb77b519f09a565e7133b686a Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Fri, 7 Nov 2025 15:52:56 +0800 Subject: [PATCH 01/21] add xlayer sdk native transfer --- Cargo.lock | 18 ++++++ Cargo.toml | 1 + crates/sdk/Cargo.toml | 32 +++++++++++ crates/sdk/README.md | 12 ++++ crates/sdk/src/bin/xlayer_cli.rs | 97 ++++++++++++++++++++++++++++++++ crates/sdk/src/lib.rs | 4 ++ 6 files changed, 164 insertions(+) create mode 100644 crates/sdk/Cargo.toml create mode 100644 crates/sdk/README.md create mode 100644 crates/sdk/src/bin/xlayer_cli.rs create mode 100644 crates/sdk/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d59ab2cd5eb..afa023c6bc8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10292,6 +10292,24 @@ dependencies = [ "strum 0.27.2", ] +[[package]] +name = "reth-sdk" +version = "1.8.3" +dependencies = [ + "alloy-genesis", + "alloy-network", + "alloy-primitives", + "alloy-provider", + "alloy-rpc-types-eth", + "alloy-signer-local", + "alloy-transport-http", + "anyhow", + "clap", + "tokio", + "tracing", + "tracing-subscriber 0.3.20", +] + [[package]] name = "reth-stages" version = "1.8.3" diff --git a/Cargo.toml b/Cargo.toml index 7e40701c20d..68f33df064e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -174,6 +174,7 @@ members = [ "testing/testing-utils", "testing/runner", "crates/tracing-otlp", + "crates/sdk", ] default-members = ["bin/reth"] exclude = ["docs/cli"] diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml new file mode 100644 index 00000000000..64cc1c3fbc3 --- /dev/null +++ b/crates/sdk/Cargo.toml @@ -0,0 +1,32 @@ +[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 + +# 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 diff --git a/crates/sdk/README.md b/crates/sdk/README.md new file mode 100644 index 00000000000..e7029fafe0b --- /dev/null +++ b/crates/sdk/README.md @@ -0,0 +1,12 @@ +# run +``` +https://xlayertestrpc.okx.com/terigon +https://testrpc.xlayer.tech/unlimited/abcd + +cargo run -p reth-sdk --bin xlayer_cli -- transfer \ + --rpc-url https://testrpc.xlayer.tech/unlimited/abcd \ + --private-key $PRIVATE_KEY \ + --to 0x44667e638246762d7ba3dfcedbd753d336e8bc81 \ + --amount 1 +``` + diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs new file mode 100644 index 00000000000..15d3acaf62c --- /dev/null +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -0,0 +1,97 @@ +//! 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_network::EthereumWallet; +use alloy_primitives::{Address, U256}; +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_rpc_types_eth::TransactionRequest; +use alloy_signer_local::PrivateKeySigner; +use anyhow::{Context, Result}; +use clap::{Parser, Subcommand}; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +/// Transfer native assets (ETH) to an address +async fn transfer_native_asset( + rpc_url: &str, + private_key: &str, + to_address: Address, + amount: Option, +) -> Result<()> { + 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")?); + + 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 { + // from: Some(from_address), + to: Some(alloy_primitives::TxKind::Call(to_address)), + value: amount, + // nonce: Some(nonce), + 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?; + + println!("✅ Transaction sent! Hash: {:?}", receipt.transaction_hash); + + Ok(()) +} + +#[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 (ETH) to a specified address. + Transfer { + /// RPC URL + #[arg(long)] + rpc_url: String, + /// Private key (hex string, with or without 0x prefix) + #[arg(long)] + private_key: String, + /// Recipient address + #[arg(long)] + to: Address, + /// Amount to send in wei (optional, defaults to 0) + #[arg(long)] + amount: Option, + }, +} + +#[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 { rpc_url, private_key, to, amount } => { + transfer_native_asset(&rpc_url, &private_key, to, amount).await?; + } + } + + Ok(()) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs new file mode 100644 index 00000000000..a9becdc7c81 --- /dev/null +++ b/crates/sdk/src/lib.rs @@ -0,0 +1,4 @@ +//! 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. From 0d9a5c0ace91e590fb6145962be605240ebddba6 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Fri, 7 Nov 2025 17:46:05 +0800 Subject: [PATCH 02/21] add token transfer --- Cargo.lock | 1 + crates/sdk/Cargo.toml | 1 + crates/sdk/README.md | 9 ++- crates/sdk/src/bin/xlayer_cli.rs | 94 +++++++++++++++++++++++++++++--- 4 files changed, 95 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index afa023c6bc8..90c38761dcd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10302,6 +10302,7 @@ dependencies = [ "alloy-provider", "alloy-rpc-types-eth", "alloy-signer-local", + "alloy-sol-types", "alloy-transport-http", "anyhow", "clap", diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 64cc1c3fbc3..8131bf37cb6 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -19,6 +19,7 @@ alloy-network.workspace = true alloy-signer-local.workspace = true alloy-primitives.workspace = true alloy-transport-http.workspace = true +alloy-sol-types.workspace = true # async tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } diff --git a/crates/sdk/README.md b/crates/sdk/README.md index e7029fafe0b..ee250198d6b 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -8,5 +8,12 @@ cargo run -p reth-sdk --bin xlayer_cli -- transfer \ --private-key $PRIVATE_KEY \ --to 0x44667e638246762d7ba3dfcedbd753d336e8bc81 \ --amount 1 -``` + +cargo run -p reth-sdk --bin xlayer_cli -- token-transfer \ + --rpc-url https://testrpc.xlayer.tech/unlimited/abcd \ + --private-key $PRIVATE_KEY \ + --to 0x44667e638246762d7ba3dfcedbd753d336e8bc81 \ + --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d \ + --amount 1 +``` diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index 15d3acaf62c..fde1a1edad3 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -3,21 +3,27 @@ //! This binary provides commands for sending transactions and interacting //! with the Reth chain via RPC. use alloy_network::EthereumWallet; -use alloy_primitives::{Address, U256}; +use alloy_primitives::{Address, Bytes, U256}; use alloy_provider::{Provider, ProviderBuilder}; use alloy_rpc_types_eth::TransactionRequest; use alloy_signer_local::PrivateKeySigner; +use alloy_sol_types::{SolCall, sol}; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; -/// Transfer native assets (ETH) to an address -async fn transfer_native_asset( +// Define ERC20 interface using sol! macro +sol! { + interface IERC20 { + function transfer(address to, uint256 amount) external returns (bool); + } +} + +/// Create a provider with wallet from RPC URL and private key +async fn create_provider_with_wallet( rpc_url: &str, private_key: &str, - to_address: Address, - amount: Option, -) -> Result<()> { +) -> 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); @@ -26,15 +32,24 @@ async fn transfer_native_asset( .wallet(wallet) .connect_http(rpc_url.parse().context("Invalid RPC URL")?); - let chain_id = provider.get_chain_id().await.context("Failed to get chain ID")?; + Ok(provider) +} + +/// Transfer native assets (ETH) to an address +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 { - // from: Some(from_address), to: Some(alloy_primitives::TxKind::Call(to_address)), value: amount, - // nonce: Some(nonce), gas: Some(21000u64), gas_price: Some(gas_price), chain_id: Some(chain_id), @@ -51,6 +66,46 @@ async fn transfer_native_asset( Ok(()) } +/// Transfer ERC20 tokens using transfer(address,uint256) function +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?; + + println!("✅ Token transfer transaction sent! Hash: {:?}", receipt.transaction_hash); + + Ok(()) +} + #[derive(Parser)] #[command(version, about, long_about = None)] struct XlayerCli { @@ -76,6 +131,24 @@ pub enum XlayerCommands { #[arg(long)] amount: Option, }, + /// Transfer ERC20 tokens using transfer(address,uint256) function. + TokenTransfer { + /// RPC URL + #[arg(long)] + rpc_url: String, + /// Private key (hex string, with or without 0x prefix) + #[arg(long)] + private_key: String, + /// Token contract address + #[arg(long)] + token: Address, + /// Recipient address + #[arg(long)] + to: Address, + /// Amount to transfer (in token's smallest unit, e.g., wei for 18 decimals) + #[arg(long)] + amount: U256, + }, } #[tokio::main] @@ -91,6 +164,9 @@ async fn main() -> Result<()> { XlayerCommands::Transfer { rpc_url, private_key, to, amount } => { transfer_native_asset(&rpc_url, &private_key, to, amount).await?; } + XlayerCommands::TokenTransfer { rpc_url, private_key, token, to, amount } => { + transfer_token(&rpc_url, &private_key, token, to, amount).await?; + } } Ok(()) From 3d6dfacf66e80746b626eee50960a2f8ffd5173f Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Fri, 7 Nov 2025 17:55:10 +0800 Subject: [PATCH 03/21] refactor with common args --- crates/sdk/src/bin/xlayer_cli.rs | 38 ++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index fde1a1edad3..de3e56a9521 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -19,6 +19,18 @@ sol! { } } +/// Common fields shared across transfer commands +#[derive(Parser, Debug)] +struct CommonTransferArgs { + /// RPC URL + #[arg(long)] + rpc_url: String, + /// Private key (hex string, with or without 0x prefix) + #[arg(long)] + private_key: String, +} + + /// Create a provider with wallet from RPC URL and private key async fn create_provider_with_wallet( rpc_url: &str, @@ -118,12 +130,8 @@ struct XlayerCli { pub enum XlayerCommands { /// Transfer native assets (ETH) to a specified address. Transfer { - /// RPC URL - #[arg(long)] - rpc_url: String, - /// Private key (hex string, with or without 0x prefix) - #[arg(long)] - private_key: String, + #[command(flatten)] + common: CommonTransferArgs, /// Recipient address #[arg(long)] to: Address, @@ -133,12 +141,8 @@ pub enum XlayerCommands { }, /// Transfer ERC20 tokens using transfer(address,uint256) function. TokenTransfer { - /// RPC URL - #[arg(long)] - rpc_url: String, - /// Private key (hex string, with or without 0x prefix) - #[arg(long)] - private_key: String, + #[command(flatten)] + common: CommonTransferArgs, /// Token contract address #[arg(long)] token: Address, @@ -160,14 +164,16 @@ async fn main() -> Result<()> { let cli = XlayerCli::parse(); + match cli.command { - XlayerCommands::Transfer { rpc_url, private_key, to, amount } => { - transfer_native_asset(&rpc_url, &private_key, to, amount).await?; + XlayerCommands::Transfer { common, to, amount } => { + transfer_native_asset(&common.rpc_url, &common.private_key, to, amount).await?; } - XlayerCommands::TokenTransfer { rpc_url, private_key, token, to, amount } => { - transfer_token(&rpc_url, &private_key, token, to, amount).await?; + XlayerCommands::TokenTransfer { common, token, to, amount } => { + transfer_token(&common.rpc_url, &common.private_key, token, to, amount).await?; } } + Ok(()) } From 5a5d440de55510c4ddc073dab3bf27dbe6a054ad Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Sat, 8 Nov 2025 16:59:13 +0800 Subject: [PATCH 04/21] add X prefix --- crates/sdk/README.md | 4 +- crates/sdk/src/bin/xlayer_cli.rs | 64 ++++++++++++++++++++++++++------ 2 files changed, 54 insertions(+), 14 deletions(-) diff --git a/crates/sdk/README.md b/crates/sdk/README.md index ee250198d6b..6de0f9498b4 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -6,14 +6,14 @@ https://testrpc.xlayer.tech/unlimited/abcd cargo run -p reth-sdk --bin xlayer_cli -- transfer \ --rpc-url https://testrpc.xlayer.tech/unlimited/abcd \ --private-key $PRIVATE_KEY \ - --to 0x44667e638246762d7ba3dfcedbd753d336e8bc81 \ + --to X44667e638246762d7ba3dfcedbd753d336e8bc81 \ --amount 1 cargo run -p reth-sdk --bin xlayer_cli -- token-transfer \ --rpc-url https://testrpc.xlayer.tech/unlimited/abcd \ --private-key $PRIVATE_KEY \ - --to 0x44667e638246762d7ba3dfcedbd753d336e8bc81 \ + --to X44667e638246762d7ba3dfcedbd753d336e8bc81 \ --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d \ --amount 1 ``` diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index de3e56a9521..89dae593889 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -10,6 +10,7 @@ use alloy_signer_local::PrivateKeySigner; use alloy_sol_types::{SolCall, sol}; use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; +use std::str::FromStr; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; // Define ERC20 interface using sol! macro @@ -19,9 +20,46 @@ sol! { } } +/// 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').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(XAddress { 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) + } +} + /// Common fields shared across transfer commands #[derive(Parser, Debug)] -struct CommonTransferArgs { +pub struct CommonTransferArgs { /// RPC URL #[arg(long)] rpc_url: String, @@ -30,7 +68,6 @@ struct CommonTransferArgs { private_key: String, } - /// Create a provider with wallet from RPC URL and private key async fn create_provider_with_wallet( rpc_url: &str, @@ -130,25 +167,27 @@ struct XlayerCli { pub enum XlayerCommands { /// Transfer native assets (ETH) to a specified address. Transfer { + /// Common transfer arguments (RPC URL and private key) #[command(flatten)] common: CommonTransferArgs, - /// Recipient address + /// Recipient address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") #[arg(long)] - to: Address, + 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 + /// Token contract address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") #[arg(long)] - token: Address, - /// Recipient address + token: XAddress, + /// Recipient address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") #[arg(long)] - to: Address, + to: XAddress, /// Amount to transfer (in token's smallest unit, e.g., wei for 18 decimals) #[arg(long)] amount: U256, @@ -164,16 +203,17 @@ async fn main() -> Result<()> { let cli = XlayerCli::parse(); - match cli.command { XlayerCommands::Transfer { common, to, amount } => { - transfer_native_asset(&common.rpc_url, &common.private_key, to, amount).await?; + let to_address: Address = to.into(); + transfer_native_asset(&common.rpc_url, &common.private_key, to_address, amount).await?; } XlayerCommands::TokenTransfer { common, token, to, amount } => { - transfer_token(&common.rpc_url, &common.private_key, token, to, amount).await?; + let token_address: Address = token.into(); + let to_address: Address = to.into(); + transfer_token(&common.rpc_url, &common.private_key, token_address, to_address, amount).await?; } } - Ok(()) } From eb509b778262866ca518c297a4f86f95d79917a8 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Sat, 8 Nov 2025 17:06:33 +0800 Subject: [PATCH 05/21] add get_balance --- crates/sdk/README.md | 2 ++ crates/sdk/src/bin/xlayer_cli.rs | 36 ++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/crates/sdk/README.md b/crates/sdk/README.md index 6de0f9498b4..0fb17e46df3 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -16,4 +16,6 @@ cargo run -p reth-sdk --bin xlayer_cli -- token-transfer \ --to X44667e638246762d7ba3dfcedbd753d336e8bc81 \ --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d \ --amount 1 + +cargo run -p reth-sdk --bin xlayer_cli -- balance --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --address X33f34D8b20696780Ba07b1ea89F209B4Dc51723A ``` diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index 89dae593889..5b7fbaf660a 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -84,6 +84,13 @@ async fn create_provider_with_wallet( Ok(provider) } +/// Create a provider without wallet (for read-only operations) +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 (ETH) to an address async fn transfer_native_asset( rpc_url: &str, @@ -155,6 +162,22 @@ async fn transfer_token( Ok(()) } +/// Get the balance of an address +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")?; + + println!("Balance: {} wei", balance); + + Ok(()) +} + #[derive(Parser)] #[command(version, about, long_about = None)] struct XlayerCli { @@ -192,6 +215,15 @@ pub enum XlayerCommands { #[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, + }, } #[tokio::main] @@ -213,6 +245,10 @@ async fn main() -> Result<()> { let to_address: Address = to.into(); transfer_token(&common.rpc_url, &common.private_key, token_address, to_address, amount).await?; } + XlayerCommands::Balance { rpc_url, address } => { + let address: Address = address.into(); + get_balance(&rpc_url, address).await?; + } } Ok(()) From 5b8b91cb218e6bdb617934533abd8187ef8ee449 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Sat, 8 Nov 2025 17:25:08 +0800 Subject: [PATCH 06/21] add get token balance --- crates/sdk/README.md | 6 ++++ crates/sdk/src/bin/xlayer_cli.rs | 51 ++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/crates/sdk/README.md b/crates/sdk/README.md index 0fb17e46df3..e8ac914695b 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -18,4 +18,10 @@ cargo run -p reth-sdk --bin xlayer_cli -- token-transfer \ --amount 1 cargo run -p reth-sdk --bin xlayer_cli -- balance --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --address X33f34D8b20696780Ba07b1ea89F209B4Dc51723A +cargo run -p reth-sdk --bin xlayer_cli -- token-balance --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d --account X33f34D8b20696780Ba07b1ea89F209B4Dc51723A +``` + +# 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 index 5b7fbaf660a..76cc58bcfc0 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -17,6 +17,7 @@ use tracing_subscriber::{fmt, prelude::*, EnvFilter}; sol! { interface IERC20 { function transfer(address to, uint256 amount) external returns (bool); + function balanceOf(address account) external view returns (uint256); } } @@ -178,6 +179,39 @@ async fn get_balance( Ok(()) } +/// Get the ERC20 token balance of an address +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")?; + + println!("Token Balance: {} (raw units)", balance); + + Ok(()) +} + #[derive(Parser)] #[command(version, about, long_about = None)] struct XlayerCli { @@ -224,6 +258,18 @@ pub enum XlayerCommands { #[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, + }, } #[tokio::main] @@ -249,6 +295,11 @@ async fn main() -> Result<()> { let address: Address = address.into(); get_balance(&rpc_url, address).await?; } + XlayerCommands::TokenBalance { rpc_url, token, account } => { + let token_address: Address = token.into(); + let account_address: Address = account.into(); + get_token_balance(&rpc_url, token_address, account_address).await?; + } } Ok(()) From 3befe353b99ad3e223eb80fb5339604aa0a33c76 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Sat, 8 Nov 2025 17:28:29 +0800 Subject: [PATCH 07/21] refactor --- crates/sdk/src/bin/xlayer_cli.rs | 199 +------------------------------ crates/sdk/src/lib.rs | 197 ++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 196 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index 76cc58bcfc0..89d2866eb26 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -2,62 +2,12 @@ //! //! This binary provides commands for sending transactions and interacting //! with the Reth chain via RPC. -use alloy_network::EthereumWallet; -use alloy_primitives::{Address, Bytes, U256}; -use alloy_provider::{Provider, ProviderBuilder}; -use alloy_rpc_types_eth::TransactionRequest; -use alloy_signer_local::PrivateKeySigner; -use alloy_sol_types::{SolCall, sol}; -use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use std::str::FromStr; +use reth_sdk::{get_balance, get_token_balance, transfer_native_asset, transfer_token, XAddress}; +use alloy_primitives::{Address, U256}; +use anyhow::Result; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; -// 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').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(XAddress { 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) - } -} - /// Common fields shared across transfer commands #[derive(Parser, Debug)] pub struct CommonTransferArgs { @@ -69,149 +19,6 @@ pub struct CommonTransferArgs { private_key: String, } -/// Create a provider with wallet from RPC URL and private key -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) -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 (ETH) to an address -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?; - - println!("✅ Transaction sent! Hash: {:?}", receipt.transaction_hash); - - Ok(()) -} - -/// Transfer ERC20 tokens using transfer(address,uint256) function -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?; - - println!("✅ Token transfer transaction sent! Hash: {:?}", receipt.transaction_hash); - - Ok(()) -} - -/// Get the balance of an address -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")?; - - println!("Balance: {} wei", balance); - - Ok(()) -} - -/// Get the ERC20 token balance of an address -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")?; - - println!("Token Balance: {} (raw units)", balance); - - Ok(()) -} - #[derive(Parser)] #[command(version, about, long_about = None)] struct XlayerCli { diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index a9becdc7c81..94d4e2c06e0 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -2,3 +2,200 @@ //! //! This crate provides utilities and functions for sending transactions //! and interacting with the Reth chain via RPC. + +use alloy_network::EthereumWallet; +use alloy_primitives::{Address, Bytes, U256}; +use alloy_provider::{Provider, ProviderBuilder}; +use alloy_rpc_types_eth::TransactionRequest; +use alloy_signer_local::PrivateKeySigner; +use alloy_sol_types::{SolCall, sol}; +use anyhow::{Context, Result}; +use std::str::FromStr; + +// 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').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(XAddress { 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 (ETH) 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?; + + println!("✅ Transaction sent! Hash: {:?}", receipt.transaction_hash); + + Ok(()) +} + +/// 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?; + + println!("✅ Token transfer transaction sent! Hash: {:?}", receipt.transaction_hash); + + Ok(()) +} + +/// 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")?; + + println!("Balance: {} wei", balance); + + Ok(()) +} + +/// 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")?; + + println!("Token Balance: {} (raw units)", balance); + + Ok(()) +} From e3d516f77a1a9afbc9a9cade1dcb7ed3e92495a5 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Sat, 8 Nov 2025 17:29:52 +0800 Subject: [PATCH 08/21] refactoring comments --- crates/sdk/src/bin/xlayer_cli.rs | 2 +- crates/sdk/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index 89d2866eb26..c788851301e 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -29,7 +29,7 @@ struct XlayerCli { /// CLI commands for X Layer operations. #[derive(Subcommand, Debug)] pub enum XlayerCommands { - /// Transfer native assets (ETH) to a specified address. + /// Transfer native assets (OKB) to a specified address. Transfer { /// Common transfer arguments (RPC URL and private key) #[command(flatten)] diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 94d4e2c06e0..e6d96394266 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -80,7 +80,7 @@ pub async fn create_provider(rpc_url: &str) -> Result Date: Mon, 10 Nov 2025 12:40:33 +0800 Subject: [PATCH 09/21] add eth_call --- Cargo.lock | 3 + Cargo.toml | 1 + crates/sdk/Cargo.toml | 3 + crates/sdk/README.md | 16 ++ crates/sdk/src/bin/xlayer_cli.rs | 244 ++++++++++++++++++++++++++++++- crates/sdk/src/lib.rs | 4 +- 6 files changed, 265 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 90c38761dcd..5289e2dcd18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10296,7 +10296,9 @@ dependencies = [ name = "reth-sdk" version = "1.8.3" dependencies = [ + "alloy-dyn-abi", "alloy-genesis", + "alloy-json-abi", "alloy-network", "alloy-primitives", "alloy-provider", @@ -10306,6 +10308,7 @@ dependencies = [ "alloy-transport-http", "anyhow", "clap", + "eyre", "tokio", "tracing", "tracing-subscriber 0.3.20", diff --git a/Cargo.toml b/Cargo.toml index 68f33df064e..e8da41b65fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -493,6 +493,7 @@ alloy-contract = { version = "1.0.37", default-features = false } alloy-eips = { version = "1.0.37", default-features = false } alloy-genesis = { version = "1.0.37", default-features = false } alloy-json-rpc = { version = "1.0.37", default-features = false } +alloy-json-abi = { version = "1.3.1", default-features = false } alloy-network = { version = "1.0.37", default-features = false } alloy-network-primitives = { version = "1.0.37", default-features = false } alloy-provider = { version = "1.0.37", features = ["reqwest"], default-features = false } diff --git a/crates/sdk/Cargo.toml b/crates/sdk/Cargo.toml index 8131bf37cb6..6cdcf99139d 100644 --- a/crates/sdk/Cargo.toml +++ b/crates/sdk/Cargo.toml @@ -20,6 +20,8 @@ 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"] } @@ -31,3 +33,4 @@ 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 index e8ac914695b..77d1505a80e 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -19,9 +19,25 @@ cargo run -p reth-sdk --bin xlayer_cli -- token-transfer \ cargo run -p reth-sdk --bin xlayer_cli -- balance --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --address X33f34D8b20696780Ba07b1ea89F209B4Dc51723A cargo run -p reth-sdk --bin xlayer_cli -- token-balance --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d --account X33f34D8b20696780Ba07b1ea89F209B4Dc51723A + +cargo run -p reth-sdk --bin xlayer_cli -- eth-call --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --to 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d --sig "balanceOf(address)(uint256)" --args X33f34D8b20696780Ba07b1ea89F209B4Dc51723A + + +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 ``` + +# appendix +``` +struct QuoteExactInputSingleParams { + address tokenIn; + address tokenOut; + uint24 fee; + uint256 amountIn; + uint160 sqrtPriceLimitX96; +} +``` diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index c788851301e..fffae5ba33b 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -2,10 +2,16 @@ //! //! This binary provides commands for sending transactions and interacting //! with the Reth chain via RPC. +use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, JsonAbiExt}; +use alloy_json_abi::{Function, Param}; +use alloy_primitives::{hex, Address, Bytes, U256}; +use alloy_provider::Provider; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; -use reth_sdk::{get_balance, get_token_balance, transfer_native_asset, transfer_token, XAddress}; -use alloy_primitives::{Address, U256}; -use anyhow::Result; +use reth_sdk::{ + create_provider, get_balance, get_token_balance, transfer_native_asset, transfer_token, + XAddress, +}; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; /// Common fields shared across transfer commands @@ -77,6 +83,22 @@ pub enum XlayerCommands { #[arg(long)] account: XAddress, }, + 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] @@ -96,7 +118,8 @@ async fn main() -> Result<()> { XlayerCommands::TokenTransfer { common, token, to, amount } => { let token_address: Address = token.into(); let to_address: Address = to.into(); - transfer_token(&common.rpc_url, &common.private_key, token_address, to_address, amount).await?; + transfer_token(&common.rpc_url, &common.private_key, token_address, to_address, amount) + .await?; } XlayerCommands::Balance { rpc_url, address } => { let address: Address = address.into(); @@ -107,7 +130,220 @@ async fn main() -> Result<()> { let account_address: Address = account.into(); get_token_balance(&rpc_url, token_address, account_address).await?; } + 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")?; + println!("result: {:?}", result); + let decoded = match func.abi_decode_output(result.as_ref()) { + Ok(decoded) => decoded, // Vec + Err(err) => { + panic!("error in decoding") + } + }; + println!("Result: decoded {:?}", decoded); + } + } } Ok(()) } + +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() { + panic!("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 { + println!("type {:?}, arg: {:?}", ty, arg); + + // Parse the type first to see if it's a tuple + let parsed_ty = DynSolType::parse(ty)?; + + // Recursively process based on the type structure + 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) => { + // Parse tuple arguments (handle nested tuples) + 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()); + } + + // Recursively process each element + 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) => { + // Parse array arguments (comma-separated, optionally wrapped in brackets) + let args = parse_array_args(arg)?; + + // Recursively process each element + 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) => { + // Parse fixed array arguments (comma-separated, optionally wrapped in brackets) + let args = parse_array_args(arg)?; + + if args.len() != *size { + anyhow::bail!("Fixed array length mismatch: expected {} elements, got {}", size, args.len()); + } + + // Recursively process each element + 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').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(); + + // Remove optional brackets + let inner = if s.starts_with('[') && s.ends_with(']') { + &s[1..s.len()-1] + } else { + s + }; + + if inner.is_empty() { + return Ok(Vec::new()); + } + + // Split by comma, handling nested structures + 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) +} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index e6d96394266..a3c8e9b2421 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -4,7 +4,7 @@ //! and interacting with the Reth chain via RPC. use alloy_network::EthereumWallet; -use alloy_primitives::{Address, Bytes, U256}; +use alloy_primitives::{Address, Bytes, U256, hex}; use alloy_provider::{Provider, ProviderBuilder}; use alloy_rpc_types_eth::TransactionRequest; use alloy_signer_local::PrivateKeySigner; @@ -177,7 +177,7 @@ pub async fn get_token_balance( let call = IERC20::balanceOfCall { account: account_address }; let calldata = call.abi_encode(); - + println!("call data: 0x{}", hex::encode(&calldata)); let result = provider .call( alloy_rpc_types_eth::TransactionRequest { From f1c7cb9ab6f9d76f221abc258da07030baae4672 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 14:12:39 +0800 Subject: [PATCH 10/21] fix build warnJ --- crates/sdk/src/bin/xlayer_cli.rs | 202 ++----------------------------- crates/sdk/src/lib.rs | 183 ++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+), 190 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index fffae5ba33b..bebb55b6d1d 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -2,15 +2,15 @@ //! //! This binary provides commands for sending transactions and interacting //! with the Reth chain via RPC. -use alloy_dyn_abi::{DynSolType, DynSolValue, FunctionExt, JsonAbiExt}; -use alloy_json_abi::{Function, Param}; -use alloy_primitives::{hex, Address, Bytes, U256}; +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, get_balance, get_token_balance, transfer_native_asset, transfer_token, - XAddress, + create_provider, encode_args, get_balance, get_token_balance, transfer_native_asset, + transfer_token, XAddress, }; use tracing_subscriber::{fmt, prelude::*, EnvFilter}; @@ -83,6 +83,7 @@ pub enum XlayerCommands { #[arg(long)] account: XAddress, }, + /// Call a contract function using eth_call RPC method. EthCall { /// RPC URL #[arg(long)] @@ -153,197 +154,18 @@ async fn main() -> Result<()> { }) .await .context("Failed to execute eth_call")?; - println!("result: {:?}", result); - let decoded = match func.abi_decode_output(result.as_ref()) { - Ok(decoded) => decoded, // Vec + + match func.abi_decode_output(result.as_ref()) { + Ok(decoded) => { + println!("Result: decoded {:?}", decoded); + } Err(err) => { - panic!("error in decoding") + eprintln!("error in decode: {:?}", err); } }; - println!("Result: decoded {:?}", decoded); } } } Ok(()) } - -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() { - panic!("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 { - println!("type {:?}, arg: {:?}", ty, arg); - - // Parse the type first to see if it's a tuple - let parsed_ty = DynSolType::parse(ty)?; - - // Recursively process based on the type structure - 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) => { - // Parse tuple arguments (handle nested tuples) - 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()); - } - - // Recursively process each element - 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) => { - // Parse array arguments (comma-separated, optionally wrapped in brackets) - let args = parse_array_args(arg)?; - - // Recursively process each element - 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) => { - // Parse fixed array arguments (comma-separated, optionally wrapped in brackets) - let args = parse_array_args(arg)?; - - if args.len() != *size { - anyhow::bail!("Fixed array length mismatch: expected {} elements, got {}", size, args.len()); - } - - // Recursively process each element - 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').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(); - - // Remove optional brackets - let inner = if s.starts_with('[') && s.ends_with(']') { - &s[1..s.len()-1] - } else { - s - }; - - if inner.is_empty() { - return Ok(Vec::new()); - } - - // Split by comma, handling nested structures - 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) -} diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index a3c8e9b2421..9ac03f4938a 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -9,6 +9,8 @@ use alloy_provider::{Provider, ProviderBuilder}; use alloy_rpc_types_eth::TransactionRequest; use alloy_signer_local::PrivateKeySigner; use alloy_sol_types::{SolCall, sol}; +use alloy_json_abi::Param; +use alloy_dyn_abi::{DynSolType, DynSolValue}; use anyhow::{Context, Result}; use std::str::FromStr; @@ -199,3 +201,184 @@ pub async fn get_token_balance( Ok(()) } + +/// 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 { + println!("type {:?}, arg: {:?}", ty, arg); + + // Parse the type first to see if it's a tuple + let parsed_ty = DynSolType::parse(ty)?; + + // Recursively process based on the type structure + 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) => { + // Parse tuple arguments (handle nested tuples) + 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()); + } + + // Recursively process each element + 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) => { + // Parse array arguments (comma-separated, optionally wrapped in brackets) + let args = parse_array_args(arg)?; + + // Recursively process each element + 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) => { + // Parse fixed array arguments (comma-separated, optionally wrapped in brackets) + let args = parse_array_args(arg)?; + + if args.len() != *size { + anyhow::bail!("Fixed array length mismatch: expected {} elements, got {}", size, args.len()); + } + + // Recursively process each element + 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').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(); + + // Remove optional brackets + let inner = if s.starts_with('[') && s.ends_with(']') { + &s[1..s.len()-1] + } else { + s + }; + + if inner.is_empty() { + return Ok(Vec::new()); + } + + // Split by comma, handling nested structures + 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) +} From e6a77e8ac73f7897f5522f2b9f2995c077b97af3 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 14:14:18 +0800 Subject: [PATCH 11/21] fix fmt --- crates/sdk/src/bin/xlayer_cli.rs | 6 +- crates/sdk/src/lib.rs | 100 +++++++++++++++---------------- 2 files changed, 52 insertions(+), 54 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index bebb55b6d1d..fbc3192cd27 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -67,7 +67,8 @@ pub enum XlayerCommands { /// RPC URL #[arg(long)] rpc_url: String, - /// Address to query balance for (supports optional "X" prefix, e.g., "X1234..." or "0x1234...") + /// Address to query balance for (supports optional "X" prefix, e.g., "X1234..." or + /// "0x1234...") #[arg(long)] address: XAddress, }, @@ -79,7 +80,8 @@ pub enum XlayerCommands { /// 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...") + /// Account address to query balance for (supports optional "X" prefix, e.g., "X1234..." or + /// "0x1234...") #[arg(long)] account: XAddress, }, diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 9ac03f4938a..e43741761f4 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -3,14 +3,14 @@ //! 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, U256, hex}; +use alloy_primitives::{hex, Address, Bytes, U256}; use alloy_provider::{Provider, ProviderBuilder}; use alloy_rpc_types_eth::TransactionRequest; use alloy_signer_local::PrivateKeySigner; -use alloy_sol_types::{SolCall, sol}; -use alloy_json_abi::Param; -use alloy_dyn_abi::{DynSolType, DynSolValue}; +use alloy_sol_types::{sol, SolCall}; use anyhow::{Context, Result}; use std::str::FromStr; @@ -41,8 +41,7 @@ impl FromStr for XAddress { format!("0x{}", addr_str) }; - let address = addr_with_prefix.parse::
() - .context("Failed to parse address")?; + let address = addr_with_prefix.parse::
().context("Failed to parse address")?; Ok(XAddress { address }) } } @@ -76,9 +75,10 @@ pub async fn create_provider_with_wallet( } /// 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")?); +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) } @@ -154,15 +154,10 @@ pub async fn transfer_token( } /// Get the balance of an address -pub async fn get_balance( - rpc_url: &str, - address: Address, -) -> Result<()> { +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")?; + let balance = provider.get_balance(address).await.context("Failed to get balance")?; println!("Balance: {} wei", balance); @@ -179,18 +174,16 @@ pub async fn get_token_balance( let call = IERC20::balanceOfCall { account: account_address }; let calldata = call.abi_encode(); - println!("call data: 0x{}", hex::encode(&calldata)); + println!("call data: 0x{}", hex::encode(&calldata)); 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() - } - ) + .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")?; @@ -211,7 +204,11 @@ where let args: Vec = args.into_iter().collect(); if inputs.len() != args.len() { - anyhow::bail!("encode length mismatch: expected {} types, got {}", inputs.len(), args.len()); + anyhow::bail!( + "encode length mismatch: expected {} types, got {}", + inputs.len(), + args.len() + ); } std::iter::zip(inputs, args) @@ -238,7 +235,11 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { 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()); + anyhow::bail!( + "Tuple length mismatch: expected {} elements, got {}", + tuple_types.len(), + args.len() + ); } // Recursively process each element @@ -255,10 +256,8 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { let args = parse_array_args(arg)?; // Recursively process each element - let values: Result> = args - .iter() - .map(|arg_str| coerce_value_recursive(inner_ty, arg_str)) - .collect(); + let values: Result> = + args.iter().map(|arg_str| coerce_value_recursive(inner_ty, arg_str)).collect(); Ok(DynSolValue::Array(values?)) } @@ -267,14 +266,16 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { let args = parse_array_args(arg)?; if args.len() != *size { - anyhow::bail!("Fixed array length mismatch: expected {} elements, got {}", size, args.len()); + anyhow::bail!( + "Fixed array length mismatch: expected {} elements, got {}", + size, + args.len() + ); } // Recursively process each element - let values: Result> = args - .iter() - .map(|arg_str| coerce_value_recursive(inner_ty, arg_str)) - .collect(); + let values: Result> = + args.iter().map(|arg_str| coerce_value_recursive(inner_ty, arg_str)).collect(); Ok(DynSolValue::FixedArray(values?)) } @@ -291,8 +292,7 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { } _ => { // For non-tuple, non-address types, use normal coercion - DynSolType::coerce_str(ty, arg) - .context("Failed to coerce value") + DynSolType::coerce_str(ty, arg).context("Failed to coerce value") } } } @@ -304,7 +304,7 @@ fn parse_tuple_args(s: &str) -> Result> { anyhow::bail!("Tuple argument must start with '(' and end with ')'"); } - let inner = &s[1..s.len()-1]; // Remove outer parentheses + let inner = &s[1..s.len() - 1]; // Remove outer parentheses let mut args = Vec::new(); let mut current = String::new(); let mut depth = 0; @@ -339,23 +339,19 @@ fn parse_tuple_args(s: &str) -> Result> { /// Parse array arguments from a string (comma-separated, optionally wrapped in brackets) fn parse_array_args(s: &str) -> Result> { let s = s.trim(); - + // Remove optional brackets - let inner = if s.starts_with('[') && s.ends_with(']') { - &s[1..s.len()-1] - } else { - s - }; - + let inner = if s.starts_with('[') && s.ends_with(']') { &s[1..s.len() - 1] } else { s }; + if inner.is_empty() { return Ok(Vec::new()); } - + // Split by comma, handling nested structures let mut args = Vec::new(); let mut current = String::new(); let mut depth = 0; - + for ch in inner.chars() { match ch { '(' | '[' => { @@ -375,10 +371,10 @@ fn parse_array_args(s: &str) -> Result> { } } } - + if !current.is_empty() { args.push(current.trim().to_string()); } - + Ok(args) } From 7002302fda1623f9d5dd1136f08863de447170e2 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 14:40:56 +0800 Subject: [PATCH 12/21] add unit test for nested tuple --- Cargo.toml | 2 +- crates/sdk/README.md | 11 --- crates/sdk/src/bin/xlayer_cli.rs | 2 + crates/sdk/src/lib.rs | 152 +++++++++++++++++++++++++++---- 4 files changed, 136 insertions(+), 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e8da41b65fa..a52bbbea973 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -286,7 +286,7 @@ too_long_first_doc_paragraph = "allow" # Uncomment this section if you're using a debugger. [profile.dev] # https://davidlattimore.github.io/posts/2024/02/04/speeding-up-the-rust-edit-build-run-cycle.html -debug = "line-tables-only" +debug = "full" split-debuginfo = "unpacked" # Speed up tests. diff --git a/crates/sdk/README.md b/crates/sdk/README.md index 77d1505a80e..19dfc2357e3 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -30,14 +30,3 @@ cargo run -p reth-sdk --bin xlayer_cli -- eth-call --rpc-url https://maximum-yol ``` cargo expand -p reth-sdk --bin xlayer_cli > expanded.rs ``` - -# appendix -``` -struct QuoteExactInputSingleParams { - address tokenIn; - address tokenOut; - uint24 fee; - uint256 amountIn; - uint160 sqrtPriceLimitX96; -} -``` diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index fbc3192cd27..c60ce698454 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -165,6 +165,8 @@ async fn main() -> Result<()> { eprintln!("error in decode: {:?}", err); } }; + } else { + panic!("signature needs contains ()"); } } } diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index e43741761f4..fcf769957f5 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -218,12 +218,7 @@ where /// Helper function to coerce a value to a [DynSolValue] given a type string pub fn coerce_value(ty: &str, arg: &str) -> Result { - println!("type {:?}, arg: {:?}", ty, arg); - - // Parse the type first to see if it's a tuple let parsed_ty = DynSolType::parse(ty)?; - - // Recursively process based on the type structure coerce_value_recursive(&parsed_ty, arg) } @@ -231,7 +226,6 @@ pub fn coerce_value(ty: &str, arg: &str) -> Result { fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { match ty { DynSolType::Tuple(tuple_types) => { - // Parse tuple arguments (handle nested tuples) let args = parse_tuple_args(arg)?; if tuple_types.len() != args.len() { @@ -241,8 +235,6 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { args.len() ); } - - // Recursively process each element let values: Result> = tuple_types .iter() .zip(args.iter()) @@ -252,19 +244,14 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { Ok(DynSolValue::Tuple(values?)) } DynSolType::Array(inner_ty) => { - // Parse array arguments (comma-separated, optionally wrapped in brackets) let args = parse_array_args(arg)?; - - // Recursively process each element 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) => { - // Parse fixed array arguments (comma-separated, optionally wrapped in brackets) let args = parse_array_args(arg)?; - if args.len() != *size { anyhow::bail!( "Fixed array length mismatch: expected {} elements, got {}", @@ -272,8 +259,6 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { args.len() ); } - - // Recursively process each element let values: Result> = args.iter().map(|arg_str| coerce_value_recursive(inner_ty, arg_str)).collect(); @@ -339,15 +324,12 @@ fn parse_tuple_args(s: &str) -> Result> { /// Parse array arguments from a string (comma-separated, optionally wrapped in brackets) fn parse_array_args(s: &str) -> Result> { let s = s.trim(); - - // Remove optional brackets let inner = if s.starts_with('[') && s.ends_with(']') { &s[1..s.len() - 1] } else { s }; if inner.is_empty() { return Ok(Vec::new()); } - - // Split by comma, handling nested structures + let mut args = Vec::new(); let mut current = String::new(); let mut depth = 0; @@ -378,3 +360,135 @@ fn parse_array_args(s: &str) -> Result> { 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"); + } + } +} From e41a38b1c15368cfc9e20299a8d6d8244cb3540c Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 14:47:34 +0800 Subject: [PATCH 13/21] add test for fixed array --- crates/sdk/src/lib.rs | 110 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index fcf769957f5..660c2879f17 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -491,4 +491,114 @@ mod tests { 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")); + } } From 878b097cc458ff672d471296a8525e313d13f7d9 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 14:53:58 +0800 Subject: [PATCH 14/21] fix clippy --- crates/sdk/src/bin/xlayer_cli.rs | 8 +-- crates/sdk/src/lib.rs | 98 +++++++++++++++----------------- 2 files changed, 50 insertions(+), 56 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index c60ce698454..d6986ed1a69 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -62,7 +62,7 @@ pub enum XlayerCommands { #[arg(long)] amount: U256, }, - /// Get the balance of an address (equivalent to eth_getBalance). + /// Get the balance of an address (equivalent to `eth_getBalance`). Balance { /// RPC URL #[arg(long)] @@ -85,7 +85,7 @@ pub enum XlayerCommands { #[arg(long)] account: XAddress, }, - /// Call a contract function using eth_call RPC method. + /// Call a contract function using `eth_call` RPC method. EthCall { /// RPC URL #[arg(long)] @@ -159,10 +159,10 @@ async fn main() -> Result<()> { match func.abi_decode_output(result.as_ref()) { Ok(decoded) => { - println!("Result: decoded {:?}", decoded); + println!("Result: decoded {decoded:?}"); } Err(err) => { - eprintln!("error in decode: {:?}", err); + eprintln!("error in decode: {err:?}"); } }; } else { diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 660c2879f17..a92e5e73b91 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -38,11 +38,11 @@ impl FromStr for XAddress { let addr_with_prefix = if addr_str.starts_with("0x") { addr_str.to_string() } else { - format!("0x{}", addr_str) + format!("0x{addr_str}") }; let address = addr_with_prefix.parse::
().context("Failed to parse address")?; - Ok(XAddress { address }) + Ok(Self { address }) } } @@ -154,14 +154,10 @@ pub async fn transfer_token( } /// Get the balance of an address -pub async fn get_balance(rpc_url: &str, address: Address) -> Result<()> { +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")?; - - println!("Balance: {} wei", balance); - - Ok(()) + Ok(balance) } /// Get the ERC20 token balance of an address @@ -169,7 +165,7 @@ pub async fn get_token_balance( rpc_url: &str, token_address: Address, account_address: Address, -) -> Result<()> { +) -> Result { let provider = create_provider(rpc_url).await?; let call = IERC20::balanceOfCall { account: account_address }; @@ -190,9 +186,7 @@ pub async fn get_token_balance( let balance = IERC20::balanceOfCall::abi_decode_returns(&result.0) .context("Failed to decode balanceOf return value")?; - println!("Token Balance: {} (raw units)", balance); - - Ok(()) + Ok(balance) } /// Encode function arguments from string inputs @@ -216,7 +210,7 @@ where .collect() } -/// Helper function to coerce a value to a [DynSolValue] given a type string +/// 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) @@ -270,7 +264,7 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { let addr_with_prefix = if addr_str.starts_with("0x") { addr_str.to_string() } else { - format!("0x{}", addr_str) + format!("0x{addr_str}") }; DynSolType::coerce_str(&DynSolType::Address, &addr_with_prefix) .context("Failed to coerce address") @@ -329,7 +323,7 @@ fn parse_array_args(s: &str) -> Result> { if inner.is_empty() { return Ok(Vec::new()); } - + let mut args = Vec::new(); let mut current = String::new(); let mut depth = 0; @@ -378,19 +372,19 @@ mod tests { // } // 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!( @@ -400,7 +394,7 @@ mod tests { } 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!( @@ -410,21 +404,21 @@ mod tests { } 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"); @@ -441,19 +435,19 @@ mod tests { // 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!( @@ -463,11 +457,11 @@ mod tests { } 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!( @@ -477,7 +471,7 @@ mod tests { } 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"); @@ -497,19 +491,19 @@ mod tests { // 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!( @@ -519,7 +513,7 @@ mod tests { } 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!( @@ -539,19 +533,19 @@ mod tests { // 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!( @@ -561,7 +555,7 @@ mod tests { } 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!( @@ -571,7 +565,7 @@ mod tests { } 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!( @@ -590,14 +584,14 @@ mod tests { 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")); } From c04ee55113ce59f2e05b5e659e54f932efd8069e Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 14:54:19 +0800 Subject: [PATCH 15/21] fix fmt --- crates/sdk/src/lib.rs | 72 +++++++++++++++---------------------------- 1 file changed, 25 insertions(+), 47 deletions(-) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index a92e5e73b91..757de516694 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -35,11 +35,8 @@ impl FromStr for XAddress { fn from_str(s: &str) -> Result { let addr_str = s.strip_prefix('X').unwrap_or(s); - let addr_with_prefix = if addr_str.starts_with("0x") { - addr_str.to_string() - } else { - format!("0x{addr_str}") - }; + 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 }) @@ -370,8 +367,10 @@ mod tests { // uint256 amountIn; // uint160 sqrtPriceLimitX96; // } - // quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32,uint256) - // with args: (XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7,1000000000000000000,3000,0) + // 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(); @@ -387,20 +386,14 @@ mod tests { // Check first address (should have X prefix stripped) if let DynSolValue::Address(addr1) = &tuple_values[0] { - assert_eq!( - addr1.to_string(), - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ); + 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" - ); + assert_eq!(addr2.to_string(), "0xdAC17F958D2ee523a2206206994597C13D831ec7"); } else { panic!("Second element should be an address"); } @@ -434,7 +427,8 @@ mod tests { fn test_encode_args_with_nested_tuple() { // Test nested tuple: (address,(address,uint256)) // Function signature: testFunction((address,(address,uint256)))(uint256) - // Args: (XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,(XdAC17F958D2ee523a2206206994597C13D831ec7,1000)) + // Args: (XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2, + // (XdAC17F958D2ee523a2206206994597C13D831ec7,1000)) let sig = "testFunction((address,(address,uint256)))(uint256)"; let func = Function::parse(sig).unwrap(); @@ -450,10 +444,7 @@ mod tests { // Check first element (address) if let DynSolValue::Address(addr1) = &outer_tuple[0] { - assert_eq!( - addr1.to_string(), - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ); + assert_eq!(addr1.to_string(), "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"); } else { panic!("First element should be an address"); } @@ -464,10 +455,7 @@ mod tests { // Check inner tuple's first element (address) if let DynSolValue::Address(addr2) = &inner_tuple[0] { - assert_eq!( - addr2.to_string(), - "0xdAC17F958D2ee523a2206206994597C13D831ec7" - ); + assert_eq!(addr2.to_string(), "0xdAC17F958D2ee523a2206206994597C13D831ec7"); } else { panic!("Inner tuple's first element should be an address"); } @@ -495,7 +483,9 @@ mod tests { let sig = "testFunction(address[])(uint256)"; let func = Function::parse(sig).unwrap(); - let args = vec!["XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7"]; + let args = vec![ + "XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7", + ]; let result = encode_args(&func.inputs, args).unwrap(); @@ -506,20 +496,14 @@ mod tests { // Check first address (should have X prefix stripped) if let DynSolValue::Address(addr1) = &array_values[0] { - assert_eq!( - addr1.to_string(), - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ); + 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" - ); + assert_eq!(addr2.to_string(), "0xdAC17F958D2ee523a2206206994597C13D831ec7"); } else { panic!("Second element should be an address"); } @@ -532,7 +516,8 @@ mod tests { fn test_encode_args_with_fixed_array() { // Test fixed-size array: address[3] // Function signature: testFunction(address[3])(uint256) - // Args: [XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7,0x1234567890123456789012345678901234567890] + // Args: [XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2, + // XdAC17F958D2ee523a2206206994597C13D831ec7,0x1234567890123456789012345678901234567890] let sig = "testFunction(address[3])(uint256)"; let func = Function::parse(sig).unwrap(); @@ -548,30 +533,21 @@ mod tests { // Check first address (should have X prefix stripped) if let DynSolValue::Address(addr1) = &array_values[0] { - assert_eq!( - addr1.to_string(), - "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2" - ); + 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" - ); + 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" - ); + assert_eq!(addr3.to_string(), "0x1234567890123456789012345678901234567890"); } else { panic!("Third element should be an address"); } @@ -588,7 +564,9 @@ mod tests { let sig = "testFunction(address[3])(uint256)"; let func = Function::parse(sig).unwrap(); - let args = vec!["[XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7]"]; + let args = vec![ + "[XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7]", + ]; let result = encode_args(&func.inputs, args); From 7ab28e75567f6ca71ab7ea00bd45e0774a10f0bf Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 15:03:55 +0800 Subject: [PATCH 16/21] refactor --- crates/sdk/README.md | 19 ++++++++++--------- crates/sdk/src/bin/xlayer_cli.rs | 6 ++++-- crates/sdk/src/lib.rs | 1 - 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/crates/sdk/README.md b/crates/sdk/README.md index 19dfc2357e3..86affd7c697 100644 --- a/crates/sdk/README.md +++ b/crates/sdk/README.md @@ -1,28 +1,29 @@ # run ``` -https://xlayertestrpc.okx.com/terigon -https://testrpc.xlayer.tech/unlimited/abcd +export RPC_URL=https://testrpc.xlayer.tech/unlimited/abcd +## OKB transfer cargo run -p reth-sdk --bin xlayer_cli -- transfer \ - --rpc-url https://testrpc.xlayer.tech/unlimited/abcd \ + --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 https://testrpc.xlayer.tech/unlimited/abcd \ + --rpc-url $RPC_URL$ \ --private-key $PRIVATE_KEY \ --to X44667e638246762d7ba3dfcedbd753d336e8bc81 \ --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d \ --amount 1 -cargo run -p reth-sdk --bin xlayer_cli -- balance --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --address X33f34D8b20696780Ba07b1ea89F209B4Dc51723A -cargo run -p reth-sdk --bin xlayer_cli -- token-balance --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d --account X33f34D8b20696780Ba07b1ea89F209B4Dc51723A - -cargo run -p reth-sdk --bin xlayer_cli -- eth-call --rpc-url https://testrpc.xlayer.tech/unlimited/abcd --to 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d --sig "balanceOf(address)(uint256)" --args X33f34D8b20696780Ba07b1ea89F209B4Dc51723A +## 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)" ``` diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index d6986ed1a69..ddf95b7af22 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -126,12 +126,14 @@ async fn main() -> Result<()> { } XlayerCommands::Balance { rpc_url, address } => { let address: Address = address.into(); - get_balance(&rpc_url, address).await?; + let balance = get_balance(&rpc_url, address).await?; + println!("Balance: {} wei", balance); } XlayerCommands::TokenBalance { rpc_url, token, account } => { let token_address: Address = token.into(); let account_address: Address = account.into(); - get_token_balance(&rpc_url, token_address, account_address).await?; + let balance = get_token_balance(&rpc_url, token_address, account_address).await?; + println!("Balance: {} wei", balance); } XlayerCommands::EthCall { rpc_url, to, sig, args } => { let sig = sig.unwrap(); diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 757de516694..f3131710aa8 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -167,7 +167,6 @@ pub async fn get_token_balance( let call = IERC20::balanceOfCall { account: account_address }; let calldata = call.abi_encode(); - println!("call data: 0x{}", hex::encode(&calldata)); let result = provider .call(alloy_rpc_types_eth::TransactionRequest { to: Some(alloy_primitives::TxKind::Call(token_address)), From 6e4e72fca1e977dd3ba94b9e032bdfb246119c3e Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 15:08:28 +0800 Subject: [PATCH 17/21] refactor --- crates/sdk/src/bin/xlayer_cli.rs | 16 +++++++++++++--- crates/sdk/src/lib.rs | 14 +++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index ddf95b7af22..d5b3f032f6f 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -116,13 +116,23 @@ async fn main() -> Result<()> { match cli.command { XlayerCommands::Transfer { common, to, amount } => { let to_address: Address = to.into(); - transfer_native_asset(&common.rpc_url, &common.private_key, to_address, amount).await?; + 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(); - transfer_token(&common.rpc_url, &common.private_key, token_address, to_address, amount) - .await?; + 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(); diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index f3131710aa8..7ae31ba5bd2 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -6,7 +6,7 @@ use alloy_dyn_abi::{DynSolType, DynSolValue}; use alloy_json_abi::Param; use alloy_network::EthereumWallet; -use alloy_primitives::{hex, Address, Bytes, U256}; +use alloy_primitives::{Address, Bytes, B256, U256}; use alloy_provider::{Provider, ProviderBuilder}; use alloy_rpc_types_eth::TransactionRequest; use alloy_signer_local::PrivateKeySigner; @@ -85,7 +85,7 @@ pub async fn transfer_native_asset( private_key: &str, to_address: Address, amount: Option, -) -> Result<()> { +) -> 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")?; @@ -105,9 +105,7 @@ pub async fn transfer_native_asset( let receipt = pending_tx.get_receipt().await?; - println!("✅ Transaction sent! Hash: {:?}", receipt.transaction_hash); - - Ok(()) + Ok(receipt.transaction_hash) } /// Transfer ERC20 tokens using transfer(address,uint256) function @@ -117,7 +115,7 @@ pub async fn transfer_token( token_address: Address, recipient: Address, amount: U256, -) -> Result<()> { +) -> 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")?; @@ -145,9 +143,7 @@ pub async fn transfer_token( let receipt = pending_tx.get_receipt().await?; - println!("✅ Token transfer transaction sent! Hash: {:?}", receipt.transaction_hash); - - Ok(()) + Ok(receipt.transaction_hash) } /// Get the balance of an address From 8b864da7c3109a2ace3a80f5c85f7130fa8c1c09 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 15:08:58 +0800 Subject: [PATCH 18/21] cargo fix clippy --- crates/sdk/src/bin/xlayer_cli.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/sdk/src/bin/xlayer_cli.rs b/crates/sdk/src/bin/xlayer_cli.rs index d5b3f032f6f..bc1b663ecc5 100644 --- a/crates/sdk/src/bin/xlayer_cli.rs +++ b/crates/sdk/src/bin/xlayer_cli.rs @@ -119,7 +119,7 @@ async fn main() -> Result<()> { let tx_hash = transfer_native_asset(&common.rpc_url, &common.private_key, to_address, amount) .await?; - println!("✅ Transaction sent! Hash: {:?}", tx_hash); + println!("✅ Transaction sent! Hash: {tx_hash:?}"); } XlayerCommands::TokenTransfer { common, token, to, amount } => { let token_address: Address = token.into(); @@ -132,18 +132,18 @@ async fn main() -> Result<()> { amount, ) .await?; - println!("✅ Token transfer transaction sent! Hash: {:?}", tx_hash); + 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: {} wei", balance); + 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: {} wei", balance); + println!("Balance: {balance} wei"); } XlayerCommands::EthCall { rpc_url, to, sig, args } => { let sig = sig.unwrap(); From a52e62a4cefc1985d7622d22166d69610de5979f Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 15:14:40 +0800 Subject: [PATCH 19/21] use back default dev profile --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a52bbbea973..e8da41b65fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -286,7 +286,7 @@ too_long_first_doc_paragraph = "allow" # Uncomment this section if you're using a debugger. [profile.dev] # https://davidlattimore.github.io/posts/2024/02/04/speeding-up-the-rust-edit-build-run-cycle.html -debug = "full" +debug = "line-tables-only" split-debuginfo = "unpacked" # Speed up tests. From dd6626e9b8a5331c9b1b833a6f8a54ab5341e0b6 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 15:21:39 +0800 Subject: [PATCH 20/21] fix conflicts --- Cargo.lock | 18 +++++++++--------- Cargo.toml | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 89748c184a3..1e7b47b967e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10711,7 +10711,7 @@ dependencies = [ [[package]] name = "reth-sdk" -version = "1.8.3" +version = "1.9.1" dependencies = [ "alloy-dyn-abi", "alloy-genesis", @@ -11625,7 +11625,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn 2.0.108", + "syn 2.0.110", "unicode-ident", ] @@ -14618,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", @@ -14653,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 d998cb69d57..aaef8b9b977 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -501,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 } From ef33a436f32f0e336a716a45693be91ee40dfae5 Mon Sep 17 00:00:00 2001 From: "cliff.yang" Date: Mon, 10 Nov 2025 15:32:50 +0800 Subject: [PATCH 21/21] use X_ADDRESS_PREFIX --- crates/sdk/src/lib.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/sdk/src/lib.rs b/crates/sdk/src/lib.rs index 7ae31ba5bd2..6b45d28cb1a 100644 --- a/crates/sdk/src/lib.rs +++ b/crates/sdk/src/lib.rs @@ -14,6 +14,9 @@ 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 { @@ -34,7 +37,7 @@ impl FromStr for XAddress { type Err = anyhow::Error; fn from_str(s: &str) -> Result { - let addr_str = s.strip_prefix('X').unwrap_or(s); + 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}") }; @@ -252,7 +255,7 @@ fn coerce_value_recursive(ty: &DynSolType, arg: &str) -> Result { } DynSolType::Address => { // Strip X prefix from address - let addr_str = arg.strip_prefix('X').unwrap_or(arg); + 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 {