From dcbcc04e6942d0d7faf92eedaa5b91875f4c6d49 Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Thu, 21 Sep 2023 11:32:43 -0400 Subject: [PATCH] feat: add key::Args and FullLedgerEntry --- Cargo.lock | 20 +- cmd/soroban-cli/src/commands/contract/bump.rs | 104 ++----- .../src/commands/contract/install.rs | 14 +- cmd/soroban-cli/src/commands/contract/mod.rs | 4 +- cmd/soroban-cli/src/commands/contract/read.rs | 274 ++++++------------ .../src/commands/contract/restore.rs | 111 +------ cmd/soroban-cli/src/key.rs | 112 +++++++ cmd/soroban-cli/src/lib.rs | 1 + cmd/soroban-cli/src/rpc/mod.rs | 80 ++++- 9 files changed, 320 insertions(+), 400 deletions(-) create mode 100644 cmd/soroban-cli/src/key.rs diff --git a/Cargo.lock b/Cargo.lock index f7fe8eab18..9583064994 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,9 +19,9 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "1.1.0" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f2135563fb5c609d2b2b87c1e8ce7bc41b0b45430fa9661f457981503dd5bf0" +checksum = "ea5d730647d4fadd988536d06fecce94b7b4f2a7efdae548f1cf4b63205518ab" dependencies = [ "memchr", ] @@ -1129,9 +1129,9 @@ checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] name = "hermit-abi" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +checksum = "d77f7ec81a6d05a3abb01ab6eb7590f6083d08449fe5a1c8b1e620283546ccb7" [[package]] name = "hex" @@ -1703,9 +1703,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "300.1.4+3.1.2" +version = "300.1.5+3.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80b6658b21fe1eba923af26cfe9c57a1a075a8190df2cd869a9bcd730093ffa2" +checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491" dependencies = [ "cc", ] @@ -3288,9 +3288,9 @@ dependencies = [ [[package]] name = "unicode-width" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "untrusted" @@ -3551,9 +3551,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] diff --git a/cmd/soroban-cli/src/commands/contract/bump.rs b/cmd/soroban-cli/src/commands/contract/bump.rs index 2bc3ab72c8..3fc63f4f2e 100644 --- a/cmd/soroban-cli/src/commands/contract/bump.rs +++ b/cmd/soroban-cli/src/commands/contract/bump.rs @@ -1,65 +1,27 @@ use std::{ fmt::Debug, - path::{Path, PathBuf}, + path::{Path}, str::FromStr, }; use clap::{command, Parser}; use soroban_env_host::xdr::{ - BumpFootprintExpirationOp, Error as XdrError, ExpirationEntry, ExtensionPoint, Hash, - LedgerEntry, LedgerEntryChange, LedgerEntryData, LedgerFootprint, LedgerKey, - LedgerKeyContractCode, LedgerKeyContractData, Memo, MuxedAccount, Operation, OperationBody, - Preconditions, ReadXdr, ScAddress, ScSpecTypeDef, ScVal, SequenceNumber, SorobanResources, + BumpFootprintExpirationOp, Error as XdrError, ExpirationEntry, ExtensionPoint, + LedgerEntry, LedgerEntryChange, LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, + Preconditions, SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, Uint256, }; -use stellar_strkey::DecodeError; + use crate::{ commands::config, - commands::contract::Durability, - rpc::{self, Client}, - utils, wasm, Pwd, + rpc::{self, Client}, wasm, Pwd, key, }; #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// Contract ID to which owns the data entries. - /// If no keys provided the Contract's instance will be bumped - #[arg( - long = "id", - required_unless_present = "wasm", - required_unless_present = "wasm_hash" - )] - contract_id: Option, - /// Storage key (symbols only) - #[arg(long = "key", conflicts_with = "key_xdr")] - key: Option, - /// Storage key (base64-encoded XDR) - #[arg(long = "key-xdr", conflicts_with = "key")] - key_xdr: Option, - /// Path to Wasm file of contract code to bump - #[arg( - long, - conflicts_with = "contract_id", - conflicts_with = "key", - conflicts_with = "key_xdr", - conflicts_with = "wasm_hash" - )] - wasm: Option, - /// Path to Wasm file of contract code to bump - #[arg( - long, - conflicts_with = "contract_id", - conflicts_with = "key", - conflicts_with = "key_xdr", - conflicts_with = "wasm" - )] - wasm_hash: Option, - /// Storage entry durability - #[arg(long, value_enum, required = true)] - durability: Durability, /// Number of ledgers to extend the entries #[arg(long, required = true)] @@ -69,6 +31,9 @@ pub struct Cmd { #[arg(long)] expiration_ledger_only: bool, + #[command(flatten)] + pub key: key::Args, + #[command(flatten)] config: config::Args, #[command(flatten)] @@ -99,8 +64,7 @@ pub enum Error { }, #[error("parsing XDR key {key}: {error}")] CannotParseXdrKey { key: String, error: XdrError }, - #[error("cannot parse contract ID {0}: {1}")] - CannotParseContractId(String, DecodeError), + #[error(transparent)] Config(#[from] config::Error), #[error("either `--key` or `--key-xdr` are required")] @@ -115,6 +79,8 @@ pub enum Error { Rpc(#[from] rpc::Error), #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] + Key(#[from] key::Error), } impl Cmd { @@ -137,7 +103,7 @@ impl Cmd { async fn run_against_rpc_server(&self) -> Result { let network = self.config.get_network()?; tracing::trace!(?network); - let needle = self.parse_key()?; + let keys = self.key.parse_keys()?; let network = &self.config.get_network()?; let client = Client::new(&network.rpc_url)?; let key = self.config.key_pair()?; @@ -165,7 +131,7 @@ impl Cmd { ext: ExtensionPoint::V0, resources: SorobanResources { footprint: LedgerFootprint { - read_only: vec![needle].try_into()?, + read_only: keys.try_into()?, read_write: vec![].try_into()?, }, instructions: 0, @@ -219,7 +185,7 @@ impl Cmd { } fn run_in_sandbox(&self) -> Result { - let needle = self.parse_key()?; + let keys = self.key.parse_keys()?; // Initialize storage and host // TODO: allow option to separate input and output file @@ -238,7 +204,7 @@ impl Cmd { Box::new(new_k.clone()), ( Box::new(new_v), - if needle == new_k { + if keys.contains(&new_k) { // It must have an expiration since it's a contract data entry let old_expiration = v.1.unwrap(); expiration_ledger_seq = Some(old_expiration + self.ledgers_to_expire); @@ -259,42 +225,6 @@ impl Cmd { Ok(new_expiration_ledger_seq) } - - fn parse_key(&self) -> Result { - let key = if let Some(key) = &self.key { - soroban_spec_tools::from_string_primitive(key, &ScSpecTypeDef::Symbol).map_err(|e| { - Error::CannotParseKey { - key: key.clone(), - error: e, - } - })? - } else if let Some(key) = &self.key_xdr { - ScVal::from_xdr_base64(key).map_err(|e| Error::CannotParseXdrKey { - key: key.clone(), - error: e, - })? - } else if let Some(wasm) = &self.wasm { - return Ok(crate::wasm::Args { wasm: wasm.clone() }.try_into()?); - } else if let Some(wasm_hash) = &self.wasm_hash { - return Ok(LedgerKey::ContractCode(LedgerKeyContractCode { - hash: Hash( - utils::contract_id_from_str(wasm_hash) - .map_err(|e| Error::CannotParseContractId(wasm_hash.clone(), e))?, - ), - })); - } else { - ScVal::LedgerKeyContractInstance - }; - let contract_id = contract_id(self.contract_id.as_ref().unwrap())?; - - Ok(LedgerKey::ContractData(LedgerKeyContractData { - contract: ScAddress::Contract(Hash(contract_id)), - durability: self.durability.into(), - key, - })) - } } -fn contract_id(s: &str) -> Result<[u8; 32], Error> { - utils::contract_id_from_str(s).map_err(|e| Error::CannotParseContractId(s.to_string(), e)) -} + diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index c5df60e4c8..809f6106d4 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -10,6 +10,7 @@ use soroban_env_host::xdr::{ }; use super::restore; +use crate::key; use crate::rpc::{self, Client}; use crate::{commands::config, utils, wasm}; @@ -112,11 +113,14 @@ impl Cmd { { // Now just need to restore it and don't have to install again restore::Cmd { - contract_id: None, - key: vec![], - key_xdr: vec![], - wasm: Some(self.wasm.wasm.clone()), - wasm_hash: None, + key: key::Args { + contract_id: None, + key: None, + key_xdr: None, + wasm: Some(self.wasm.wasm.clone()), + wasm_hash: None, + durability: super::Durability::Persistent, + }, config: self.config.clone(), fee: self.fee.clone(), } diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 0dce3a7cd3..c2dc1a6775 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -122,8 +122,8 @@ pub enum Durability { Temporary, } -impl From for soroban_env_host::xdr::ContractDataDurability { - fn from(d: Durability) -> Self { +impl From<&Durability> for soroban_env_host::xdr::ContractDataDurability { + fn from(d: &Durability) -> Self { match d { Durability::Persistent => soroban_env_host::xdr::ContractDataDurability::Persistent, Durability::Temporary => soroban_env_host::xdr::ContractDataDurability::Temporary, diff --git a/cmd/soroban-cli/src/commands/contract/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index 4a4d9ea3ce..207119b024 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -1,45 +1,32 @@ use std::{ convert::Into, fmt::Debug, - io::{self, stdout}, + io::{self}, }; use clap::{command, Parser, ValueEnum}; +use sha2::{Digest, Sha256}; use soroban_env_host::{ xdr::{ - self, ContractDataDurability, ContractDataEntry, Error as XdrError, Hash, LedgerEntryData, - LedgerKey, LedgerKeyContractData, ReadXdr, ScAddress, ScSpecTypeDef, ScVal, WriteXdr, + Error as XdrError, ExpirationEntry, Hash, ScVal, WriteXdr, }, HostError, }; use crate::{ commands::config, - commands::contract::Durability, - rpc::{self, Client}, - utils, + key, + rpc::{self, Client, FullLedgerEntries, FullLedgerEntry}, }; #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// Contract ID to invoke - #[arg(long = "id")] - contract_id: String, - /// Storage key (symbols only) - #[arg(long = "key", conflicts_with = "key_xdr")] - key: Option, - /// Storage key (base64-encoded XDR ScVal) - #[arg(long = "key-xdr", conflicts_with = "key")] - key_xdr: Option, - /// Storage entry durability - #[arg(long, value_enum)] - durability: Option, - /// Type of output to generate #[arg(long, value_enum, default_value("string"))] - output: Output, - + pub output: Output, + #[command(flatten)] + pub key: key::Args, #[command(flatten)] config: config::Args, } @@ -88,7 +75,7 @@ pub enum Error { KeyIsRequired, #[error(transparent)] Rpc(#[from] rpc::Error), - #[error("xdr processing error: {0}")] + #[error(transparent)] Xdr(#[from] XdrError), #[error(transparent)] // TODO: the Display impl of host errors is pretty user-unfriendly @@ -96,197 +83,108 @@ pub enum Error { Host(#[from] HostError), #[error("no matching contract data entries were found for the specified contract id")] NoContractDataEntryFoundForContractID, + #[error(transparent)] + Key(#[from] key::Error), } impl Cmd { pub async fn run(&self) -> Result<(), Error> { - let contract_id: [u8; 32] = - utils::contract_id_from_str(&self.contract_id).map_err(|e| { - Error::CannotParseContractId { - contract_id: self.contract_id.clone(), - error: e, - } - })?; - let key = if let Some(key) = &self.key { - Some( - soroban_spec_tools::from_string_primitive(key, &ScSpecTypeDef::Symbol).map_err( - |e| Error::CannotParseKey { - key: key.clone(), - error: e, - }, - )?, - ) - } else if let Some(key) = &self.key_xdr { - Some( - ScVal::from_xdr_base64(key).map_err(|e| Error::CannotParseXdrKey { - key: key.clone(), - error: e, - })?, - ) - } else { - None - }; - let entries = if self.config.is_no_network() { - self.run_in_sandbox(contract_id, &key)? + self.run_in_sandbox()? } else { - self.run_against_rpc_server(contract_id, key).await? + self.run_against_rpc_server().await? }; self.output_entries(&entries) } - async fn run_against_rpc_server( - &self, - contract_id: [u8; 32], - maybe_key: Option, - ) -> Result, Error> { + async fn run_against_rpc_server(&self) -> Result { let network = self.config.get_network()?; tracing::trace!(?network); let network = &self.config.get_network()?; let client = Client::new(&network.rpc_url)?; - - let key = maybe_key.ok_or(Error::KeyIsRequired)?; - - let keys: Vec = match self.durability { - Some(Durability::Persistent) => { - vec![Durability::Persistent] - } - Some(Durability::Temporary) => { - vec![Durability::Temporary] - } - None => { - vec![Durability::Persistent, Durability::Temporary] - } - } - .iter() - .map(|durability| { - LedgerKey::ContractData(LedgerKeyContractData { - contract: ScAddress::Contract(Hash(contract_id)), - key: key.clone(), - durability: (*durability).into(), - }) - }) - .collect::>(); - - tracing::trace!(?keys); - - client - .get_ledger_entries(&keys) - .await? - .entries - .unwrap_or_default() - .iter() - .map(|result| { - let key = LedgerKey::from_xdr_base64(result.key.as_bytes()); - let entry = LedgerEntryData::from_xdr_base64(result.xdr.as_bytes()); - match (key, entry) { - (Ok(k), Ok(e)) => Ok((k, e)), - (Err(e), _) | (_, Err(e)) => Err(e), - } - }) - .collect::, _>>() - .map_err(Error::Xdr) + let keys = self.key.parse_keys()?; + tracing::trace!("{keys:#?}"); + Ok(client.get_full_ledger_entries(keys.as_slice()).await?) } #[allow(clippy::too_many_lines)] - fn run_in_sandbox( - &self, - contract_id: [u8; 32], - key: &Option, - ) -> Result, Error> { + fn run_in_sandbox(&self) -> Result { let state = self.config.get_state()?; let ledger_entries = &state.ledger_entries; - let contract = ScAddress::Contract(xdr::Hash(contract_id)); - let durability: Option = self.durability.map(Into::into); - - Ok(ledger_entries + let keys = self.key.parse_keys()?; + let entries = ledger_entries .iter() .map(|(k, v)| (k.as_ref().clone(), (v.0.as_ref().clone(), v.1))) - .filter(|(k, _v)| { - if let LedgerKey::ContractData(LedgerKeyContractData { contract: c, .. }) = k { - if c == &contract { - return true; - } - } - false - }) - .filter(|(k, _v)| { - if key.is_none() { - return true; - } - if let LedgerKey::ContractData(LedgerKeyContractData { key: k, .. }) = k { - if Some(k) == key.as_ref() { - return true; - } - } - false - }) - .filter(|(k, _v)| { - if durability.is_none() { - return true; - } - if let LedgerKey::ContractData(LedgerKeyContractData { durability: d, .. }) = k { - if Some(*d) == durability { - return true; - } - } - false + .filter(|(k, _v)| keys.contains(k)) + .map(|(key, (v, expiration))| { + Ok(FullLedgerEntry { + expiration: ExpirationEntry { + key_hash: Hash(Sha256::digest(key.to_xdr()?).into()), + expiration_ledger_seq: expiration.unwrap_or_default(), + }, + key, + val: v.data, + }) }) - .map(|(k, (v, _))| (k, v.data)) - .collect::>()) + .collect::, Error>>()?; + Ok(FullLedgerEntries { + entries, + latest_ledger: 0, + }) } - fn output_entries(&self, raw_entries: &[(LedgerKey, LedgerEntryData)]) -> Result<(), Error> { - let entries = raw_entries - .iter() - .filter_map(|(_k, data)| { - if let LedgerEntryData::ContractData(ContractDataEntry { key, val, .. }) = &data { - Some((key.clone(), val.clone())) - } else { - None - } - }) - .collect::>(); - - if entries.is_empty() { - return Err(Error::NoContractDataEntryFoundForContractID); - } - - let mut out = csv::Writer::from_writer(stdout()); - for (key, val) in entries { - let output = match self.output { - Output::String => [ - soroban_spec_tools::to_string(&key).map_err(|e| Error::CannotPrintResult { - result: key.clone(), - error: e, - })?, - soroban_spec_tools::to_string(&val).map_err(|e| Error::CannotPrintResult { - result: val.clone(), - error: e, - })?, - ], - Output::Json => [ - serde_json::to_string_pretty(&key).map_err(|e| { - Error::CannotPrintJsonResult { - result: key.clone(), - error: e, - } - })?, - serde_json::to_string_pretty(&val).map_err(|e| { - Error::CannotPrintJsonResult { - result: val.clone(), - error: e, - } - })?, - ], - Output::Xdr => [key.to_xdr_base64()?, val.to_xdr_base64()?], - }; - out.write_record(output) - .map_err(|e| Error::CannotPrintAsCsv { error: e })?; - } - out.flush() - .map_err(|e| Error::CannotPrintFlush { error: e })?; + fn output_entries(&self, raw_entries: &FullLedgerEntries) -> Result<(), Error> { + println!("{raw_entries:#?}"); + // let entries = raw_entries + // .iter() + // .filter_map(|(_k, data)| { + // if let LedgerEntryData::ContractData(ContractDataEntry { key, val, .. }) = &data { + // Some((key.clone(), val.clone())) + // } else { + // None + // } + // }) + // .collect::>(); + + // if entries.is_empty() { + // return Err(Error::NoContractDataEntryFoundForContractID); + // } + + // let mut out = csv::Writer::from_writer(stdout()); + // for (key, val) in entries { + // let output = match self.output { + // Output::String => [ + // soroban_spec_tools::to_string(&key).map_err(|e| Error::CannotPrintResult { + // result: key.clone(), + // error: e, + // })?, + // soroban_spec_tools::to_string(&val).map_err(|e| Error::CannotPrintResult { + // result: val.clone(), + // error: e, + // })?, + // ], + // Output::Json => [ + // serde_json::to_string_pretty(&key).map_err(|e| { + // Error::CannotPrintJsonResult { + // result: key.clone(), + // error: e, + // } + // })?, + // serde_json::to_string_pretty(&val).map_err(|e| { + // Error::CannotPrintJsonResult { + // result: val.clone(), + // error: e, + // } + // })?, + // ], + // Output::Xdr => [key.to_xdr_base64()?, val.to_xdr_base64()?], + // }; + // out.write_record(output) + // .map_err(|e| Error::CannotPrintAsCsv { error: e })?; + // } + // out.flush() + // .map_err(|e| Error::CannotPrintFlush { error: e })?; Ok(()) } } diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 2834c19950..9808c9fea8 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -1,15 +1,10 @@ -use std::{ - fmt::Debug, - path::{Path, PathBuf}, - str::FromStr, -}; +use std::{fmt::Debug, path::Path, str::FromStr}; use clap::{command, Parser}; use soroban_env_host::xdr::{ - ContractDataDurability, Error as XdrError, ExpirationEntry, ExtensionPoint, Hash, LedgerEntry, - LedgerEntryChange, LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyContractCode, - LedgerKeyContractData, Memo, MuxedAccount, Operation, OperationBody, OperationMeta, - Preconditions, ReadXdr, RestoreFootprintOp, ScAddress, ScSpecTypeDef, ScVal, SequenceNumber, + Error as XdrError, ExpirationEntry, ExtensionPoint, LedgerEntry, + LedgerEntryChange, LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, OperationMeta, + Preconditions, RestoreFootprintOp, SequenceNumber, SorobanResources, SorobanTransactionData, Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, Uint256, }; @@ -17,47 +12,15 @@ use stellar_strkey::DecodeError; use crate::{ commands::config::{self, locator}, - rpc::{self, Client}, - utils, wasm, Pwd, + key, + rpc::{self, Client}, wasm, Pwd, }; #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// Contract ID to which owns the data entries. - /// If no keys provided the Contract's instance will be restored - #[arg( - long = "id", - required_unless_present = "wasm", - required_unless_present = "wasm_hash" - )] - pub contract_id: Option, - /// Storage key (symbols only) - #[arg(long = "key")] - pub key: Vec, - /// Storage key (base64-encoded XDR) - #[arg(long = "key-xdr")] - pub key_xdr: Vec, - /// Path to Wasm file of contract code to restore - #[arg( - long, - conflicts_with = "key", - conflicts_with = "key_xdr", - conflicts_with = "contract_id", - conflicts_with = "wasm_hash" - )] - pub wasm: Option, - - /// Hash of contract code to restore - #[arg( - long = "wasm-hash", - conflicts_with = "key", - conflicts_with = "key_xdr", - conflicts_with = "contract_id", - conflicts_with = "wasm" - )] - pub wasm_hash: Option, - + #[command(flatten)] + pub key: key::Args, #[command(flatten)] pub config: config::Args, #[command(flatten)] @@ -106,6 +69,8 @@ pub enum Error { Rpc(#[from] rpc::Error), #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] + Key(#[from] key::Error), } impl Cmd { @@ -125,19 +90,7 @@ impl Cmd { pub async fn run_against_rpc_server(&self) -> Result { let network = self.config.get_network()?; tracing::trace!(?network); - let entry_keys = if let Some(wasm) = &self.wasm { - vec![crate::wasm::Args { wasm: wasm.clone() }.try_into()?] - } else if let Some(wasm_hash) = &self.wasm_hash { - vec![LedgerKey::ContractCode(LedgerKeyContractCode { - hash: Hash( - utils::contract_id_from_str(wasm_hash) - .map_err(|e| Error::CannotParseContractId(wasm_hash.clone(), e))?, - ), - })] - } else { - let contract_id = self.contract_id()?; - self.parse_keys(contract_id)? - }; + let entry_keys = self.key.parse_keys()?; let network = &self.config.get_network()?; let client = Client::new(&network.rpc_url)?; let key = self.config.key_pair()?; @@ -212,48 +165,6 @@ impl Cmd { // eviction, and restoration with that evicted state store. todo!("Restoring ledger entries is not supported in the local sandbox mode"); } - - fn contract_id(&self) -> Result<[u8; 32], Error> { - utils::contract_id_from_str(self.contract_id.as_ref().unwrap()) - .map_err(|e| Error::CannotParseContractId(self.contract_id.clone().unwrap(), e)) - } - - fn parse_keys(&self, contract_id: [u8; 32]) -> Result, Error> { - let mut keys: Vec = vec![]; - for key in &self.key { - keys.push( - soroban_spec_tools::from_string_primitive(key, &ScSpecTypeDef::Symbol).map_err( - |e| Error::CannotParseKey { - key: key.clone(), - error: e, - }, - )?, - ); - } - for key in &self.key_xdr { - keys.push( - ScVal::from_xdr_base64(key).map_err(|e| Error::CannotParseXdrKey { - key: key.clone(), - error: e, - })?, - ); - } - - if keys.is_empty() { - keys.push(ScVal::LedgerKeyContractInstance); - }; - - Ok(keys - .iter() - .map(|key| { - LedgerKey::ContractData(LedgerKeyContractData { - contract: ScAddress::Contract(Hash(contract_id)), - durability: ContractDataDurability::Persistent, - key: key.clone(), - }) - }) - .collect()) - } } fn parse_operations(ops: &[OperationMeta]) -> Option { diff --git a/cmd/soroban-cli/src/key.rs b/cmd/soroban-cli/src/key.rs new file mode 100644 index 0000000000..f6e48d0860 --- /dev/null +++ b/cmd/soroban-cli/src/key.rs @@ -0,0 +1,112 @@ +use clap::arg; +use soroban_env_host::xdr::{ + self, LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, ReadXdr, ScAddress, ScVal, +}; +use std::{ + path::{PathBuf}, +}; + +use crate::{ + commands::contract::Durability, + utils::{self}, + wasm, +}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error(transparent)] + Spec(#[from] soroban_spec_tools::Error), + #[error(transparent)] + Xdr(#[from] xdr::Error), + #[error("cannot parse contract ID {0}: {1}")] + CannotParseContractId(String, stellar_strkey::DecodeError), + #[error(transparent)] + Wasm(#[from] wasm::Error), +} + +#[derive(Debug, clap::Args, Clone)] +#[group(skip)] +pub struct Args { + /// Contract ID to which owns the data entries. + /// If no keys provided the Contract's instance will be bumped + #[arg( + long = "id", + required_unless_present = "wasm", + required_unless_present = "wasm_hash" + )] + pub contract_id: Option, + /// Storage key (symbols only) + #[arg(long = "key", conflicts_with = "key_xdr")] + pub key: Option>, + /// Storage key (base64-encoded XDR) + #[arg(long = "key-xdr", conflicts_with = "key")] + pub key_xdr: Option>, + /// Path to Wasm file of contract code to bump + #[arg( + long, + conflicts_with = "contract_id", + conflicts_with = "key", + conflicts_with = "key_xdr", + conflicts_with = "wasm_hash" + )] + pub wasm: Option, + /// Path to Wasm file of contract code to bump + #[arg( + long, + conflicts_with = "contract_id", + conflicts_with = "key", + conflicts_with = "key_xdr", + conflicts_with = "wasm" + )] + pub wasm_hash: Option, + /// Storage entry durability + #[arg(long, value_enum, required = true)] + pub durability: Durability, +} + +impl Args { + + pub fn parse_keys(&self) -> Result, Error> { + let keys = if let Some(keys) = &self.key { + keys.iter() + .map(|key| { + Ok(soroban_spec_tools::from_string_primitive( + key, + &xdr::ScSpecTypeDef::Symbol, + )?) + }) + .collect::, Error>>()? + } else if let Some(keys) = &self.key_xdr { + keys.iter() + .map(|s|Ok(ScVal::from_xdr_base64(s)?)) + .collect::, Error>>()? + } else if let Some(wasm) = &self.wasm { + return Ok(vec![crate::wasm::Args { wasm: wasm.clone() }.try_into()?]); + } else if let Some(wasm_hash) = &self.wasm_hash { + return Ok(vec![LedgerKey::ContractCode(LedgerKeyContractCode { + hash: xdr::Hash( + utils::contract_id_from_str(wasm_hash) + .map_err(|e| Error::CannotParseContractId(wasm_hash.clone(), e))?, + ), + })]); + } else { + vec![ScVal::LedgerKeyContractInstance] + }; + let contract_id = contract_id(self.contract_id.as_ref().unwrap())?; + + Ok(keys + .into_iter() + .map(|key| { + LedgerKey::ContractData(LedgerKeyContractData { + contract: ScAddress::Contract(xdr::Hash(contract_id)), + durability: (&self.durability).into(), + key, + }) + }) + .collect()) + } +} + +fn contract_id(s: &str) -> Result<[u8; 32], Error> { + utils::contract_id_from_str(s).map_err(|e| Error::CannotParseContractId(s.to_string(), e)) +} diff --git a/cmd/soroban-cli/src/lib.rs b/cmd/soroban-cli/src/lib.rs index 4bb5459d7c..1589bc4851 100644 --- a/cmd/soroban-cli/src/lib.rs +++ b/cmd/soroban-cli/src/lib.rs @@ -5,6 +5,7 @@ )] pub mod commands; pub mod fee; +pub mod key; pub mod log; pub mod network; pub mod rpc; diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index 58702e6c79..95d0f0a070 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -4,12 +4,13 @@ use jsonrpsee_core::params::ObjectParams; use jsonrpsee_core::{self, client::ClientT, rpc_params}; use jsonrpsee_http_client::{HeaderMap, HttpClient, HttpClientBuilder}; use serde_aux::prelude::{deserialize_default_from_null, deserialize_number_from_string}; +use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{ - self, AccountEntry, AccountId, ContractDataEntry, DiagnosticEvent, Error as XdrError, - LedgerEntryData, LedgerFootprint, LedgerKey, LedgerKeyAccount, PublicKey, ReadXdr, - SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, TransactionEnvelope, - TransactionMeta, TransactionMetaV3, TransactionResult, TransactionV1Envelope, Uint256, VecM, - WriteXdr, + self, AccountEntry, AccountId, ContractDataEntry, DiagnosticEvent, + Error as XdrError, ExpirationEntry, LedgerEntryData, LedgerFootprint, LedgerKey, + LedgerKeyAccount, LedgerKeyExpiration, PublicKey, ReadXdr, SequenceNumber, SorobanAuthorizationEntry, SorobanResources, Transaction, + TransactionEnvelope, TransactionMeta, TransactionMetaV3, TransactionResult, + TransactionV1Envelope, Uint256, VecM, WriteXdr, }; use soroban_env_host::xdr::{DepthLimitedRead, SorobanAuthorizedFunction}; use soroban_sdk::token; @@ -39,7 +40,7 @@ pub type LogResources = fn(resources: &SorobanResources) -> (); #[derive(thiserror::Error, Debug)] pub enum Error { - #[error("invalid address: {0}")] + #[error(transparent)] InvalidAddress(#[from] stellar_strkey::DecodeError), #[error("invalid response from server")] InvalidResponse, @@ -95,6 +96,8 @@ pub enum Error { SpecBase64(#[from] soroban_spec::read::ParseSpecBase64Error), #[error("Fee was too large {0}")] LargeFee(u64), + #[error("Failed to parse LedgerEntryData")] + FailedParseLedgerEntryData, } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -150,8 +153,8 @@ pub struct LedgerEntryResult { #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct GetLedgerEntriesResponse { pub entries: Option>, - #[serde(rename = "latestLedger")] - pub latest_ledger: String, + #[serde(rename = "latestLedger", deserialize_with = "deserialize_number_from_string")] + pub latest_ledger: i64, } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -417,6 +420,19 @@ pub enum EventStart { Cursor(String), } +#[derive(Debug)] +pub struct FullLedgerEntry { + pub key: LedgerKey, + pub val: LedgerEntryData, + pub expiration: ExpirationEntry, +} + +#[derive(Debug)] +pub struct FullLedgerEntries { + pub entries: Vec, + pub latest_ledger: i64, +} + pub struct Client { base_url: String, } @@ -742,6 +758,47 @@ soroban config identity fund {address} --helper-url "# .await?) } + pub async fn get_full_ledger_entries( + &self, + ledger_keys: &[LedgerKey], + ) -> Result { + let keys = ledger_keys + .iter() + .map(|key| Ok(into_keys(key.clone())?.into_iter())) + .flatten_ok() + .collect::, Error>>()?; + tracing::trace!("{keys:#?}"); + let GetLedgerEntriesResponse { + entries, + latest_ledger, + } = self.get_ledger_entries(&keys).await?; + tracing::trace!(?entries); + let entries = entries + .as_deref() + .unwrap_or_default() + .iter() + .tuple_windows() + .map(|(key_res, entry_res)| { + let expiration = LedgerEntryData::from_xdr_base64(&entry_res.xdr)?; + if let LedgerEntryData::Expiration(expiration) = expiration { + let key = LedgerKey::from_xdr_base64(&key_res.key)?; + let val = LedgerEntryData::from_xdr_base64(&key_res.xdr)?; + Ok(FullLedgerEntry { + key, + val, + expiration, + }) + } else { + Err(Error::FailedParseLedgerEntryData) + } + }) + .collect::, Error>>()?; + Ok(FullLedgerEntries { + entries, + latest_ledger, + }) + } + pub async fn get_events( &self, start: EventStart, @@ -892,6 +949,13 @@ pub fn parse_cursor(c: &str) -> Result<(u64, i32), Error> { Ok((toid_part, start_index)) } +fn into_keys(key: LedgerKey) -> Result<[LedgerKey; 2], Error> { + let expiration = LedgerKey::Expiration(LedgerKeyExpiration { + key_hash: xdr::Hash(Sha256::digest(key.to_xdr()?).into()), + }); + Ok([key, expiration]) +} + #[cfg(test)] mod tests { use super::*;