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