Skip to content

Commit cdc7199

Browse files
snapshot source using meta, rpc, archives
1 parent 9415cee commit cdc7199

File tree

67 files changed

+7662
-321
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+7662
-321
lines changed

Cargo.lock

Lines changed: 1947 additions & 308 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,28 @@ resolver = "2"
44
members = [
55
"soroban-sdk",
66
"soroban-sdk-macros",
7+
"cache-to-file",
78
"soroban-meta",
89
"soroban-spec",
910
"soroban-spec-rust",
1011
"soroban-ledger-snapshot",
1112
"soroban-token-sdk",
1213
"soroban-token-spec",
1314
"stellar-asset-spec",
15+
16+
"soroban-ledger-fetch-from/rpc",
17+
"soroban-ledger-fetch-from/meta-storage",
18+
"soroban-ledger-fetch-from/history-archive",
19+
"soroban-ledger-fetch",
20+
"soroban-ledger-snapshot-source-live",
21+
"soroban-ledger-snapshot-source-tx",
22+
1423
"tests/*",
1524
]
1625

1726
[workspace.package]
1827
version = "23.4.0"
19-
rust-version = "1.84.0"
28+
rust-version = "1.85.0"
2029

2130
[workspace.dependencies]
2231
soroban-sdk = { version = "23.4.0", path = "soroban-sdk" }
@@ -29,6 +38,14 @@ soroban-token-sdk = { version = "23.4.0", path = "soroban-token-sdk" }
2938
soroban-token-spec = { version = "23.4.0", path = "soroban-token-spec" }
3039
stellar-asset-spec = { version = "23.4.0", path = "stellar-asset-spec" }
3140

41+
cache-to-file = { version = "0.0.1", path = "cache-to-file" }
42+
soroban-ledger-fetch-from-rpc = { version = "23.4.0", path = "soroban-ledger-fetch-from/rpc" }
43+
soroban-ledger-fetch-from-meta-storage = { version = "23.4.0", path = "soroban-ledger-fetch-from/meta-storage" }
44+
soroban-ledger-fetch-from-history-archive = { version = "23.4.0", path = "soroban-ledger-fetch-from/history-archive" }
45+
soroban-ledger-fetch = { version = "23.4.0", path = "soroban-ledger-fetch" }
46+
soroban-ledger-snapshot-source-live = { version = "23.4.0", path = "soroban-ledger-snapshot-source-live" }
47+
soroban-ledger-snapshot-source-tx = { version = "23.4.0", path = "soroban-ledger-snapshot-source-tx" }
48+
3249
[workspace.dependencies.soroban-env-common]
3350
version = "=23.0.1"
3451
#git = "https://github.com/stellar/rs-soroban-env"

cache-to-file/Cargo.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "cache-to-file"
3+
description = "File caching utilities with locking"
4+
homepage = "https://github.com/stellar/rs-soroban-sdk"
5+
repository = "https://github.com/stellar/rs-soroban-sdk"
6+
authors = ["Stellar Development Foundation <info@stellar.org>"]
7+
readme = "../README.md"
8+
license = "Apache-2.0"
9+
version = "0.0.1"
10+
edition = "2021"
11+
rust-version.workspace = true
12+
13+
[dependencies]
14+
thiserror = "2.0"
15+
fs2 = "0.4"

cache-to-file/src/lib.rs

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use std::fs::{self, File};
2+
use std::io::{Read, Write};
3+
use std::path::Path;
4+
5+
use fs2::FileExt;
6+
7+
// TODO: Move this crate to its own repo.
8+
9+
/// Cache a file, collecting bytes if not present, and return a reader
10+
pub fn cache<P, C, CE>(path: P, collect: C) -> Result<impl Read, CacheError<CE>>
11+
where
12+
P: AsRef<Path>,
13+
C: FnOnce(&mut dyn Write) -> Result<(), CE>,
14+
{
15+
let path = path.as_ref();
16+
17+
// Fast path: if file already exists, just read it (no lock needed).
18+
// Slow path: acquire lock, check again, collect and write if needed.
19+
if !path.exists() {
20+
let lock_path = path.with_extension("lock");
21+
let lock_file = File::create(&lock_path).map_err(CacheError::Io)?;
22+
lock_file.lock_exclusive().map_err(CacheError::Io)?;
23+
24+
if !path.exists() {
25+
// Write atomically via temp file and rename.
26+
let temp_path = path.with_extension("dl");
27+
{
28+
let mut temp = File::create(&temp_path).map_err(CacheError::Io)?;
29+
collect(&mut temp).map_err(CacheError::Collector)?;
30+
temp.sync_all().map_err(CacheError::Io)?;
31+
}
32+
fs::rename(&temp_path, path).map_err(CacheError::Io)?;
33+
}
34+
}
35+
36+
// Open data file for reading.
37+
let file = File::open(path).map_err(CacheError::Io)?;
38+
Ok(file)
39+
}
40+
41+
/// Error type for cache operations
42+
#[derive(Debug, thiserror::Error)]
43+
pub enum CacheError<CE> {
44+
#[error("collector error: {0}")]
45+
Collector(CE),
46+
#[error("io error: {0}")]
47+
#[allow(dead_code)]
48+
Io(#[from] std::io::Error),
49+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "soroban-ledger-fetch-from-history-archive"
3+
description = "Client for accessing Stellar History Archives to retrieve ledger snapshots."
4+
homepage = "https://github.com/stellar/rs-soroban-sdk"
5+
repository = "https://github.com/stellar/rs-soroban-sdk"
6+
authors = ["Stellar Development Foundation <info@stellar.org>"]
7+
readme = "../README.md"
8+
license = "Apache-2.0"
9+
version.workspace = true
10+
edition = "2021"
11+
rust-version.workspace = true
12+
13+
[dependencies]
14+
stellar-xdr = { workspace = true, features = ["curr", "std"] }
15+
thiserror = "1.0"
16+
reqwest = { version = "0.12", default-features = false, features = ["json", "blocking", "rustls-tls"] }
17+
flate2 = { version = "1.0", features = ["rust_backend"] }
18+
serde = { version = "1.0.0", features = ["derive"] }
19+
serde_json = "1.0.0"
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use flate2::read::GzDecoder;
2+
use std::io::{self, copy, Cursor, Write};
3+
use stellar_xdr::curr::{self as xdr, Frame, Limited, ReadXdr};
4+
5+
#[derive(thiserror::Error, Debug)]
6+
pub enum Error {
7+
#[error("downloading history: {0}")]
8+
DownloadingHistory(reqwest::Error),
9+
10+
#[error("downloading history: got status code {0}")]
11+
DownloadingHistoryGotStatusCode(reqwest::StatusCode),
12+
13+
#[error("json decoding history: {0}")]
14+
JsonDecodingHistory(serde_json::Error),
15+
16+
#[error("getting bucket: {0}")]
17+
GettingBucket(reqwest::Error),
18+
19+
#[error("getting bucket: got status code {0}")]
20+
GettingBucketGotStatusCode(reqwest::StatusCode),
21+
22+
#[error("streaming bucket: {0}")]
23+
StreamingBucket(io::Error),
24+
25+
#[error("streaming history: {0}")]
26+
StreamingHistory(io::Error),
27+
28+
#[error("xdr parsing error: {0}")]
29+
Xdr(#[from] xdr::Error),
30+
}
31+
32+
pub fn history(archive_url: &str, ledger: u32) -> Result<History, Error> {
33+
let mut bytes = Vec::new();
34+
get_history(archive_url, ledger, &mut bytes)?;
35+
parse_history(Cursor::new(bytes))
36+
}
37+
38+
pub fn get_history<W: Write + ?Sized>(
39+
archive_url: &str,
40+
ledger: u32,
41+
writer: &mut W,
42+
) -> Result<(), Error> {
43+
let history_url = {
44+
let ledger_hex = format!("{ledger:08x}");
45+
let ledger_hex_0 = ledger_hex[0..=1].to_string();
46+
let ledger_hex_1 = ledger_hex[2..=3].to_string();
47+
let ledger_hex_2 = ledger_hex[4..=5].to_string();
48+
format!("{archive_url}/history/{ledger_hex_0}/{ledger_hex_1}/{ledger_hex_2}/history-{ledger_hex}.json")
49+
};
50+
//eprintln!("url: {history_url}");
51+
52+
let mut response = reqwest::blocking::Client::new()
53+
.get(&history_url)
54+
.send()
55+
.map_err(Error::DownloadingHistory)?;
56+
57+
if !response.status().is_success() {
58+
return Err(Error::DownloadingHistoryGotStatusCode(response.status()));
59+
}
60+
61+
copy(&mut response, writer).map_err(Error::StreamingHistory)?;
62+
Ok(())
63+
}
64+
65+
pub fn parse_history<R: std::io::Read>(reader: R) -> Result<History, Error> {
66+
let history: History = serde_json::from_reader(reader).map_err(Error::JsonDecodingHistory)?;
67+
Ok(history)
68+
}
69+
70+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)]
71+
#[serde(rename_all = "camelCase")]
72+
pub struct History {
73+
pub current_ledger: u32,
74+
pub current_buckets: Vec<HistoryBucket>,
75+
pub hot_archive_buckets: Option<Vec<HistoryBucket>>,
76+
pub network_passphrase: Option<String>,
77+
}
78+
79+
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Deserialize)]
80+
#[serde(rename_all = "camelCase")]
81+
pub struct HistoryBucket {
82+
pub curr: String,
83+
pub snap: String,
84+
}
85+
86+
pub fn get_bucket<W: Write + ?Sized>(
87+
archive_url: &str,
88+
bucket: &str,
89+
writer: &mut W,
90+
) -> Result<Option<u64>, Error> {
91+
let bucket_0 = &bucket[0..=1];
92+
let bucket_1 = &bucket[2..=3];
93+
let bucket_2 = &bucket[4..=5];
94+
let bucket_url =
95+
format!("{archive_url}/bucket/{bucket_0}/{bucket_1}/{bucket_2}/bucket-{bucket}.xdr.gz");
96+
97+
let response = reqwest::blocking::Client::new()
98+
.get(&bucket_url)
99+
.send()
100+
.map_err(Error::GettingBucket)?;
101+
102+
if !response.status().is_success() {
103+
return Err(Error::GettingBucketGotStatusCode(response.status()));
104+
}
105+
106+
let content_length = response.content_length();
107+
108+
let mut decoder = GzDecoder::new(response);
109+
copy(&mut decoder, writer).map_err(Error::StreamingBucket)?;
110+
Ok(content_length)
111+
}
112+
113+
pub fn parse_bucket<'a, R: std::io::Read + 'a>(
114+
reader: &'a mut Limited<R>,
115+
) -> impl Iterator<Item = Result<Frame<xdr::BucketEntry>, xdr::Error>> + 'a {
116+
Frame::<xdr::BucketEntry>::read_xdr_iter(reader)
117+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "soroban-ledger-fetch-from-meta-storage"
3+
description = "Download LedgerCloseMeta from SEP-54 compatible ledger storage."
4+
homepage = "https://github.com/stellar/rs-soroban-sdk"
5+
repository = "https://github.com/stellar/rs-soroban-sdk"
6+
authors = ["Stellar Development Foundation <info@stellar.org>"]
7+
readme = "../README.md"
8+
license = "Apache-2.0"
9+
version.workspace = true
10+
edition = "2021"
11+
rust-version.workspace = true
12+
13+
[features]
14+
default = []
15+
cli = ["dep:clap"]
16+
17+
[[bin]]
18+
name = "soroban-ledger-meta-storage"
19+
path = "src/bin/soroban-ledger-meta-storage/main.rs"
20+
required-features = ["cli"]
21+
22+
[dependencies]
23+
stellar-xdr = { workspace = true, features = ["curr", "std", "serde"] }
24+
thiserror = "1.0"
25+
reqwest = { version = "0.12", default-features = false, features = ["blocking", "rustls-tls"] }
26+
zstd = "0.13"
27+
serde = { version = "1.0", features = ["derive"] }
28+
serde_json = "1.0"
29+
clap = { version = "4.0", features = ["derive"], optional = true }
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use soroban_ledger_fetch_from_meta_storage::cli;
2+
use std::env;
3+
4+
fn main() {
5+
if let Err(e) = cli::run(env::args_os()) {
6+
match e {
7+
cli::Error::Clap(e) => e.exit(),
8+
_ => {
9+
eprintln!("Error: {e}");
10+
std::process::exit(1);
11+
}
12+
}
13+
}
14+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
//! CLI module for downloading LedgerCloseMeta from Stellar public blockchain data.
2+
//!
3+
//! This module is only available when the `cli` feature is enabled.
4+
5+
use crate::ledger;
6+
use clap::{Parser, ValueEnum};
7+
use std::{ffi::OsString, fmt::Debug, io::Write};
8+
use stellar_xdr::curr::{Limited, Limits, WriteXdr};
9+
10+
/// CLI tool to download LedgerCloseMeta from Stellar public blockchain data
11+
#[derive(Parser, Debug, Clone)]
12+
#[command(
13+
author,
14+
version,
15+
about,
16+
long_about = None,
17+
disable_help_subcommand = true,
18+
)]
19+
pub struct Root {
20+
/// Ledger sequence number to download
21+
#[arg(short, long)]
22+
sequence: u32,
23+
24+
/// Output format
25+
#[arg(short, long, value_enum, default_value_t)]
26+
format: Format,
27+
28+
/// Meta storage URL (SEP-54 compatible)
29+
#[arg(
30+
long,
31+
default_value = "https://aws-public-blockchain.s3.us-east-2.amazonaws.com/v1.1/stellar/ledgers/pubnet"
32+
)]
33+
meta_url: String,
34+
}
35+
36+
#[derive(Default, Clone, ValueEnum, Debug)]
37+
pub enum Format {
38+
#[default]
39+
Json,
40+
Xdr,
41+
}
42+
43+
#[derive(thiserror::Error, Debug)]
44+
pub enum Error {
45+
#[error(transparent)]
46+
Clap(#[from] clap::Error),
47+
#[error("failed to download ledger meta: {0}")]
48+
Download(#[from] crate::Error),
49+
#[error("failed to serialize to JSON: {0}")]
50+
Json(#[from] serde_json::Error),
51+
#[error("failed to write XDR: {0}")]
52+
Xdr(#[from] stellar_xdr::curr::Error),
53+
#[error("I/O error: {0}")]
54+
Io(#[from] std::io::Error),
55+
}
56+
57+
impl Root {
58+
/// Run the CLI command.
59+
///
60+
/// ## Errors
61+
///
62+
/// If the command fails to execute.
63+
pub fn run(&self) -> Result<(), Error> {
64+
let meta = ledger(&self.meta_url, self.sequence)?;
65+
66+
match self.format {
67+
Format::Json => {
68+
let json = serde_json::to_string_pretty(&meta)?;
69+
println!("{}", json);
70+
}
71+
Format::Xdr => {
72+
let limits = Limits::none();
73+
let mut stdout = std::io::stdout();
74+
let mut limited_writer = Limited::new(&mut stdout, limits);
75+
meta.write_xdr(&mut limited_writer)?;
76+
stdout.flush()?;
77+
}
78+
}
79+
80+
Ok(())
81+
}
82+
}
83+
84+
/// Run the CLI with the given args.
85+
///
86+
/// ## Errors
87+
///
88+
/// If the input cannot be parsed or the command fails.
89+
pub fn run<I, T>(args: I) -> Result<(), Error>
90+
where
91+
I: IntoIterator<Item = T>,
92+
T: Into<OsString> + Clone,
93+
{
94+
let root = Root::try_parse_from(args)?;
95+
root.run()
96+
}

0 commit comments

Comments
 (0)