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