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