soroban_cli/commands/contract/
extend.rs1use std::{fmt::Debug, num::TryFromIntError, path::Path, str::FromStr};
2
3use crate::{
4 log::extract_events,
5 print::Print,
6 xdr::{
7 Error as XdrError, ExtendFootprintTtlOp, ExtensionPoint, LedgerEntry, LedgerEntryChange,
8 LedgerEntryData, LedgerFootprint, Limits, Memo, Operation, OperationBody, Preconditions,
9 SequenceNumber, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
10 Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TransactionMetaV4,
11 TtlEntry, WriteXdr,
12 },
13};
14use clap::{command, Parser};
15
16use crate::{
17 assembled::simulate_and_assemble_transaction,
18 commands::{
19 global,
20 txn_result::{TxnEnvelopeResult, TxnResult},
21 NetworkRunnable,
22 },
23 config::{self, data, locator, network},
24 key, rpc, wasm, Pwd,
25};
26
27const MAX_LEDGERS_TO_EXTEND: u32 = 535_679;
28
29#[derive(Parser, Debug, Clone)]
30#[group(skip)]
31pub struct Cmd {
32 #[arg(long, required = true)]
34 pub ledgers_to_extend: u32,
35 #[arg(long)]
37 pub ttl_ledger_only: bool,
38 #[command(flatten)]
39 pub key: key::Args,
40 #[command(flatten)]
41 pub config: config::Args,
42 #[command(flatten)]
43 pub fee: crate::fee::Args,
44}
45
46impl FromStr for Cmd {
47 type Err = clap::error::Error;
48
49 fn from_str(s: &str) -> Result<Self, Self::Err> {
50 use clap::{CommandFactory, FromArgMatches};
51 Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
52 }
53}
54
55impl Pwd for Cmd {
56 fn set_pwd(&mut self, pwd: &Path) {
57 self.config.set_pwd(pwd);
58 }
59}
60
61#[derive(thiserror::Error, Debug)]
62pub enum Error {
63 #[error("parsing key {key}: {error}")]
64 CannotParseKey {
65 key: String,
66 error: soroban_spec_tools::Error,
67 },
68 #[error("parsing XDR key {key}: {error}")]
69 CannotParseXdrKey { key: String, error: XdrError },
70
71 #[error(transparent)]
72 Config(#[from] config::Error),
73 #[error("either `--key` or `--key-xdr` are required")]
74 KeyIsRequired,
75 #[error("xdr processing error: {0}")]
76 Xdr(#[from] XdrError),
77 #[error("Ledger entry not found")]
78 LedgerEntryNotFound,
79 #[error("missing operation result")]
80 MissingOperationResult,
81 #[error(transparent)]
82 Rpc(#[from] rpc::Error),
83 #[error(transparent)]
84 Wasm(#[from] wasm::Error),
85 #[error(transparent)]
86 Key(#[from] key::Error),
87 #[error(transparent)]
88 Data(#[from] data::Error),
89 #[error(transparent)]
90 Network(#[from] network::Error),
91 #[error(transparent)]
92 Locator(#[from] locator::Error),
93 #[error(transparent)]
94 IntError(#[from] TryFromIntError),
95}
96
97impl Cmd {
98 #[allow(clippy::too_many_lines)]
99 pub async fn run(&self) -> Result<(), Error> {
100 let res = self.run_against_rpc_server(None, None).await?.to_envelope();
101 match res {
102 TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
103 TxnEnvelopeResult::Res(ttl_ledger) => {
104 if self.ttl_ledger_only {
105 println!("{ttl_ledger}");
106 } else {
107 println!("New ttl ledger: {ttl_ledger}");
108 }
109 }
110 }
111
112 Ok(())
113 }
114
115 fn ledgers_to_extend(&self) -> u32 {
116 let res = u32::min(self.ledgers_to_extend, MAX_LEDGERS_TO_EXTEND);
117 if res < self.ledgers_to_extend {
118 tracing::warn!(
119 "Ledgers to extend is too large, using max value of {MAX_LEDGERS_TO_EXTEND}"
120 );
121 }
122 res
123 }
124}
125
126#[async_trait::async_trait]
127impl NetworkRunnable for Cmd {
128 type Error = Error;
129 type Result = TxnResult<u32>;
130
131 #[allow(clippy::too_many_lines)]
132 async fn run_against_rpc_server(
133 &self,
134 args: Option<&global::Args>,
135 config: Option<&config::Args>,
136 ) -> Result<TxnResult<u32>, Self::Error> {
137 let config = config.unwrap_or(&self.config);
138 let print = Print::new(args.is_some_and(|a| a.quiet));
139 let network = config.get_network()?;
140 tracing::trace!(?network);
141 let keys = self.key.parse_keys(&config.locator, &network)?;
142 let client = network.rpc_client()?;
143 let source_account = config.source_account().await?;
144 let extend_to = self.ledgers_to_extend();
145
146 let account_details = client
148 .get_account(&source_account.clone().to_string())
149 .await?;
150 let sequence: i64 = account_details.seq_num.into();
151
152 let tx = Box::new(Transaction {
153 source_account,
154 fee: self.fee.fee,
155 seq_num: SequenceNumber(sequence + 1),
156 cond: Preconditions::None,
157 memo: Memo::None,
158 operations: vec![Operation {
159 source_account: None,
160 body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
161 ext: ExtensionPoint::V0,
162 extend_to,
163 }),
164 }]
165 .try_into()?,
166 ext: TransactionExt::V1(SorobanTransactionData {
167 ext: SorobanTransactionDataExt::V0,
168 resources: SorobanResources {
169 footprint: LedgerFootprint {
170 read_only: keys.clone().try_into()?,
171 read_write: vec![].try_into()?,
172 },
173 instructions: self.fee.instructions.unwrap_or_default(),
174 disk_read_bytes: 0,
175 write_bytes: 0,
176 },
177 resource_fee: 0,
178 }),
179 });
180 if self.fee.build_only {
181 return Ok(TxnResult::Txn(tx));
182 }
183 let tx = simulate_and_assemble_transaction(&client, &tx)
184 .await?
185 .transaction()
186 .clone();
187 let res = client
188 .send_transaction_polling(&config.sign(tx).await?)
189 .await?;
190 if args.is_none_or(|a| !a.no_cache) {
191 data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
192 }
193
194 let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
195 let events = extract_events(&meta);
196
197 crate::log::event::all(&events);
198 crate::log::event::contract(&events, &print);
199
200 let changes = match meta {
203 TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
204 if operations.is_empty() {
207 return Err(Error::LedgerEntryNotFound);
208 }
209
210 operations[0].changes.clone()
211 }
212 TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
213 if operations.is_empty() {
216 return Err(Error::LedgerEntryNotFound);
217 }
218
219 operations[0].changes.clone()
220 }
221 _ => return Err(Error::LedgerEntryNotFound),
222 };
223
224 if changes.is_empty() {
225 print.infoln("No changes detected, transaction was a no-op.");
226 let entry = client.get_full_ledger_entries(&keys).await?;
227 let extension = entry.entries[0].live_until_ledger_seq;
228
229 return Ok(TxnResult::Res(extension));
230 }
231
232 match (&changes[0], &changes[1]) {
233 (
234 LedgerEntryChange::State(_),
235 LedgerEntryChange::Updated(LedgerEntry {
236 data:
237 LedgerEntryData::Ttl(TtlEntry {
238 live_until_ledger_seq,
239 ..
240 }),
241 ..
242 }),
243 ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
244 _ => Err(Error::LedgerEntryNotFound),
245 }
246 }
247}