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/soroban-cli/src/commands/contract/bump.rs b/cmd/soroban-cli/src/commands/contract/bump.rs index 3fc63f4f2..3ee74af62 100644 --- a/cmd/soroban-cli/src/commands/contract/bump.rs +++ b/cmd/soroban-cli/src/commands/contract/bump.rs @@ -1,41 +1,38 @@ -use std::{ - fmt::Debug, - path::{Path}, - 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, - LedgerEntry, LedgerEntryChange, LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, - Preconditions, 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 crate::{ commands::config, - rpc::{self, Client}, wasm, Pwd, key, + key, + rpc::{self, Client}, + wasm, Pwd, }; +const MAX_LEDGERS_TO_EXPIRE: u32 = 535_679; + #[derive(Parser, Debug, Clone)] #[group(skip)] pub struct Cmd { - /// 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)] - expiration_ledger_only: bool, + 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, } @@ -100,6 +97,16 @@ impl Cmd { 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); @@ -107,6 +114,7 @@ impl Cmd { 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(); @@ -123,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()?, @@ -131,7 +139,7 @@ impl Cmd { ext: ExtensionPoint::V0, resources: SorobanResources { footprint: LedgerFootprint { - read_only: keys.try_into()?, + read_only: keys.clone().try_into()?, read_write: vec![].try_into()?, }, instructions: 0, @@ -164,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]) { @@ -226,5 +238,3 @@ impl Cmd { Ok(new_expiration_ledger_seq) } } - - diff --git a/cmd/soroban-cli/src/commands/contract/install.rs b/cmd/soroban-cli/src/commands/contract/install.rs index 809f6106d..80bbacac2 100644 --- a/cmd/soroban-cli/src/commands/contract/install.rs +++ b/cmd/soroban-cli/src/commands/contract/install.rs @@ -123,6 +123,7 @@ impl Cmd { }, 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/read.rs b/cmd/soroban-cli/src/commands/contract/read.rs index 207119b02..c0f40291c 100644 --- a/cmd/soroban-cli/src/commands/contract/read.rs +++ b/cmd/soroban-cli/src/commands/contract/read.rs @@ -1,14 +1,15 @@ use std::{ convert::Into, fmt::Debug, - io::{self}, + io::{self, stdout}, }; use clap::{command, Parser, ValueEnum}; use sha2::{Digest, Sha256}; use soroban_env_host::{ xdr::{ - Error as XdrError, ExpirationEntry, Hash, ScVal, WriteXdr, + ContractDataEntry, Error as XdrError, ExpirationEntry, Hash, LedgerEntryData, LedgerKey, + LedgerKeyContractData, ScVal, WriteXdr, }, HostError, }; @@ -85,6 +86,8 @@ pub enum Error { NoContractDataEntryFoundForContractID, #[error(transparent)] Key(#[from] key::Error), + #[error("Only contract data and code keys are allowed")] + OnlyDataAllowed, } impl Cmd { @@ -103,8 +106,7 @@ impl Cmd { let network = &self.config.get_network()?; let client = Client::new(&network.rpc_url)?; let keys = self.key.parse_keys()?; - tracing::trace!("{keys:#?}"); - Ok(client.get_full_ledger_entries(keys.as_slice()).await?) + Ok(client.get_full_ledger_entries(&keys).await?) } #[allow(clippy::too_many_lines)] @@ -134,57 +136,70 @@ impl Cmd { }) } - 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::>(); + 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 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; - // 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 })?; + 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, + })?, + expiration.to_string(), + ], + Output::Json => [ + serde_json::to_string_pretty(&key).map_err(|error| { + Error::CannotPrintJsonResult { + result: key.clone(), + error, + } + })?, + serde_json::to_string_pretty(&val).map_err(|error| { + Error::CannotPrintJsonResult { + result: val.clone(), + error, + } + })?, + serde_json::to_string_pretty(&expiration).map_err(|error| { + Error::CannotPrintJsonResult { + result: val.clone(), + error, + } + })?, + ], + 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 })?; + } + 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 9808c9fea..bf1f761d0 100644 --- a/cmd/soroban-cli/src/commands/contract/restore.rs +++ b/cmd/soroban-cli/src/commands/contract/restore.rs @@ -2,18 +2,21 @@ use std::{fmt::Debug, path::Path, str::FromStr}; use clap::{command, Parser}; use soroban_env_host::xdr::{ - Error as XdrError, ExpirationEntry, ExtensionPoint, LedgerEntry, - LedgerEntryChange, LedgerEntryData, LedgerFootprint, Memo, MuxedAccount, Operation, OperationBody, OperationMeta, - Preconditions, RestoreFootprintOp, 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}, wasm, Pwd, + rpc::{self, Client}, + wasm, Pwd, }; #[derive(Parser, Debug, Clone)] @@ -21,6 +24,9 @@ use crate::{ pub struct Cmd { #[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)] @@ -71,6 +77,8 @@ pub enum Error { Wasm(#[from] wasm::Error), #[error(transparent)] Key(#[from] key::Error), + #[error(transparent)] + Bump(#[from] bump::Error), } impl Cmd { @@ -82,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(()) } diff --git a/cmd/soroban-cli/src/key.rs b/cmd/soroban-cli/src/key.rs index f6e48d086..d48c61683 100644 --- a/cmd/soroban-cli/src/key.rs +++ b/cmd/soroban-cli/src/key.rs @@ -2,9 +2,7 @@ use clap::arg; use soroban_env_host::xdr::{ self, LedgerKey, LedgerKeyContractCode, LedgerKeyContractData, ReadXdr, ScAddress, ScVal, }; -use std::{ - path::{PathBuf}, -}; +use std::path::PathBuf; use crate::{ commands::contract::Durability, @@ -65,7 +63,6 @@ pub struct Args { } impl Args { - pub fn parse_keys(&self) -> Result, Error> { let keys = if let Some(keys) = &self.key { keys.iter() @@ -78,7 +75,7 @@ impl Args { .collect::, Error>>()? } else if let Some(keys) = &self.key_xdr { keys.iter() - .map(|s|Ok(ScVal::from_xdr_base64(s)?)) + .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()?]); diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index 95d0f0a07..551186cfc 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -6,11 +6,11 @@ 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, ExpirationEntry, LedgerEntryData, LedgerFootprint, LedgerKey, - LedgerKeyAccount, LedgerKeyExpiration, 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; @@ -153,7 +153,10 @@ pub struct LedgerEntryResult { #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct GetLedgerEntriesResponse { pub entries: Option>, - #[serde(rename = "latestLedger", deserialize_with = "deserialize_number_from_string")] + #[serde( + rename = "latestLedger", + deserialize_with = "deserialize_number_from_string" + )] pub latest_ledger: i64, } diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go index e9d7484d3..c33f1c669 100644 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ b/cmd/soroban-rpc/internal/test/cli_test.go @@ -95,25 +95,6 @@ func TestCLIBump(t *testing.T) { expirationKey := getExpirationKeyForCounterLedgerEntry(t, strkeyContractID) initialExpirationSeq := getExpirationForLedgerEntry(t, client, expirationKey) - // Bump the contract's instance - // Without error it is successful - initialContractExpirationSeq := runSuccessfulCLICmd( - t, - fmt.Sprintf( - "contract bump --expiration-ledger-only --id %s --durability persistent --ledgers-to-expire 15", - strkeyContractID, - ), - ) - - newContractExpirationSeq := runSuccessfulCLICmd( - t, - fmt.Sprintf( - "contract bump --expiration-ledger-only --id %s --durability persistent --ledgers-to-expire 30", - strkeyContractID, - ), - ) - require.Greater(t, parseInt(t, newContractExpirationSeq), parseInt(t, initialContractExpirationSeq)) - bumpOutput := runSuccessfulCLICmd( t, fmt.Sprintf( @@ -126,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 := getExpirationForLedgerEntry(t, client, expirationKey) + + bumpOutput := bump(t, strkeyContractID, "400", "--key COUNTER ") + + newExpirationSeq := getExpirationForLedgerEntry(t, client, expirationKey) + 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 := getExpirationForLedgerEntry(t, client, expirationKey) + + bumpOutput := bump(t, strkeyContractID, "100000000", "--key COUNTER ") + + newExpirationSeq := getExpirationForLedgerEntry(t, client, expirationKey) + assert.Greater(t, newExpirationSeq, initialExpirationSeq) + assert.Equal(t, newExpirationSeq, bumpOutput) +} func TestCLIRestore(t *testing.T) { test := NewCLITest(t) @@ -230,3 +251,18 @@ func parseInt(t *testing.T, s string) uint64 { 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) +}