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    xdr::{
8        ConfigSettingEntry, ConfigSettingId, Error as XdrError, ExtendFootprintTtlOp,
9        ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData, LedgerFootprint,
10        LedgerKey, LedgerKeyConfigSetting, Limits, Memo, Operation, OperationBody, Preconditions,
11        SequenceNumber, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
12        Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TransactionMetaV4,
13        TtlEntry, WriteXdr,
14    },
15};
16use clap::Parser;
17
18use crate::commands::tx::fetch;
19use crate::{
20    assembled::simulate_and_assemble_transaction,
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        let assembled = simulate_and_assemble_transaction(
240            &client,
241            &tx,
242            self.resources.resource_config(),
243            self.resources.resource_fee,
244        )
245        .await?;
246
247        let tx = assembled.transaction().clone();
248        let res = client
249            .send_transaction_polling(&config.sign(tx, quiet).await?)
250            .await?;
251        self.resources.print_cost_info(&res)?;
252
253        if !no_cache {
254            data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
255        }
256
257        let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
258        let events = extract_events(&meta);
259
260        crate::log::event::all(&events);
261        crate::log::event::contract(&events, &print);
262
263        // The transaction from core will succeed regardless of whether it actually found & extended
264        // the entry, so we have to inspect the result meta to tell if it worked or not.
265        let changes = match meta {
266            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
267                // Simply check if there is exactly one entry here. We only support extending a single
268                // entry via this command (which we should fix separately, but).
269                if operations.is_empty() {
270                    return Err(Error::LedgerEntryNotFound);
271                }
272
273                operations[0].changes.clone()
274            }
275            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
276                // Simply check if there is exactly one entry here. We only support extending a single
277                // entry via this command (which we should fix separately, but).
278                if operations.is_empty() {
279                    return Err(Error::LedgerEntryNotFound);
280                }
281
282                operations[0].changes.clone()
283            }
284            _ => return Err(Error::LedgerEntryNotFound),
285        };
286
287        if changes.is_empty() {
288            print.infoln("No changes detected, transaction was a no-op.");
289            let entry = client.get_full_ledger_entries(&keys).await?;
290            let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
291
292            return Ok(TxnResult::Res(extension));
293        }
294
295        match (&changes[0], &changes[1]) {
296            (
297                LedgerEntryChange::State(_),
298                LedgerEntryChange::Updated(LedgerEntry {
299                    data:
300                        LedgerEntryData::Ttl(TtlEntry {
301                            live_until_ledger_seq,
302                            ..
303                        }),
304                    ..
305                }),
306            ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
307            _ => Err(Error::LedgerEntryNotFound),
308        }
309    }
310}