From 3e201ada4ba23455b07f06b7f9b3e19a85039d1c Mon Sep 17 00:00:00 2001 From: Willem Wyndham Date: Fri, 22 Sep 2023 10:48:04 -0400 Subject: [PATCH] Feat(CLI): add `key::Args` and `FullLedgerEntry`/`FullLedgerEntries` (#981) * fix: Upgrade jsonprsee fixes #976 * feat: add expiration_ledger_only for bump and add test for contract bump * feat: add key::Args and FullLedgerEntry * fix: add max bump and return current expiration if bump is too small * fix: parseInt * fix: update tests with new read output and add durability arg --- Cargo.lock | 97 ++++++-- cmd/crates/soroban-spec-tools/src/lib.rs | 3 +- .../soroban-test/tests/it/contract_sandbox.rs | 7 +- cmd/soroban-cli/Cargo.toml | 4 +- cmd/soroban-cli/src/commands/contract/bump.rs | 149 ++++------- .../src/commands/contract/install.rs | 15 +- cmd/soroban-cli/src/commands/contract/mod.rs | 4 +- cmd/soroban-cli/src/commands/contract/read.rs | 231 ++++++------------ .../src/commands/contract/restore.rs | 137 +++-------- cmd/soroban-cli/src/key.rs | 109 +++++++++ cmd/soroban-cli/src/lib.rs | 1 + cmd/soroban-cli/src/rpc/mod.rs | 81 +++++- cmd/soroban-rpc/internal/test/cli_test.go | 64 ++++- docs/soroban-cli-full-docs.md | 48 ++-- 14 files changed, 522 insertions(+), 428 deletions(-) create mode 100644 cmd/soroban-cli/src/key.rs diff --git a/Cargo.lock b/Cargo.lock index 09fd3aed7..86bee737e 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", ] @@ -563,9 +563,9 @@ dependencies = [ [[package]] name = "curve25519-dalek" -version = "4.1.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622178105f911d937a42cdb140730ba4a3ed2becd8ae6ce39c7d28b5d75d4588" +checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c" dependencies = [ "cfg-if", "cpufeatures", @@ -822,7 +822,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7277392b266383ef8396db7fdeb1e77b6c52fed775f5df15bb24f35b72156980" dependencies = [ - "curve25519-dalek 4.1.0", + "curve25519-dalek 4.1.1", "ed25519 2.2.2", "rand_core 0.6.4", "serde", @@ -940,6 +940,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "form_urlencoded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a62bc1cf6f830c2ec14a513a9fb124d0a213a629668a4186f329db21fe045652" +dependencies = [ + "percent-encoding", +] + [[package]] name = "fs_extra" version = "1.3.0" @@ -1120,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" @@ -1277,6 +1286,16 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "ignore" version = "0.4.20" @@ -1386,9 +1405,9 @@ dependencies = [ [[package]] name = "jsonrpsee-core" -version = "0.18.2" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64c6832a55f662b5a6ecc844db24b8b9c387453f923de863062c60ce33d62b81" +checksum = "35dc957af59ce98373bcdde0c1698060ca6c2d2e9ae357b459c7158b6df33330" dependencies = [ "anyhow", "async-trait", @@ -1405,9 +1424,9 @@ dependencies = [ [[package]] name = "jsonrpsee-http-client" -version = "0.18.2" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1705c65069729e3dccff6fd91ee431d5d31cabcf00ce68a62a2c6435ac713af9" +checksum = "0dd865d0072764cb937b0110a92b5f53e995f7101cb346beca03d93a2dea79de" dependencies = [ "async-trait", "hyper", @@ -1420,13 +1439,14 @@ dependencies = [ "tokio", "tower", "tracing", + "url", ] [[package]] name = "jsonrpsee-types" -version = "0.18.2" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e5bf6c75ce2a4217421154adfc65a24d2b46e77286e59bba5d9fa6544ccc8f4" +checksum = "fa9e25aec855b2a7d3ed90fded6c41e8c3fb72b63f071e1be3f0004eba19b625" dependencies = [ "anyhow", "beef", @@ -1683,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", ] @@ -1753,6 +1773,12 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "percent-encoding" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" + [[package]] name = "pin-project" version = "1.1.3" @@ -2119,9 +2145,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.13" +version = "0.38.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" +checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" dependencies = [ "bitflags 2.4.0", "errno", @@ -2165,9 +2191,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.101.5" +version = "0.101.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a27e3b59326c16e23d30aeb7a36a24cc0d29e71d68ff611cdfb4a01d013bed" +checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe" dependencies = [ "ring", "untrusted", @@ -2450,9 +2476,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" +checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" [[package]] name = "socket2" @@ -3105,9 +3131,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.8" +version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d" +checksum = "1d68074620f57a0b21594d9735eb2e98ab38b17f80d3fcb189fca266771ca60d" dependencies = [ "bytes", "futures-core", @@ -3239,6 +3265,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + [[package]] name = "unicode-ident" version = "1.0.12" @@ -3256,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" @@ -3266,6 +3298,17 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "url" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" @@ -3508,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/crates/soroban-spec-tools/src/lib.rs b/cmd/crates/soroban-spec-tools/src/lib.rs index be5580196..dbac121a8 100644 --- a/cmd/crates/soroban-spec-tools/src/lib.rs +++ b/cmd/crates/soroban-spec-tools/src/lib.rs @@ -905,6 +905,7 @@ pub fn to_string(v: &ScVal) -> Result { ScVal::Symbol(v) => std::str::from_utf8(v.as_slice()) .map_err(|_| Error::InvalidValue(Some(ScType::Symbol)))? .to_string(), + ScVal::LedgerKeyContractInstance => "LedgerKeyContractInstance".to_string(), _ => serde_json::to_string(&to_json(v)?)?, }) } @@ -918,7 +919,7 @@ pub fn to_json(v: &ScVal) -> Result { let val: Value = match v { ScVal::Bool(b) => Value::Bool(*b), ScVal::Void => Value::Null, - ScVal::LedgerKeyContractInstance => return Err(Error::InvalidValue(None)), + ScVal::LedgerKeyContractInstance => Value::String("LedgerKeyContractInstance".to_string()), ScVal::U64(v) => Value::Number(serde_json::Number::from(*v)), ScVal::Timepoint(tp) => Value::Number(serde_json::Number::from(tp.0)), ScVal::Duration(d) => Value::Number(serde_json::Number::from(d.0)), diff --git a/cmd/crates/soroban-test/tests/it/contract_sandbox.rs b/cmd/crates/soroban-test/tests/it/contract_sandbox.rs index df0088b3c..5acfe90d4 100644 --- a/cmd/crates/soroban-test/tests/it/contract_sandbox.rs +++ b/cmd/crates/soroban-test/tests/it/contract_sandbox.rs @@ -271,6 +271,7 @@ fn contract_data_read_failure() { .arg("read") .arg("--id=1") .arg("--key=COUNTER") + .arg("--durability=persistent") .assert() .failure() .stderr( @@ -315,9 +316,10 @@ fn contract_data_read() { .arg("read") .arg("--id=1") .arg("--key=COUNTER") + .arg("--durability=persistent") .assert() .success() - .stdout("COUNTER,1\n"); + .stdout("COUNTER,1,4096\n"); sandbox .new_assert_cmd("contract") @@ -333,9 +335,10 @@ fn contract_data_read() { .arg("read") .arg("--id=1") .arg("--key=COUNTER") + .arg("--durability=persistent") .assert() .success() - .stdout("COUNTER,2\n"); + .stdout("COUNTER,2,4096\n"); } #[test] diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index 11ecca1c7..a4d28c822 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -67,8 +67,8 @@ wasmparser = { workspace = true } sha2 = { workspace = true } csv = "1.1.6" ed25519-dalek = "1.0.1" -jsonrpsee-http-client = "0.18.1" -jsonrpsee-core = "0.18.1" +jsonrpsee-http-client = "0.20.1" +jsonrpsee-core = "0.20.1" hyper = "0.14.27" hyper-tls = "0.5" http = "0.2.9" diff --git a/cmd/soroban-cli/src/commands/contract/bump.rs b/cmd/soroban-cli/src/commands/contract/bump.rs index 126c3ac09..3ee74af62 100644 --- a/cmd/soroban-cli/src/commands/contract/bump.rs +++ b/cmd/soroban-cli/src/commands/contract/bump.rs @@ -1,72 +1,38 @@ -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::{ - 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, - SorobanTransactionData, Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, - Uint256, + 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, + key, rpc::{self, Client}, - utils, wasm, Pwd, + wasm, Pwd, }; +const MAX_LEDGERS_TO_EXPIRE: u32 = 535_679; + #[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)] - ledgers_to_expire: u32, + pub ledgers_to_expire: u32, + + /// Only print the new expiration ledger + #[arg(long)] + pub expiration_ledger_only: bool, + + #[command(flatten)] + pub key: key::Args, #[command(flatten)] - config: config::Args, + pub config: config::Args, #[command(flatten)] pub fee: crate::fee::Args, } @@ -95,8 +61,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")] @@ -111,6 +76,8 @@ pub enum Error { Rpc(#[from] rpc::Error), #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] + Key(#[from] key::Error), } impl Cmd { @@ -121,19 +88,33 @@ impl Cmd { } else { self.run_against_rpc_server().await? }; - - println!("New expiration ledger: {expiration_ledger_seq}"); + if self.expiration_ledger_only { + println!("{expiration_ledger_seq}"); + } else { + println!("New expiration ledger: {expiration_ledger_seq}"); + } Ok(()) } + fn ledgers_to_expire(&self) -> u32 { + let res = u32::min(self.ledgers_to_expire, MAX_LEDGERS_TO_EXPIRE); + if res < self.ledgers_to_expire { + tracing::warn!( + "Ledgers to expire is too large, using max value of {MAX_LEDGERS_TO_EXPIRE}" + ); + } + res + } + 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()?; + let ledgers_to_expire = self.ledgers_to_expire(); // Get the account sequence number let public_strkey = stellar_strkey::ed25519::PublicKey(key.public.to_bytes()).to_string(); @@ -150,7 +131,7 @@ impl Cmd { source_account: None, body: OperationBody::BumpFootprintExpiration(BumpFootprintExpirationOp { ext: ExtensionPoint::V0, - ledgers_to_expire: self.ledgers_to_expire, + ledgers_to_expire, }), }] .try_into()?, @@ -158,7 +139,7 @@ impl Cmd { ext: ExtensionPoint::V0, resources: SorobanResources { footprint: LedgerFootprint { - read_only: vec![needle].try_into()?, + read_only: keys.clone().try_into()?, read_write: vec![].try_into()?, }, instructions: 0, @@ -191,8 +172,12 @@ impl Cmd { return Err(Error::LedgerEntryNotFound); } - if operations[0].changes.len() != 2 { - return Err(Error::LedgerEntryNotFound); + if operations[0].changes.is_empty() { + let entry = client.get_full_ledger_entries(&keys).await?; + let expire = entry.entries[0].expiration.expiration_ledger_seq; + if entry.latest_ledger + i64::from(ledgers_to_expire) < i64::from(expire) { + return Ok(expire); + } } match (&operations[0].changes[0], &operations[0].changes[1]) { @@ -212,7 +197,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 @@ -231,7 +216,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); @@ -252,42 +237,4 @@ 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 c5df60e4c..80bbacac2 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,13 +113,17 @@ 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(), + ledgers_to_expire: None, } .run_against_rpc_server() .await?; diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 0dce3a7cd..c2dc1a677 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 4a4d9ea3c..c0f40291c 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -5,41 +5,29 @@ use std::{ }; 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, + ContractDataEntry, Error as XdrError, ExpirationEntry, Hash, LedgerEntryData, LedgerKey, + LedgerKeyContractData, 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 +76,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,191 +84,116 @@ 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), + #[error("Only contract data and code keys are allowed")] + OnlyDataAllowed, } 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()?; + Ok(client.get_full_ledger_entries(&keys).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() { + fn output_entries(&self, entries: &FullLedgerEntries) -> Result<(), Error> { + if entries.entries.is_empty() { return Err(Error::NoContractDataEntryFoundForContractID); } - + tracing::trace!("{entries:#?}"); let mut out = csv::Writer::from_writer(stdout()); - for (key, val) in entries { + for FullLedgerEntry { + key, + val, + expiration, + } in &entries.entries + { + let ( + LedgerKey::ContractData(LedgerKeyContractData { key, .. }), + LedgerEntryData::ContractData(ContractDataEntry { val, .. }), + ) = (key, val) + else { + return Err(Error::OnlyDataAllowed); + }; + let expiration = expiration.expiration_ledger_seq; + let output = match self.output { Output::String => [ - soroban_spec_tools::to_string(&key).map_err(|e| Error::CannotPrintResult { + 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 { + soroban_spec_tools::to_string(val).map_err(|e| Error::CannotPrintResult { result: val.clone(), error: e, })?, + expiration.to_string(), ], Output::Json => [ - serde_json::to_string_pretty(&key).map_err(|e| { + serde_json::to_string_pretty(&key).map_err(|error| { Error::CannotPrintJsonResult { result: key.clone(), - error: e, + error, + } + })?, + serde_json::to_string_pretty(&val).map_err(|error| { + Error::CannotPrintJsonResult { + result: val.clone(), + error, } })?, - serde_json::to_string_pretty(&val).map_err(|e| { + serde_json::to_string_pretty(&expiration).map_err(|error| { Error::CannotPrintJsonResult { result: val.clone(), - error: e, + error, } })?, ], - Output::Xdr => [key.to_xdr_base64()?, val.to_xdr_base64()?], + Output::Xdr => [ + key.to_xdr_base64()?, + val.to_xdr_base64()?, + expiration.to_xdr_base64()?, + ], }; out.write_record(output) .map_err(|e| Error::CannotPrintAsCsv { error: e })?; diff --git a/cmd/soroban-cli/src/commands/contract/restore.rs b/cmd/soroban-cli/src/commands/contract/restore.rs index 2834c1995..bf1f761d0 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -1,63 +1,32 @@ -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, - SorobanResources, SorobanTransactionData, Transaction, TransactionExt, TransactionMeta, - TransactionMetaV3, Uint256, + Error as XdrError, ExpirationEntry, ExtensionPoint, LedgerEntry, LedgerEntryChange, + LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, OperationMeta, + Preconditions, RestoreFootprintOp, SequenceNumber, SorobanResources, SorobanTransactionData, + Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, Uint256, }; use stellar_strkey::DecodeError; use crate::{ - commands::config::{self, locator}, + commands::{ + config::{self, locator}, + contract::bump, + }, + key, rpc::{self, Client}, - utils, wasm, Pwd, + 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, + /// Number of ledgers to extend the entry + #[arg(long)] + pub ledgers_to_expire: Option, #[command(flatten)] pub config: config::Args, #[command(flatten)] @@ -106,6 +75,10 @@ pub enum Error { Rpc(#[from] rpc::Error), #[error(transparent)] Wasm(#[from] wasm::Error), + #[error(transparent)] + Key(#[from] key::Error), + #[error(transparent)] + Bump(#[from] bump::Error), } impl Cmd { @@ -117,7 +90,19 @@ impl Cmd { self.run_against_rpc_server().await? }; - println!("New expiration ledger: {expiration_ledger_seq}"); + if let Some(ledgers_to_expire) = self.ledgers_to_expire { + bump::Cmd { + key: self.key.clone(), + ledgers_to_expire, + config: self.config.clone(), + fee: self.fee.clone(), + expiration_ledger_only: false, + } + .run() + .await?; + } else { + println!("New expiration ledger: {expiration_ledger_seq}"); + } Ok(()) } @@ -125,19 +110,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 +185,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 000000000..d48c61683 --- /dev/null +++ b/cmd/soroban-cli/src/key.rs @@ -0,0 +1,109 @@ +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 4bb5459d7..1589bc485 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 58702e6c7..551186cfc 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, + 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,11 @@ 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 +423,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 +761,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 +952,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::*; diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index 58a236f41..8ceb2f213 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "os" + "strconv" "strings" "testing" @@ -106,6 +107,46 @@ func TestCLIBump(t *testing.T) { assert.Greater(t, newExpirationSeq, initialExpirationSeq) assert.Equal(t, fmt.Sprintf("New expiration ledger: %d", newExpirationSeq), bumpOutput) } +func TestCLIBumpTooLow(t *testing.T) { + test := NewCLITest(t) + strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) + count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) + require.Equal(t, "1", count) + + ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) + client := jrpc2.NewClient(ch, nil) + + expirationKey := getExpirationKeyForCounterLedgerEntry(t, strkeyContractID) + initialExpirationSeq := parseInt(t, getExpirationForLedgerEntry(t, client, expirationKey).GoString()) + + bumpOutput := bump(t, strkeyContractID, "400", "--key COUNTER ") + + newExpirationSeq := parseInt(t, getExpirationForLedgerEntry(t, client, expirationKey).GoString()) + assert.Greater(t, newExpirationSeq, initialExpirationSeq) + assert.Equal(t, newExpirationSeq, bumpOutput) + + updatedExpirationSeq := bump(t, strkeyContractID, "15", "--key COUNTER") + assert.Equal(t, bumpOutput, updatedExpirationSeq) +} + +func TestCLIBumpTooHigh(t *testing.T) { + test := NewCLITest(t) + strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) + count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) + require.Equal(t, "1", count) + + ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) + client := jrpc2.NewClient(ch, nil) + + expirationKey := getExpirationKeyForCounterLedgerEntry(t, strkeyContractID) + initialExpirationSeq := parseInt(t, getExpirationForLedgerEntry(t, client, expirationKey).GoString()) + + bumpOutput := bump(t, strkeyContractID, "100000000", "--key COUNTER ") + + newExpirationSeq := parseInt(t, getExpirationForLedgerEntry(t, client, expirationKey).GoString()) + assert.Greater(t, newExpirationSeq, initialExpirationSeq) + assert.Equal(t, newExpirationSeq, bumpOutput) +} func TestCLIRestore(t *testing.T) { test := NewCLITest(t) @@ -125,7 +166,7 @@ func TestCLIRestore(t *testing.T) { restoreOutput := runSuccessfulCLICmd( t, fmt.Sprintf( - "contract restore --id %s --key COUNTER", + "contract restore --id %s --key COUNTER --durability persistent", strkeyContractID, ), ) @@ -204,3 +245,24 @@ func fundAccount(t *testing.T, test *Test, account string, amount string) { require.NoError(t, err) sendSuccessfulTransaction(t, client, sourceAccount, tx) } + +func parseInt(t *testing.T, s string) uint64 { + i, err := strconv.ParseUint(strings.TrimSpace(s), 10, 64) + require.NoError(t, err) + return i +} + +func bump(t *testing.T, contractId string, amount string, rest string) uint64 { + + res := runSuccessfulCLICmd( + t, + fmt.Sprintf( + "contract bump --expiration-ledger-only --id=%s --durability persistent --ledgers-to-expire=%s %s", + contractId, + amount, + rest, + ), + ) + + return parseInt(t, res) +} diff --git a/docs/soroban-cli-full-docs.md b/docs/soroban-cli-full-docs.md index ea17a2183..ce09608ed 100644 --- a/docs/soroban-cli-full-docs.md +++ b/docs/soroban-cli-full-docs.md @@ -230,10 +230,12 @@ Extend the expiry ledger of a contract-data ledger entry. If no keys are specified the contract itself is bumped. -**Usage:** `soroban contract bump [OPTIONS] --durability --ledgers-to-expire ` +**Usage:** `soroban contract bump [OPTIONS] --ledgers-to-expire --durability ` ###### **Options:** +* `--ledgers-to-expire ` — Number of ledgers to extend the entries +* `--expiration-ledger-only` — Only print the new expiration ledger * `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be bumped * `--key ` — Storage key (symbols only) * `--key-xdr ` — Storage key (base64-encoded XDR) @@ -247,7 +249,6 @@ If no keys are specified the contract itself is bumped. - `temporary`: Temporary -* `--ledgers-to-expire ` — Number of ledgers to extend the entries * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server * `--network ` — Name of network to use from config @@ -408,21 +409,10 @@ Optimize a WASM file Print the current value of a contract-data ledger entry -**Usage:** `soroban contract read [OPTIONS] --id ` +**Usage:** `soroban contract read [OPTIONS] --durability ` ###### **Options:** -* `--id ` — Contract ID to invoke -* `--key ` — Storage key (symbols only) -* `--key-xdr ` — Storage key (base64-encoded XDR ScVal) -* `--durability ` — Storage entry durability - - Possible values: - - `persistent`: - Persistent - - `temporary`: - Temporary - * `--output ` — Type of output to generate Default value: `string` @@ -435,6 +425,19 @@ Print the current value of a contract-data ledger entry - `xdr`: XDR +* `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be bumped +* `--key ` — Storage key (symbols only) +* `--key-xdr ` — Storage key (base64-encoded XDR) +* `--wasm ` — Path to Wasm file of contract code to bump +* `--wasm-hash ` — Path to Wasm file of contract code to bump +* `--durability ` — Storage entry durability + + Possible values: + - `persistent`: + Persistent + - `temporary`: + Temporary + * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server * `--network ` — Name of network to use from config @@ -452,15 +455,24 @@ Restore an evicted value for a contract-data legder entry. If no keys are specificed the contract itself is restored. -**Usage:** `soroban contract restore [OPTIONS]` +**Usage:** `soroban contract restore [OPTIONS] --durability ` ###### **Options:** -* `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be restored +* `--id ` — Contract ID to which owns the data entries. If no keys provided the Contract's instance will be bumped * `--key ` — Storage key (symbols only) * `--key-xdr ` — Storage key (base64-encoded XDR) -* `--wasm ` — Path to Wasm file of contract code to restore -* `--wasm-hash ` — Hash of contract code to restore +* `--wasm ` — Path to Wasm file of contract code to bump +* `--wasm-hash ` — Path to Wasm file of contract code to bump +* `--durability ` — Storage entry durability + + Possible values: + - `persistent`: + Persistent + - `temporary`: + Temporary + +* `--ledgers-to-expire ` — Number of ledgers to extend the entry * `--rpc-url ` — RPC server endpoint * `--network-passphrase ` — Network passphrase to sign the transaction sent to the rpc server * `--network ` — Name of network to use from config