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
33 changes: 16 additions & 17 deletions src/agent_deposit.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! AgentDeposit — Create Protocol agent funding primitive.
//! `AgentDeposit` — Create Protocol agent funding primitive.
//!
//! AgentDeposit is the core contract in [Create Protocol] Phase 1: agents
//! `AgentDeposit` is the core contract in [Create Protocol] Phase 1: agents
//! register, deposit USDC, spend it on compute/tasks, and the protocol
//! distributes fees. This module wraps the read-only surface (balance + agent
//! metadata) so an AI agent can introspect its own on-chain state using only
Expand All @@ -11,7 +11,7 @@
//! eth_sendRawTransaction` (or any wallet). This keeps the CLI read-safe by
//! default; no keys ever touch this binary.
//!
//! **Contract addresses.** AgentDeposit is deployed on Sepolia today; Arbitrum
//! **Contract addresses.** `AgentDeposit` is deployed on Sepolia today; Arbitrum
//! One redeployment lands with Phase 1. To wire the real address, update the
//! [`agent_deposit_address`] match — it's a one-line swap per chain.
//!
Expand All @@ -21,7 +21,7 @@ use crate::rpc::{hex_to_u64, rpc_call};
use eyre::{eyre, Result};
use serde_json::{json, Value};

/// ABI selectors for the Create Protocol AgentDeposit contract.
/// ABI selectors for the Create Protocol `AgentDeposit` contract.
///
/// These match the deployed Sepolia ABI (`AgentDeposit.sol`). Kept as string
/// constants so they're easy to eyeball against an ABI file or a block
Expand All @@ -37,25 +37,23 @@ pub mod selectors {
pub const IS_REGISTERED: &str = "0xc3c5a547";
}

/// Resolve the AgentDeposit contract address for a given chain id.
/// Resolve the `AgentDeposit` contract address for a given chain id.
///
/// Returns `None` until Create Protocol Phase 1 lands on that chain. Swapping
/// in a real deployment is one line per arm.
///
/// - Arbitrum One (42161): placeholder — Phase 1 deploy pending
/// - Arbitrum Sepolia (421614): placeholder — mirrors staging deploy
/// - All other chains: unsupported (AgentDeposit is Arbitrum-first)
/// - All other chains: unsupported (`AgentDeposit` is Arbitrum-first)
pub fn agent_deposit_address(chain_id: u64) -> Option<&'static str> {
match chain_id {
// TODO(create-protocol): replace with real Arbitrum One deployment
42161 => Some("0x0000000000000000000000000000000000000000"),
// TODO(create-protocol): replace with real Arbitrum Sepolia deployment
421614 => Some("0x0000000000000000000000000000000000000000"),
// TODO(create-protocol): replace with real Arbitrum One/Sepolia deployment
42_161 | 421_614 => Some("0x0000000000000000000000000000000000000000"),
_ => None,
}
}

/// What the user asked us to do with the AgentDeposit contract.
/// What the user asked us to do with the `AgentDeposit` contract.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Action {
/// Read: `balanceOf(agent)` — returns USDC on deposit (raw + decimal).
Expand Down Expand Up @@ -112,7 +110,7 @@ pub fn encode_address_call(selector: &str, address: &str) -> Result<String> {
/// Encode `selector(uint256)` calldata.
///
/// The amount is a raw on-chain integer (e.g., USDC has 6 decimals, so
/// $1.00 = 1_000_000). We keep the CLI honest — no hidden decimal scaling.
/// $1.00 = `1_000_000`). We keep the CLI honest — no hidden decimal scaling.
pub fn encode_uint256_call(selector: &str, amount: u128) -> Result<String> {
let sel = selector.trim_start_matches("0x");
if sel.len() != 8 {
Expand All @@ -136,7 +134,7 @@ pub fn decode_uint_result(hex_word: &str) -> Result<u128> {
}

/// Fetch the chain id from the RPC endpoint so we can resolve the right
/// AgentDeposit deployment without asking the user.
/// `AgentDeposit` deployment without asking the user.
pub async fn fetch_chain_id(rpc: &str) -> Result<u64> {
let result = rpc_call(rpc, "eth_chainId", json!([])).await?;
let hex = result
Expand All @@ -145,7 +143,7 @@ pub async fn fetch_chain_id(rpc: &str) -> Result<u64> {
hex_to_u64(hex)
}

/// Call `balanceOf(agent)` on AgentDeposit. Returns raw USDC (6 decimals).
/// Call `balanceOf(agent)` on `AgentDeposit`. Returns raw USDC (6 decimals).
pub async fn read_balance(rpc: &str, contract: &str, agent: &str) -> Result<u128> {
let data = encode_address_call(selectors::BALANCE_OF, agent)?;
let result = rpc_call(
Expand Down Expand Up @@ -183,7 +181,8 @@ pub fn format_balance_response(
// AgentDeposit settles in USDC — 6 decimals. Scaling stays local to
// presentation; the raw value is always the source of truth.
const USDC_DECIMALS: u32 = 6;
let balance_human = balance_raw as f64 / 10f64.powi(USDC_DECIMALS as i32);
#[allow(clippy::cast_precision_loss)]
let balance_human = balance_raw as f64 / 10f64.powi(i32::try_from(USDC_DECIMALS).unwrap_or(6));
json!({
"agent": agent,
"contract": contract,
Expand Down Expand Up @@ -304,8 +303,8 @@ mod tests {
fn known_chains_resolve_address() {
// Both arms return Some — the value is placeholder until Phase 1,
// but the resolver contract must remain stable.
assert!(agent_deposit_address(42161).is_some());
assert!(agent_deposit_address(421614).is_some());
assert!(agent_deposit_address(42_161).is_some());
assert!(agent_deposit_address(421_614).is_some());
assert!(agent_deposit_address(1).is_none());
}

Expand Down
22 changes: 13 additions & 9 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,15 @@ struct SupportedChain {

const SUPPORTED_CHAINS: &[SupportedChain] = &[
SupportedChain {
chain_id: 42161,
chain_id: 42_161,
name: "Arbitrum One",
rpc_default: "https://arb1.arbitrum.io/rpc",
explorer: "https://arbiscan.io",
usdc: "0xaf88d065e77c8cC2239327C5EDb3A432268e5831",
uniswap_v3_quoter: Some("0xb27308f9F90D607463bb33eA1BeBb41C27CE5AB6"),
},
SupportedChain {
chain_id: 421614,
chain_id: 421_614,
name: "Arbitrum Sepolia",
rpc_default: "https://sepolia-rollup.arbitrum.io/rpc",
explorer: "https://sepolia.arbiscan.io",
Expand Down Expand Up @@ -119,7 +119,8 @@ pub async fn token_balance(rpc: &str, token: &str, address: &str, mode: Mode) ->
.unwrap_or(18);

let balance_raw = u128::from_str_radix(raw.trim_start_matches("0x"), 16).unwrap_or(0);
let balance_human = balance_raw as f64 / 10f64.powi(decimals as i32);
#[allow(clippy::cast_precision_loss)]
let balance_human = balance_raw as f64 / 10f64.powi(i32::try_from(decimals).unwrap_or(18));

let out = json!({
"token": token,
Expand Down Expand Up @@ -150,6 +151,7 @@ pub async fn gas(rpc: &str, mode: Mode) -> Result<()> {
let block_num = rpc_call(rpc, "eth_blockNumber", json!([])).await?;

let gas_hex = gas_price.as_str().unwrap_or("0x0");
#[allow(clippy::cast_precision_loss)]
let gwei = u128::from_str_radix(gas_hex.trim_start_matches("0x"), 16).unwrap_or(0) as f64 / 1e9;

let out = json!({
Expand Down Expand Up @@ -219,7 +221,7 @@ fn chain_inventory() -> Vec<Value> {
.collect()
}

fn subcommand_inventory(command: Command) -> Vec<Value> {
fn subcommand_inventory(command: &Command) -> Vec<Value> {
command
.get_subcommands()
.map(|subcommand| {
Expand All @@ -229,21 +231,21 @@ fn subcommand_inventory(command: Command) -> Vec<Value> {
json!({
"name": arg.get_id().as_str(),
"required": arg.is_required_set(),
"help": arg.get_help().map(|help| help.to_string()),
"help": arg.get_help().map(std::string::ToString::to_string),
})
})
.collect();

json!({
"name": subcommand.get_name(),
"description": subcommand.get_about().map(|about| about.to_string()),
"description": subcommand.get_about().map(std::string::ToString::to_string),
"args": args,
})
})
.collect()
}

pub(crate) fn info_inventory(command: Command) -> Value {
pub(crate) fn info_inventory(command: &Command) -> Value {
json!({
"name": "arbitrum-cli",
"version": env!("CARGO_PKG_VERSION"),
Expand Down Expand Up @@ -300,7 +302,8 @@ fn print_info_human(inventory: &Value) {
}

// ── info ──
pub fn info(mode: Mode, command: Command) -> Result<()> {
#[allow(clippy::unnecessary_wraps)]
pub fn info(mode: Mode, command: &Command) -> Result<()> {
let inventory = info_inventory(command);
match mode {
Mode::Json => emit(mode, "arbitrum-cli info", &inventory),
Expand Down Expand Up @@ -381,7 +384,8 @@ pub async fn agent_deposit(
}

// ── mcp (stub) ──
pub async fn mcp(_rpc: &str, bind: &str) -> Result<()> {
#[allow(clippy::unnecessary_wraps)]
pub fn mcp(_rpc: &str, bind: &str) -> Result<()> {
// MCP server stub — production version would expose tools via stdio or SSE
// following the Model Context Protocol spec.
eprintln!("MCP server mode — stub implementation");
Expand Down
26 changes: 13 additions & 13 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ mod rpc;
long_about = "A single Rust binary to query Arbitrum, interact with contracts, monitor events, and expose an MCP server for AI agents. Default output is JSON (agent-friendly). Use --human for pretty terminal output."
)]
struct Cli {
/// RPC URL (default: https://arb1.arbitrum.io/rpc)
/// RPC URL (default: <https://arb1.arbitrum.io/rpc>)
#[arg(long, global = true, env = "ARBITRUM_RPC_URL")]
rpc: Option<String>,

Expand Down Expand Up @@ -54,7 +54,7 @@ enum Commands {
address: String,
},

/// Read from a contract (eth_call)
/// Read from a contract (`eth_call`)
Call {
/// Contract address
to: String,
Expand All @@ -75,7 +75,7 @@ enum Commands {

/// Execute a generic JSON-RPC call (agent-friendly)
Exec {
/// RPC method name (e.g., eth_blockNumber)
/// RPC method name (e.g., `eth_blockNumber`)
method: String,

/// Params as JSON array
Expand All @@ -90,7 +90,7 @@ enum Commands {
bind: String,
},

/// Interact with Create Protocol AgentDeposit on Arbitrum
/// Interact with Create Protocol `AgentDeposit` on Arbitrum
///
/// Read agent balance / registration state, or produce unsigned calldata
/// for deposit/withdraw (sign + broadcast externally — the CLI never
Expand All @@ -108,7 +108,7 @@ enum Commands {
#[arg(long)]
amount: Option<u128>,

/// Override the AgentDeposit contract address (advanced; defaults to
/// Override the `AgentDeposit` contract address (advanced; defaults to
/// the deployment registered for the connected chain).
#[arg(long)]
contract: Option<String>,
Expand All @@ -135,15 +135,15 @@ async fn main() -> eyre::Result<()> {
Commands::Tx { hash } => commands::tx(&rpc_url, &hash, out_mode).await?,
Commands::Balance { address } => commands::balance(&rpc_url, &address, out_mode).await?,
Commands::Token { token, address } => {
commands::token_balance(&rpc_url, &token, &address, out_mode).await?
commands::token_balance(&rpc_url, &token, &address, out_mode).await?;
}
Commands::Call { to, data } => commands::call(&rpc_url, &to, &data, out_mode).await?,
Commands::Gas => commands::gas(&rpc_url, out_mode).await?,
Commands::Watch { target } => commands::watch(&rpc_url, &target, out_mode).await?,
Commands::Exec { method, params } => {
commands::exec(&rpc_url, &method, &params, out_mode).await?
commands::exec(&rpc_url, &method, &params, out_mode).await?;
}
Commands::Mcp { bind } => commands::mcp(&rpc_url, &bind).await?,
Commands::Mcp { bind } => commands::mcp(&rpc_url, &bind)?,
Commands::AgentDeposit {
address,
action,
Expand All @@ -158,9 +158,9 @@ async fn main() -> eyre::Result<()> {
contract.as_deref(),
out_mode,
)
.await?
.await?;
}
Commands::Info => commands::info(out_mode, Cli::command())?,
Commands::Info => commands::info(out_mode, &Cli::command())?,
}

Ok(())
Expand All @@ -172,8 +172,8 @@ mod tests {

#[test]
fn info_inventory_starts_with_arbitrum_one() {
let inventory = commands::info_inventory(Cli::command());
assert_eq!(inventory["chains"][0]["chain_id"], 42161);
let inventory = commands::info_inventory(&Cli::command());
assert_eq!(inventory["chains"][0]["chain_id"], 42_161);
}

#[test]
Expand All @@ -183,7 +183,7 @@ mod tests {
.get_subcommands()
.map(|subcommand| subcommand.get_name().to_string())
.collect();
let inventory = commands::info_inventory(command);
let inventory = commands::info_inventory(&command);
let actual: Vec<String> = inventory["subcommands"]
.as_array()
.expect("subcommands array")
Expand Down
1 change: 1 addition & 0 deletions src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,5 +38,6 @@ pub fn hex_to_u64(hex: &str) -> Result<u64> {
pub fn wei_hex_to_eth(hex: &str) -> Result<f64> {
let stripped = hex.trim_start_matches("0x");
let wei = u128::from_str_radix(stripped, 16).map_err(|e| eyre!("Invalid wei: {e}"))?;
#[allow(clippy::cast_precision_loss)]
Ok(wei as f64 / 1e18)
}