diff --git a/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml index 8ec1b35c5d..bbd1c647cc 100644 --- a/.github/workflows/soroban-rpc.yml +++ b/.github/workflows/soroban-rpc.yml @@ -112,7 +112,7 @@ jobs: env: SOROBAN_RPC_INTEGRATION_TESTS_ENABLED: true SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 19.14.1-1547.f2d06fbce.focal + PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 19.14.1-1553.f4c4e2fca.focal steps: - uses: actions/checkout@v3 with: diff --git a/cmd/crates/soroban-spec-typescript/src/lib.rs b/cmd/crates/soroban-spec-typescript/src/lib.rs index 7dff047563..5f7fcdc212 100644 --- a/cmd/crates/soroban-spec-typescript/src/lib.rs +++ b/cmd/crates/soroban-spec-typescript/src/lib.rs @@ -192,12 +192,12 @@ pub fn entry_to_ts(entry: &Entry) -> String { output = format!(r#"return {output};"#); }; let parse_result_xdr = if return_type == "void" { - r#"parseResultXdr: () => {}"#.to_owned() + r"parseResultXdr: () => {}".to_owned() } else { format!( - r#"parseResultXdr: (xdr): {return_type} => {{ + r"parseResultXdr: (xdr): {return_type} => {{ {output} - }}"# + }}" ) }; let js_name = jsify_name(name); diff --git a/cmd/crates/soroban-spec-typescript/src/project_template/package.json b/cmd/crates/soroban-spec-typescript/src/project_template/package.json index 168e942586..d75b89c74c 100644 --- a/cmd/crates/soroban-spec-typescript/src/project_template/package.json +++ b/cmd/crates/soroban-spec-typescript/src/project_template/package.json @@ -4,7 +4,7 @@ "dependencies": { "@stellar/freighter-api": "1.5.1", "buffer": "6.0.3", - "soroban-client": "1.0.0-beta.2" + "soroban-client": "1.0.0-beta.3" }, "scripts": { "build": "node ./scripts/build.mjs" diff --git a/cmd/crates/soroban-spec-typescript/src/project_template/src/invoke.ts b/cmd/crates/soroban-spec-typescript/src/project_template/src/invoke.ts index d69166ce12..27a85c0ac5 100644 --- a/cmd/crates/soroban-spec-typescript/src/project_template/src/invoke.ts +++ b/cmd/crates/soroban-spec-typescript/src/project_template/src/invoke.ts @@ -17,6 +17,7 @@ import type { export type Tx = Transaction, Operation[]>; +export class SendFailedError extends Error { } /** * Get account details from the Soroban network for the publicKey currently * selected in Freighter. If not connected to Freighter, return null. @@ -82,7 +83,7 @@ export async function invoke({ contractId, wallet, }: InvokeArgs): Promise { - wallet = wallet ?? (await import("@stellar/freighter-api")); + wallet = wallet ?? (await import("@stellar/freighter-api")).default; let parse = parseResultXdr; const server = new SorobanClient.Server(rpcUrl, { allowHttp: rpcUrl.startsWith("http://"), @@ -162,7 +163,11 @@ export async function invoke({ if ("returnValue" in raw) return parse(raw.returnValue!); // otherwise, it returned the result of `sendTransaction` - if ("errorResultXdr" in raw) return parse(raw.errorResultXdr!); + if ("errorResult" in raw) { + throw new SendFailedError( + `errorResult.result(): ${JSON.stringify(raw.errorResult?.result())}` + ) + } // if neither of these are present, something went wrong console.error("Don't know how to parse result! Returning full RPC response."); diff --git a/cmd/crates/soroban-test/tests/it/arg_parsing.rs b/cmd/crates/soroban-test/tests/it/arg_parsing.rs index fd685b5a9b..c245fd8c96 100644 --- a/cmd/crates/soroban-test/tests/it/arg_parsing.rs +++ b/cmd/crates/soroban-test/tests/it/arg_parsing.rs @@ -90,7 +90,7 @@ fn parse_i256() { #[test] fn parse_bytes() { - let b = from_string_primitive(r#"beefface"#, &ScSpecTypeDef::Bytes).unwrap(); + let b = from_string_primitive(r"beefface", &ScSpecTypeDef::Bytes).unwrap(); assert_eq!( b, ScVal::Bytes(ScBytes(vec![0xbe, 0xef, 0xfa, 0xce].try_into().unwrap())) @@ -100,7 +100,7 @@ fn parse_bytes() { #[test] fn parse_bytes_when_hex_is_all_numbers() { - let b = from_string_primitive(r#"4554"#, &ScSpecTypeDef::Bytes).unwrap(); + let b = from_string_primitive(r"4554", &ScSpecTypeDef::Bytes).unwrap(); assert_eq!( b, ScVal::Bytes(ScBytes(vec![0x45, 0x54].try_into().unwrap())) @@ -111,7 +111,7 @@ fn parse_bytes_when_hex_is_all_numbers() { #[test] fn parse_bytesn() { let b = from_string_primitive( - r#"beefface"#, + r"beefface", &ScSpecTypeDef::BytesN(ScSpecTypeBytesN { n: 4 }), ) .unwrap(); @@ -124,8 +124,8 @@ fn parse_bytesn() { #[test] fn parse_bytesn_when_hex_is_all_numbers() { - let b = from_string_primitive(r#"4554"#, &ScSpecTypeDef::BytesN(ScSpecTypeBytesN { n: 2 })) - .unwrap(); + let b = + from_string_primitive(r"4554", &ScSpecTypeDef::BytesN(ScSpecTypeBytesN { n: 2 })).unwrap(); assert_eq!( b, ScVal::Bytes(ScBytes(vec![0x45, 0x54].try_into().unwrap())) diff --git a/cmd/crates/soroban-test/tests/it/integration.rs b/cmd/crates/soroban-test/tests/it/integration.rs index d196ce07b5..4e92b931a9 100644 --- a/cmd/crates/soroban-test/tests/it/integration.rs +++ b/cmd/crates/soroban-test/tests/it/integration.rs @@ -2,3 +2,4 @@ mod custom_types; mod dotenv; mod hello_world; mod util; +mod wrap; diff --git a/cmd/crates/soroban-test/tests/it/integration/wrap.rs b/cmd/crates/soroban-test/tests/it/integration/wrap.rs new file mode 100644 index 0000000000..f1a453e7a1 --- /dev/null +++ b/cmd/crates/soroban-test/tests/it/integration/wrap.rs @@ -0,0 +1,79 @@ +use soroban_cli::CommandParser; +use soroban_cli::{ + commands::{ + config::{self}, + lab::token::wrap, + }, + utils::contract_id_hash_from_asset, +}; +use soroban_test::TestEnv; + +use super::util::network_passphrase; + +#[tokio::test] +#[ignore] +async fn burn() { + let sandbox = &TestEnv::default(); + let address = config::identity::address::Cmd::parse("--hd-path=0") + .unwrap() + .public_key() + .unwrap(); + let asset = format!("native:{address}"); + wrap_cmd(&asset).run().await.unwrap(); + let asset = soroban_cli::utils::parsing::parse_asset(&asset).unwrap(); + let hash = contract_id_hash_from_asset(&asset, &network_passphrase().unwrap()).unwrap(); + let id = stellar_strkey::Contract(hash.0).to_string(); + assert_eq!( + "CAMTHSPKXZJIRTUXQP5QWJIFH3XIDMKLFAWVQOFOXPTKAW5GKV37ZC4N", + id + ); + assert_eq!( + "true", + sandbox + .invoke(&[ + "--id", + &id, + "--", + "authorized", + "--id", + &address.to_string() + ]) + .await + .unwrap() + ); + assert_eq!( + "\"9223372036854775807\"", + sandbox + .invoke(&["--id", &id, "--", "balance", "--id", &address.to_string()]) + .await + .unwrap(), + ); + + println!( + "{}", + sandbox + .invoke(&[ + "--id", + &id, + "--", + "burn", + "--id", + &address.to_string(), + "--amount=100" + ]) + .await + .unwrap() + ); + + assert_eq!( + "\"9223372036854775707\"", + sandbox + .invoke(&["--id", &id, "--", "balance", "--id", &address.to_string()]) + .await + .unwrap(), + ); +} + +fn wrap_cmd(asset: &str) -> wrap::Cmd { + wrap::Cmd::parse_arg_vec(&[&format!("--asset={asset}")]).unwrap() +} diff --git a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs index c0a7801535..1c39cf2ae1 100644 --- a/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs +++ b/cmd/soroban-cli/src/commands/contract/bindings/typescript.rs @@ -61,13 +61,17 @@ pub enum Error { Fetch(#[from] fetch::Error), #[error(transparent)] Spec(#[from] contract_spec::Error), + #[error(transparent)] + Wasm(#[from] wasm::Error), + #[error("Failed to get file name from path: {0:?}")] + FailedToGetFileName(PathBuf), } impl Cmd { pub async fn run(&self) -> Result<(), Error> { let spec = if let Some(wasm) = &self.wasm { let wasm: wasm::Args = wasm.into(); - wasm.parse().unwrap().spec + wasm.parse()?.spec } else { let fetch = contract::fetch::Cmd { contract_id: self.contract_id.clone(), @@ -100,7 +104,9 @@ impl Cmd { .ok() .unwrap_or_else(Network::futurenet); let absolute_path = self.output_dir.canonicalize()?; - let file_name = absolute_path.file_name().unwrap(); + let file_name = absolute_path + .file_name() + .ok_or_else(|| Error::FailedToGetFileName(absolute_path.clone()))?; let contract_name = &file_name .to_str() .ok_or_else(|| Error::NotUtf8(file_name.to_os_string()))?; diff --git a/cmd/soroban-cli/src/commands/contract/deploy.rs b/cmd/soroban-cli/src/commands/contract/deploy.rs index 5ef18a833b..5467b992a2 100644 --- a/cmd/soroban-cli/src/commands/contract/deploy.rs +++ b/cmd/soroban-cli/src/commands/contract/deploy.rs @@ -85,6 +85,8 @@ pub enum Error { Config(#[from] config::Error), #[error(transparent)] StrKey(#[from] stellar_strkey::DecodeError), + #[error(transparent)] + Infallible(#[from] std::convert::Infallible), } impl Cmd { @@ -204,11 +206,7 @@ fn get_contract_id( contract_id_preimage: ContractIdPreimage, network_passphrase: &str, ) -> Result { - let network_id = Hash( - Sha256::digest(network_passphrase.as_bytes()) - .try_into() - .unwrap(), - ); + let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).try_into()?); let preimage = HashIdPreimage::ContractId(HashIdPreimageContractId { network_id, contract_id_preimage, diff --git a/cmd/soroban-cli/src/commands/contract/invoke.rs b/cmd/soroban-cli/src/commands/contract/invoke.rs index 59c9bf4400..2d4c77c61b 100644 --- a/cmd/soroban-cli/src/commands/contract/invoke.rs +++ b/cmd/soroban-cli/src/commands/contract/invoke.rs @@ -167,8 +167,12 @@ impl Cmd { cmd = cmd.subcommand(build_custom_cmd(&name.to_string_lossy(), &spec)?); } cmd.build(); + let long_help = cmd.render_long_help(); let mut matches_ = cmd.get_matches_from(&self.slop); - let (function, matches_) = &matches_.remove_subcommand().unwrap(); + let Some((function, matches_)) = &matches_.remove_subcommand() else { + println!("{long_help}"); + std::process::exit(1); + }; let func = spec.find_function(function)?; // create parsed_args in same order as the inputs to func @@ -177,7 +181,7 @@ impl Cmd { .inputs .iter() .map(|i| { - let name = i.name.to_string().unwrap(); + let name = i.name.to_string()?; if let Some(mut val) = matches_.get_raw(&name) { let mut s = val.next().unwrap().to_string_lossy().to_string(); if matches!(i.type_, ScSpecTypeDef::Address) { @@ -205,7 +209,10 @@ impl Cmd { &std::fs::read(arg_path) .map_err(|_| Error::MissingFileArg(arg_path.clone()))?, ) - .unwrap()) + .map_err(|()| Error::CannotParseArg { + arg: name.clone(), + error: soroban_spec_tools::Error::Unknown, + })?) } else { let file_contents = std::fs::read_to_string(arg_path) .map_err(|_| Error::MissingFileArg(arg_path.clone()))?; @@ -414,7 +421,6 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result { if kebab_name != name { cmd = cmd.alias(kebab_name); } - let func = spec.find_function(name).unwrap(); let doc: &'static str = Box::leak(func.doc.to_string_lossy().into_boxed_str()); let long_doc: &'static str = Box::leak(arg_file_help(doc).into_boxed_str()); @@ -428,7 +434,7 @@ fn build_custom_cmd(name: &str, spec: &Spec) -> Result { .alias(name.to_kebab_case()) .num_args(1) .value_parser(clap::builder::NonEmptyStringValueParser::new()) - .long_help(spec.doc(name, type_).unwrap()); + .long_help(spec.doc(name, type_)?); file_arg = file_arg .long(&file_arg_name) diff --git a/cmd/soroban-cli/src/commands/plugin.rs b/cmd/soroban-cli/src/commands/plugin.rs index e847dd0439..27c191f082 100644 --- a/cmd/soroban-cli/src/commands/plugin.rs +++ b/cmd/soroban-cli/src/commands/plugin.rs @@ -19,6 +19,8 @@ pub enum Error { ExecutableNotFound(String, String), #[error(transparent)] Which(#[from] which::Error), + #[error(transparent)] + Regex(#[from] regex::Error), } const SUBCOMMAND_TOLERANCE: f64 = 0.75; @@ -82,7 +84,7 @@ pub fn list() -> Result, Error> { } else { r"^soroban-.*" }; - let re = regex::Regex::new(re_str).unwrap(); + let re = regex::Regex::new(re_str)?; Ok(which::which_re(re)? .filter_map(|b| { let s = b.file_name()?.to_str()?; diff --git a/cmd/soroban-cli/src/rpc/mod.rs b/cmd/soroban-cli/src/rpc/mod.rs index e8fc3285cf..d9fe541a31 100644 --- a/cmd/soroban-cli/src/rpc/mod.rs +++ b/cmd/soroban-cli/src/rpc/mod.rs @@ -7,14 +7,14 @@ use serde_aux::prelude::{ deserialize_default_from_null, deserialize_number_from_string, deserialize_option_number_from_string, }; +use soroban_env_host::xdr::DepthLimitedRead; 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, + SorobanAuthorizationEntry, SorobanResources, SorobanTransactionData, Transaction, + TransactionEnvelope, TransactionMeta, TransactionMetaV3, TransactionResult, Uint256, VecM, WriteXdr, }; -use soroban_env_host::xdr::{DepthLimitedRead, SorobanAuthorizedFunction}; use soroban_sdk::token; use std::{ fmt::Display, @@ -25,10 +25,9 @@ use termcolor::{Color, ColorChoice, StandardStream, WriteColor}; use termcolor_output::colored; use tokio::time::sleep; -use crate::utils::{self, contract_spec}; +use crate::utils::contract_spec; -mod transaction; -use transaction::{assemble, build_restore_txn, sign_soroban_authorizations}; +mod txn; const VERSION: Option<&str> = option_env!("CARGO_PKG_VERSION"); @@ -98,6 +97,8 @@ pub enum Error { SpecBase64(#[from] soroban_spec::read::ParseSpecBase64Error), #[error("Fee was too large {0}")] LargeFee(u64), + #[error("Cannot authorize raw transactions")] + CannotAuthorizeRawTransaction, } #[derive(serde::Deserialize, serde::Serialize, Debug)] @@ -123,7 +124,7 @@ pub struct SendTransactionResponse { } #[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct GetTransactionResponse { +pub struct GetTransactionResponseRaw { pub status: String, #[serde( rename = "envelopeXdr", @@ -142,6 +143,33 @@ pub struct GetTransactionResponse { // TODO: add ledger info and application order } +#[derive(serde::Deserialize, serde::Serialize, Debug)] +pub struct GetTransactionResponse { + pub status: String, + pub envelope: Option, + pub result: Option, + pub result_meta: Option, +} + +impl TryInto for GetTransactionResponseRaw { + type Error = xdr::Error; + + fn try_into(self) -> Result { + Ok(GetTransactionResponse { + status: self.status, + envelope: self + .envelope_xdr + .map(ReadXdr::from_xdr_base64) + .transpose()?, + result: self.result_xdr.map(ReadXdr::from_xdr_base64).transpose()?, + result_meta: self + .result_meta_xdr + .map(ReadXdr::from_xdr_base64) + .transpose()?, + }) + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug)] pub struct LedgerEntryResult { pub key: String, @@ -212,12 +240,18 @@ pub struct Cost { } #[derive(serde::Deserialize, serde::Serialize, Debug)] -pub struct SimulateHostFunctionResult { +pub struct SimulateHostFunctionResultRaw { #[serde(deserialize_with = "deserialize_default_from_null")] pub auth: Vec, pub xdr: String, } +#[derive(Debug)] +pub struct SimulateHostFunctionResult { + pub auth: Vec, + pub xdr: xdr::ScVal, +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Default)] pub struct SimulateTransactionResponse { #[serde( @@ -229,7 +263,7 @@ pub struct SimulateTransactionResponse { #[serde(default)] pub cost: Cost, #[serde(skip_serializing_if = "Vec::is_empty", default)] - pub results: Vec, + pub results: Vec, #[serde(rename = "transactionData", default)] pub transaction_data: String, #[serde( @@ -253,6 +287,37 @@ pub struct SimulateTransactionResponse { pub error: Option, } +impl SimulateTransactionResponse { + pub fn results(&self) -> Result, Error> { + self.results + .iter() + .map(|r| { + Ok(SimulateHostFunctionResult { + auth: r + .auth + .iter() + .map(|a| Ok(SorobanAuthorizationEntry::from_xdr_base64(a)?)) + .collect::>()?, + xdr: xdr::ScVal::from_xdr_base64(&r.xdr)?, + }) + }) + .collect() + } + + pub fn events(&self) -> Result, Error> { + self.events + .iter() + .map(|e| Ok(DiagnosticEvent::from_xdr_base64(e)?)) + .collect() + } + + pub fn transaction_data(&self) -> Result { + Ok(SorobanTransactionData::from_xdr_base64( + &self.transaction_data, + )?) + } +} + #[derive(serde::Deserialize, serde::Serialize, Debug, Default)] pub struct RestorePreamble { #[serde(rename = "transactionData")] @@ -563,7 +628,7 @@ soroban config identity fund {address} --helper-url "# tx: &TransactionEnvelope, ) -> Result<(TransactionResult, TransactionMeta, Vec), Error> { let client = self.client()?; - tracing::trace!(?tx); + tracing::trace!("Sending:\n{tx:#?}"); let SendTransactionResponse { hash, error_result_xdr, @@ -572,7 +637,9 @@ soroban config identity fund {address} --helper-url "# } = client .request("sendTransaction", rpc_params![tx.to_xdr_base64()?]) .await - .map_err(|err| Error::TransactionSubmissionFailed(format!("{err:#?}")))?; + .map_err(|err| { + Error::TransactionSubmissionFailed(format!("No status yet:\n {err:#?}")) + })?; if status == "ERROR" { let error = error_result_xdr @@ -585,7 +652,7 @@ soroban config identity fund {address} --helper-url "# .map_err(|_| Error::InvalidResponse) }) .map(|r| r.result); - tracing::error!(?error); + tracing::error!("TXN failed:\n {error:#?}"); return Err(Error::TransactionSubmissionFailed(format!("{:#?}", error?))); } // even if status == "success" we need to query the transaction status in order to get the result @@ -593,27 +660,27 @@ soroban config identity fund {address} --helper-url "# // Poll the transaction status let start = Instant::now(); loop { - let response = self.get_transaction(&hash).await?; + let response: GetTransactionResponse = self.get_transaction(&hash).await?.try_into()?; match response.status.as_str() { "SUCCESS" => { // TODO: the caller should probably be printing this - tracing::trace!(?response); - let result = TransactionResult::from_xdr_base64( - response.result_xdr.clone().ok_or(Error::MissingResult)?, - )?; - let meta = TransactionMeta::from_xdr_base64( - response - .result_meta_xdr - .clone() - .ok_or(Error::MissingResult)?, - )?; + tracing::trace!("{response:#?}"); + let GetTransactionResponse { + result, + result_meta, + .. + } = response; + let meta = result_meta.ok_or(Error::MissingResult)?; let events = extract_events(&meta); - return Ok((result, meta, events)); + return Ok((result.ok_or(Error::MissingResult)?, meta, events)); } "FAILED" => { - tracing::error!(?response); + tracing::error!("{response:#?}"); // TODO: provide a more elaborate error - return Err(Error::TransactionSubmissionFailed(format!("{response:#?}"))); + return Err(Error::TransactionSubmissionFailed(format!( + "{:#?}", + response.result + ))); } "NOT_FOUND" => (), _ => { @@ -633,13 +700,13 @@ soroban config identity fund {address} --helper-url "# &self, tx: &TransactionEnvelope, ) -> Result { - tracing::trace!(?tx); + tracing::trace!("Simulating:\n{tx:#?}"); let base64_tx = tx.to_xdr_base64()?; let response: SimulateTransactionResponse = self .client()? .request("simulateTransaction", rpc_params![base64_tx]) .await?; - tracing::trace!(?response); + tracing::trace!("Simulation response:\n {response:#?}"); match response.error { None => Ok(response), Some(e) => { @@ -649,31 +716,6 @@ soroban config identity fund {address} --helper-url "# } } - // Simulate a transaction, then assemble the result of the simulation into the envelope, so it - // is ready for sending to the network. - pub async fn prepare_transaction( - &self, - tx: &Transaction, - ) -> Result<(Transaction, Option, Vec), Error> { - tracing::trace!(?tx); - let sim_response = self - .simulate_transaction(&TransactionEnvelope::Tx(TransactionV1Envelope { - tx: tx.clone(), - signatures: VecM::default(), - })) - .await?; - let events = sim_response - .events - .iter() - .map(DiagnosticEvent::from_xdr_base64) - .collect::, _>>()?; - Ok(( - assemble(tx, &sim_response)?, - sim_response.restore_preamble, - events, - )) - } - pub async fn prepare_and_send_transaction( &self, tx_without_preflight: &Transaction, @@ -683,68 +725,19 @@ soroban config identity fund {address} --helper-url "# log_events: Option, log_resources: Option, ) -> Result<(TransactionResult, TransactionMeta, Vec), Error> { - let GetLatestLedgerResponse { sequence, .. } = self.get_latest_ledger().await?; - let (mut unsigned_tx, restore_preamble, events) = - self.prepare_transaction(tx_without_preflight).await?; - if let Some(restore) = restore_preamble { - // Build and submit the restore transaction - self.send_transaction(&utils::sign_transaction( - source_key, - &build_restore_txn(&unsigned_tx, &restore)?, - network_passphrase, - )?) + let txn = txn::Assembled::new(tx_without_preflight, self).await?; + let seq_num = txn.sim_res().latest_ledger + 60; //5 min; + let authorized = txn + .handle_restore(self, source_key, network_passphrase) + .await? + .authorize(self, source_key, signers, seq_num, network_passphrase) .await?; - // Increment the original txn's seq_num so it doesn't conflict - unsigned_tx.seq_num = SequenceNumber(unsigned_tx.seq_num.0 + 1); - } - let (part_signed_tx, signed_auth_entries) = sign_soroban_authorizations( - &unsigned_tx, - source_key, - signers, - sequence + 60, // ~5 minutes of ledgers - network_passphrase, - )?; - let (fee_ready_txn, events) = if signed_auth_entries.is_empty() - || (signed_auth_entries.len() == 1 - && matches!( - signed_auth_entries[0].root_invocation.function, - SorobanAuthorizedFunction::CreateContractHostFn(_) - )) { - (part_signed_tx, events) - } else { - // re-simulate to calculate the new fees - let (tx, _, events) = self.prepare_transaction(&part_signed_tx).await?; - (tx, events) - }; - - // Try logging stuff if requested - if let Transaction { - ext: xdr::TransactionExt::V1(xdr::SorobanTransactionData { resources, .. }), - .. - } = fee_ready_txn.clone() - { - if let Some(log) = log_events { - if let xdr::Operation { - body: - xdr::OperationBody::InvokeHostFunction(xdr::InvokeHostFunctionOp { - auth, .. - }), - .. - } = &fee_ready_txn.operations[0] - { - log(&resources.footprint, &[auth.clone()], &events); - } - } - if let Some(log) = log_resources { - log(&resources); - } - } - - let tx = utils::sign_transaction(source_key, &fee_ready_txn, network_passphrase)?; + authorized.log(log_events, log_resources)?; + let tx = authorized.sign(source_key, network_passphrase)?; self.send_transaction(&tx).await } - pub async fn get_transaction(&self, tx_id: &str) -> Result { + pub async fn get_transaction(&self, tx_id: &str) -> Result { Ok(self .client()? .request("getTransaction", rpc_params![tx_id]) diff --git a/cmd/soroban-cli/src/rpc/transaction.rs b/cmd/soroban-cli/src/rpc/txn.rs similarity index 71% rename from cmd/soroban-cli/src/rpc/transaction.rs rename to cmd/soroban-cli/src/rpc/txn.rs index bdd3dfc683..32cda06335 100644 --- a/cmd/soroban-cli/src/rpc/transaction.rs +++ b/cmd/soroban-cli/src/rpc/txn.rs @@ -1,14 +1,161 @@ use ed25519_dalek::Signer; use sha2::{Digest, Sha256}; use soroban_env_host::xdr::{ - AccountId, ExtensionPoint, Hash, HashIdPreimage, HashIdPreimageSorobanAuthorization, Memo, - Operation, OperationBody, Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress, - ScMap, ScSymbol, ScVal, SorobanAddressCredentials, SorobanAuthorizationEntry, - SorobanCredentials, SorobanTransactionData, Transaction, TransactionExt, Uint256, VecM, - WriteXdr, + self, AccountId, DecoratedSignature, ExtensionPoint, Hash, HashIdPreimage, + HashIdPreimageSorobanAuthorization, InvokeHostFunctionOp, Memo, Operation, OperationBody, + Preconditions, PublicKey, ReadXdr, RestoreFootprintOp, ScAddress, ScMap, ScSymbol, ScVal, + Signature, SignatureHint, SorobanAddressCredentials, SorobanAuthorizationEntry, + SorobanAuthorizedFunction, SorobanCredentials, SorobanResources, SorobanTransactionData, + Transaction, TransactionEnvelope, TransactionExt, TransactionSignaturePayload, + TransactionSignaturePayloadTaggedTransaction, TransactionV1Envelope, Uint256, VecM, WriteXdr, }; -use crate::rpc::{Error, RestorePreamble, SimulateTransactionResponse}; +use crate::rpc::{Client, Error, RestorePreamble, SimulateTransactionResponse}; + +use super::{LogEvents, LogResources}; + +pub struct Assembled { + txn: Transaction, + sim_res: SimulateTransactionResponse, +} + +impl Assembled { + pub async fn new(txn: &Transaction, client: &Client) -> Result { + let sim_res = Self::simulate(txn, client).await?; + let txn = assemble(txn, &sim_res)?; + Ok(Self { txn, sim_res }) + } + + pub fn hash(&self, network_passphrase: &str) -> Result<[u8; 32], xdr::Error> { + let signature_payload = TransactionSignaturePayload { + network_id: Hash(Sha256::digest(network_passphrase).into()), + tagged_transaction: TransactionSignaturePayloadTaggedTransaction::Tx(self.txn.clone()), + }; + Ok(Sha256::digest(signature_payload.to_xdr()?).into()) + } + + pub fn sign( + self, + key: &ed25519_dalek::SigningKey, + network_passphrase: &str, + ) -> Result { + let tx = self.txn(); + let tx_hash = self.hash(network_passphrase)?; + let tx_signature = key.sign(&tx_hash); + + let decorated_signature = DecoratedSignature { + hint: SignatureHint(key.verifying_key().to_bytes()[28..].try_into()?), + signature: Signature(tx_signature.to_bytes().try_into()?), + }; + + Ok(TransactionEnvelope::Tx(TransactionV1Envelope { + tx: tx.clone(), + signatures: vec![decorated_signature].try_into()?, + })) + } + + pub async fn simulate( + tx: &Transaction, + client: &Client, + ) -> Result { + client + .simulate_transaction(&TransactionEnvelope::Tx(TransactionV1Envelope { + tx: tx.clone(), + signatures: VecM::default(), + })) + .await + } + + pub async fn handle_restore( + self, + client: &Client, + source_key: &ed25519_dalek::SigningKey, + network_passphrase: &str, + ) -> Result { + if let Some(restore_preamble) = &self.sim_res.restore_preamble { + // Build and submit the restore transaction + client + .send_transaction( + &Assembled::new(&restore(self.txn(), restore_preamble)?, client) + .await? + .sign(source_key, network_passphrase)?, + ) + .await?; + Ok(self.bump_seq_num()) + } else { + Ok(self) + } + } + + pub fn txn(&self) -> &Transaction { + &self.txn + } + + pub fn sim_res(&self) -> &SimulateTransactionResponse { + &self.sim_res + } + + pub async fn authorize( + self, + client: &Client, + source_key: &ed25519_dalek::SigningKey, + signers: &[ed25519_dalek::SigningKey], + seq_num: u32, + network_passphrase: &str, + ) -> Result { + if let Some(txn) = sign_soroban_authorizations( + self.txn(), + source_key, + signers, + seq_num, + network_passphrase, + )? { + Self::new(&txn, client).await + } else { + Ok(self) + } + } + + pub fn bump_seq_num(mut self) -> Self { + self.txn.seq_num.0 += 1; + self + } + + pub fn auth(&self) -> VecM { + self.txn + .operations + .get(0) + .and_then(|op| match op.body { + OperationBody::InvokeHostFunction(ref body) => (matches!( + body.auth.get(0).map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + )) + .then_some(body.auth.clone()), + _ => None, + }) + .unwrap_or_default() + } + + pub fn log( + &self, + log_events: Option, + log_resources: Option, + ) -> Result<(), Error> { + if let TransactionExt::V1(SorobanTransactionData { + resources: resources @ SorobanResources { footprint, .. }, + .. + }) = &self.txn.ext + { + if let Some(log) = log_resources { + log(resources); + } + if let Some(log) = log_events { + log(footprint, &[self.auth()], &self.sim_res.events()?); + }; + } + Ok(()) + } +} // Apply the result of a simulateTransaction onto a transaction envelope, preparing it for // submission to the network. @@ -28,7 +175,7 @@ pub fn assemble( }); } - let transaction_data = SorobanTransactionData::from_xdr_base64(&simulation.transaction_data)?; + let transaction_data = simulation.transaction_data()?; let mut op = tx.operations[0].clone(); if let OperationBody::InvokeHostFunction(ref mut body) = &mut op.body { @@ -74,23 +221,34 @@ pub fn assemble( // Use the given source_key and signers, to sign all SorobanAuthorizationEntry's in the given // transaction. If unable to sign, return an error. -pub fn sign_soroban_authorizations( +fn sign_soroban_authorizations( raw: &Transaction, source_key: &ed25519_dalek::SigningKey, signers: &[ed25519_dalek::SigningKey], signature_expiration_ledger: u32, network_passphrase: &str, -) -> Result<(Transaction, Vec), Error> { +) -> Result, Error> { let mut tx = raw.clone(); + let mut op = match tx.operations.as_slice() { + [op @ Operation { + body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp { auth, .. }), + .. + }] if matches!( + auth.get(0).map(|x| &x.root_invocation.function), + Some(&SorobanAuthorizedFunction::ContractFn(_)) + ) => + { + op.clone() + } + _ => return Ok(None), + }; - if tx.operations.len() != 1 { - // This must not be an invokeHostFunction operation, so nothing to do - return Ok((tx, Vec::new())); - } - - let mut op = tx.operations[0].clone(); - let OperationBody::InvokeHostFunction(ref mut body) = &mut op.body else { - return Ok((tx, Vec::new())); + let Operation { + body: OperationBody::InvokeHostFunction(ref mut body), + .. + } = op + else { + return Ok(None); }; let network_id = Hash(Sha256::digest(network_passphrase.as_bytes()).into()); @@ -100,6 +258,7 @@ pub fn sign_soroban_authorizations( let signed_auths = body .auth + .as_slice() .iter() .map(|raw_auth| { let mut auth = raw_auth.clone(); @@ -153,12 +312,12 @@ pub fn sign_soroban_authorizations( }) .collect::, Error>>()?; - body.auth = signed_auths.clone().try_into()?; + body.auth = signed_auths.try_into()?; tx.operations = vec![op].try_into()?; - Ok((tx, signed_auths)) + Ok(Some(tx)) } -pub fn sign_soroban_authorization_entry( +fn sign_soroban_authorization_entry( raw: &SorobanAuthorizationEntry, signer: &ed25519_dalek::SigningKey, signature_expiration_ledger: u32, @@ -218,12 +377,8 @@ pub fn sign_soroban_authorization_entry( Ok(auth) } -pub fn build_restore_txn( - parent: &Transaction, - restore: &RestorePreamble, -) -> Result { - let transaction_data = - SorobanTransactionData::from_xdr_base64(restore.transaction_data.clone())?; +pub fn restore(parent: &Transaction, restore: &RestorePreamble) -> Result { + let transaction_data = SorobanTransactionData::from_xdr_base64(&restore.transaction_data)?; let fee = u32::try_from(restore.min_resource_fee) .map_err(|_| Error::LargeFee(restore.min_resource_fee))?; Ok(Transaction { @@ -251,7 +406,7 @@ pub fn build_restore_txn( mod tests { use super::*; - use super::super::SimulateHostFunctionResult; + use super::super::SimulateHostFunctionResultRaw; use soroban_env_host::xdr::{ self, AccountId, ChangeTrustAsset, ChangeTrustOp, ExtensionPoint, Hash, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, LedgerFootprint, Memo, MuxedAccount, Operation, @@ -303,7 +458,7 @@ mod tests { SimulateTransactionResponse { min_resource_fee: 115, latest_ledger: 3, - results: vec![SimulateHostFunctionResult { + results: vec![SimulateHostFunctionResultRaw { auth: vec![fn_auth.to_xdr_base64().unwrap()], xdr: ScVal::U32(0).to_xdr_base64().unwrap(), }], diff --git a/cmd/soroban-cli/src/utils.rs b/cmd/soroban-cli/src/utils.rs index fcc3964c98..27faee7334 100644 --- a/cmd/soroban-cli/src/utils.rs +++ b/cmd/soroban-cli/src/utils.rs @@ -131,6 +131,8 @@ pub mod parsing { CannotParseAccountId { account_id: String }, #[error("cannot parse asset: {asset}")] CannotParseAsset { asset: String }, + #[error(transparent)] + Regex(#[from] regex::Error), } pub fn parse_asset(str: &str) -> Result { @@ -145,7 +147,7 @@ pub mod parsing { } let code = split[0]; let issuer = split[1]; - let re = Regex::new("^[[:alnum:]]{1,12}$").unwrap(); + let re = Regex::new("^[[:alnum:]]{1,12}$")?; if !re.is_match(code) { return Err(Error::InvalidAssetCode { asset: str.to_string(), diff --git a/cmd/soroban-rpc/internal/config/config.go b/cmd/soroban-rpc/internal/config/config.go index b16d02bb67..1f89ab2b57 100644 --- a/cmd/soroban-rpc/internal/config/config.go +++ b/cmd/soroban-rpc/internal/config/config.go @@ -15,7 +15,6 @@ type Config struct { Strict bool StellarCoreURL string - CaptiveCoreUseDB bool CaptiveCoreStoragePath string StellarCoreBinaryPath string CaptiveCoreConfigPath string diff --git a/cmd/soroban-rpc/internal/config/options.go b/cmd/soroban-rpc/internal/config/options.go index c3b4f2eda0..cecfb2e7d9 100644 --- a/cmd/soroban-rpc/internal/config/options.go +++ b/cmd/soroban-rpc/internal/config/options.go @@ -174,12 +174,6 @@ func (cfg *Config) options() ConfigOptions { } }, }, - { - Name: "captive-core-use-db", - Usage: "informs captive core to use on disk mode. the db will by default be created in current runtime directory of soroban-rpc, unless DATABASE= setting is present in captive core config file.", - ConfigKey: &cfg.CaptiveCoreUseDB, - DefaultValue: false, - }, { Name: "history-archive-urls", Usage: "comma-separated list of stellar history archives to connect with", diff --git a/cmd/soroban-rpc/internal/config/test.soroban.rpc.config b/cmd/soroban-rpc/internal/config/test.soroban.rpc.config index 96d9247432..c28a9c17b5 100644 --- a/cmd/soroban-rpc/internal/config/test.soroban.rpc.config +++ b/cmd/soroban-rpc/internal/config/test.soroban.rpc.config @@ -4,7 +4,6 @@ NETWORK_PASSPHRASE="Standalone Network ; February 2017" STELLAR_CORE_URL="http://localhost:11626" CAPTIVE_CORE_CONFIG_PATH="/opt/stellar/soroban-rpc/etc/stellar-captive-core.cfg" CAPTIVE_CORE_STORAGE_PATH="/opt/stellar/soroban-rpc/captive-core" -CAPTIVE_CORE_USE_DB=true STELLAR_CORE_BINARY_PATH="/usr/bin/stellar-core" HISTORY_ARCHIVE_URLS=["http://localhost:1570"] DB_PATH="/opt/stellar/soroban-rpc/rpc_db.sqlite" diff --git a/cmd/soroban-rpc/internal/config/toml_test.go b/cmd/soroban-rpc/internal/config/toml_test.go index 603151a317..93fa1809b2 100644 --- a/cmd/soroban-rpc/internal/config/toml_test.go +++ b/cmd/soroban-rpc/internal/config/toml_test.go @@ -19,7 +19,6 @@ NETWORK_PASSPHRASE = "Test SDF Future Network ; October 2022" # testing comments work ok STELLAR_CORE_BINARY_PATH = "/usr/bin/stellar-core" -CAPTIVE_CORE_USE_DB = true CAPTIVE_CORE_STORAGE_PATH = "/etc/stellar/soroban-rpc" CAPTIVE_CORE_CONFIG_PATH = "/etc/stellar/soroban-rpc/captive-core.cfg" ` @@ -31,7 +30,6 @@ func TestBasicTomlReading(t *testing.T) { // Check the fields got read correctly assert.Equal(t, []string{"http://history-futurenet.stellar.org"}, cfg.HistoryArchiveURLs) assert.Equal(t, network.FutureNetworkPassphrase, cfg.NetworkPassphrase) - assert.Equal(t, true, cfg.CaptiveCoreUseDB) assert.Equal(t, "/etc/stellar/soroban-rpc", cfg.CaptiveCoreStoragePath) assert.Equal(t, "/etc/stellar/soroban-rpc/captive-core.cfg", cfg.CaptiveCoreConfigPath) } diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go index 0f0ab80f80..63afb9a710 100644 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ b/cmd/soroban-rpc/internal/daemon/daemon.go @@ -105,7 +105,7 @@ func newCaptiveCore(cfg *config.Config, logger *supportlog.Entry) (*ledgerbacken HistoryArchiveURLs: cfg.HistoryArchiveURLs, NetworkPassphrase: cfg.NetworkPassphrase, Strict: true, - UseDB: cfg.CaptiveCoreUseDB, + UseDB: true, EnforceSorobanDiagnosticEvents: true, } captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromFile(cfg.CaptiveCoreConfigPath, captiveCoreTomlParams) @@ -122,7 +122,7 @@ func newCaptiveCore(cfg *config.Config, logger *supportlog.Entry) (*ledgerbacken Log: logger.WithField("subservice", "stellar-core"), Toml: captiveCoreToml, UserAgent: "captivecore", - UseDB: cfg.CaptiveCoreUseDB, + UseDB: true, } return ledgerbackend.NewCaptive(captiveConfig) diff --git a/cmd/soroban-rpc/internal/db/ledgerentry.go b/cmd/soroban-rpc/internal/db/ledgerentry.go index 2e22cbc7a8..1553ecf599 100644 --- a/cmd/soroban-rpc/internal/db/ledgerentry.go +++ b/cmd/soroban-rpc/internal/db/ledgerentry.go @@ -260,16 +260,22 @@ func entryKeyToTTLEntryKey(key xdr.LedgerKey) (xdr.LedgerKey, error) { } func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKeyAndEntry, error) { - encodedKeys := make([]string, len(keys), 2*len(keys)) - encodedKeyToKey := make(map[string]xdr.LedgerKey, len(keys)) - encodedKeyToEncodedTTLLedgerKey := make(map[string]string, len(keys)) - for _, k := range keys { + encodedKeys := make([]string, 0, 2*len(keys)) + type keyToEncoded struct { + key xdr.LedgerKey + encodedKey string + encodedTTLKey *string + } + keysToEncoded := make([]keyToEncoded, len(keys)) + for i, k := range keys { + k2 := k + keysToEncoded[i].key = k2 encodedKey, err := encodeLedgerKey(l.buffer, k) if err != nil { return nil, err } + keysToEncoded[i].encodedKey = encodedKey encodedKeys = append(encodedKeys, encodedKey) - encodedKeyToKey[encodedKey] = k if !hasTTLKey(k) { continue } @@ -281,7 +287,7 @@ func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKey if err != nil { return nil, err } - encodedKeyToEncodedTTLLedgerKey[encodedKey] = encodedTTLKey + keysToEncoded[i].encodedTTLKey = &encodedTTLKey encodedKeys = append(encodedKeys, encodedTTLKey) } @@ -290,9 +296,9 @@ func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKey return nil, err } - result := make([]LedgerKeyAndEntry, 0, len(rawResult)) - for encodedKey, key := range encodedKeyToKey { - encodedEntry, ok := rawResult[encodedKey] + result := make([]LedgerKeyAndEntry, 0, len(keys)) + for _, k2e := range keysToEncoded { + encodedEntry, ok := rawResult[k2e.encodedKey] if !ok { continue } @@ -300,22 +306,21 @@ func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKey if err := xdr.SafeUnmarshal([]byte(encodedEntry), &entry); err != nil { return nil, errors.Wrap(err, "cannot decode ledger entry from DB") } - encodedExpKey, has := encodedKeyToEncodedTTLLedgerKey[encodedKey] - if !has { - result = append(result, LedgerKeyAndEntry{key, entry, nil}) + if k2e.encodedTTLKey == nil { + result = append(result, LedgerKeyAndEntry{k2e.key, entry, nil}) continue } - encodedExpEntry, ok := rawResult[encodedExpKey] + encodedTTLEntry, ok := rawResult[*k2e.encodedTTLKey] if !ok { // missing ttl key. This should not happen. return nil, errors.New("missing ttl key entry") } - var expEntry xdr.LedgerEntry - if err := xdr.SafeUnmarshal([]byte(encodedExpEntry), &expEntry); err != nil { + var ttlEntry xdr.LedgerEntry + if err := xdr.SafeUnmarshal([]byte(encodedTTLEntry), &ttlEntry); err != nil { return nil, errors.Wrap(err, "cannot decode TTL ledger entry from DB") } - liveUntilSeq := uint32(expEntry.Data.Ttl.LiveUntilLedgerSeq) - result = append(result, LedgerKeyAndEntry{key, entry, &liveUntilSeq}) + liveUntilSeq := uint32(ttlEntry.Data.Ttl.LiveUntilLedgerSeq) + result = append(result, LedgerKeyAndEntry{k2e.key, entry, &liveUntilSeq}) } return result, nil diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go index b7c9c326bc..0b444ce1c1 100644 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go +++ b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go @@ -108,8 +108,18 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn } } - for i, ledgerKeyAndEntry := range ledgerKeysAndEntries { - ledgerXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data) + for _, ledgerKeyAndEntry := range ledgerKeysAndEntries { + keyXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Key) + if err != nil { + logger.WithError(err).WithField("request", request). + Infof("could not serialize ledger key %v", ledgerKeyAndEntry.Key) + return GetLedgerEntriesResponse{}, &jrpc2.Error{ + Code: jrpc2.InternalError, + Message: fmt.Sprintf("could not serialize ledger key %v", ledgerKeyAndEntry.Key), + } + } + + entryXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data) if err != nil { logger.WithError(err).WithField("request", request). Infof("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry) @@ -120,8 +130,8 @@ func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEn } ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{ - Key: request.Keys[i], - XDR: ledgerXDR, + Key: keyXDR, + XDR: entryXDR, LastModifiedLedger: int64(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), LiveUntilLedgerSeq: ledgerKeyAndEntry.LiveUntilLedgerSeq, }) diff --git a/cmd/soroban-rpc/internal/test/docker-compose.yml b/cmd/soroban-rpc/internal/test/docker-compose.yml index 3e8af4bfe7..39e3f7ed67 100644 --- a/cmd/soroban-rpc/internal/test/docker-compose.yml +++ b/cmd/soroban-rpc/internal/test/docker-compose.yml @@ -15,7 +15,7 @@ services: # Note: Please keep the image pinned to an immutable tag matching the Captive Core version. # This avoids implicit updates which break compatibility between # the Core container and captive core. - image: ${CORE_IMAGE:-stellar/stellar-core:19.14.1-1547.f2d06fbce.focal} + image: ${CORE_IMAGE:-stellar/stellar-core:19.14.1-1553.f4c4e2fca.focal} depends_on: - core-postgres restart: on-failure diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go b/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go index 74e6dce30c..728f4d46b8 100644 --- a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go +++ b/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go @@ -11,7 +11,6 @@ import ( "github.com/stretchr/testify/require" "github.com/stellar/go/keypair" - proto "github.com/stellar/go/protocols/stellarcore" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" @@ -80,8 +79,9 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) client := jrpc2.NewClient(ch, nil) - kp := keypair.Root(StandaloneNetworkPassphrase) - account := txnbuild.NewSimpleAccount(kp.Address(), 0) + sourceAccount := keypair.Root(StandaloneNetworkPassphrase) + address := sourceAccount.Address() + account := txnbuild.NewSimpleAccount(address, 0) contractBinary := getHelloWorldContract(t) params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ @@ -96,35 +96,40 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { }, }) tx, err := txnbuild.NewTransaction(params) - require.NoError(t, err) - tx, err = tx.Sign(StandaloneNetworkPassphrase, kp) - require.NoError(t, err) - b64, err := tx.Base64() - require.NoError(t, err) + assert.NoError(t, err) + sendSuccessfulTransaction(t, client, sourceAccount, tx) - sendTxRequest := methods.SendTransactionRequest{Transaction: b64} - var sendTxResponse methods.SendTransactionResponse - err = client.CallResult(context.Background(), "sendTransaction", sendTxRequest, &sendTxResponse) - require.NoError(t, err) - require.Equal(t, proto.TXStatusPending, sendTxResponse.Status) + params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ + SourceAccount: &account, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + createCreateContractOperation(address, contractBinary), + }, + BaseFee: txnbuild.MinBaseFee, + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewInfiniteTimeout(), + }, + }) + tx, err = txnbuild.NewTransaction(params) + assert.NoError(t, err) + sendSuccessfulTransaction(t, client, sourceAccount, tx) - txStatusResponse := getTransaction(t, client, sendTxResponse.Hash) - require.Equal(t, methods.TransactionStatusSuccess, txStatusResponse.Status) + contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) contractHash := sha256.Sum256(contractBinary) - contractKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ + contractCodeKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ Type: xdr.LedgerEntryTypeContractCode, ContractCode: &xdr.LedgerKeyContractCode{ Hash: contractHash, }, }) - require.NoError(t, err) // Doesn't exist. - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) + notFoundKeyB64, err := xdr.MarshalBase64(getCounterLedgerKey(contractID)) + require.NoError(t, err) + contractIDHash := xdr.Hash(contractID) - notFoundKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ + contractInstanceKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ Type: xdr.LedgerEntryTypeContractData, ContractData: &xdr.LedgerKeyContractData{ Contract: xdr.ScAddress{ @@ -139,9 +144,7 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { }) require.NoError(t, err) - var keys []string - keys = append(keys, contractKeyB64) - keys = append(keys, notFoundKeyB64) + keys := []string{contractCodeKeyB64, notFoundKeyB64, contractInstanceKeyB64} request := methods.GetLedgerEntriesRequest{ Keys: keys, } @@ -149,11 +152,28 @@ func TestGetLedgerEntriesSucceeds(t *testing.T) { var result methods.GetLedgerEntriesResponse err = client.CallResult(context.Background(), "getLedgerEntries", request, &result) require.NoError(t, err) - require.Equal(t, 1, len(result.Entries)) + require.Equal(t, 2, len(result.Entries)) require.Greater(t, result.LatestLedger, int64(0)) + require.Greater(t, result.Entries[0].LastModifiedLedger, int64(0)) + require.LessOrEqual(t, result.Entries[0].LastModifiedLedger, result.LatestLedger) + require.NotNil(t, result.Entries[0].LiveUntilLedgerSeq) + require.Greater(t, *result.Entries[0].LiveUntilLedgerSeq, uint32(result.LatestLedger)) + require.Equal(t, contractCodeKeyB64, result.Entries[0].Key) var firstEntry xdr.LedgerEntryData require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &firstEntry)) + require.Equal(t, xdr.LedgerEntryTypeContractCode, firstEntry.Type) require.Equal(t, contractBinary, firstEntry.MustContractCode().Code) - require.Equal(t, contractKeyB64, result.Entries[0].Key) + + require.Greater(t, result.Entries[1].LastModifiedLedger, int64(0)) + require.LessOrEqual(t, result.Entries[1].LastModifiedLedger, result.LatestLedger) + require.NotNil(t, result.Entries[1].LiveUntilLedgerSeq) + require.Greater(t, *result.Entries[1].LiveUntilLedgerSeq, uint32(result.LatestLedger)) + require.Equal(t, contractInstanceKeyB64, result.Entries[1].Key) + var secondEntry xdr.LedgerEntryData + require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[1].XDR, &secondEntry)) + require.Equal(t, xdr.LedgerEntryTypeContractData, secondEntry.Type) + require.True(t, secondEntry.MustContractData().Key.Equals(xdr.ScVal{ + Type: xdr.ScValTypeScvLedgerKeyContractInstance, + })) } diff --git a/cmd/soroban-rpc/internal/test/integration.go b/cmd/soroban-rpc/internal/test/integration.go index 9521dfc759..f9d4f31b0f 100644 --- a/cmd/soroban-rpc/internal/test/integration.go +++ b/cmd/soroban-rpc/internal/test/integration.go @@ -121,7 +121,6 @@ func (i *Test) launchDaemon(coreBinaryPath string) { config.CaptiveCoreConfigPath = path.Join(i.composePath, "captive-core-integration-tests.cfg") config.CaptiveCoreStoragePath = i.t.TempDir() config.CaptiveCoreHTTPPort = 0 - config.CaptiveCoreUseDB = true config.FriendbotURL = friendbotURL config.NetworkPassphrase = StandaloneNetworkPassphrase config.HistoryArchiveURLs = []string{"http://localhost:1570"} diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go index 04ac395ea4..b47c609f0c 100644 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go @@ -233,7 +233,7 @@ func TestSimulateTransactionSucceeds(t *testing.T) { }, }, Instructions: 6062311, - ReadBytes: 48, + ReadBytes: 0, WriteBytes: 7048, }, ResourceFee: 130498, diff --git a/cmd/soroban-rpc/internal/test/transaction_test.go b/cmd/soroban-rpc/internal/test/transaction_test.go index ff84734208..e370d7d192 100644 --- a/cmd/soroban-rpc/internal/test/transaction_test.go +++ b/cmd/soroban-rpc/internal/test/transaction_test.go @@ -259,7 +259,21 @@ func sendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.F var txMeta xdr.TransactionMeta err = xdr.SafeUnmarshalBase64(response.ResultMetaXdr, &txMeta) assert.NoError(t, err) - fmt.Printf("meta: %#v\n", txMeta) + if txMeta.V == 3 && txMeta.V3.SorobanMeta != nil { + if len(txMeta.V3.SorobanMeta.Events) > 0 { + fmt.Println("Contract events:") + for i, e := range txMeta.V3.SorobanMeta.Events { + fmt.Printf(" %d: %s\n", i, e) + } + } + + if len(txMeta.V3.SorobanMeta.DiagnosticEvents) > 0 { + fmt.Println("Diagnostic events:") + for i, d := range txMeta.V3.SorobanMeta.DiagnosticEvents { + fmt.Printf(" %d: %s\n", i, d) + } + } + } } require.NotNil(t, response.ResultXdr) diff --git a/cmd/soroban-rpc/lib/preflight/src/fees.rs b/cmd/soroban-rpc/lib/preflight/src/fees.rs index 20e002b3d8..e32ab3e9b9 100644 --- a/cmd/soroban-rpc/lib/preflight/src/fees.rs +++ b/cmd/soroban-rpc/lib/preflight/src/fees.rs @@ -7,7 +7,7 @@ use soroban_env_host::e2e_invoke::{ use soroban_env_host::fees::{ compute_rent_fee, compute_transaction_resource_fee, compute_write_fee_per_1kb, FeeConfiguration, LedgerEntryRentChange, RentFeeConfiguration, TransactionResources, - WriteFeeConfiguration, TTL_ENTRY_SIZE, + WriteFeeConfiguration, }; use soroban_env_host::storage::{AccessType, Footprint, Storage}; use soroban_env_host::xdr; @@ -131,10 +131,7 @@ fn calculate_host_function_soroban_resources( ) -> Result { let ledger_footprint = storage_footprint_to_ledger_footprint(footprint) .context("cannot convert storage footprint to ledger footprint")?; - let read_bytes: u32 = ledger_changes - .iter() - .map(|c| c.old_entry_size_bytes + c.ttl_change.as_ref().map_or(0, |_| TTL_ENTRY_SIZE)) - .sum(); + let read_bytes: u32 = ledger_changes.iter().map(|c| c.old_entry_size_bytes).sum(); let write_bytes: u32 = ledger_changes .iter() @@ -224,17 +221,6 @@ fn get_fee_configurations( Ok((fee_configuration, rent_fee_configuration)) } -// Calculate the implicit TTLEntry bytes that will be read for TTLLedgerEntries -fn calculate_ttl_entry_bytes(ledger_entries: &[LedgerKey]) -> u32 { - ledger_entries - .iter() - .map(|lk| match lk { - LedgerKey::ContractData(_) | LedgerKey::ContractCode(_) => TTL_ENTRY_SIZE, - _ => 0, - }) - .sum() -} - #[allow(clippy::cast_possible_truncation)] fn calculate_unmodified_ledger_entry_bytes( ledger_entries: &[LedgerKey], @@ -322,8 +308,6 @@ pub(crate) fn compute_extend_footprint_ttl_transaction_data_and_min_fee( ) .context("cannot compute extend rent changes")?; - let ttl_bytes: u32 = calculate_ttl_entry_bytes(footprint.read_only.as_vec()); - let unmodified_entry_bytes = calculate_unmodified_ledger_entry_bytes( footprint.read_only.as_slice(), ledger_storage, @@ -334,7 +318,7 @@ pub(crate) fn compute_extend_footprint_ttl_transaction_data_and_min_fee( let soroban_resources = SorobanResources { footprint, instructions: 0, - read_bytes: unmodified_entry_bytes + ttl_bytes, + read_bytes: unmodified_entry_bytes, write_bytes: 0, }; let transaction_size_bytes = estimate_max_transaction_size_for_operation( @@ -420,7 +404,6 @@ pub(crate) fn compute_restore_footprint_transaction_data_and_min_fee( ) .context("cannot compute restore rent changes")?; - let ttl_bytes: u32 = calculate_ttl_entry_bytes(footprint.read_write.as_vec()); let write_bytes = calculate_unmodified_ledger_entry_bytes( footprint.read_write.as_vec(), ledger_storage, @@ -430,7 +413,7 @@ pub(crate) fn compute_restore_footprint_transaction_data_and_min_fee( let soroban_resources = SorobanResources { footprint, instructions: 0, - read_bytes: write_bytes + ttl_bytes, + read_bytes: write_bytes, write_bytes, }; let transaction_size_bytes = estimate_max_transaction_size_for_operation( diff --git a/go.mod b/go.mod index fd4fca5f1e..eb69da2886 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ toolchain go1.21.1 require ( github.com/Masterminds/squirrel v1.5.4 github.com/cenkalti/backoff/v4 v4.2.1 - github.com/creachadair/jrpc2 v1.1.1 + github.com/creachadair/jrpc2 v1.1.2 github.com/go-chi/chi v4.1.2+incompatible github.com/go-git/go-git/v5 v5.9.0 github.com/mattn/go-sqlite3 v1.14.17 @@ -18,7 +18,7 @@ require ( github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.7.0 github.com/spf13/pflag v1.0.5 - github.com/stellar/go v0.0.0-20231016174715-c7d3a47ee7a2 + github.com/stellar/go v0.0.0-20231114175958-eb2984b58392 github.com/stretchr/testify v1.8.4 golang.org/x/mod v0.13.0 gotest.tools/v3 v3.5.0 @@ -52,7 +52,7 @@ require ( github.com/aws/aws-sdk-go v1.45.27 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/creachadair/mds v0.2.3 // indirect + github.com/creachadair/mds v0.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect @@ -60,7 +60,7 @@ require ( github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/google/go-cmp v0.5.9 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -89,7 +89,7 @@ require ( golang.org/x/crypto v0.14.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/net v0.17.0 // indirect - golang.org/x/sync v0.4.0 // indirect + golang.org/x/sync v0.5.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.13.0 // indirect google.golang.org/protobuf v1.31.0 // indirect diff --git a/go.sum b/go.sum index f0d20235fe..1daea539cd 100644 --- a/go.sum +++ b/go.sum @@ -82,10 +82,10 @@ github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGX github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creachadair/jrpc2 v1.1.1 h1:xa7p3C5eSvwn/dFwCCmksp+RyQ/ytFY0NYzY7npsoI0= -github.com/creachadair/jrpc2 v1.1.1/go.mod h1:KajsO5dx7yfcwmuRJb5SHXsCVTzBoq7EsPzu5wxnsc0= -github.com/creachadair/mds v0.2.3 h1:Svuw/AXrUUMxGHdRyuDsWJ36oFJRprqP8+iI86XzZjM= -github.com/creachadair/mds v0.2.3/go.mod h1:PmXHgspUECelJVsAgDxWvjblna5BGjPxdEpr7SIEvNs= +github.com/creachadair/jrpc2 v1.1.2 h1:UOYMipEFYlwd5qmcvs9GZBurn3oXt1UDIX5JLjWWFzo= +github.com/creachadair/jrpc2 v1.1.2/go.mod h1:JcCe2Eny3lIvVwZLm92WXyU+tNUgTBWFCLMsfNkjEGk= +github.com/creachadair/mds v0.3.0 h1:uKbCKVtd3iOKVv3uviOm13fFNfe9qoCXJh1Vo7y3Kr0= +github.com/creachadair/mds v0.3.0/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -180,8 +180,8 @@ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU= github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -333,8 +333,8 @@ github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stellar/go v0.0.0-20231016174715-c7d3a47ee7a2 h1:IOOHd1yrwmK0wiAuNDmoHUPTucO0oGkkKa3CE1pgn2E= -github.com/stellar/go v0.0.0-20231016174715-c7d3a47ee7a2/go.mod h1:g78pyZyDFnKMJUaBIXxH7xyQ7PdDrvrJTFCxdGMMb3c= +github.com/stellar/go v0.0.0-20231114175958-eb2984b58392 h1:sYxHgLDT3z6cJrWuf0O9Fbs/E2UNGh3PPoOlM8DJ2vk= +github.com/stellar/go v0.0.0-20231114175958-eb2984b58392/go.mod h1:g78pyZyDFnKMJUaBIXxH7xyQ7PdDrvrJTFCxdGMMb3c= github.com/stellar/go-xdr v0.0.0-20230919160922-6c7b68458206 h1:UFuvvpbWL8+jqO1QmKYWSVhiMp4MRiIFd8/zQlUINH0= github.com/stellar/go-xdr v0.0.0-20230919160922-6c7b68458206/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -498,8 +498,8 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=