soroban_cli/commands/contract/
restore.rs1use 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 #[arg(long)]
34 pub ledgers_to_extend: Option<u32>,
35 #[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 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 let TransactionMeta::V3(TransactionMetaV3 { operations, .. }) = meta else {
200 return Err(Error::LedgerEntryNotFound);
201 };
202 tracing::debug!("Operations:\nlen:{}\n{operations:#?}", operations.len());
203
204 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}