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