Skip to main content

bucks_api/
sdk.rs

1use solana_program::pubkey::Pubkey;
2use spl_associated_token_account::get_associated_token_address;
3use steel::*;
4
5use crate::{consts::*, instruction::*, state::*};
6
7// =============================================================================
8// Protocol Account Helpers
9// =============================================================================
10
11/// Returns the accounts needed for Kamino protocol operations.
12pub fn kamino_accounts(vault_address: Pubkey) -> Vec<AccountMeta> {
13    let vault_ctoken_address = get_associated_token_address(&vault_address, &CTOKEN_ADDRESS);
14
15    vec![
16        AccountMeta::new(vault_address, false),
17        AccountMeta::new(OBLIGATION_ADDRESS, false),
18        AccountMeta::new(LENDING_MARKET_ADDRESS, false),
19        AccountMeta::new(LENDING_MARKET_AUTHORITY_ADDRESS, false),
20        AccountMeta::new(RESERVE_ADDRESS, false),
21        AccountMeta::new(USDC_ADDRESS, false),
22        AccountMeta::new(RESERVE_LIQUIDITY_SUPPLY_ADDRESS, false),
23        AccountMeta::new(CTOKEN_ADDRESS, false),
24        AccountMeta::new(RESERVE_CTOKEN_ADDRESS, false),
25        AccountMeta::new(get_associated_token_address(&vault_address, &USDC_ADDRESS), false),
26        AccountMeta::new(vault_ctoken_address, false),
27        AccountMeta::new_readonly(spl_token::ID, false),
28        AccountMeta::new_readonly(spl_token::ID, false),
29        AccountMeta::new_readonly(sysvar::instructions::ID, false),
30        AccountMeta::new(RESERVE_FARM_USER_STATE_ADDRESS, false),
31        AccountMeta::new(RESERVE_FARM_STATE_ADDRESS, false),
32        AccountMeta::new_readonly(KFARMS_PROGRAM_ID, false),
33        AccountMeta::new_readonly(SCOPE_PRICES_ADDRESS, false),
34        AccountMeta::new_readonly(KLEND_PROGRAM_ID, false),
35    ]
36}
37
38/// Returns the accounts needed for Perena protocol operations.
39/// Matches the bankineco IDL structure for mint_w_yielding_gen / burn_for_yielding_gen.
40pub fn perena_accounts(vault_address: Pubkey) -> Vec<AccountMeta> {
41    let vault_usdc_address = get_associated_token_address(&vault_address, &USDC_ADDRESS);
42    let vault_usd_star_address = get_associated_token_address(&vault_address, &USD_STAR_MINT);
43
44    vec![
45        // 0. vault_info (signer) - the "user" in Perena terms (our vault PDA)
46        AccountMeta::new(vault_address, false),
47        // 1. bank_state
48        AccountMeta::new(PERENA_BANK_STATE, false),
49        // 2. vault_state (Perena's vault state, not our vault)
50        AccountMeta::new(PERENA_VAULT_STATE, false),
51        // 3. oracle_state
52        AccountMeta::new_readonly(PERENA_ORACLE_STATE, false),
53        // 4. yielding_mint (USDC)
54        AccountMeta::new_readonly(USDC_ADDRESS, false),
55        // 5. bank_mint (USD*)
56        AccountMeta::new(USD_STAR_MINT, false),
57        // 6. vault_usdc_ata (user's USDC ATA - our vault's USDC)
58        AccountMeta::new(vault_usdc_address, false),
59        // 7. vault_usd_star_ata (user's USD* ATA - our vault's USD*)
60        AccountMeta::new(vault_usd_star_address, false),
61        // 8. yielding_vault_ata (Perena's USDC vault)
62        AccountMeta::new(PERENA_YIELDING_VAULT, false),
63        // 9. team_state
64        AccountMeta::new(PERENA_TEAM_STATE, false),
65        // 10. fee_team_ata
66        AccountMeta::new(PERENA_FEE_TEAM_ATA, false),
67        // 11. system_program
68        AccountMeta::new_readonly(system_program::ID, false),
69        // 12. token_program
70        AccountMeta::new_readonly(spl_token::ID, false),
71        // 13. yielding_mint_program (USDC token program)
72        AccountMeta::new_readonly(spl_token::ID, false),
73        // 14. associated_token_program
74        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
75        // 15. perena_program
76        AccountMeta::new_readonly(PERENA_PROGRAM_ID, false),
77    ]
78}
79
80/// Returns protocol accounts based on protocol ID.
81pub fn protocol_accounts(protocol_id: u8, vault_address: Pubkey) -> Vec<AccountMeta> {
82    match protocol_id {
83        PROTOCOL_KAMINO => kamino_accounts(vault_address),
84        PROTOCOL_PERENA => perena_accounts(vault_address),
85        _ => vec![],
86    }
87}
88
89// =============================================================================
90// Log Helpers
91// =============================================================================
92
93/// Build a Log instruction (for CPI from within the program).
94pub fn log(signer: Pubkey, msg: &[u8]) -> Instruction {
95    let mut data = Log {}.to_bytes();
96    data.extend_from_slice(msg);
97    Instruction {
98        program_id: crate::ID,
99        accounts: vec![AccountMeta::new(signer, true)],
100        data,
101    }
102}
103
104/// CPI call to emit a log event. The vault PDA signs.
105pub fn program_log(accounts: &[AccountInfo], msg: &[u8]) -> Result<(), ProgramError> {
106    invoke_signed(
107        &log(*accounts[0].key, msg),
108        accounts,
109        &crate::ID,
110        &[VAULT],
111    )
112}
113
114// =============================================================================
115// Admin Instructions
116// =============================================================================
117
118/// Initialize the vault with fee parameters and treasury.
119pub fn init(signer: Pubkey, platform_fee_bps: u16, treasury: Pubkey) -> Instruction {
120    let vault_address = vault_pda().0;
121    let vault_usdc_address = get_associated_token_address(&vault_address, &USDC_ADDRESS);
122
123    Instruction {
124        program_id: crate::ID,
125        accounts: vec![
126            AccountMeta::new(signer, true),
127            AccountMeta::new_readonly(USDC_ADDRESS, false),
128            AccountMeta::new(vault_address, false),
129            AccountMeta::new(vault_usdc_address, false),
130            AccountMeta::new_readonly(system_program::ID, false),
131            AccountMeta::new_readonly(spl_token::ID, false),
132            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
133        ],
134        data: Init {
135            platform_fee_bps: platform_fee_bps.to_le_bytes(),
136            _padding: [0u8; 6],
137            treasury: treasury.to_bytes(),
138        }
139        .to_bytes(),
140    }
141}
142
143/// Initialize a protocol registry.
144pub fn init_protocol(signer: Pubkey, protocol_id: u8, name: &str, user_lookup_table: Option<Pubkey>) -> Instruction {
145    let vault_address = vault_pda().0;
146    let protocol_address = protocol_pda(protocol_id).0;
147
148    // Get receipt token mint based on protocol.
149    let receipt_token_mint = match protocol_id {
150        PROTOCOL_KAMINO => CTOKEN_ADDRESS,
151        PROTOCOL_PERENA => USD_STAR_MINT,
152        _ => panic!("Unknown protocol"),
153    };
154
155    let vault_receipt_token_address = get_associated_token_address(&vault_address, &receipt_token_mint);
156
157    // Convert name to bytes.
158    let name_bytes = name.as_bytes();
159    let mut name_array = [0u8; 32];
160    let len = name_bytes.len().min(32);
161    name_array[..len].copy_from_slice(&name_bytes[..len]);
162
163    let mut accounts = vec![
164        AccountMeta::new(signer, true),
165        AccountMeta::new(vault_address, false),
166        AccountMeta::new(protocol_address, false),
167        AccountMeta::new_readonly(receipt_token_mint, false),
168        AccountMeta::new(vault_receipt_token_address, false),
169        AccountMeta::new_readonly(system_program::ID, false),
170        AccountMeta::new_readonly(spl_token::ID, false),
171        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
172    ];
173
174    // Add protocol-specific accounts.
175    if protocol_id == PROTOCOL_KAMINO {
176        let user_metadata_address = Pubkey::find_program_address(
177            &[b"user_meta", vault_address.as_ref()],
178            &klend_sdk::programs::KAMINO_LENDING_ID.to_bytes().into(),
179        )
180        .0;
181
182        accounts.extend(vec![
183            AccountMeta::new(OBLIGATION_ADDRESS, false),
184            AccountMeta::new(LENDING_MARKET_ADDRESS, false),
185            AccountMeta::new_readonly(USDC_ADDRESS, false),
186            AccountMeta::new_readonly(USDC_ADDRESS, false),
187            AccountMeta::new(user_metadata_address, false),
188            AccountMeta::new(user_lookup_table.unwrap_or(signer), false),
189            AccountMeta::new(LENDING_MARKET_AUTHORITY_ADDRESS, false),
190            AccountMeta::new(RESERVE_ADDRESS, false),
191            AccountMeta::new(RESERVE_FARM_USER_STATE_ADDRESS, false),
192            AccountMeta::new(RESERVE_FARM_STATE_ADDRESS, false),
193            AccountMeta::new_readonly(KFARMS_PROGRAM_ID, false),
194            AccountMeta::new_readonly(KLEND_PROGRAM_ID, false),
195            AccountMeta::new_readonly(sysvar::rent::ID, false),
196            AccountMeta::new_readonly(system_program::ID, false),
197        ]);
198    }
199
200    Instruction {
201        program_id: crate::ID,
202        accounts,
203        data: InitProtocol {
204            protocol_id,
205            name: name_array,
206        }
207        .to_bytes(),
208    }
209}
210
211/// Checkpoint a protocol's exchange rate.
212pub fn checkpoint(signer: Pubkey, protocol_id: u8) -> Instruction {
213    let vault_address = vault_pda().0;
214    let protocol_address = protocol_pda(protocol_id).0;
215
216    let mut accounts = vec![
217        AccountMeta::new(signer, true),
218        AccountMeta::new(vault_address, false),
219        AccountMeta::new(protocol_address, false),
220    ];
221
222    // Add protocol-specific read accounts for getting exchange rate.
223    match protocol_id {
224        PROTOCOL_KAMINO => {
225            accounts.push(AccountMeta::new_readonly(RESERVE_ADDRESS, false));
226        }
227        PROTOCOL_PERENA => {
228            accounts.push(AccountMeta::new_readonly(PERENA_ORACLE_STATE, false));
229        }
230        _ => {}
231    }
232
233    Instruction {
234        program_id: crate::ID,
235        accounts,
236        data: Checkpoint { protocol_id }.to_bytes(),
237    }
238}
239
240/// Rebalance a single stable's position from one protocol to another.
241///
242/// This is a per-stable operation. To rebalance the entire pool, call this
243/// for each stable that has funds in the source protocol. Can be parallelized
244/// via Jito bundles.
245pub fn rebalance(
246    signer: Pubkey,
247    stable_address: Pubkey,
248    from_protocol: u8,
249    to_protocol: u8,
250    amount: u64,
251) -> Instruction {
252    let vault_address = vault_pda().0;
253    let vault_usdc_address = get_associated_token_address(&vault_address, &USDC_ADDRESS);
254    let from_protocol_address = protocol_pda(from_protocol).0;
255    let to_protocol_address = protocol_pda(to_protocol).0;
256    let from_position_address = position_pda(stable_address, from_protocol).0;
257    let to_position_address = position_pda(stable_address, to_protocol).0;
258
259    let mut accounts = vec![
260        AccountMeta::new(signer, true),
261        AccountMeta::new(vault_address, false),
262        AccountMeta::new(stable_address, false),
263        AccountMeta::new(from_protocol_address, false),
264        AccountMeta::new(to_protocol_address, false),
265        AccountMeta::new(from_position_address, false),
266        AccountMeta::new(to_position_address, false),
267        AccountMeta::new(vault_usdc_address, false),
268        AccountMeta::new_readonly(system_program::ID, false),
269        AccountMeta::new_readonly(spl_token::ID, false),
270    ];
271
272    // Add from protocol accounts.
273    accounts.extend(protocol_accounts(from_protocol, vault_address));
274    // Add to protocol accounts.
275    accounts.extend(protocol_accounts(to_protocol, vault_address));
276
277    Instruction {
278        program_id: crate::ID,
279        accounts,
280        data: Rebalance {
281            from_protocol,
282            to_protocol,
283            _padding: [0u8; 6],
284            amount: amount.to_le_bytes(),
285        }
286        .to_bytes(),
287    }
288}
289
290/// Set the primary protocol for new deposits.
291pub fn set_primary_protocol(signer: Pubkey, protocol_id: u8) -> Instruction {
292    let vault_address = vault_pda().0;
293    let protocol_address = protocol_pda(protocol_id).0;
294
295    Instruction {
296        program_id: crate::ID,
297        accounts: vec![
298            AccountMeta::new(signer, true),
299            AccountMeta::new(vault_address, false),
300            AccountMeta::new_readonly(protocol_address, false),
301        ],
302        data: SetPrimaryProtocol { protocol_id }.to_bytes(),
303    }
304}
305
306/// Pause or unpause the vault.
307pub fn set_paused(signer: Pubkey, paused: bool) -> Instruction {
308    let vault_address = vault_pda().0;
309
310    Instruction {
311        program_id: crate::ID,
312        accounts: vec![
313            AccountMeta::new(signer, true),
314            AccountMeta::new(vault_address, false),
315        ],
316        data: SetPaused {
317            paused: if paused { 1 } else { 0 },
318        }
319        .to_bytes(),
320    }
321}
322
323/// Collect platform fees from a position.
324/// Admin can call this to collect the protocol's share of yield from any position.
325pub fn collect_platform_fee(
326    signer: Pubkey,
327    treasury: Pubkey,
328    stable_address: Pubkey,
329    protocol_id: u8,
330) -> Instruction {
331    let vault_address = vault_pda().0;
332    let vault_usdc_address = get_associated_token_address(&vault_address, &USDC_ADDRESS);
333    let protocol_address = protocol_pda(protocol_id).0;
334    let position_address = position_pda(stable_address, protocol_id).0;
335
336    let mut accounts = vec![
337        AccountMeta::new(signer, true),
338        AccountMeta::new(vault_address, false),
339        AccountMeta::new(treasury, false),
340        AccountMeta::new(vault_usdc_address, false),
341        AccountMeta::new_readonly(stable_address, false),
342        AccountMeta::new(protocol_address, false),
343        AccountMeta::new(position_address, false),
344        AccountMeta::new_readonly(spl_token::ID, false),
345    ];
346
347    // Add protocol-specific accounts for withdrawal.
348    accounts.extend(protocol_accounts(protocol_id, vault_address));
349
350    Instruction {
351        program_id: crate::ID,
352        accounts,
353        data: CollectPlatformFee { protocol_id }.to_bytes(),
354    }
355}
356
357/// Set the treasury address for platform fee collection.
358pub fn set_treasury(signer: Pubkey, treasury: Pubkey) -> Instruction {
359    let vault_address = vault_pda().0;
360
361    Instruction {
362        program_id: crate::ID,
363        accounts: vec![
364            AccountMeta::new(signer, true),
365            AccountMeta::new(vault_address, false),
366            AccountMeta::new_readonly(treasury, false),
367        ],
368        data: SetTreasury {
369            treasury: treasury.to_bytes(),
370        }
371        .to_bytes(),
372    }
373}
374
375/// Delete the vault account (for re-initialization after schema changes).
376/// WARNING: This is destructive and should only be used during development/testing.
377pub fn delete_vault(signer: Pubkey) -> Instruction {
378    let vault_address = vault_pda().0;
379
380    Instruction {
381        program_id: crate::ID,
382        accounts: vec![
383            AccountMeta::new(signer, true),
384            AccountMeta::new(vault_address, false),
385        ],
386        data: DeleteVault {}.to_bytes(),
387    }
388}
389
390// =============================================================================
391// Creator Instructions
392// =============================================================================
393
394/// Create a new stablecoin.
395pub fn create(
396    signer: Pubkey,
397    payer: Pubkey,
398    id: String,
399    name: String,
400    symbol: String,
401    noise: [u8; 32],
402) -> Instruction {
403    // Convert `id` into [u8; 32].
404    let id_bytes = id.as_bytes();
405    if id_bytes.len() > 32 {
406        panic!("Id must be at most 32 bytes");
407    }
408    let mut id_array = [0u8; 32];
409    id_array[..id_bytes.len()].copy_from_slice(id_bytes);
410
411    // Convert `symbol` into [u8; 32].
412    let symbol_bytes = symbol.as_bytes();
413    if symbol_bytes.len() > 32 {
414        panic!("Symbol must be at most 32 bytes");
415    }
416    let mut symbol_array = [0u8; 32];
417    symbol_array[..symbol_bytes.len()].copy_from_slice(symbol_bytes);
418
419    // Convert `name` into [u8; 32].
420    let name_bytes = name.as_bytes();
421    if name_bytes.len() > 32 {
422        panic!("Name must be at most 32 bytes");
423    }
424    let mut name_array = [0u8; 32];
425    name_array[..name_bytes.len()].copy_from_slice(name_bytes);
426
427    let mint_pda =
428        Pubkey::find_program_address(&[MINT, noise.as_ref()], &crate::ID.to_bytes().into());
429
430    let metadata_address = mpl_token_metadata::accounts::Metadata::find_pda(&mint_pda.0).0;
431    let stable_address = stable_pda(mint_pda.0).0;
432
433    Instruction {
434        program_id: crate::ID,
435        accounts: vec![
436            AccountMeta::new(signer, true),
437            AccountMeta::new(payer, true),
438            AccountMeta::new(metadata_address, false),
439            AccountMeta::new(stable_address, false),
440            AccountMeta::new(mint_pda.0, false),
441            AccountMeta::new_readonly(system_program::ID, false),
442            AccountMeta::new_readonly(spl_token::ID, false),
443            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
444            AccountMeta::new_readonly(mpl_token_metadata::ID, false),
445            AccountMeta::new_readonly(sysvar::rent::ID, false),
446        ],
447        data: Create {
448            id: id_array,
449            name: name_array,
450            symbol: symbol_array,
451            noise,
452            bump: mint_pda.1,
453        }
454        .to_bytes(),
455    }
456}
457
458/// Update stablecoin authority.
459pub fn update_authority(signer: Pubkey, mint_stable: Pubkey, new_authority: Pubkey) -> Instruction {
460    let stable_address = stable_pda(mint_stable).0;
461
462    Instruction {
463        program_id: crate::ID,
464        accounts: vec![
465            AccountMeta::new(signer, true),
466            AccountMeta::new(stable_address, false),
467        ],
468        data: UpdateAuthority {
469            new_authority: new_authority.to_bytes(),
470        }
471        .to_bytes(),
472    }
473}
474
475/// Claim yield from a stablecoin's position in a specific protocol.
476pub fn claim(signer: Pubkey, mint_stable: Pubkey, protocol_id: u8, amount: u64, min_usdc_out: u64) -> Instruction {
477    let stable_address = stable_pda(mint_stable).0;
478    let signer_stable_address = get_associated_token_address(&signer, &mint_stable);
479    let signer_usdc_address = get_associated_token_address(&signer, &USDC_ADDRESS);
480    let vault_address = vault_pda().0;
481    let vault_usdc_address = get_associated_token_address(&vault_address, &USDC_ADDRESS);
482    let protocol_address = protocol_pda(protocol_id).0;
483    let position_address = position_pda(stable_address, protocol_id).0;
484
485    let mut accounts = vec![
486        AccountMeta::new(signer, true),
487        AccountMeta::new(signer, true),
488        AccountMeta::new(signer_stable_address, false),
489        AccountMeta::new(signer_usdc_address, false),
490        AccountMeta::new_readonly(USDC_ADDRESS, false),
491        AccountMeta::new(mint_stable, false),
492        AccountMeta::new(stable_address, false),
493        AccountMeta::new(vault_address, false),
494        AccountMeta::new(vault_usdc_address, false),
495        AccountMeta::new(protocol_address, false),
496        AccountMeta::new(position_address, false),
497        AccountMeta::new_readonly(system_program::ID, false),
498        AccountMeta::new_readonly(spl_token::ID, false),
499        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
500        AccountMeta::new_readonly(sysvar::instructions::ID, false),
501        AccountMeta::new_readonly(crate::ID, false),
502    ];
503
504    // Add protocol-specific accounts.
505    accounts.extend(protocol_accounts(protocol_id, vault_address));
506
507    Instruction {
508        program_id: crate::ID,
509        accounts,
510        data: Claim {
511            amount: amount.to_le_bytes(),
512            min_usdc_out: min_usdc_out.to_le_bytes(),
513        }
514        .to_bytes(),
515    }
516}
517
518// =============================================================================
519// User Instructions
520// =============================================================================
521
522/// Wrap USDC into stablecoin.
523/// Creates the position account if it doesn't exist.
524pub fn wrap(signer: Pubkey, mint_stable: Pubkey, protocol_id: u8, amount: u64) -> Instruction {
525    let stable_address = stable_pda(mint_stable).0;
526    let signer_stable_address = get_associated_token_address(&signer, &mint_stable);
527    let signer_usdc_address = get_associated_token_address(&signer, &USDC_ADDRESS);
528    let vault_address = vault_pda().0;
529    let vault_usdc_address = get_associated_token_address(&vault_address, &USDC_ADDRESS);
530    let protocol_address = protocol_pda(protocol_id).0;
531    let position_address = position_pda(stable_address, protocol_id).0;
532
533    let mut accounts = vec![
534        AccountMeta::new(signer, true),
535        AccountMeta::new(signer, true),
536        AccountMeta::new(signer_stable_address, false),
537        AccountMeta::new(signer_usdc_address, false),
538        AccountMeta::new_readonly(USDC_ADDRESS, false),
539        AccountMeta::new(mint_stable, false),
540        AccountMeta::new(stable_address, false),
541        AccountMeta::new(vault_address, false),
542        AccountMeta::new(vault_usdc_address, false),
543        AccountMeta::new(protocol_address, false),
544        AccountMeta::new(position_address, false),
545        AccountMeta::new_readonly(system_program::ID, false),
546        AccountMeta::new_readonly(spl_token::ID, false),
547        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
548        AccountMeta::new_readonly(sysvar::instructions::ID, false),
549        AccountMeta::new_readonly(crate::ID, false),
550    ];
551
552    // Add protocol-specific accounts.
553    accounts.extend(protocol_accounts(protocol_id, vault_address));
554
555    Instruction {
556        program_id: crate::ID,
557        accounts,
558        data: Wrap {
559            amount: amount.to_le_bytes(),
560        }
561        .to_bytes(),
562    }
563}
564
565/// Unwrap stablecoin to USDC from a single protocol.
566/// For large withdrawals spanning multiple protocols, use `build_unwrap_bundle`.
567/// Position must exist (created during wrap).
568pub fn unwrap(signer: Pubkey, mint_stable: Pubkey, protocol_id: u8, amount: u64, min_usdc_out: u64) -> Instruction {
569    let stable_address = stable_pda(mint_stable).0;
570    let signer_stable_address = get_associated_token_address(&signer, &mint_stable);
571    let signer_usdc_address = get_associated_token_address(&signer, &USDC_ADDRESS);
572    let vault_address = vault_pda().0;
573    let vault_usdc_address = get_associated_token_address(&vault_address, &USDC_ADDRESS);
574    let protocol_address = protocol_pda(protocol_id).0;
575    let position_address = position_pda(stable_address, protocol_id).0;
576
577    let mut accounts = vec![
578        AccountMeta::new(signer, true),
579        AccountMeta::new(signer, true),
580        AccountMeta::new(signer_stable_address, false),
581        AccountMeta::new(signer_usdc_address, false),
582        AccountMeta::new_readonly(USDC_ADDRESS, false),
583        AccountMeta::new(mint_stable, false),
584        AccountMeta::new(stable_address, false),
585        AccountMeta::new(vault_address, false),
586        AccountMeta::new(vault_usdc_address, false),
587        AccountMeta::new(protocol_address, false),
588        AccountMeta::new(position_address, false),
589        AccountMeta::new_readonly(system_program::ID, false),
590        AccountMeta::new_readonly(spl_token::ID, false),
591        AccountMeta::new_readonly(spl_associated_token_account::ID, false),
592        AccountMeta::new_readonly(sysvar::instructions::ID, false),
593        AccountMeta::new_readonly(crate::ID, false),
594    ];
595
596    // Add protocol-specific accounts.
597    accounts.extend(protocol_accounts(protocol_id, vault_address));
598
599    Instruction {
600        program_id: crate::ID,
601        accounts,
602        data: Unwrap {
603            amount: amount.to_le_bytes(),
604            min_usdc_out: min_usdc_out.to_le_bytes(),
605        }
606        .to_bytes(),
607    }
608}
609
610// =============================================================================
611// Bundle Helpers
612// =============================================================================
613
614/// Protocol allocation info for building unwrap bundles.
615pub struct ProtocolAllocation {
616    /// Protocol ID.
617    pub protocol_id: u8,
618    /// Amount of USDC available in this protocol.
619    pub usdc_available: u64,
620}
621
622/// Builds a bundle of unwrap instructions for withdrawing across multiple protocols.
623///
624/// For large withdrawals that span multiple protocols, this function calculates
625/// the optimal split and returns multiple instructions that should be submitted
626/// as a Jito bundle for atomic execution.
627///
628/// # Arguments
629/// * `signer` - The user's wallet address
630/// * `mint_stable` - The stablecoin mint address
631/// * `amount` - Total amount of stablecoins to unwrap
632/// * `protocols` - List of protocols with their available USDC balances
633///
634/// # Returns
635/// A vector of instructions, one per protocol that needs to be withdrawn from.
636/// Submit these as a Jito bundle for atomic execution.
637///
638/// # Example
639/// ```ignore
640/// let protocols = vec![
641///     ProtocolAllocation { protocol_id: PROTOCOL_KAMINO, usdc_available: 1000_000000 },
642///     ProtocolAllocation { protocol_id: PROTOCOL_PERENA, usdc_available: 500_000000 },
643/// ];
644/// let bundle = build_unwrap_bundle(signer, mint, 1200_000000, &protocols);
645/// // bundle[0] = unwrap 1000 USDC from Kamino
646/// // bundle[1] = unwrap 200 USDC from Perena
647/// ```
648pub fn build_unwrap_bundle(
649    _signer: Pubkey,
650    _mint_stable: Pubkey,
651    _amount: u64,
652    _protocols: &[ProtocolAllocation],
653) -> Vec<Instruction> {
654    // TODO: Implement bundle building logic
655    //
656    // 1. Sort protocols by available USDC (descending) for optimal withdrawal
657    // 2. Calculate how much to withdraw from each protocol:
658    //    - Start with the protocol with most funds
659    //    - Take min(remaining_amount, protocol.usdc_available)
660    //    - Move to next protocol if more needed
661    // 3. Build one unwrap instruction per protocol
662    // 4. Return the vector of instructions
663    //
664    // The caller should submit these instructions as a Jito bundle
665    // to ensure atomic execution across all protocols.
666    todo!("Implement multi-protocol unwrap bundle builder")
667}