@@ -16,19 +16,19 @@ use std::{
1616} ;
1717use 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} ;
2525use tokio:: fs:: OpenOptions ;
2626use tokio:: io:: BufReader ;
2727use tokio_util:: io:: StreamReader ;
2828use url:: Url ;
2929
3030use 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+
473523async 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+
527650async 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) ) ;
0 commit comments