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