soroban_cli/commands/contract/
restore.rs

1use std::{fmt::Debug, path::Path, str::FromStr};
2
3use crate::{
4    log::extract_events,
5    xdr::{
6        Error as XdrError, ExtensionPoint, LedgerEntry, LedgerEntryChange, LedgerEntryData,
7        LedgerFootprint, Limits, Memo, Operation, OperationBody, OperationMeta, Preconditions,
8        RestoreFootprintOp, SequenceNumber, SorobanResources, SorobanTransactionData,
9        SorobanTransactionDataExt, Transaction, TransactionExt, TransactionMeta, TransactionMetaV3,
10        TtlEntry, WriteXdr,
11    },
12};
13use clap::{command, Parser};
14use stellar_strkey::DecodeError;
15
16use crate::{
17    commands::{
18        contract::extend,
19        global,
20        txn_result::{TxnEnvelopeResult, TxnResult},
21        NetworkRunnable,
22    },
23    config::{self, data, locator, network},
24    key, rpc, wasm, Pwd,
25};
26
27#[derive(Parser, Debug, Clone)]
28#[group(skip)]
29pub struct Cmd {
30    #[command(flatten)]
31    pub key: key::Args,
32    /// Number of ledgers to extend the entry
33    #[arg(long)]
34    pub ledgers_to_extend: Option<u32>,
35    /// Only print the new Time To Live ledger
36    #[arg(long)]
37    pub ttl_ledger_only: bool,
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    #[error("cannot parse contract ID {0}: {1}")]
69    CannotParseContractId(String, DecodeError),
70    #[error(transparent)]
71    Config(#[from] config::Error),
72    #[error("either `--key` or `--key-xdr` are required")]
73    KeyIsRequired,
74    #[error("xdr processing error: {0}")]
75    Xdr(#[from] XdrError),
76    #[error("Ledger entry not found")]
77    LedgerEntryNotFound,
78    #[error(transparent)]
79    Locator(#[from] locator::Error),
80    #[error("missing operation result")]
81    MissingOperationResult,
82    #[error(transparent)]
83    Rpc(#[from] rpc::Error),
84    #[error(transparent)]
85    Wasm(#[from] wasm::Error),
86    #[error(transparent)]
87    Key(#[from] key::Error),
88    #[error(transparent)]
89    Extend(#[from] extend::Error),
90    #[error(transparent)]
91    Data(#[from] data::Error),
92    #[error(transparent)]
93    Network(#[from] network::Error),
94}
95
96impl Cmd {
97    #[allow(clippy::too_many_lines)]
98    pub async fn run(&self) -> Result<(), Error> {
99        let res = self.run_against_rpc_server(None, None).await?.to_envelope();
100        let expiration_ledger_seq = match res {
101            TxnEnvelopeResult::TxnEnvelope(tx) => {
102                println!("{}", tx.to_xdr_base64(Limits::none())?);
103                return Ok(());
104            }
105            TxnEnvelopeResult::Res(res) => res,
106        };
107        if let Some(ledgers_to_extend) = self.ledgers_to_extend {
108            extend::Cmd {
109                key: self.key.clone(),
110                ledgers_to_extend,
111                config: self.config.clone(),
112                fee: self.fee.clone(),
113                ttl_ledger_only: false,
114            }
115            .run()
116            .await?;
117        } else {
118            println!("New ttl ledger: {expiration_ledger_seq}");
119        }
120
121        Ok(())
122    }
123}
124
125#[async_trait::async_trait]
126impl NetworkRunnable for Cmd {
127    type Error = Error;
128    type Result = TxnResult<u32>;
129
130    async fn run_against_rpc_server(
131        &self,
132        args: Option<&global::Args>,
133        config: Option<&config::Args>,
134    ) -> Result<TxnResult<u32>, Error> {
135        let config = config.unwrap_or(&self.config);
136        let print = crate::print::Print::new(args.is_some_and(|a| a.quiet));
137        let network = config.get_network()?;
138        tracing::trace!(?network);
139        let entry_keys = self.key.parse_keys(&config.locator, &network)?;
140        let client = network.rpc_client()?;
141        let source_account = config.source_account().await?;
142
143        // Get the account sequence number
144        let account_details = client
145            .get_account(&source_account.clone().to_string())
146            .await?;
147        let sequence: i64 = account_details.seq_num.into();
148
149        let tx = Box::new(Transaction {
150            source_account,
151            fee: self.fee.fee,
152            seq_num: SequenceNumber(sequence + 1),
153            cond: Preconditions::None,
154            memo: Memo::None,
155            operations: vec![Operation {
156                source_account: None,
157                body: OperationBody::RestoreFootprint(RestoreFootprintOp {
158                    ext: ExtensionPoint::V0,
159                }),
160            }]
161            .try_into()?,
162            ext: TransactionExt::V1(SorobanTransactionData {
163                ext: SorobanTransactionDataExt::V0,
164                resources: SorobanResources {
165                    footprint: LedgerFootprint {
166                        read_only: vec![].try_into()?,
167                        read_write: entry_keys.try_into()?,
168                    },
169                    instructions: self.fee.instructions.unwrap_or_default(),
170                    disk_read_bytes: 0,
171                    write_bytes: 0,
172                },
173                resource_fee: 0,
174            }),
175        });
176        if self.fee.build_only {
177            return Ok(TxnResult::Txn(tx));
178        }
179        let res = client
180            .send_transaction_polling(&config.sign(*tx).await?)
181            .await?;
182        if args.is_none_or(|a| !a.no_cache) {
183            data::write(res.clone().try_into()?, &network.rpc_uri()?)?;
184        }
185        let meta = res
186            .result_meta
187            .as_ref()
188            .ok_or(Error::MissingOperationResult)?;
189
190        tracing::trace!(?meta);
191
192        let events = extract_events(meta);
193
194        crate::log::event::all(&events);
195        crate::log::event::contract(&events, &print);
196
197        // The transaction from core will succeed regardless of whether it actually found &
198        // restored the entry, so we have to inspect the result meta to tell if it worked or not.
199        let TransactionMeta::V3(TransactionMetaV3 { operations, .. }) = meta else {
200            return Err(Error::LedgerEntryNotFound);
201        };
202        tracing::debug!("Operations:\nlen:{}\n{operations:#?}", operations.len());
203
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        if operations.len() != 1 {
211            tracing::warn!(
212                "Unexpected number of operations: {}. Currently only handle one.",
213                operations[0].changes.len()
214            );
215        }
216        Ok(TxnResult::Res(
217            parse_operations(&operations.to_vec()).ok_or(Error::MissingOperationResult)?,
218        ))
219    }
220}
221
222fn parse_operations(ops: &[OperationMeta]) -> Option<u32> {
223    ops.first().and_then(|op| {
224        op.changes.iter().find_map(|entry| match entry {
225            LedgerEntryChange::Updated(LedgerEntry {
226                data:
227                    LedgerEntryData::Ttl(TtlEntry {
228                        live_until_ledger_seq,
229                        ..
230                    }),
231                ..
232            })
233            | LedgerEntryChange::Created(LedgerEntry {
234                data:
235                    LedgerEntryData::Ttl(TtlEntry {
236                        live_until_ledger_seq,
237                        ..
238                    }),
239                ..
240            }) => Some(*live_until_ledger_seq),
241            _ => None,
242        })
243    })
244}