1use std::convert::{Infallible, TryInto};
2use std::ffi::OsString;
3use std::num::ParseIntError;
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::{fmt::Debug, fs, io};
7
8use clap::{arg, command, Parser, ValueEnum};
9use soroban_rpc::{Client, SimulateHostFunctionResult, SimulateTransactionResponse};
10use soroban_spec::read::FromWasmError;
11
12use super::super::events;
13use super::arg_parsing;
14use crate::assembled::Assembled;
15use crate::{
16 assembled::simulate_and_assemble_transaction,
17 commands::{
18 contract::arg_parsing::{build_host_function_parameters, output_to_string},
19 global,
20 txn_result::{TxnEnvelopeResult, TxnResult},
21 NetworkRunnable,
22 },
23 config::{self, data, locator, network},
24 get_spec::{self, get_remote_contract_spec},
25 print, rpc,
26 xdr::{
27 self, AccountEntry, AccountEntryExt, AccountId, ContractEvent, ContractEventType,
28 DiagnosticEvent, HostFunction, InvokeContractArgs, InvokeHostFunctionOp, Limits, Memo,
29 MuxedAccount, Operation, OperationBody, Preconditions, PublicKey, ScSpecEntry,
30 SequenceNumber, String32, StringM, Thresholds, Transaction, TransactionExt, Uint256, VecM,
31 WriteXdr,
32 },
33 Pwd,
34};
35use soroban_spec_tools::contract;
36
37#[derive(Parser, Debug, Default, Clone)]
38#[allow(clippy::struct_excessive_bools)]
39#[group(skip)]
40pub struct Cmd {
41 #[arg(long = "id", env = "STELLAR_CONTRACT_ID")]
43 pub contract_id: config::UnresolvedContract,
44 #[arg(skip)]
46 pub wasm: Option<std::path::PathBuf>,
47 #[arg(long, env = "STELLAR_INVOKE_VIEW")]
49 pub is_view: bool,
50 #[arg(last = true, id = "CONTRACT_FN_AND_ARGS")]
52 pub slop: Vec<OsString>,
53 #[command(flatten)]
54 pub config: config::Args,
55 #[command(flatten)]
56 pub fee: crate::fee::Args,
57 #[arg(long, value_enum, default_value_t, env = "STELLAR_SEND")]
59 pub send: Send,
60}
61
62impl FromStr for Cmd {
63 type Err = clap::error::Error;
64
65 fn from_str(s: &str) -> Result<Self, Self::Err> {
66 use clap::{CommandFactory, FromArgMatches};
67 Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
68 }
69}
70
71impl Pwd for Cmd {
72 fn set_pwd(&mut self, pwd: &Path) {
73 self.config.set_pwd(pwd);
74 }
75}
76
77#[derive(thiserror::Error, Debug)]
78pub enum Error {
79 #[error("cannot add contract to ledger entries: {0}")]
80 CannotAddContractToLedgerEntries(xdr::Error),
81 #[error("reading file {0:?}: {1}")]
82 CannotReadContractFile(PathBuf, io::Error),
83 #[error("committing file {filepath}: {error}")]
84 CannotCommitEventsFile {
85 filepath: std::path::PathBuf,
86 error: events::Error,
87 },
88 #[error("parsing contract spec: {0}")]
89 CannotParseContractSpec(FromWasmError),
90 #[error(transparent)]
91 Xdr(#[from] xdr::Error),
92 #[error("error parsing int: {0}")]
93 ParseIntError(#[from] ParseIntError),
94 #[error(transparent)]
95 Rpc(#[from] rpc::Error),
96 #[error("missing operation result")]
97 MissingOperationResult,
98 #[error("error loading signing key: {0}")]
99 SignatureError(#[from] ed25519_dalek::SignatureError),
100 #[error(transparent)]
101 Config(#[from] config::Error),
102 #[error("unexpected ({length}) simulate transaction result length")]
103 UnexpectedSimulateTransactionResultSize { length: usize },
104 #[error(transparent)]
105 Clap(#[from] clap::Error),
106 #[error(transparent)]
107 Locator(#[from] locator::Error),
108 #[error("Contract Error\n{0}: {1}")]
109 ContractInvoke(String, String),
110 #[error(transparent)]
111 StrKey(#[from] stellar_strkey::DecodeError),
112 #[error(transparent)]
113 ContractSpec(#[from] contract::Error),
114 #[error(transparent)]
115 Io(#[from] std::io::Error),
116 #[error(transparent)]
117 Data(#[from] data::Error),
118 #[error(transparent)]
119 Network(#[from] network::Error),
120 #[error(transparent)]
121 GetSpecError(#[from] get_spec::Error),
122 #[error(transparent)]
123 ArgParsing(#[from] arg_parsing::Error),
124}
125
126impl From<Infallible> for Error {
127 fn from(_: Infallible) -> Self {
128 unreachable!()
129 }
130}
131
132impl Cmd {
133 pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
134 let res = self.invoke(global_args).await?.to_envelope();
135 match res {
136 TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
137 TxnEnvelopeResult::Res(output) => {
138 println!("{output}");
139 }
140 }
141 Ok(())
142 }
143
144 pub async fn invoke(&self, global_args: &global::Args) -> Result<TxnResult<String>, Error> {
145 self.run_against_rpc_server(Some(global_args), None).await
146 }
147
148 pub fn read_wasm(&self) -> Result<Option<Vec<u8>>, Error> {
149 Ok(if let Some(wasm) = self.wasm.as_ref() {
150 Some(fs::read(wasm).map_err(|e| Error::CannotReadContractFile(wasm.clone(), e))?)
151 } else {
152 None
153 })
154 }
155
156 pub fn spec_entries(&self) -> Result<Option<Vec<ScSpecEntry>>, Error> {
157 self.read_wasm()?
158 .map(|wasm| {
159 soroban_spec::read::from_wasm(&wasm).map_err(Error::CannotParseContractSpec)
160 })
161 .transpose()
162 }
163
164 fn should_send_tx(&self, sim_res: &SimulateTransactionResponse) -> Result<ShouldSend, Error> {
165 Ok(match self.send {
166 Send::Default => {
167 if self.is_view {
168 ShouldSend::No
169 } else if has_write(sim_res)? || has_published_event(sim_res)? || has_auth(sim_res)?
170 {
171 ShouldSend::Yes
172 } else {
173 ShouldSend::DefaultNo
174 }
175 }
176 Send::No => ShouldSend::No,
177 Send::Yes => ShouldSend::Yes,
178 })
179 }
180
181 async fn simulate(
183 &self,
184 host_function_params: &InvokeContractArgs,
185 account_details: &AccountEntry,
186 rpc_client: &Client,
187 ) -> Result<Assembled, Error> {
188 let sequence: i64 = account_details.seq_num.0;
189 let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) =
190 account_details.account_id.clone();
191
192 let tx = build_invoke_contract_tx(
193 host_function_params.clone(),
194 sequence + 1,
195 self.fee.fee,
196 account_id,
197 )?;
198 Ok(simulate_and_assemble_transaction(rpc_client, &tx).await?)
199 }
200}
201
202#[async_trait::async_trait]
203impl NetworkRunnable for Cmd {
204 type Error = Error;
205 type Result = TxnResult<String>;
206
207 async fn run_against_rpc_server(
208 &self,
209 global_args: Option<&global::Args>,
210 config: Option<&config::Args>,
211 ) -> Result<TxnResult<String>, Error> {
212 let config = config.unwrap_or(&self.config);
213 let print = print::Print::new(global_args.map_or(false, |g| g.quiet));
214 let network = config.get_network()?;
215 tracing::trace!(?network);
216 let contract_id = self
217 .contract_id
218 .resolve_contract_id(&config.locator, &network.network_passphrase)?;
219
220 let spec_entries = self.spec_entries()?;
221 if let Some(spec_entries) = &spec_entries {
222 build_host_function_parameters(&contract_id, &self.slop, spec_entries, config)?;
224 }
225 let client = network.rpc_client()?;
226
227 let spec_entries = get_remote_contract_spec(
228 &contract_id.0,
229 &config.locator,
230 &config.network,
231 global_args,
232 Some(config),
233 )
234 .await
235 .map_err(Error::from)?;
236
237 let params =
238 build_host_function_parameters(&contract_id, &self.slop, &spec_entries, config)?;
239
240 let (function, spec, host_function_params, signers) = params;
241
242 let assembled = self
243 .simulate(&host_function_params, &default_account_entry(), &client)
244 .await?;
245 let should_send = self.should_send_tx(&assembled.sim_res)?;
246
247 let account_details = if should_send == ShouldSend::Yes {
248 client
249 .verify_network_passphrase(Some(&network.network_passphrase))
250 .await?;
251
252 client
253 .get_account(&config.source_account().await?.to_string())
254 .await?
255 } else {
256 if should_send == ShouldSend::DefaultNo {
257 print.infoln(
258 "Simulation identified as read-only. Send by rerunning with `--send=yes`.",
259 );
260 }
261 let sim_res = assembled.sim_response();
262 let (return_value, events) = (sim_res.results()?, sim_res.events()?);
263 crate::log::event::all(&events);
264 crate::log::event::contract(&events, &print);
265 return Ok(output_to_string(&spec, &return_value[0].xdr, &function)?);
266 };
267 let sequence: i64 = account_details.seq_num.into();
268 let AccountId(PublicKey::PublicKeyTypeEd25519(account_id)) = account_details.account_id;
269
270 let tx = Box::new(build_invoke_contract_tx(
271 host_function_params.clone(),
272 sequence + 1,
273 self.fee.fee,
274 account_id,
275 )?);
276 if self.fee.build_only {
277 return Ok(TxnResult::Txn(tx));
278 }
279 let txn = simulate_and_assemble_transaction(&client, &tx).await?;
280 let assembled = self.fee.apply_to_assembled_txn(txn);
281 let mut txn = Box::new(assembled.transaction().clone());
282 #[cfg(feature = "version_lt_23")]
283 if self.fee.sim_only {
284 return Ok(TxnResult::Txn(txn));
285 }
286 let sim_res = assembled.sim_response();
287 if global_args.map_or(true, |a| !a.no_cache) {
288 data::write(sim_res.clone().into(), &network.rpc_uri()?)?;
289 }
290 let global::Args { no_cache, .. } = global_args.cloned().unwrap_or_default();
291 if let Some(tx) = config.sign_soroban_authorizations(&txn, &signers).await? {
293 txn = Box::new(tx);
294 }
295 let res = client
296 .send_transaction_polling(&config.sign_with_local_key(*txn).await?)
297 .await?;
298 if !no_cache {
299 data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
300 }
301 let events = res
302 .result_meta
303 .as_ref()
304 .map(crate::log::extract_events)
305 .unwrap_or_default();
306 let return_value = res.return_value()?;
307
308 crate::log::event::all(&events);
309 crate::log::event::contract(&events, &print);
310 Ok(output_to_string(&spec, &return_value, &function)?)
311 }
312}
313
314const DEFAULT_ACCOUNT_ID: AccountId = AccountId(PublicKey::PublicKeyTypeEd25519(Uint256([0; 32])));
315
316fn default_account_entry() -> AccountEntry {
317 AccountEntry {
318 account_id: DEFAULT_ACCOUNT_ID,
319 balance: 0,
320 seq_num: SequenceNumber(0),
321 num_sub_entries: 0,
322 inflation_dest: None,
323 flags: 0,
324 home_domain: String32::from(unsafe { StringM::<32>::from_str("TEST").unwrap_unchecked() }),
325 thresholds: Thresholds([0; 4]),
326 signers: unsafe { [].try_into().unwrap_unchecked() },
327 ext: AccountEntryExt::V0,
328 }
329}
330
331fn build_invoke_contract_tx(
332 parameters: InvokeContractArgs,
333 sequence: i64,
334 fee: u32,
335 source_account_id: Uint256,
336) -> Result<Transaction, Error> {
337 let op = Operation {
338 source_account: None,
339 body: OperationBody::InvokeHostFunction(InvokeHostFunctionOp {
340 host_function: HostFunction::InvokeContract(parameters),
341 auth: VecM::default(),
342 }),
343 };
344 Ok(Transaction {
345 source_account: MuxedAccount::Ed25519(source_account_id),
346 fee,
347 seq_num: SequenceNumber(sequence),
348 cond: Preconditions::None,
349 memo: Memo::None,
350 operations: vec![op].try_into()?,
351 ext: TransactionExt::V0,
352 })
353}
354
355#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum, Default)]
356pub enum Send {
357 #[default]
360 Default,
361 No,
363 Yes,
365}
366
367#[derive(Debug, PartialEq)]
368enum ShouldSend {
369 DefaultNo,
370 No,
371 Yes,
372}
373
374fn has_write(sim_res: &SimulateTransactionResponse) -> Result<bool, Error> {
375 Ok(!sim_res
376 .transaction_data()?
377 .resources
378 .footprint
379 .read_write
380 .is_empty())
381}
382
383fn has_published_event(sim_res: &SimulateTransactionResponse) -> Result<bool, Error> {
384 Ok(sim_res.events()?.iter().any(
385 |DiagnosticEvent {
386 event: ContractEvent { type_, .. },
387 ..
388 }| matches!(type_, ContractEventType::Contract),
389 ))
390}
391
392fn has_auth(sim_res: &SimulateTransactionResponse) -> Result<bool, Error> {
393 Ok(sim_res
394 .results()?
395 .iter()
396 .any(|SimulateHostFunctionResult { auth, .. }| !auth.is_empty()))
397}