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        NetworkRunnable,
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)]
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            .run_against_rpc_server(Some(global_args), None)
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
189#[async_trait::async_trait]
190impl NetworkRunnable for Cmd {
191    type Error = Error;
192    type Result = TxnResult<u32>;
193
194    #[allow(clippy::too_many_lines)]
195    async fn run_against_rpc_server(
196        &self,
197        args: Option<&global::Args>,
198        config: Option<&config::Args>,
199    ) -> Result<TxnResult<u32>, Self::Error> {
200        let config = config.unwrap_or(&self.config);
201        let quiet = args.is_some_and(|a| a.quiet);
202        let print = Print::new(quiet);
203        let network = config.get_network()?;
204        tracing::trace!(?network);
205        let keys = self.key.parse_keys(&config.locator, &network)?;
206        let client = network.rpc_client()?;
207        let source_account = config.source_account().await?;
208        let extend_to = self.ledgers_to_extend(&client).await?;
209
210        // Get the account sequence number
211        let account_details = client
212            .get_account(&source_account.clone().to_string())
213            .await?;
214        let sequence: i64 = account_details.seq_num.into();
215
216        let tx = Box::new(Transaction {
217            source_account,
218            fee: config.get_inclusion_fee()?,
219            seq_num: SequenceNumber(sequence + 1),
220            cond: Preconditions::None,
221            memo: Memo::None,
222            operations: vec![Operation {
223                source_account: None,
224                body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
225                    ext: ExtensionPoint::V0,
226                    extend_to,
227                }),
228            }]
229            .try_into()?,
230            ext: TransactionExt::V1(SorobanTransactionData {
231                ext: SorobanTransactionDataExt::V0,
232                resources: SorobanResources {
233                    footprint: LedgerFootprint {
234                        read_only: keys.clone().try_into()?,
235                        read_write: vec![].try_into()?,
236                    },
237                    instructions: self.resources.instructions.unwrap_or_default(),
238                    disk_read_bytes: 0,
239                    write_bytes: 0,
240                },
241                resource_fee: 0,
242            }),
243        });
244        if self.build_only {
245            return Ok(TxnResult::Txn(tx));
246        }
247        let assembled = simulate_and_assemble_transaction(
248            &client,
249            &tx,
250            self.resources.resource_config(),
251            self.resources.resource_fee,
252        )
253        .await?;
254
255        let tx = assembled.transaction().clone();
256        let res = client
257            .send_transaction_polling(&config.sign(tx, quiet).await?)
258            .await?;
259        self.resources.print_cost_info(&res)?;
260
261        if args.is_none_or(|a| !a.no_cache) {
262            data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
263        }
264
265        let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
266        let events = extract_events(&meta);
267
268        crate::log::event::all(&events);
269        crate::log::event::contract(&events, &print);
270
271        // The transaction from core will succeed regardless of whether it actually found & extended
272        // the entry, so we have to inspect the result meta to tell if it worked or not.
273        let changes = match meta {
274            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
275                // Simply check if there is exactly one entry here. We only support extending a single
276                // entry via this command (which we should fix separately, but).
277                if operations.is_empty() {
278                    return Err(Error::LedgerEntryNotFound);
279                }
280
281                operations[0].changes.clone()
282            }
283            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
284                // Simply check if there is exactly one entry here. We only support extending a single
285                // entry via this command (which we should fix separately, but).
286                if operations.is_empty() {
287                    return Err(Error::LedgerEntryNotFound);
288                }
289
290                operations[0].changes.clone()
291            }
292            _ => return Err(Error::LedgerEntryNotFound),
293        };
294
295        if changes.is_empty() {
296            print.infoln("No changes detected, transaction was a no-op.");
297            let entry = client.get_full_ledger_entries(&keys).await?;
298            let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
299
300            return Ok(TxnResult::Res(extension));
301        }
302
303        match (&changes[0], &changes[1]) {
304            (
305                LedgerEntryChange::State(_),
306                LedgerEntryChange::Updated(LedgerEntry {
307                    data:
308                        LedgerEntryData::Ttl(TtlEntry {
309                            live_until_ledger_seq,
310                            ..
311                        }),
312                    ..
313                }),
314            ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
315            _ => Err(Error::LedgerEntryNotFound),
316        }
317    }
318}