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