@@ -53,6 +53,9 @@ pub enum LoadAccountsError {
5353 #[ error( "Cold PDA at index {index} (pubkey {pubkey}) missing data" ) ]
5454 MissingPdaCompressed { index : usize , pubkey : Pubkey } ,
5555
56+ #[ error( "Cold PDA (pubkey {pubkey}) missing data" ) ]
57+ MissingPdaCompressedData { pubkey : Pubkey } ,
58+
5659 #[ error( "Cold ATA at index {index} (pubkey {pubkey}) missing data" ) ]
5760 MissingAtaCompressed { index : usize , pubkey : Pubkey } ,
5861
@@ -67,6 +70,7 @@ pub enum LoadAccountsError {
6770}
6871
6972const MAX_ATAS_PER_IX : usize = 8 ;
73+ const MAX_PDAS_PER_IX : usize = 8 ;
7074
7175/// Build load instructions for cold accounts. Returns empty vec if all hot.
7276///
@@ -113,14 +117,18 @@ where
113117 } )
114118 . collect ( ) ;
115119
116- let pda_hashes = collect_pda_hashes ( & cold_pdas) ?;
120+ let pda_groups = group_pda_specs ( & cold_pdas, MAX_PDAS_PER_IX ) ;
121+ let pda_hashes = pda_groups
122+ . iter ( )
123+ . map ( |group| collect_pda_hashes ( group) )
124+ . collect :: < Result < Vec < _ > , _ > > ( ) ?;
117125 let ata_hashes = collect_ata_hashes ( & cold_atas) ?;
118126 let mint_hashes = collect_mint_hashes ( & cold_mints) ?;
119127
120128 let ( pda_proofs, ata_proofs, mint_proofs) = futures:: join!(
121- fetch_proofs ( & pda_hashes, indexer) ,
129+ fetch_proof_batches ( & pda_hashes, indexer) ,
122130 fetch_proofs_batched( & ata_hashes, MAX_ATAS_PER_IX , indexer) ,
123- fetch_proofs ( & mint_hashes, indexer) ,
131+ fetch_individual_proofs ( & mint_hashes, indexer) ,
124132 ) ;
125133
126134 let pda_proofs = pda_proofs?;
@@ -136,18 +144,17 @@ where
136144
137145 // 2. DecompressAccountsIdempotent for all cold PDAs (including token PDAs).
138146 // Token PDAs are created on-chain via CPI inside DecompressVariant.
139- for ( spec , proof) in cold_pdas . iter ( ) . zip ( pda_proofs) {
147+ for ( group , proof) in pda_groups . into_iter ( ) . zip ( pda_proofs) {
140148 out. push ( build_pda_load (
141- & [ spec ] ,
149+ & group ,
142150 proof,
143151 fee_payer,
144152 compression_config,
145153 ) ?) ;
146154 }
147155
148156 // 3. ATA loads (CreateAssociatedTokenAccount + Transfer2) - requires mint to exist
149- let ata_chunks: Vec < _ > = cold_atas. chunks ( MAX_ATAS_PER_IX ) . collect ( ) ;
150- for ( chunk, proof) in ata_chunks. into_iter ( ) . zip ( ata_proofs) {
157+ for ( chunk, proof) in cold_atas. chunks ( MAX_ATAS_PER_IX ) . zip ( ata_proofs) {
151158 out. extend ( build_ata_load ( chunk, proof, fee_payer) ?) ;
152159 }
153160
@@ -195,23 +202,77 @@ fn collect_mint_hashes(ifaces: &[&AccountInterface]) -> Result<Vec<[u8; 32]>, Lo
195202 . collect ( )
196203}
197204
198- async fn fetch_proofs < I : Indexer > (
205+ /// Groups already-ordered PDA specs into contiguous runs of the same program id.
206+ ///
207+ /// This preserves input order rather than globally regrouping by program. Callers that
208+ /// want maximal batching across interleaved program ids should sort before calling.
209+ fn group_pda_specs < ' a , V > (
210+ specs : & [ & ' a PdaSpec < V > ] ,
211+ max_per_group : usize ,
212+ ) -> Vec < Vec < & ' a PdaSpec < V > > > {
213+ assert ! ( max_per_group > 0 , "max_per_group must be non-zero" ) ;
214+ if specs. is_empty ( ) {
215+ return Vec :: new ( ) ;
216+ }
217+
218+ let mut groups = Vec :: new ( ) ;
219+ let mut current = Vec :: with_capacity ( max_per_group) ;
220+ let mut current_program: Option < Pubkey > = None ;
221+
222+ for spec in specs {
223+ let program_id = spec. program_id ( ) ;
224+ let should_split = current_program
225+ . map ( |existing| existing != program_id || current. len ( ) >= max_per_group)
226+ . unwrap_or ( false ) ;
227+
228+ if should_split {
229+ groups. push ( current) ;
230+ current = Vec :: with_capacity ( max_per_group) ;
231+ }
232+
233+ current_program = Some ( program_id) ;
234+ current. push ( * spec) ;
235+ }
236+
237+ if !current. is_empty ( ) {
238+ groups. push ( current) ;
239+ }
240+
241+ groups
242+ }
243+
244+ async fn fetch_individual_proofs < I : Indexer > (
199245 hashes : & [ [ u8 ; 32 ] ] ,
200246 indexer : & I ,
201247) -> Result < Vec < ValidityProofWithContext > , IndexerError > {
202248 if hashes. is_empty ( ) {
203249 return Ok ( vec ! [ ] ) ;
204250 }
205- let mut proofs = Vec :: with_capacity ( hashes. len ( ) ) ;
206- for hash in hashes {
207- proofs. push (
208- indexer
209- . get_validity_proof ( vec ! [ * hash] , vec ! [ ] , None )
210- . await ?
211- . value ,
212- ) ;
251+
252+ futures:: future:: try_join_all ( hashes. iter ( ) . map ( |hash| async move {
253+ indexer
254+ . get_validity_proof ( vec ! [ * hash] , vec ! [ ] , None )
255+ . await
256+ . map ( |response| response. value )
257+ } ) )
258+ . await
259+ }
260+
261+ async fn fetch_proof_batches < I : Indexer > (
262+ hash_batches : & [ Vec < [ u8 ; 32 ] > ] ,
263+ indexer : & I ,
264+ ) -> Result < Vec < ValidityProofWithContext > , IndexerError > {
265+ if hash_batches. is_empty ( ) {
266+ return Ok ( vec ! [ ] ) ;
213267 }
214- Ok ( proofs)
268+
269+ futures:: future:: try_join_all ( hash_batches. iter ( ) . map ( |hashes| async move {
270+ indexer
271+ . get_validity_proof ( hashes. clone ( ) , vec ! [ ] , None )
272+ . await
273+ . map ( |response| response. value )
274+ } ) )
275+ . await
215276}
216277
217278async fn fetch_proofs_batched < I : Indexer > (
@@ -222,16 +283,13 @@ async fn fetch_proofs_batched<I: Indexer>(
222283 if hashes. is_empty ( ) {
223284 return Ok ( vec ! [ ] ) ;
224285 }
225- let mut proofs = Vec :: with_capacity ( hashes. len ( ) . div_ceil ( batch_size) ) ;
226- for chunk in hashes. chunks ( batch_size) {
227- proofs. push (
228- indexer
229- . get_validity_proof ( chunk. to_vec ( ) , vec ! [ ] , None )
230- . await ?
231- . value ,
232- ) ;
233- }
234- Ok ( proofs)
286+
287+ let hash_batches = hashes
288+ . chunks ( batch_size)
289+ . map ( |chunk| chunk. to_vec ( ) )
290+ . collect :: < Vec < _ > > ( ) ;
291+
292+ fetch_proof_batches ( & hash_batches, indexer) . await
235293}
236294
237295fn build_pda_load < V > (
@@ -262,11 +320,16 @@ where
262320 let hot_addresses: Vec < Pubkey > = specs. iter ( ) . map ( |s| s. address ( ) ) . collect ( ) ;
263321 let cold_accounts: Vec < ( CompressedAccount , V ) > = specs
264322 . iter ( )
265- . map ( |s| {
266- let compressed = s. compressed ( ) . expect ( "cold spec must have data" ) . clone ( ) ;
267- ( compressed, s. variant . clone ( ) )
323+ . map ( |s| -> Result < _ , LoadAccountsError > {
324+ let compressed =
325+ s. compressed ( )
326+ . cloned ( )
327+ . ok_or ( LoadAccountsError :: MissingPdaCompressedData {
328+ pubkey : s. address ( ) ,
329+ } ) ?;
330+ Ok ( ( compressed, s. variant . clone ( ) ) )
268331 } )
269- . collect ( ) ;
332+ . collect :: < Result < Vec < _ > , _ > > ( ) ? ;
270333
271334 let program_id = specs. first ( ) . map ( |s| s. program_id ( ) ) . unwrap_or_default ( ) ;
272335
0 commit comments