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            quiet,
251            no_cache,
252        )
253        .await?;
254
255        let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
256        let events = extract_events(&meta);
257
258        crate::log::event::all(&events);
259        crate::log::event::contract(&events, &print);
260
261        // The transaction from core will succeed regardless of whether it actually found & extended
262        // the entry, so we have to inspect the result meta to tell if it worked or not.
263        let changes = match meta {
264            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
265                // Simply check if there is exactly one entry here. We only support extending a single
266                // entry via this command (which we should fix separately, but).
267                if operations.is_empty() {
268                    return Err(Error::LedgerEntryNotFound);
269                }
270
271                operations[0].changes.clone()
272            }
273            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
274                // Simply check if there is exactly one entry here. We only support extending a single
275                // entry via this command (which we should fix separately, but).
276                if operations.is_empty() {
277                    return Err(Error::LedgerEntryNotFound);
278                }
279
280                operations[0].changes.clone()
281            }
282            _ => return Err(Error::LedgerEntryNotFound),
283        };
284
285        if changes.is_empty() {
286            print.infoln("No changes detected, transaction was a no-op.");
287            let entry = client.get_full_ledger_entries(&keys).await?;
288            let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
289
290            return Ok(TxnResult::Res(extension));
291        }
292
293        match (&changes[0], &changes[1]) {
294            (
295                LedgerEntryChange::State(_),
296                LedgerEntryChange::Updated(LedgerEntry {
297                    data:
298                        LedgerEntryData::Ttl(TtlEntry {
299                            live_until_ledger_seq,
300                            ..
301                        }),
302                    ..
303                }),
304            ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
305            _ => Err(Error::LedgerEntryNotFound),
306        }
307    }
308}