Skip to content

Commit 473224f

Browse files
authored
Fetch snapshot values dynamically rather than hardcoding them. (#2219)
1 parent 27f3c82 commit 473224f

4 files changed

Lines changed: 163 additions & 28 deletions

File tree

FULL_HELP_DOCS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1587,13 +1587,13 @@ Any invalid contract id passed as `--address` will be ignored.
15871587
* `--out <OUT>` — Out path that the snapshot is written to
15881588

15891589
Default value: `snapshot.json`
1590+
* `--archive-url <ARCHIVE_URL>` — Archive URL
15901591
* `--global` — ⚠️ Deprecated: global config is always on
15911592
* `--config-dir <CONFIG_DIR>` — Location of config directory. By default, it uses `$XDG_CONFIG_HOME/stellar` if set, falling back to `~/.config/stellar` otherwise. Contains configuration files, aliases, and other persistent settings
15921593
* `--rpc-url <RPC_URL>` — RPC server endpoint
15931594
* `--rpc-header <RPC_HEADERS>` — RPC Header(s) to include in requests to the RPC provider
15941595
* `--network-passphrase <NETWORK_PASSPHRASE>` — Network passphrase to sign the transaction sent to the rpc server
15951596
* `-n`, `--network <NETWORK>` — Name of network to use from config
1596-
* `--archive-url <ARCHIVE_URL>` — Archive URL
15971597

15981598

15991599

cmd/soroban-cli/src/commands/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ pub mod version;
2626
pub mod txn_result;
2727

2828
pub const HEADING_RPC: &str = "Options (RPC)";
29+
pub const HEADING_ARCHIVE: &str = "Options (Archive)";
2930
pub const HEADING_GLOBAL: &str = "Options (Global)";
3031
const ABOUT: &str =
3132
"Work seamlessly with Stellar accounts, contracts, and assets from the command line.

cmd/soroban-cli/src/commands/snapshot/create.rs

Lines changed: 148 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,19 @@ use std::{
1616
};
1717
use stellar_xdr::curr::{
1818
self as xdr, AccountId, Asset, BucketEntry, ConfigSettingEntry, ConfigSettingId,
19-
ContractExecutable, Frame, Hash, LedgerEntry, LedgerEntryData, LedgerKey, LedgerKeyAccount,
20-
LedgerKeyClaimableBalance, LedgerKeyConfigSetting, LedgerKeyContractCode,
21-
LedgerKeyContractData, LedgerKeyData, LedgerKeyLiquidityPool, LedgerKeyOffer,
22-
LedgerKeyTrustLine, LedgerKeyTtl, Limited, Limits, ReadXdr, ScAddress, ScContractInstance,
23-
ScVal,
19+
ContractExecutable, Frame, Hash, LedgerEntry, LedgerEntryData, LedgerHeaderHistoryEntry,
20+
LedgerKey, LedgerKeyAccount, LedgerKeyClaimableBalance, LedgerKeyConfigSetting,
21+
LedgerKeyContractCode, LedgerKeyContractData, LedgerKeyData, LedgerKeyLiquidityPool,
22+
LedgerKeyOffer, LedgerKeyTrustLine, LedgerKeyTtl, Limited, Limits, ReadXdr, ScAddress,
23+
ScContractInstance, ScVal,
2424
};
2525
use tokio::fs::OpenOptions;
2626
use tokio::io::BufReader;
2727
use tokio_util::io::StreamReader;
2828
use url::Url;
2929

3030
use crate::{
31-
commands::{config::data, global, HEADING_RPC},
31+
commands::{config::data, global, HEADING_ARCHIVE},
3232
config::{self, locator, network::passphrase},
3333
print,
3434
tx::builder,
@@ -73,25 +73,32 @@ pub struct Cmd {
7373
/// The ledger sequence number to snapshot. Defaults to latest history archived ledger.
7474
#[arg(long)]
7575
ledger: Option<u32>,
76+
7677
/// Account or contract address/alias to include in the snapshot.
7778
#[arg(long = "address", help_heading = "Filter Options")]
7879
address: Vec<String>,
80+
7981
/// WASM hashes to include in the snapshot.
8082
#[arg(long = "wasm-hash", help_heading = "Filter Options")]
8183
wasm_hashes: Vec<Hash>,
84+
8285
/// Format of the out file.
8386
#[arg(long)]
8487
output: Output,
88+
8589
/// Out path that the snapshot is written to.
8690
#[arg(long, default_value=default_out_path().into_os_string())]
8791
out: PathBuf,
92+
93+
/// Archive URL
94+
#[arg(long, help_heading = HEADING_ARCHIVE, env = "STELLAR_ARCHIVE_URL")]
95+
archive_url: Option<Url>,
96+
8897
#[command(flatten)]
8998
locator: locator::Args,
99+
90100
#[command(flatten)]
91101
network: config::network::Args,
92-
/// Archive URL
93-
#[arg(long, help_heading = HEADING_RPC, env = "STELLAR_ARCHIVE_URL")]
94-
archive_url: Option<Url>,
95102
}
96103

97104
#[derive(thiserror::Error, Debug)]
@@ -140,6 +147,10 @@ pub enum Error {
140147
ParseAssetName(String),
141148
#[error(transparent)]
142149
Asset(#[from] builder::asset::Error),
150+
#[error("ledger not found in archive")]
151+
LedgerNotFound,
152+
#[error("xdr parsing error: {0}")]
153+
Xdr(#[from] xdr::Error),
143154
}
144155

145156
/// Checkpoint frequency is usually 64 ledgers, but in local test nets it'll
@@ -165,6 +176,21 @@ impl Cmd {
165176
print.infoln(format!("Network Passphrase: {network_passphrase}"));
166177
print.infoln(format!("Network id: {}", hex::encode(network_id)));
167178

179+
// Get ledger close time and base reserve from archive
180+
let (ledger_close_time, base_reserve) =
181+
match get_ledger_metadata_from_archive(&print, &archive_url, ledger).await {
182+
Ok((close_time, reserve)) => {
183+
print.infoln(format!("Ledger Close Time: {close_time}"));
184+
print.infoln(format!("Base Reserve: {reserve}"));
185+
(close_time, reserve)
186+
}
187+
Err(e) => {
188+
print.warnln(format!("Failed to get ledger metadata from archive: {e}"));
189+
print.infoln("Using default values: close_time=0, base_reserve=1");
190+
(0u64, 1u32) // Default values
191+
}
192+
};
193+
168194
// Prepare a flat list of buckets to read. They'll be ordered by their
169195
// level so that they can iterated higher level to lower level.
170196
let buckets = history
@@ -182,12 +208,11 @@ impl Cmd {
182208
// The snapshot is what will be written to file at the end. Fields will
183209
// be updated while parsing the history archive.
184210
let mut snapshot = LedgerSnapshot {
185-
// TODO: Update more of the fields.
186211
protocol_version: 0,
187212
sequence_number: ledger,
188-
timestamp: 0,
213+
timestamp: ledger_close_time,
189214
network_id: network_id.into(),
190-
base_reserve: 1,
215+
base_reserve,
191216
min_persistent_entry_ttl: 0,
192217
min_temp_entry_ttl: 0,
193218
max_entry_ttl: 0,
@@ -284,29 +309,47 @@ impl Cmd {
284309
continue;
285310
}
286311
};
312+
287313
if seen.contains(&key) {
288314
continue;
289315
}
316+
290317
let keep = match &key {
291318
LedgerKey::Account(k) => current.account_ids.contains(&k.account_id),
292319
LedgerKey::Trustline(k) => current.account_ids.contains(&k.account_id),
293320
LedgerKey::ContractData(k) => current.contract_ids.contains(&k.contract),
294321
LedgerKey::ContractCode(e) => current.wasm_hashes.contains(&e.hash),
322+
LedgerKey::ConfigSetting(_) => true,
295323
_ => false,
296324
};
325+
297326
if !keep {
298327
continue;
299328
}
329+
300330
seen.insert(key.clone());
301-
let Some(val) = val else { continue };
331+
332+
let Some(val) = val else {
333+
continue;
334+
};
335+
302336
match &val.data {
337+
LedgerEntryData::ConfigSetting(ConfigSettingEntry::StateArchival(
338+
state_archival,
339+
)) => {
340+
snapshot.min_persistent_entry_ttl = state_archival.min_persistent_ttl;
341+
snapshot.min_temp_entry_ttl = state_archival.min_temporary_ttl;
342+
snapshot.max_entry_ttl = state_archival.max_entry_ttl;
343+
false
344+
}
345+
303346
LedgerEntryData::ContractData(e) => {
304347
// If a contract instance references contract
305348
// executable stored in another ledger entry, add
306349
// that ledger entry to the filter so that Wasm for
307350
// any filtered contract is collected too in the
308351
// second pass.
309-
if keep && e.key == ScVal::LedgerKeyContractInstance {
352+
if e.key == ScVal::LedgerKeyContractInstance {
310353
match &e.val {
311354
ScVal::ContractInstance(ScContractInstance {
312355
executable: ContractExecutable::Wasm(hash),
@@ -429,7 +472,6 @@ impl Cmd {
429472

430473
// Resolve an account address to an account id. The address can be a
431474
// G-address or a key name (as in `stellar keys address NAME`).
432-
433475
async fn resolve_account(&self, address: &str) -> Option<AccountId> {
434476
let address: UnresolvedMuxedAccount = address.parse().ok()?;
435477
Some(AccountId(xdr::PublicKey::PublicKeyTypeEd25519(
@@ -470,6 +512,14 @@ impl Cmd {
470512
}
471513
}
472514

515+
fn ledger_to_path_components(ledger: u32) -> (String, String, String, String) {
516+
let ledger_hex = format!("{ledger:08x}");
517+
let ledger_hex_0 = ledger_hex[0..=1].to_string();
518+
let ledger_hex_1 = ledger_hex[2..=3].to_string();
519+
let ledger_hex_2 = ledger_hex[4..=5].to_string();
520+
(ledger_hex, ledger_hex_0, ledger_hex_1, ledger_hex_2)
521+
}
522+
473523
async fn get_history(
474524
print: &print::Print,
475525
archive_url: &Url,
@@ -478,17 +528,15 @@ async fn get_history(
478528
let archive_url = archive_url.to_string();
479529
let archive_url = archive_url.strip_suffix('/').unwrap_or(&archive_url);
480530
let history_url = if let Some(ledger) = ledger {
481-
let ledger_hex = format!("{ledger:08x}");
482-
let ledger_hex_0 = &ledger_hex[0..=1];
483-
let ledger_hex_1 = &ledger_hex[2..=3];
484-
let ledger_hex_2 = &ledger_hex[4..=5];
531+
let (ledger_hex, ledger_hex_0, ledger_hex_1, ledger_hex_2) =
532+
ledger_to_path_components(ledger);
485533
format!("{archive_url}/history/{ledger_hex_0}/{ledger_hex_1}/{ledger_hex_2}/history-{ledger_hex}.json")
486534
} else {
487535
format!("{archive_url}/.well-known/stellar-history.json")
488536
};
489537
let history_url = Url::from_str(&history_url).unwrap();
490538

491-
print.globe(format!("Downloading history {history_url}"));
539+
print.globeln(format!("Downloading history {history_url}"));
492540

493541
let response = http::client()
494542
.get(history_url.as_str())
@@ -502,7 +550,6 @@ async fn get_history(
502550
let ledger_offset = (ledger + 1) % CHECKPOINT_FREQUENCY;
503551

504552
if ledger_offset != 0 {
505-
print.println("");
506553
print.errorln(format!(
507554
"Ledger {ledger} may not be a checkpoint ledger, try {} or {}",
508555
ledger - ledger_offset,
@@ -518,12 +565,88 @@ async fn get_history(
518565
.await
519566
.map_err(Error::ReadHistoryHttpStream)?;
520567

521-
print.clear_line();
568+
print.clear_previous_line();
522569
print.globeln(format!("Downloaded history {}", &history_url));
523570

524571
serde_json::from_slice::<History>(&body).map_err(Error::JsonDecodingHistory)
525572
}
526573

574+
async fn get_ledger_metadata_from_archive(
575+
print: &print::Print,
576+
archive_url: &Url,
577+
ledger: u32,
578+
) -> Result<(u64, u32), Error> {
579+
let archive_url = archive_url.to_string();
580+
let archive_url = archive_url.strip_suffix('/').unwrap_or(&archive_url);
581+
582+
// Calculate the path to the ledger header file
583+
let (ledger_hex, ledger_hex_0, ledger_hex_1, ledger_hex_2) = ledger_to_path_components(ledger);
584+
let ledger_url = format!(
585+
"{archive_url}/ledger/{ledger_hex_0}/{ledger_hex_1}/{ledger_hex_2}/ledger-{ledger_hex}.xdr.gz"
586+
);
587+
588+
print.globeln(format!("Downloading ledger headers {ledger_url}"));
589+
590+
let ledger_url = Url::from_str(&ledger_url).map_err(Error::ParsingBucketUrl)?;
591+
let response = http::client()
592+
.get(ledger_url.as_str())
593+
.send()
594+
.await
595+
.map_err(Error::DownloadingHistory)?;
596+
597+
if !response.status().is_success() {
598+
return Err(Error::DownloadingHistoryGotStatusCode(response.status()));
599+
}
600+
601+
// Cache the ledger file to disk like bucket files
602+
let ledger_dir = data::bucket_dir().map_err(Error::GetBucketDir)?;
603+
let cache_path = ledger_dir.join(format!("ledger-{ledger_hex}.xdr"));
604+
let dl_path = cache_path.with_extension("dl");
605+
606+
let stream = response
607+
.bytes_stream()
608+
.map(|result| result.map_err(std::io::Error::other));
609+
let stream_reader = StreamReader::new(stream);
610+
let buf_reader = BufReader::new(stream_reader);
611+
let mut decoder = GzipDecoder::new(buf_reader);
612+
613+
let mut file = OpenOptions::new()
614+
.create(true)
615+
.truncate(true)
616+
.write(true)
617+
.open(&dl_path)
618+
.await
619+
.map_err(Error::WriteOpeningCachedBucket)?;
620+
621+
tokio::io::copy(&mut decoder, &mut file)
622+
.await
623+
.map_err(Error::StreamingBucket)?;
624+
625+
fs::rename(&dl_path, &cache_path).map_err(Error::RenameDownloadFile)?;
626+
627+
print.clear_previous_line();
628+
print.globeln(format!("Downloaded ledger headers for ledger {ledger}"));
629+
630+
// Now read the cached file
631+
let file = std::fs::File::open(&cache_path).map_err(Error::ReadOpeningCachedBucket)?;
632+
let limited = &mut Limited::new(file, Limits::none());
633+
634+
// Find the specific ledger header entry we need
635+
let entries = Frame::<LedgerHeaderHistoryEntry>::read_xdr_iter(limited);
636+
for entry in entries {
637+
let Frame(header_entry) = entry.map_err(Error::Xdr)?;
638+
639+
if header_entry.header.ledger_seq == ledger {
640+
let close_time = header_entry.header.scp_value.close_time.0;
641+
let base_reserve = header_entry.header.base_reserve;
642+
643+
return Ok((close_time, base_reserve));
644+
}
645+
}
646+
647+
Err(Error::LedgerNotFound)
648+
}
649+
527650
async fn cache_bucket(
528651
print: &print::Print,
529652
archive_url: &Url,
@@ -539,7 +662,7 @@ async fn cache_bucket(
539662
let bucket_url =
540663
format!("{archive_url}/bucket/{bucket_0}/{bucket_1}/{bucket_2}/bucket-{bucket}.xdr.gz");
541664

542-
print.globe(format!("Downloading bucket {bucket_index} {bucket}…"));
665+
print.globeln(format!("Downloading bucket {bucket_index} {bucket}…"));
543666

544667
let bucket_url = Url::from_str(&bucket_url).map_err(Error::ParsingBucketUrl)?;
545668

@@ -555,15 +678,13 @@ async fn cache_bucket(
555678
}
556679

557680
if let Some(len) = response.content_length() {
558-
print.clear_line();
559-
print.globe(format!(
681+
print.clear_previous_line();
682+
print.globeln(format!(
560683
"Downloaded bucket {bucket_index} {bucket} ({})",
561684
ByteSize(len)
562685
));
563686
}
564687

565-
print.println("");
566-
567688
let stream = response
568689
.bytes_stream()
569690
.map(|result| result.map_err(std::io::Error::other));

cmd/soroban-cli/src/print.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use std::io::{self, Write};
12
use std::{env, fmt::Display};
23

34
use crate::xdr::{Error as XdrError, Transaction};
@@ -38,6 +39,18 @@ impl Print {
3839
}
3940
}
4041

42+
pub fn clear_previous_line(&self) {
43+
if !self.quiet {
44+
if cfg!(windows) {
45+
eprint!("\x1b[2A\r\x1b[2K");
46+
} else {
47+
eprint!("\x1b[1A\x1b[2K\r");
48+
}
49+
50+
io::stderr().flush().unwrap();
51+
}
52+
}
53+
4154
// Some terminals like vscode's and macOS' default terminal will not render
4255
// the subsequent space if the emoji codepoints size is 2; in this case,
4356
// we need an additional space.

0 commit comments

Comments
 (0)