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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 30 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ members = [
"testing/runner",
"tests/",
"crates/tracing-otlp",
"crates/sdk",
]
default-members = ["bin/reth"]
exclude = ["docs/cli"]
Expand Down Expand Up @@ -500,6 +501,7 @@ alloy-contract = { version = "1.1.0", default-features = false }
alloy-eips = { version = "1.1.0", default-features = false }
alloy-genesis = { version = "1.1.0", default-features = false }
alloy-json-rpc = { version = "1.1.0", default-features = false }
alloy-json-abi = { version = "1.4.1", default-features = false }
alloy-network = { version = "1.1.0", default-features = false }
alloy-network-primitives = { version = "1.1.0", default-features = false }
alloy-provider = { version = "1.1.0", features = ["reqwest"], default-features = false }
Expand Down
36 changes: 36 additions & 0 deletions crates/sdk/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "reth-sdk"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[lints]
workspace = true

[dependencies]
# reth
alloy-genesis.workspace = true
alloy-provider.workspace = true
alloy-rpc-types-eth.workspace = true
alloy-network.workspace = true
alloy-signer-local.workspace = true
alloy-primitives.workspace = true
alloy-transport-http.workspace = true
alloy-sol-types.workspace = true
alloy-json-abi.workspace = true
alloy-dyn-abi.workspace = true

# async
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }

# tracing
tracing.workspace = true
tracing-subscriber = { workspace = true, features = ["env-filter", "fmt"] }

# misc
clap = { workspace = true, features = ["derive"] }
anyhow.workspace=true
eyre.workspace=true
33 changes: 33 additions & 0 deletions crates/sdk/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# run
```
export RPC_URL=https://testrpc.xlayer.tech/unlimited/abcd

## OKB transfer
cargo run -p reth-sdk --bin xlayer_cli -- transfer \
--rpc-url $RPC_URL$ \
--private-key $PRIVATE_KEY \
--to X44667e638246762d7ba3dfcedbd753d336e8bc81 \
--amount 1

## USDC transfer
cargo run -p reth-sdk --bin xlayer_cli -- token-transfer \
--rpc-url $RPC_URL$ \
--private-key $PRIVATE_KEY \
--to X44667e638246762d7ba3dfcedbd753d336e8bc81 \
--token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d \
--amount 1

## OKB balance
cargo run -p reth-sdk --bin xlayer_cli -- balance --rpc-url $RPC_URL$ --address X33f34D8b20696780Ba07b1ea89F209B4Dc51723A

## USDC balance
cargo run -p reth-sdk --bin xlayer_cli -- token-balance --rpc-url $RPC_URL$ --token 0xaf5eb02c7bfa28caf1ec3c30a58dce903162096d --account X33f34D8b20696780Ba07b1ea89F209B4Dc51723A

## General contract call, use uniswap v3 quoteExactInputSingle as example
cargo run -p reth-sdk --bin xlayer_cli -- eth-call --rpc-url https://maximum-yolo-seed.quiknode.pro/e4a602e14006c812850883f288b1574b36c48ef6 --to 0x61fFE014bA17989E743c5F6cB21bF9697530B21e --sig "quoteExactInputSingle((address,address,uint256,uint24,uint160))(uint256,uint160,uint32,uint256)" --args "(XC02aaa39b223FE8D0A0e5C4F27eAD9083C756Cc2,XdAC17F958D2ee523a2206206994597C13D831ec7,1000000000000000000,3000,0)"
```

# troubleshooting
```
cargo expand -p reth-sdk --bin xlayer_cli > expanded.rs
```
187 changes: 187 additions & 0 deletions crates/sdk/src/bin/xlayer_cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
//! X Layer CLI - Command-line interface for interacting with X Layer blockchain.
//!
//! This binary provides commands for sending transactions and interacting
//! with the Reth chain via RPC.
use alloy_dyn_abi::{FunctionExt, JsonAbiExt};
use alloy_json_abi::Function;
use alloy_primitives::{Address, Bytes, U256};
use alloy_provider::Provider;
use anyhow::{Context, Result};
use clap::{Parser, Subcommand};
use reth_sdk::{
create_provider, encode_args, get_balance, get_token_balance, transfer_native_asset,
transfer_token, XAddress,
};
use tracing_subscriber::{fmt, prelude::*, EnvFilter};

/// Common fields shared across transfer commands
#[derive(Parser, Debug)]
pub struct CommonTransferArgs {
/// RPC URL
#[arg(long)]
rpc_url: String,
/// Private key (hex string, with or without 0x prefix)
#[arg(long)]
private_key: String,
}

#[derive(Parser)]
#[command(version, about, long_about = None)]
struct XlayerCli {
#[command(subcommand)]
command: XlayerCommands,
}

/// CLI commands for X Layer operations.
#[derive(Subcommand, Debug)]
pub enum XlayerCommands {
/// Transfer native assets (OKB) to a specified address.
Transfer {
/// Common transfer arguments (RPC URL and private key)
#[command(flatten)]
common: CommonTransferArgs,
/// Recipient address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...")
#[arg(long)]
to: XAddress,
/// Amount to send in wei (optional, defaults to 0)
#[arg(long)]
amount: Option<U256>,
},
/// Transfer ERC20 tokens using transfer(address,uint256) function.
TokenTransfer {
/// Common transfer arguments (RPC URL and private key)
#[command(flatten)]
common: CommonTransferArgs,
/// Token contract address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...")
#[arg(long)]
token: XAddress,
/// Recipient address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...")
#[arg(long)]
to: XAddress,
/// Amount to transfer (in token's smallest unit, e.g., wei for 18 decimals)
#[arg(long)]
amount: U256,
},
/// Get the balance of an address (equivalent to `eth_getBalance`).
Balance {
/// RPC URL
#[arg(long)]
rpc_url: String,
/// Address to query balance for (supports optional "X" prefix, e.g., "X1234..." or
/// "0x1234...")
#[arg(long)]
address: XAddress,
},
/// Get the ERC20 token balance of an address (equivalent to calling balanceOf(address)).
TokenBalance {
/// RPC URL
#[arg(long)]
rpc_url: String,
/// Token contract address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...")
#[arg(long)]
token: XAddress,
/// Account address to query balance for (supports optional "X" prefix, e.g., "X1234..." or
/// "0x1234...")
#[arg(long)]
account: XAddress,
},
/// Call a contract function using `eth_call` RPC method.
EthCall {
/// RPC URL
#[arg(long)]
rpc_url: String,
/// Token contract address (supports optional "X" prefix, e.g., "X1234..." or "0x1234...")
#[arg(long)]
to: XAddress,
/// The signature of the function.
#[arg(long)]
sig: Option<String>,

/// The arguments of the function.
#[arg(allow_negative_numbers = true)]
#[arg(long, num_args = 1..)]
args: Vec<String>,
},
}

#[tokio::main]
async fn main() -> Result<()> {
// RUST_LOG=alloy_provider=debug,alloy_transport_http=debug,alloy_json_rpc=debug
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));

tracing_subscriber::registry().with(fmt::layer().with_target(false)).with(filter).init();

let cli = XlayerCli::parse();

match cli.command {
XlayerCommands::Transfer { common, to, amount } => {
let to_address: Address = to.into();
let tx_hash =
transfer_native_asset(&common.rpc_url, &common.private_key, to_address, amount)
.await?;
println!("✅ Transaction sent! Hash: {tx_hash:?}");
}
XlayerCommands::TokenTransfer { common, token, to, amount } => {
let token_address: Address = token.into();
let to_address: Address = to.into();
let tx_hash = transfer_token(
&common.rpc_url,
&common.private_key,
token_address,
to_address,
amount,
)
.await?;
println!("✅ Token transfer transaction sent! Hash: {tx_hash:?}");
}
XlayerCommands::Balance { rpc_url, address } => {
let address: Address = address.into();
let balance = get_balance(&rpc_url, address).await?;
println!("Balance: {balance} wei");
}
XlayerCommands::TokenBalance { rpc_url, token, account } => {
let token_address: Address = token.into();
let account_address: Address = account.into();
let balance = get_token_balance(&rpc_url, token_address, account_address).await?;
println!("Balance: {balance} wei");
}
XlayerCommands::EthCall { rpc_url, to, sig, args } => {
let sig = sig.unwrap();

if sig.contains('(') {
let func = Function::parse(&sig).unwrap();
let values = encode_args(&func.inputs, args).unwrap();
let data = func.abi_encode_input(&values).unwrap();

let to_address: Address = to.into();

let provider = create_provider(&rpc_url).await?;

let result = provider
.call(alloy_rpc_types_eth::TransactionRequest {
to: Some(alloy_primitives::TxKind::Call(to_address)),
input: alloy_rpc_types_eth::TransactionInput {
input: None,
data: Some(Bytes::from(data)),
},
..Default::default()
})
.await
.context("Failed to execute eth_call")?;

match func.abi_decode_output(result.as_ref()) {
Ok(decoded) => {
println!("Result: decoded {decoded:?}");
}
Err(err) => {
eprintln!("error in decode: {err:?}");
}
};
} else {
panic!("signature needs contains ()");
}
}
}

Ok(())
}
Loading