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    xdr::{
7        ConfigSettingEntry, ConfigSettingId, Error as XdrError, ExtendFootprintTtlOp,
8        ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData, LedgerFootprint,
9        LedgerKey, LedgerKeyConfigSetting, Limits, Memo, Operation, OperationBody, Preconditions,
10        SequenceNumber, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
11        Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TransactionMetaV4,
12        TtlEntry, WriteXdr,
13    },
14};
15use clap::{command, Parser};
16
17use crate::commands::tx::fetch;
18use crate::{
19    assembled::simulate_and_assemble_transaction,
20    commands::{
21        global,
22        txn_result::{TxnEnvelopeResult, TxnResult},
23        NetworkRunnable,
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 fee: crate::fee::Args,
48}
49
50impl FromStr for Cmd {
51    type Err = clap::error::Error;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        use clap::{CommandFactory, FromArgMatches};
55        Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
56    }
57}
58
59impl Pwd for Cmd {
60    fn set_pwd(&mut self, pwd: &Path) {
61        self.config.set_pwd(pwd);
62    }
63}
64
65#[derive(thiserror::Error, Debug)]
66pub enum Error {
67    #[error("parsing key {key}: {error}")]
68    CannotParseKey {
69        key: String,
70        error: soroban_spec_tools::Error,
71    },
72
73    #[error("parsing XDR key {key}: {error}")]
74    CannotParseXdrKey { key: String, error: XdrError },
75
76    #[error(transparent)]
77    Config(#[from] config::Error),
78
79    #[error("either `--key` or `--key-xdr` are required")]
80    KeyIsRequired,
81
82    #[error("xdr processing error: {0}")]
83    Xdr(#[from] XdrError),
84
85    #[error("Ledger entry not found")]
86    LedgerEntryNotFound,
87
88    #[error("missing operation result")]
89    MissingOperationResult,
90
91    #[error(transparent)]
92    Rpc(#[from] rpc::Error),
93
94    #[error(transparent)]
95    Wasm(#[from] wasm::Error),
96
97    #[error(transparent)]
98    Key(#[from] key::Error),
99
100    #[error(transparent)]
101    Data(#[from] data::Error),
102
103    #[error(transparent)]
104    Network(#[from] network::Error),
105
106    #[error(transparent)]
107    Locator(#[from] locator::Error),
108
109    #[error(transparent)]
110    IntError(#[from] TryFromIntError),
111
112    #[error("Failed to fetch state archival settings from network")]
113    StateArchivalSettingsNotFound,
114
115    #[error("Ledgers to extend ({requested}) exceeds network maximum ({max})")]
116    LedgersToExtendTooLarge { requested: u32, max: u32 },
117
118    #[error(transparent)]
119    Fee(#[from] fetch::fee::Error),
120
121    #[error(transparent)]
122    Fetch(#[from] fetch::Error),
123}
124
125impl Cmd {
126    #[allow(clippy::too_many_lines)]
127    pub async fn run(&self, global_args: &global::Args) -> Result<(), Error> {
128        let res = self
129            .run_against_rpc_server(Some(global_args), None)
130            .await?
131            .to_envelope();
132        match res {
133            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
134            TxnEnvelopeResult::Res(ttl_ledger) => {
135                if self.ttl_ledger_only {
136                    println!("{ttl_ledger}");
137                } else {
138                    println!("New ttl ledger: {ttl_ledger}");
139                }
140            }
141        }
142
143        Ok(())
144    }
145
146    async fn get_max_entry_ttl(client: &rpc::Client) -> Result<u32, Error> {
147        let key = LedgerKey::ConfigSetting(LedgerKeyConfigSetting {
148            config_setting_id: ConfigSettingId::StateArchival,
149        });
150
151        let entries = client.get_full_ledger_entries(&[key]).await?;
152
153        if let Some(entry) = entries.entries.first() {
154            if let LedgerEntryData::ConfigSetting(ConfigSettingEntry::StateArchival(settings)) =
155                &entry.val
156            {
157                return Ok(settings.max_entry_ttl);
158            }
159        }
160
161        Err(Error::StateArchivalSettingsNotFound)
162    }
163
164    async fn ledgers_to_extend(&self, client: &rpc::Client) -> Result<u32, Error> {
165        let max_entry_ttl = Self::get_max_entry_ttl(client).await?;
166
167        tracing::trace!(
168            "Checking ledgers_to_extend: requested={}, max_entry_ttl={}",
169            self.ledgers_to_extend,
170            max_entry_ttl
171        );
172
173        if self.ledgers_to_extend > max_entry_ttl {
174            return Err(Error::LedgersToExtendTooLarge {
175                requested: self.ledgers_to_extend,
176                max: max_entry_ttl,
177            });
178        }
179
180        Ok(self.ledgers_to_extend)
181    }
182}
183
184#[async_trait::async_trait]
185impl NetworkRunnable for Cmd {
186    type Error = Error;
187    type Result = TxnResult<u32>;
188
189    #[allow(clippy::too_many_lines)]
190    async fn run_against_rpc_server(
191        &self,
192        args: Option<&global::Args>,
193        config: Option<&config::Args>,
194    ) -> Result<TxnResult<u32>, Self::Error> {
195        let config = config.unwrap_or(&self.config);
196        let quiet = args.is_some_and(|a| a.quiet);
197        let print = Print::new(quiet);
198        let network = config.get_network()?;
199        tracing::trace!(?network);
200        let keys = self.key.parse_keys(&config.locator, &network)?;
201        let client = network.rpc_client()?;
202        let source_account = config.source_account().await?;
203        let extend_to = self.ledgers_to_extend(&client).await?;
204
205        // Get the account sequence number
206        let account_details = client
207            .get_account(&source_account.clone().to_string())
208            .await?;
209        let sequence: i64 = account_details.seq_num.into();
210
211        let tx = Box::new(Transaction {
212            source_account,
213            fee: self.fee.fee,
214            seq_num: SequenceNumber(sequence + 1),
215            cond: Preconditions::None,
216            memo: Memo::None,
217            operations: vec![Operation {
218                source_account: None,
219                body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
220                    ext: ExtensionPoint::V0,
221                    extend_to,
222                }),
223            }]
224            .try_into()?,
225            ext: TransactionExt::V1(SorobanTransactionData {
226                ext: SorobanTransactionDataExt::V0,
227                resources: SorobanResources {
228                    footprint: LedgerFootprint {
229                        read_only: keys.clone().try_into()?,
230                        read_write: vec![].try_into()?,
231                    },
232                    instructions: self.fee.instructions.unwrap_or_default(),
233                    disk_read_bytes: 0,
234                    write_bytes: 0,
235                },
236                resource_fee: 0,
237            }),
238        });
239        if self.fee.build_only {
240            return Ok(TxnResult::Txn(tx));
241        }
242        let assembled =
243            simulate_and_assemble_transaction(&client, &tx, self.fee.resource_config()).await?;
244
245        let tx = assembled.transaction().clone();
246        let res = client
247            .send_transaction_polling(&config.sign(tx, quiet).await?)
248            .await?;
249        self.fee.print_cost_info(&res)?;
250
251        if args.is_none_or(|a| !a.no_cache) {
252            data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
253        }
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}