soroban_cli/commands/contract/
extend.rs

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