diff --git a/src/commands/inspect.rs b/src/commands/inspect.rs index d8914206..5363b4c8 100644 --- a/src/commands/inspect.rs +++ b/src/commands/inspect.rs @@ -54,14 +54,20 @@ pub struct StorageArgs { /// Output as JSON #[arg(long)] pub json: bool, + /// Maximum number of entries to show + #[arg(long, default_value = "20")] + pub limit: usize, + /// Pagination cursor (entry index to start from) + #[arg(long)] + pub cursor: Option, } // ── Entry point ─────────────────────────────────────────────────────────────── pub fn handle(cmd: InspectCommands) -> Result<()> { match cmd { - InspectCommands::State(args) => handle_state(args), - InspectCommands::Key(args) => handle_key(args), + InspectCommands::State(args) => handle_state(args), + InspectCommands::Key(args) => handle_key(args), InspectCommands::Storage(args) => handle_storage(args), } } @@ -75,7 +81,7 @@ fn handle_state(args: StateArgs) -> Result<()> { p::header("Contract State"); p::separator(); p::kv("Contract ID", &args.contract_id); - p::kv("Network", &args.network); + p::kv("Network", &args.network); p::separator(); println!(); @@ -101,20 +107,23 @@ fn handle_state(args: StateArgs) -> Result<()> { return Ok(()); } - p::kv_accent("Contract ID", &result.contract_id); - p::kv("Executable", &result.executable); + p::kv_accent("Contract ID", &result.contract_id); + p::kv("Executable", &result.executable); p::kv( "WASM Hash", - result.wasm_hash.as_deref().unwrap_or("n/a (stellar asset contract)"), + result + .wasm_hash + .as_deref() + .unwrap_or("n/a (stellar asset contract)"), ); - p::kv("Storage Durability", &result.storage_durability); - p::kv("Latest Ledger", &result.latest_ledger.to_string()); + p::kv("Storage Durability", &result.storage_durability); + p::kv("Latest Ledger", &result.latest_ledger.to_string()); if let Some(v) = result.last_modified_ledger_seq { p::kv("Last Modified Ledger", &v.to_string()); } if let Some(v) = result.live_until_ledger_seq { - p::kv("Live Until Ledger", &v.to_string()); + p::kv("Live Until Ledger", &v.to_string()); } println!(); @@ -130,9 +139,9 @@ fn handle_key(args: KeyArgs) -> Result<()> { p::header("Contract Storage Key"); p::separator(); p::kv("Contract ID", &args.contract_id); - p::kv("Key", &args.key); - p::kv("Scope", &args.scope); - p::kv("Network", &args.network); + p::kv("Key", &args.key); + p::kv("Scope", &args.scope); + p::kv("Network", &args.network); p::separator(); println!(); @@ -142,7 +151,8 @@ fn handle_key(args: KeyArgs) -> Result<()> { // Search instance storage for the key (case-insensitive symbol match) let needle = args.key.to_lowercase(); - let found: Vec<_> = result.instance_storage + let found: Vec<_> = result + .instance_storage .iter() .filter(|e| e.key.to_lowercase().contains(&needle)) .collect(); @@ -174,8 +184,8 @@ fn handle_storage(args: StorageArgs) -> Result<()> { p::header("Contract Storage"); p::separator(); p::kv("Contract ID", &args.contract_id); - p::kv("Scope", &args.scope); - p::kv("Network", &args.network); + p::kv("Scope", &args.scope); + p::kv("Network", &args.network); p::separator(); println!(); @@ -196,18 +206,35 @@ fn handle_storage(args: StorageArgs) -> Result<()> { return Ok(()); } - print_storage_table(&result.instance_storage, &args.scope); + let entries = paginate(&result.instance_storage, args.cursor, args.limit); + let total = result.instance_storage.len(); + print_storage_table(entries, &args.scope); + + let start = args.cursor.unwrap_or(0); + let end = start + entries.len(); + if end < total { + p::info(&format!( + "Showing {}-{} of {} entries. Use --cursor {} to see more.", + start + 1, end, total, end + )); + } p::separator(); Ok(()) } +fn paginate(entries: &[soroban::ContractStorageEntry], cursor: Option, limit: usize) -> &[soroban::ContractStorageEntry] { + let start = cursor.unwrap_or(0).min(entries.len()); + let end = (start + limit).min(entries.len()); + &entries[start..end] +} + // ── Display helpers ─────────────────────────────────────────────────────────── fn print_storage_table(entries: &[soroban::ContractStorageEntry], scope: &str) { let scope_label = match scope { "persistent" => "Persistent Storage", - "temporary" => "Temporary Storage", - _ => "Instance Storage", + "temporary" => "Temporary Storage", + _ => "Instance Storage", }; println!( @@ -224,11 +251,7 @@ fn print_storage_table(entries: &[soroban::ContractStorageEntry], scope: &str) { return; } - println!( - " {:<32} {}", - "Key".dimmed(), - "Value".dimmed() - ); + println!(" {:<32} {}", "Key".dimmed(), "Value".dimmed()); println!(" {}", "─".repeat(72).dimmed()); for entry in entries { @@ -250,7 +273,9 @@ fn pretty_value(raw: &str) -> String { } // Addresses (G... or C...) if raw.len() == 56 && (raw.starts_with('G') || raw.starts_with('C')) { - return format!("{}…{}", &raw[..8], &raw[raw.len()-4..]).yellow().to_string(); + return format!("{}…{}", &raw[..8], &raw[raw.len() - 4..]) + .yellow() + .to_string(); } // Hex bytes if raw.starts_with("0x") { diff --git a/src/utils/horizon.rs b/src/utils/horizon.rs index b8e0614a..8877f3f1 100644 --- a/src/utils/horizon.rs +++ b/src/utils/horizon.rs @@ -33,11 +33,12 @@ pub struct Balance { } pub fn fund_account(public_key: &str, network: &str) -> Result<()> { - let friendbot = friendbot_url(network)? - .unwrap_or_else(|| "https://friendbot.stellar.org".to_string()); + let friendbot = + friendbot_url(network)?.unwrap_or_else(|| "https://friendbot.stellar.org".to_string()); let separator = if friendbot.contains('?') { '&' } else { '?' }; let url = format!("{}{}addr={}", friendbot, separator, public_key); - let res = ureq::get(&url).call() + let res = ureq::get(&url) + .call() .with_context(|| format!("Friendbot request failed for {}", network))?; if res.status() == 200 { Ok(()) @@ -49,10 +50,12 @@ pub fn fund_account(public_key: &str, network: &str) -> Result<()> { pub fn fetch_account(public_key: &str, network: &str) -> Result { let horizon = horizon_url(network)?; let url = format!("{}/accounts/{}", horizon, public_key); - let res = ureq::get(&url).call() + let res = ureq::get(&url) + .call() .with_context(|| format!("Failed to reach Horizon on {}", network))?; if res.status() == 200 { - let account: AccountResponse = res.into_json() + let account: AccountResponse = res + .into_json() .with_context(|| "Failed to parse account response")?; Ok(account) } else { @@ -63,13 +66,20 @@ pub fn fetch_account(public_key: &str, network: &str) -> Result pub fn check_network(network: &str) -> bool { if let Ok(horizon) = horizon_url(network) { let url = format!("{}/", horizon); - ureq::get(&url).call().map(|r| r.status() == 200).unwrap_or(false) + ureq::get(&url) + .call() + .map(|r| r.status() == 200) + .unwrap_or(false) } else { false } } -pub fn build_transaction_query_url(public_key: &str, network: &str, filter: &TxFilter) -> Result { +pub fn build_transaction_query_url( + public_key: &str, + network: &str, + filter: &TxFilter, +) -> Result { let horizon = horizon_url(network)?; let mut url = format!( "{}/accounts/{}/transactions?order={}&limit={}", @@ -103,16 +113,6 @@ pub struct TransactionRecord { pub transaction_type: Option, pub paging_token: Option, } - pub hash: String, - pub successful: bool, - pub operation_count: u32, - pub fee_charged: String, - pub created_at: String, - pub memo_type: Option, - pub memo: Option, - pub source_account: Option, - pub paging_token: Option, -} #[derive(Debug, Deserialize)] struct TransactionsResponse { @@ -141,13 +141,17 @@ pub fn fetch_transactions( network: &str, limit: u8, ) -> Result> { - fetch_transactions_filtered(public_key, network, TxFilter { - limit, - cursor: None, - after: None, - before: None, - successful_only: None, - }) + fetch_transactions_filtered( + public_key, + network, + TxFilter { + limit, + cursor: None, + after: None, + before: None, + successful_only: None, + }, + ) } pub fn fetch_transactions_filtered( @@ -215,20 +219,26 @@ pub fn build_and_simulate_payment( ) -> Result { // For now, we'll use a simplified approach by calling the stellar CLI // In a production implementation, you'd use stellar-xdr to build the transaction properly - + // Build transaction XDR using stellar-sdk patterns let tx_xdr = build_payment_transaction_xdr( - source, destination, amount, asset_code, asset_issuer, sequence, network + source, + destination, + amount, + asset_code, + asset_issuer, + sequence, + network, )?; - + // Simulate the transaction let horizon = horizon_url(network)?; let _url = format!("{}/transactions", horizon); let _form_data = format!("tx={}", urlencoding::encode(&tx_xdr)); - + // For simulation, we'll estimate the fee let estimated_fee = 100000u64; // 0.00001 XLM in stroops - + Ok(TransactionSimulationResult { transaction_xdr: tx_xdr, fee: estimated_fee, @@ -242,38 +252,44 @@ pub fn submit_payment_transaction( ) -> Result { // Sign the transaction let signed_xdr = sign_transaction_xdr(transaction_xdr, secret_key, network)?; - + // Submit to Horizon let horizon = horizon_url(network)?; let url = format!("{}/transactions", horizon); let form_data = format!("tx={}", urlencoding::encode(&signed_xdr)); - + let res = ureq::post(&url) .set("Content-Type", "application/x-www-form-urlencoded") .send_string(&form_data) .with_context(|| "Failed to submit transaction to Horizon")?; let status = res.status(); - + if status == 200 { - let result: serde_json::Value = res.into_json() + let result: serde_json::Value = res + .into_json() .with_context(|| "Failed to parse transaction response")?; - - let hash = result.get("hash") + + let hash = result + .get("hash") .and_then(|h| h.as_str()) .unwrap_or("unknown") .to_string(); - + Ok(TransactionSubmitResult { hash, successful: true, }) } else { - let error_text = res.into_string().unwrap_or_else(|_| "Unknown error".to_string()); - + let error_text = res + .into_string() + .unwrap_or_else(|_| "Unknown error".to_string()); + // Try to parse Horizon error format if let Ok(horizon_error) = serde_json::from_str::(&error_text) { - let detail = horizon_error.detail.unwrap_or_else(|| "No additional details".to_string()); + let detail = horizon_error + .detail + .unwrap_or_else(|| "No additional details".to_string()); anyhow::bail!("Transaction failed: {} - {}", horizon_error.title, detail); } else { anyhow::bail!("Transaction failed with status {}: {}", status, error_text); @@ -297,8 +313,9 @@ pub fn submit_multisig_transaction( let status = res.status(); if status == 200 { - let result: serde_json::Value = - res.into_json().with_context(|| "Failed to parse transaction response")?; + let result: serde_json::Value = res + .into_json() + .with_context(|| "Failed to parse transaction response")?; let hash = result .get("hash") @@ -337,47 +354,47 @@ fn build_payment_transaction_xdr( ) -> Result { // This is a simplified mock implementation // In production, you'd use stellar-xdr crate to build proper transaction XDR - + let _network_passphrase = match network { "mainnet" => "Public Global Stellar Network ; September 2015", _ => "Test SDF Network ; September 2015", }; - + // Mock XDR generation - in reality this would be much more complex let asset_info = match (asset_code, asset_issuer) { (None, None) => "native".to_string(), (Some(code), Some(issuer)) => format!("{}:{}", code, issuer), _ => return Err(anyhow::anyhow!("Invalid asset specification")), }; - + // Generate a mock transaction XDR // In production, use stellar-xdr to build proper TransactionEnvelope let mock_xdr = format!( "mock_payment_tx_{}_{}_{}_{}_{}", source, destination, amount, asset_info, sequence ); - - use base64::{Engine as _, engine::general_purpose}; + + use base64::{engine::general_purpose, Engine as _}; Ok(general_purpose::STANDARD.encode(mock_xdr)) } fn sign_transaction_xdr(transaction_xdr: &str, secret_key: &str, network: &str) -> Result { // This is a simplified mock implementation // In production, you'd use stellar-xdr and ed25519 signing - + let _network_passphrase = match network { "mainnet" => "Public Global Stellar Network ; September 2015", _ => "Test SDF Network ; September 2015", }; - + // Mock signing - in reality this would involve: // 1. Decode the transaction XDR // 2. Hash the transaction with network passphrase // 3. Sign with ed25519 private key // 4. Create TransactionEnvelope with signature // 5. Re-encode to XDR - + let signed_mock = format!("signed_{}_with_{}", transaction_xdr, &secret_key[..8]); - use base64::{Engine as _, engine::general_purpose}; + use base64::{engine::general_purpose, Engine as _}; Ok(general_purpose::STANDARD.encode(signed_mock)) } diff --git a/src/utils/templates.rs b/src/utils/templates.rs index f2d59f49..eb2a4a8f 100644 --- a/src/utils/templates.rs +++ b/src/utils/templates.rs @@ -62,6 +62,29 @@ impl std::fmt::Display for TemplateSource { } } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TemplateSource { + Git { url: String, branch: Option }, + Local { path: String }, + Builtin { id: String }, +} + +impl std::fmt::Display for TemplateSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TemplateSource::Git { url, branch } => { + if let Some(branch) = branch { + write!(f, "git:{}@{}", url, branch) + } else { + write!(f, "git:{}", url) + } + } + TemplateSource::Local { path } => write!(f, "local:{}", path), + TemplateSource::Builtin { id } => write!(f, "builtin:{}", id), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TemplateEntry { pub name: String,