Skip to main content

soroban_cli/commands/contract/
extend.rs

1use std::{fmt::Debug, num::TryFromIntError, path::Path, str::FromStr};
2
3use crate::{
4    log::extract_events,
5    print::Print,
6    resources,
7    tx::sim_sign_and_send_tx,
8    xdr::{
9        ConfigSettingEntry, ConfigSettingId, Error as XdrError, ExtendFootprintTtlOp,
10        ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData, LedgerFootprint,
11        LedgerKey, LedgerKeyConfigSetting, Limits, Memo, Operation, OperationBody, Preconditions,
12        SequenceNumber, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
13        Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TransactionMetaV4,
14        TtlEntry, WriteXdr,
15    },
16};
17use clap::Parser;
18
19use crate::commands::tx::fetch;
20use crate::utils::XDR_DEPTH_LIMIT;
21use crate::{
22    commands::{
23        global,
24        txn_result::{TxnEnvelopeResult, TxnResult},
25        HEADING_TRANSACTION,
26    },
27    config::{self, data, locator, network},
28    key, rpc, wasm, Pwd,
29};
30
31#[derive(Parser, Debug, Clone)]
32#[group(skip)]
33pub struct Cmd {
34    /// Number of ledgers to extend the entries
35    #[arg(long, required = true)]
36    pub ledgers_to_extend: u32,
37
38    /// Only print the new Time To Live ledger
39    #[arg(long)]
40    pub ttl_ledger_only: bool,
41
42    #[command(flatten)]
43    pub key: key::Args,
44
45    #[command(flatten)]
46    pub config: config::Args,
47
48    #[command(flatten)]
49    pub resources: resources::Args,
50
51    /// Build the transaction and only write the base64 xdr to stdout
52    #[arg(long, help_heading = HEADING_TRANSACTION)]
53    pub build_only: bool,
54}
55
56impl FromStr for Cmd {
57    type Err = clap::error::Error;
58
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        use clap::{CommandFactory, FromArgMatches};
61        Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
62    }
63}
64
65impl Pwd for Cmd {
66    fn set_pwd(&mut self, pwd: &Path) {
67        self.config.set_pwd(pwd);
68    }
69}
70
71#[derive(thiserror::Error, Debug)]
72pub enum Error {
73    #[error("parsing key {key}: {error}")]
74    CannotParseKey {
75        key: String,
76        error: soroban_spec_tools::Error,
77    },
78
79    #[error("parsing XDR key {key}: {error}")]
80    CannotParseXdrKey { key: String, error: XdrError },
81
82    #[error(transparent)]
83    Config(#[from] config::Error),
84
85    #[error("either `--key` or `--key-xdr` are required")]
86    KeyIsRequired,
87
88    #[error("xdr processing error: {0}")]
89    Xdr(#[from] XdrError),
90
91    #[error("Ledger entry not found")]
92    LedgerEntryNotFound,
93
94    #[error("missing operation result")]
95    MissingOperationResult,
96
97    #[error(transparent)]
98    Rpc(#[from] rpc::Error),
99
100    #[error(transparent)]
101    Wasm(#[from] wasm::Error),
102
103    #[error(transparent)]
104    Key(#[from] key::Error),
105
106    #[error(transparent)]
107    Data(#[from] data::Error),
108
109    #[error(transparent)]
110    Network(#[from] network::Error),
111
112    #[error(transparent)]
113    Locator(#[from] locator::Error),
114
115    #[error(transparent)]
116    IntError(#[from] TryFromIntError),
117
118    #[error("Failed to fetch state archival settings from network")]
119    StateArchivalSettingsNotFound,
120
121    #[error("Ledgers to extend ({requested}) exceeds network maximum ({max})")]
122    LedgersToExtendTooLarge { requested: u32, max: u32 },
123
124    #[error(transparent)]
125    Fee(#[from] fetch::fee::Error),
126
127    #[error(transparent)]
128    Fetch(#[from] fetch::Error),
129}
130
131impl Cmd {
132    #[allow(clippy::too_many_lines)]
133    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
134        let res = self
135            .execute(&self.config, global_args.quiet, global_args.no_cache)
136            .await?
137            .to_envelope();
138        match res {
139            TxnEnvelopeResult::TxnEnvelope(tx) => {
140                println!("{}", tx.to_xdr_base64(Limits::depth(XDR_DEPTH_LIMIT))?);
141            }
142            TxnEnvelopeResult::Res(ttl_ledger) => {
143                if self.ttl_ledger_only {
144                    println!("{ttl_ledger}");
145                } else {
146                    println!("New ttl ledger: {ttl_ledger}");
147                }
148            }
149        }
150
151        Ok(())
152    }
153
154    async fn get_max_entry_ttl(client: &rpc::Client) -> Result<u32, Error> {
155        let key = LedgerKey::ConfigSetting(LedgerKeyConfigSetting {
156            config_setting_id: ConfigSettingId::StateArchival,
157        });
158
159        let entries = client.get_full_ledger_entries(&[key]).await?;
160
161        if let Some(entry) = entries.entries.first() {
162            if let LedgerEntryData::ConfigSetting(ConfigSettingEntry::StateArchival(settings)) =
163                &entry.val
164            {
165                return Ok(settings.max_entry_ttl);
166            }
167        }
168
169        Err(Error::StateArchivalSettingsNotFound)
170    }
171
172    async fn ledgers_to_extend(&self, client: &rpc::Client) -> Result<u32, Error> {
173        let max_entry_ttl = Self::get_max_entry_ttl(client).await?;
174
175        tracing::trace!(
176            "Checking ledgers_to_extend: requested={}, max_entry_ttl={}",
177            self.ledgers_to_extend,
178            max_entry_ttl
179        );
180
181        if self.ledgers_to_extend > max_entry_ttl {
182            return Err(Error::LedgersToExtendTooLarge {
183                requested: self.ledgers_to_extend,
184                max: max_entry_ttl,
185            });
186        }
187
188        Ok(self.ledgers_to_extend)
189    }
190
191    #[allow(clippy::too_many_lines)]
192    pub async fn execute(
193        &self,
194        config: &config::Args,
195        quiet: bool,
196        no_cache: bool,
197    ) -> Result<TxnResult<u32>, Error> {
198        let print = Print::new(quiet);
199        let network = config.get_network()?;
200        tracing::trace!(?network);
201        let keys = self.key.parse_keys(&config.locator, &network)?;
202        let client = network.rpc_client()?;
203        client
204            .verify_network_passphrase(Some(&network.network_passphrase))
205            .await?;
206        let source_account = config.source_account()?;
207        let extend_to = self.ledgers_to_extend(&client).await?;
208
209        // Get the account sequence number
210        let account_details = client
211            .get_account(&source_account.clone().to_string())
212            .await?;
213        let sequence: i64 = account_details.seq_num.into();
214
215        let tx = Box::new(Transaction {
216            source_account,
217            fee: config.get_inclusion_fee()?,
218            seq_num: SequenceNumber(sequence + 1),
219            cond: Preconditions::None,
220            memo: Memo::None,
221            operations: vec![Operation {
222                source_account: None,
223                body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
224                    ext: ExtensionPoint::V0,
225                    extend_to,
226                }),
227            }]
228            .try_into()?,
229            ext: TransactionExt::V1(SorobanTransactionData {
230                ext: SorobanTransactionDataExt::V0,
231                resources: SorobanResources {
232                    footprint: LedgerFootprint {
233                        read_only: keys.clone().try_into()?,
234                        read_write: vec![].try_into()?,
235                    },
236                    instructions: self.resources.instructions.unwrap_or_default(),
237                    disk_read_bytes: 0,
238                    write_bytes: 0,
239                },
240                resource_fee: 0,
241            }),
242        });
243        if self.build_only {
244            return Ok(TxnResult::Txn(tx));
245        }
246
247        let res = sim_sign_and_send_tx::<Error>(
248            &client,
249            &tx,
250            config,
251            &self.resources,
252            &[],
253            // Footprint extend is not an InvokeHostFunction op, so the RPC does
254            // not accept an auth mode.
255            None,
256            quiet,
257            no_cache,
258        )
259        .await?;
260
261        let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
262        let events = extract_events(&meta);
263
264        crate::log::event::all(&events);
265        crate::log::event::contract(&events, &print);
266
267        // The transaction from core will succeed regardless of whether it actually found & extended
268        // the entry, so we have to inspect the result meta to tell if it worked or not.
269        let changes = match meta {
270            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
271                // Simply check if there is exactly one entry here. We only support extending a single
272                // entry via this command (which we should fix separately, but).
273                if operations.is_empty() {
274                    return Err(Error::LedgerEntryNotFound);
275                }
276
277                operations[0].changes.clone()
278            }
279            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
280                // Simply check if there is exactly one entry here. We only support extending a single
281                // entry via this command (which we should fix separately, but).
282                if operations.is_empty() {
283                    return Err(Error::LedgerEntryNotFound);
284                }
285
286                operations[0].changes.clone()
287            }
288            _ => return Err(Error::LedgerEntryNotFound),
289        };
290
291        if changes.is_empty() {
292            print.infoln("No changes detected, transaction was a no-op.");
293            let entry = client.get_full_ledger_entries(&keys).await?;
294            // A no-op extend against a non-existent entry returns no entries, so
295            // avoid indexing into an empty vec (which would panic).
296            let extension = entry
297                .entries
298                .first()
299                .ok_or(Error::LedgerEntryNotFound)?
300                .live_until_ledger_seq
301                .unwrap_or_default();
302
303            return Ok(TxnResult::Res(extension));
304        }
305
306        match (&changes[0], &changes[1]) {
307            (
308                LedgerEntryChange::State(_),
309                LedgerEntryChange::Updated(LedgerEntry {
310                    data:
311                        LedgerEntryData::Ttl(TtlEntry {
312                            live_until_ledger_seq,
313                            ..
314                        }),
315                    ..
316                }),
317            ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
318            _ => Err(Error::LedgerEntryNotFound),
319        }
320    }
321}