soroban_cli/commands/contract/
extend.rs1use 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 #[arg(long, required = true)]
32 pub ledgers_to_extend: u32,
33 #[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 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 let TransactionMeta::V3(TransactionMetaV3 { operations, .. }) = meta else {
199 return Err(Error::LedgerEntryNotFound);
200 };
201
202 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}