soroban_cli/commands/contract/
restore.rs

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