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        Error as XdrError, ExtendFootprintTtlOp, ExtensionPoint, LedgerEntry, LedgerEntryChange,
8        LedgerEntryData, LedgerFootprint, Limits, Memo, Operation, OperationBody, Preconditions,
9        SequenceNumber, SorobanResources, SorobanTransactionData, SorobanTransactionDataExt,
10        Transaction, TransactionExt, TransactionMeta, TransactionMetaV3, TransactionMetaV4,
11        TtlEntry, WriteXdr,
12    },
13};
14use clap::{command, Parser};
15
16use crate::{
17    assembled::simulate_and_assemble_transaction,
18    commands::{
19        global,
20        txn_result::{TxnEnvelopeResult, TxnResult},
21        NetworkRunnable,
22    },
23    config::{self, data, locator, network},
24    key, rpc, wasm, Pwd,
25};
26
27const MAX_LEDGERS_TO_EXTEND: u32 = 535_679;
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    /// Only print the new Time To Live ledger
36    #[arg(long)]
37    pub ttl_ledger_only: bool,
38    #[command(flatten)]
39    pub key: key::Args,
40    #[command(flatten)]
41    pub config: config::Args,
42    #[command(flatten)]
43    pub fee: crate::fee::Args,
44}
45
46impl FromStr for Cmd {
47    type Err = clap::error::Error;
48
49    fn from_str(s: &str) -> Result<Self, Self::Err> {
50        use clap::{CommandFactory, FromArgMatches};
51        Self::from_arg_matches_mut(&mut Self::command().get_matches_from(s.split_whitespace()))
52    }
53}
54
55impl Pwd for Cmd {
56    fn set_pwd(&mut self, pwd: &Path) {
57        self.config.set_pwd(pwd);
58    }
59}
60
61#[derive(thiserror::Error, Debug)]
62pub enum Error {
63    #[error("parsing key {key}: {error}")]
64    CannotParseKey {
65        key: String,
66        error: soroban_spec_tools::Error,
67    },
68    #[error("parsing XDR key {key}: {error}")]
69    CannotParseXdrKey { key: String, error: XdrError },
70
71    #[error(transparent)]
72    Config(#[from] config::Error),
73    #[error("either `--key` or `--key-xdr` are required")]
74    KeyIsRequired,
75    #[error("xdr processing error: {0}")]
76    Xdr(#[from] XdrError),
77    #[error("Ledger entry not found")]
78    LedgerEntryNotFound,
79    #[error("missing operation result")]
80    MissingOperationResult,
81    #[error(transparent)]
82    Rpc(#[from] rpc::Error),
83    #[error(transparent)]
84    Wasm(#[from] wasm::Error),
85    #[error(transparent)]
86    Key(#[from] key::Error),
87    #[error(transparent)]
88    Data(#[from] data::Error),
89    #[error(transparent)]
90    Network(#[from] network::Error),
91    #[error(transparent)]
92    Locator(#[from] locator::Error),
93    #[error(transparent)]
94    IntError(#[from] TryFromIntError),
95}
96
97impl Cmd {
98    #[allow(clippy::too_many_lines)]
99    pub async fn run(&self) -> Result<(), Error> {
100        let res = self.run_against_rpc_server(None, None).await?.to_envelope();
101        match res {
102            TxnEnvelopeResult::TxnEnvelope(tx) => println!("{}", tx.to_xdr_base64(Limits::none())?),
103            TxnEnvelopeResult::Res(ttl_ledger) => {
104                if self.ttl_ledger_only {
105                    println!("{ttl_ledger}");
106                } else {
107                    println!("New ttl ledger: {ttl_ledger}");
108                }
109            }
110        }
111
112        Ok(())
113    }
114
115    fn ledgers_to_extend(&self) -> u32 {
116        let res = u32::min(self.ledgers_to_extend, MAX_LEDGERS_TO_EXTEND);
117        if res < self.ledgers_to_extend {
118            tracing::warn!(
119                "Ledgers to extend is too large, using max value of {MAX_LEDGERS_TO_EXTEND}"
120            );
121        }
122        res
123    }
124}
125
126#[async_trait::async_trait]
127impl NetworkRunnable for Cmd {
128    type Error = Error;
129    type Result = TxnResult<u32>;
130
131    #[allow(clippy::too_many_lines)]
132    async fn run_against_rpc_server(
133        &self,
134        args: Option<&global::Args>,
135        config: Option<&config::Args>,
136    ) -> Result<TxnResult<u32>, Self::Error> {
137        let config = config.unwrap_or(&self.config);
138        let print = Print::new(args.is_some_and(|a| a.quiet));
139        let network = config.get_network()?;
140        tracing::trace!(?network);
141        let keys = self.key.parse_keys(&config.locator, &network)?;
142        let client = network.rpc_client()?;
143        let source_account = config.source_account().await?;
144        let extend_to = self.ledgers_to_extend();
145
146        // Get the account sequence number
147        let account_details = client
148            .get_account(&source_account.clone().to_string())
149            .await?;
150        let sequence: i64 = account_details.seq_num.into();
151
152        let tx = Box::new(Transaction {
153            source_account,
154            fee: self.fee.fee,
155            seq_num: SequenceNumber(sequence + 1),
156            cond: Preconditions::None,
157            memo: Memo::None,
158            operations: vec![Operation {
159                source_account: None,
160                body: OperationBody::ExtendFootprintTtl(ExtendFootprintTtlOp {
161                    ext: ExtensionPoint::V0,
162                    extend_to,
163                }),
164            }]
165            .try_into()?,
166            ext: TransactionExt::V1(SorobanTransactionData {
167                ext: SorobanTransactionDataExt::V0,
168                resources: SorobanResources {
169                    footprint: LedgerFootprint {
170                        read_only: keys.clone().try_into()?,
171                        read_write: vec![].try_into()?,
172                    },
173                    instructions: self.fee.instructions.unwrap_or_default(),
174                    disk_read_bytes: 0,
175                    write_bytes: 0,
176                },
177                resource_fee: 0,
178            }),
179        });
180        if self.fee.build_only {
181            return Ok(TxnResult::Txn(tx));
182        }
183        let tx = simulate_and_assemble_transaction(&client, &tx)
184            .await?
185            .transaction()
186            .clone();
187        let res = client
188            .send_transaction_polling(&config.sign(tx).await?)
189            .await?;
190        if args.is_none_or(|a| !a.no_cache) {
191            data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
192        }
193
194        let meta = res.result_meta.ok_or(Error::MissingOperationResult)?;
195        let events = extract_events(&meta);
196
197        crate::log::event::all(&events);
198        crate::log::event::contract(&events, &print);
199
200        // The transaction from core will succeed regardless of whether it actually found & extended
201        // the entry, so we have to inspect the result meta to tell if it worked or not.
202        let changes = match meta {
203            TransactionMeta::V4(TransactionMetaV4 { operations, .. }) => {
204                // Simply check if there is exactly one entry here. We only support extending a single
205                // entry via this command (which we should fix separately, but).
206                if operations.is_empty() {
207                    return Err(Error::LedgerEntryNotFound);
208                }
209
210                operations[0].changes.clone()
211            }
212            TransactionMeta::V3(TransactionMetaV3 { operations, .. }) => {
213                // Simply check if there is exactly one entry here. We only support extending a single
214                // entry via this command (which we should fix separately, but).
215                if operations.is_empty() {
216                    return Err(Error::LedgerEntryNotFound);
217                }
218
219                operations[0].changes.clone()
220            }
221            _ => return Err(Error::LedgerEntryNotFound),
222        };
223
224        if changes.is_empty() {
225            print.infoln("No changes detected, transaction was a no-op.");
226            let entry = client.get_full_ledger_entries(&keys).await?;
227            let extension = entry.entries[0].live_until_ledger_seq;
228
229            return Ok(TxnResult::Res(extension));
230        }
231
232        match (&changes[0], &changes[1]) {
233            (
234                LedgerEntryChange::State(_),
235                LedgerEntryChange::Updated(LedgerEntry {
236                    data:
237                        LedgerEntryData::Ttl(TtlEntry {
238                            live_until_ledger_seq,
239                            ..
240                        }),
241                    ..
242                }),
243            ) => Ok(TxnResult::Res(*live_until_ledger_seq)),
244            _ => Err(Error::LedgerEntryNotFound),
245        }
246    }
247}