Skip to main content

sla_escrow_api/
sdk.rs

1use steel::*;
2
3use crate::{
4    consts::{
5        BASIS_POINTS_DENOMINATOR, DEFAULT_FEE_BPS, PAYMENT_STATE_FUNDED, PAYMENT_STATE_REFUNDED,
6        PAYMENT_STATE_RELEASED,
7    },
8    error::EscrowError,
9    instruction::*,
10    state::{bank_pda, escrow_pda, normalize_payment_uid, payment_pda, sol_storage_pda, Payment},
11};
12
13/// SDK for interacting with the Escrow program
14pub struct EscrowSdk;
15
16impl EscrowSdk {
17    /// Get the program ID
18    pub fn program_id() -> Pubkey {
19        crate::ID
20    }
21
22    /// Get the bank PDA
23    pub fn bank_pda() -> (Pubkey, u8) {
24        bank_pda()
25    }
26
27    /// Get the authority transfer PDA
28    pub fn authority_transfer_pda(bank: Pubkey) -> (Pubkey, u8) {
29        crate::state::authority_transfer_pda(bank)
30    }
31
32    /// Get the config PDA
33    pub fn config_pda() -> (Pubkey, u8) {
34        crate::state::config_pda()
35    }
36
37    /// Get an escrow PDA for a specific mint
38    pub fn escrow_pda(mint: Pubkey) -> (Pubkey, u8) {
39        let (bank_pda, _) = Self::bank_pda();
40        escrow_pda(mint, bank_pda)
41    }
42
43    /// Get a payment PDA for a specific payment UID
44    pub fn payment_pda(payment_uid: &str, bank: Pubkey) -> (Pubkey, u8) {
45        payment_pda(payment_uid, bank)
46    }
47
48    /// Get a SOL storage PDA for a specific escrow
49    pub fn sol_storage_pda(mint: Pubkey, bank: Pubkey, escrow: Pubkey) -> (Pubkey, u8) {
50        sol_storage_pda(mint, bank, escrow)
51    }
52
53    /// Get the associated token account for a wallet and mint
54    pub fn associated_token_account(wallet: Pubkey, mint: Pubkey) -> Pubkey {
55        Self::associated_token_account_with_program(wallet, mint, spl_token::ID)
56    }
57
58    /// Get the associated token account for a wallet and mint using the given token program.
59    pub fn associated_token_account_with_program(
60        wallet: Pubkey,
61        mint: Pubkey,
62        token_program: Pubkey,
63    ) -> Pubkey {
64        spl_associated_token_account::get_associated_token_address_with_program_id(
65            &wallet,
66            &mint,
67            &token_program,
68        )
69    }
70
71    /// Get the associated token account for an escrow
72    pub fn escrow_token_account(mint: Pubkey) -> Pubkey {
73        Self::escrow_token_account_with_program(mint, spl_token::ID)
74    }
75
76    /// Get the associated token account for an escrow using the given token program.
77    pub fn escrow_token_account_with_program(mint: Pubkey, token_program: Pubkey) -> Pubkey {
78        let (escrow_pda, _) = Self::escrow_pda(mint);
79        Self::associated_token_account_with_program(escrow_pda, mint, token_program)
80    }
81
82    /// Calculate the minimum gross amount to charge a Buyer in order to receive a specific net payout
83    /// hitting the Seller's wallet, taking into account protocol fees and oracle tips.
84    ///
85    /// # Explanation of `desired_net`
86    /// `desired_net` is the final actual quantity of tokens deposited into the Seller's wallet after
87    /// `sla-escrow` subtracts all protocol fees and oracle fees during `ReleasePayment`.
88    ///
89    /// *Example:*
90    /// If you are an AI Image Generation Agent where a single generation costs you $0.80 in cloud GPU overhead,
91    /// and you want to make exactly $0.20 in profit, your `desired_net` must be `$1.00` (which is $0.80 cost + $0.20 profit).
92    /// You pass `$1.00` (in raw token decimals) as `desired_net`, and this function will return the inflated `gross_quote` (e.g. $1.15)
93    /// that you must charge the Buyer in order to mathematically satisfy the protocol fees and walk away with exactly `$1.00`.
94    pub fn calculate_gross_quote(
95        desired_net: u64,
96        fee_bps: u16,
97        min_fee_amount: u64,
98        oracle_fee_bps: u16,
99    ) -> u64 {
100        // Option A: Protocol fee is based on min_fee_amount
101        // payout = amount - min_fee_amount - amount * oracle_fee_bps / 10000
102        // amount * (10000 - oracle_fee_bps) = (payout + min_fee_amount) * 10000
103        let numerator_a =
104            (desired_net as u128 + min_fee_amount as u128).saturating_mul(BASIS_POINTS_DENOMINATOR);
105        let denominator_a = (BASIS_POINTS_DENOMINATOR - oracle_fee_bps as u128).max(1);
106        let amount_a = (numerator_a.saturating_add(denominator_a - 1) / denominator_a) as u64;
107
108        // Option B: Protocol fee is based strictly on fee_bps percentage
109        // payout = amount - amount * (fee_bps + oracle_fee_bps) / 10000
110        // amount * (10000 - fee_bps - oracle_fee_bps) = payout * 10000
111        let numerator_b = (desired_net as u128).saturating_mul(BASIS_POINTS_DENOMINATOR);
112        let denominator_b =
113            (BASIS_POINTS_DENOMINATOR - fee_bps as u128 - oracle_fee_bps as u128).max(1);
114        let amount_b = (numerator_b.saturating_add(denominator_b - 1) / denominator_b) as u64;
115
116        // Choose the correct amount: taking the maximum of the two ensures both constraints are satisfied.
117        amount_a.max(amount_b)
118    }
119
120    // ============================================================================
121    // INSTRUCTION BUILDERS (Following Steel Framework Patterns)
122    // ============================================================================
123
124    /// Create an initialize instruction
125    pub fn initialize(signer: Pubkey, fee_bps: Option<u16>) -> Instruction {
126        let (bank_pda, _) = Self::bank_pda();
127        let (config_pda, _) = Self::config_pda();
128        Instruction {
129            program_id: Self::program_id(),
130            accounts: vec![
131                AccountMeta::new(signer, true),
132                AccountMeta::new(bank_pda, false),
133                AccountMeta::new(config_pda, false),
134                AccountMeta::new_readonly(solana_program::system_program::ID, false),
135                AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
136            ],
137            data: Initialize {
138                fee_bps: (fee_bps.unwrap_or(DEFAULT_FEE_BPS)).to_le_bytes(),
139                _padding: [0; 6], // Pad to 8 bytes for alignment
140            }
141            .to_bytes(),
142        }
143    }
144
145    /// Create an open escrow instruction
146    /// signer: bank authority
147    /// payer: funder
148    /// mint: token mint
149    /// min_payment_amount: minimum payment amount
150    /// max_payment_amount: maximum payment amount
151    /// fee_bps: fee basis points
152    /// oracle_fee_bps: oracle tip basis points (0 = disabled)
153    #[allow(clippy::too_many_arguments)]
154    pub fn open_escrow(
155        signer: Pubkey,
156        payer: Pubkey,
157        mint: Pubkey,
158        min_payment_amount: u64,
159        max_payment_amount: u64,
160        min_fee_amount: u64,
161        fee_bps: u16,
162        oracle_fee_bps: u16,
163    ) -> Instruction {
164        Self::open_escrow_with_token_program(
165            signer,
166            payer,
167            mint,
168            min_payment_amount,
169            max_payment_amount,
170            min_fee_amount,
171            fee_bps,
172            oracle_fee_bps,
173            spl_token::ID,
174        )
175    }
176
177    /// Create an open escrow instruction with an explicit token program.
178    #[allow(clippy::too_many_arguments)]
179    pub fn open_escrow_with_token_program(
180        signer: Pubkey,
181        payer: Pubkey,
182        mint: Pubkey,
183        min_payment_amount: u64,
184        max_payment_amount: u64,
185        min_fee_amount: u64,
186        fee_bps: u16,
187        oracle_fee_bps: u16,
188        token_program: Pubkey,
189    ) -> Instruction {
190        let (bank_pda, _) = Self::bank_pda();
191        let (escrow_pda, _) = Self::escrow_pda(mint);
192        let escrow_tokens = if mint == Pubkey::default() {
193            // For SOL, the escrow account itself holds the SOL
194            escrow_pda
195        } else {
196            // For SPL tokens, use the associated token account
197            Self::escrow_token_account_with_program(mint, token_program)
198        };
199
200        let mut accounts = vec![
201            AccountMeta::new(signer, true),
202            AccountMeta::new(payer, true),
203            AccountMeta::new_readonly(bank_pda, false),
204            AccountMeta::new(escrow_pda, false),
205            AccountMeta::new(escrow_tokens, false),
206            AccountMeta::new_readonly(mint, false),
207            AccountMeta::new_readonly(solana_program::system_program::ID, false),
208            AccountMeta::new_readonly(token_program, false),
209            AccountMeta::new_readonly(spl_associated_token_account::ID, false),
210            AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
211        ];
212
213        // For SOL, add SOL storage PDA account
214        if mint == Pubkey::default() {
215            let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
216            accounts.push(AccountMeta::new(sol_storage_pda, false));
217        }
218
219        Instruction {
220            program_id: Self::program_id(),
221            accounts,
222            data: OpenEscrow {
223                min_payment_amount: min_payment_amount.to_le_bytes(),
224                max_payment_amount: max_payment_amount.to_le_bytes(),
225                min_fee_amount: min_fee_amount.to_le_bytes(),
226                fee_bps: fee_bps.to_le_bytes(),
227                oracle_fee_bps: oracle_fee_bps.to_le_bytes(),
228                _padding: [0; 4],
229            }
230            .to_bytes(),
231        }
232    }
233
234    /// Create a fund payment instruction (supports both SOL and SPL tokens)
235    /// For SOL: mint should be Pubkey::default()
236    /// For SPL tokens: mint should be the actual token mint
237    #[allow(clippy::too_many_arguments)]
238    pub fn fund_payment(
239        buyer: Pubkey,
240        buyer_tokens: Option<Pubkey>, // None for SOL, Some(ata) for SPL tokens
241        seller: Pubkey,
242        mint: Pubkey,
243        amount: u64,
244        ttl_seconds: i64,
245        payment_uid: &str,
246        sla_hash: [u8; 32],
247        oracle_authority: Pubkey,
248    ) -> Instruction {
249        Self::fund_payment_with_token_program(
250            buyer,
251            buyer_tokens,
252            seller,
253            mint,
254            amount,
255            ttl_seconds,
256            payment_uid,
257            sla_hash,
258            oracle_authority,
259            spl_token::ID,
260        )
261    }
262
263    /// Create a fund payment instruction with an explicit token program.
264    #[allow(clippy::too_many_arguments)]
265    pub fn fund_payment_with_token_program(
266        buyer: Pubkey,
267        buyer_tokens: Option<Pubkey>,
268        seller: Pubkey,
269        mint: Pubkey,
270        amount: u64,
271        ttl_seconds: i64,
272        payment_uid: &str,
273        sla_hash: [u8; 32],
274        oracle_authority: Pubkey,
275        token_program: Pubkey,
276    ) -> Instruction {
277        let (bank_pda, _) = Self::bank_pda();
278        let (config_pda, _) = Self::config_pda();
279        let (escrow_pda, _) = Self::escrow_pda(mint);
280        let (payment_pda, _) = crate::state::payment_pda(payment_uid, bank_pda);
281
282        let is_sol = mint == Pubkey::default();
283
284        let mut accounts = vec![
285            AccountMeta::new(buyer, true),
286            AccountMeta::new_readonly(bank_pda, false),
287            AccountMeta::new_readonly(config_pda, false),
288            AccountMeta::new(escrow_pda, false),
289            AccountMeta::new(payment_pda, false),
290            AccountMeta::new_readonly(mint, false),
291        ];
292
293        if is_sol {
294            // SOL-specific accounts: [sol_storage_pda, system_program]
295            let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
296            accounts.push(AccountMeta::new(sol_storage_pda, false));
297            accounts.push(AccountMeta::new_readonly(
298                solana_program::system_program::ID,
299                false,
300            ));
301        } else {
302            // SPL Token-specific accounts: [escrow_tokens, buyer_tokens, token_program, system_program]
303            let buyer_tokens = buyer_tokens.expect("buyer_tokens required for SPL tokens");
304            let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
305            accounts.push(AccountMeta::new(escrow_tokens, false));
306            accounts.push(AccountMeta::new(buyer_tokens, false));
307            accounts.push(AccountMeta::new_readonly(token_program, false));
308            accounts.push(AccountMeta::new_readonly(
309                solana_program::system_program::ID,
310                false,
311            ));
312        }
313
314        Instruction {
315            program_id: Self::program_id(),
316            accounts,
317            data: FundPayment {
318                seller,
319                mint,
320                oracle_authority,
321                payment_uid: normalize_payment_uid(payment_uid),
322                sla_hash,
323                amount: amount.to_le_bytes(),
324                ttl_seconds: ttl_seconds.to_le_bytes(),
325            }
326            .to_bytes(),
327        }
328    }
329
330    /// Create a release payment instruction (supports both SOL and SPL tokens)
331    /// For SOL: mint should be Pubkey::default()
332    /// For SPL tokens: mint should be the actual token mint
333    pub fn release_payment(
334        caller: Pubkey,
335        seller_tokens: Option<Pubkey>, // None for SOL, Some(ata) for SPL tokens
336        seller: Option<Pubkey>,        // Seller's account info for ATA creation (SPL tokens only)
337        mint: Pubkey,
338        payment_uid: &str,
339        oracle_tokens: Option<Pubkey>,
340        oracle_authority: Option<Pubkey>,
341    ) -> Instruction {
342        Self::release_payment_with_token_program(
343            caller,
344            seller_tokens,
345            seller,
346            mint,
347            payment_uid,
348            oracle_tokens,
349            oracle_authority,
350            spl_token::ID,
351        )
352    }
353
354    /// Create a release payment instruction with an explicit token program.
355    #[allow(clippy::too_many_arguments)]
356    pub fn release_payment_with_token_program(
357        caller: Pubkey,
358        seller_tokens: Option<Pubkey>,
359        seller: Option<Pubkey>,
360        mint: Pubkey,
361        payment_uid: &str,
362        oracle_tokens: Option<Pubkey>,
363        oracle_authority: Option<Pubkey>,
364        token_program: Pubkey,
365    ) -> Instruction {
366        let (bank_pda, _) = Self::bank_pda();
367        let (config_pda, _) = Self::config_pda();
368        let (escrow_pda, _) = Self::escrow_pda(mint);
369        let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
370
371        let is_sol = mint == Pubkey::default();
372
373        let mut accounts = vec![
374            AccountMeta::new(caller, true),
375            AccountMeta::new_readonly(bank_pda, false),
376            AccountMeta::new_readonly(config_pda, false),
377            AccountMeta::new(escrow_pda, false),
378            AccountMeta::new(payment_pda, false),
379            AccountMeta::new_readonly(mint, false),
380        ];
381
382        if is_sol {
383            // SOL accounts: [caller, bank, config, escrow, payment, mint, sol_storage_pda, seller, system_program]
384            let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
385            accounts.push(AccountMeta::new(sol_storage_pda, false));
386            accounts.push(AccountMeta::new(
387                seller.expect("seller required for SOL"),
388                false,
389            ));
390            accounts.push(AccountMeta::new_readonly(
391                solana_program::system_program::ID,
392                false,
393            ));
394            if let Some(oracle_authority) = oracle_authority {
395                accounts.push(AccountMeta::new(oracle_authority, false));
396            }
397        } else {
398            // SPL Token accounts: [caller, bank, config, escrow, payment, mint, escrow_tokens, seller_tokens, seller, token_program, associated_token_program, system_program]
399            let seller_tokens = seller_tokens.expect("seller_tokens required for SPL tokens");
400            let seller = seller.expect("seller account required for SPL tokens");
401            let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
402            accounts.push(AccountMeta::new(escrow_tokens, false));
403            accounts.push(AccountMeta::new(seller_tokens, false));
404            accounts.push(AccountMeta::new(seller, false));
405            accounts.push(AccountMeta::new_readonly(token_program, false));
406            accounts.push(AccountMeta::new_readonly(
407                spl_associated_token_account::ID,
408                false,
409            ));
410            accounts.push(AccountMeta::new_readonly(
411                solana_program::system_program::ID,
412                false,
413            ));
414            if let (Some(oracle_tokens), Some(oracle_authority)) = (oracle_tokens, oracle_authority)
415            {
416                accounts.push(AccountMeta::new(oracle_tokens, false));
417                accounts.push(AccountMeta::new(oracle_authority, false));
418            }
419        }
420
421        Instruction {
422            program_id: Self::program_id(),
423            accounts,
424            data: ReleasePayment {}.to_bytes(),
425        }
426    }
427
428    /// Create a refund payment instruction (supports both SOL and SPL tokens)
429    /// For SOL: mint should be Pubkey::default()
430    /// For SPL tokens: mint should be the actual token mint
431    pub fn refund_payment(
432        caller: Pubkey,
433        buyer_tokens: Option<Pubkey>, // None for SOL, Some(ata) for SPL tokens
434        mint: Pubkey,
435        payment_uid: &str,
436        oracle_tokens: Option<Pubkey>,
437        oracle_authority: Option<Pubkey>,
438    ) -> Instruction {
439        Self::refund_payment_with_token_program(
440            caller,
441            buyer_tokens,
442            mint,
443            payment_uid,
444            oracle_tokens,
445            oracle_authority,
446            spl_token::ID,
447        )
448    }
449
450    /// Create a refund payment instruction with an explicit token program.
451    #[allow(clippy::too_many_arguments)]
452    pub fn refund_payment_with_token_program(
453        caller: Pubkey,
454        buyer_tokens: Option<Pubkey>,
455        mint: Pubkey,
456        payment_uid: &str,
457        oracle_tokens: Option<Pubkey>,
458        oracle_authority: Option<Pubkey>,
459        token_program: Pubkey,
460    ) -> Instruction {
461        let (bank_pda, _) = Self::bank_pda();
462        let (config_pda, _) = Self::config_pda();
463        let (escrow_pda, _) = Self::escrow_pda(mint);
464        let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
465
466        let is_sol = mint == Pubkey::default();
467
468        let mut accounts = vec![
469            AccountMeta::new(caller, true),
470            AccountMeta::new_readonly(bank_pda, false),
471            AccountMeta::new_readonly(config_pda, false),
472            AccountMeta::new(escrow_pda, false),
473            AccountMeta::new(payment_pda, false),
474            AccountMeta::new_readonly(mint, false),
475        ];
476
477        if is_sol {
478            // SOL accounts: [caller, bank, config, escrow, payment, mint, sol_storage_pda, buyer, system_program]
479            let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
480            accounts.push(AccountMeta::new(sol_storage_pda, false));
481            accounts.push(AccountMeta::new(
482                buyer_tokens.expect("buyer required for SOL"),
483                false,
484            ));
485            accounts.push(AccountMeta::new_readonly(
486                solana_program::system_program::ID,
487                false,
488            ));
489            if let Some(oracle_authority) = oracle_authority {
490                accounts.push(AccountMeta::new(oracle_authority, false));
491            }
492        } else {
493            // SPL Token accounts: [caller, bank, config, escrow, payment, mint, escrow_tokens, buyer_tokens, token_program]
494            let buyer_tokens = buyer_tokens.expect("buyer_tokens required for SPL tokens");
495            let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
496            accounts.push(AccountMeta::new(escrow_tokens, false));
497            accounts.push(AccountMeta::new(buyer_tokens, false));
498            accounts.push(AccountMeta::new_readonly(token_program, false));
499            if let (Some(oracle_tokens), Some(oracle_authority)) = (oracle_tokens, oracle_authority)
500            {
501                accounts.push(AccountMeta::new(oracle_tokens, false));
502                accounts.push(AccountMeta::new(oracle_authority, false));
503                accounts.push(AccountMeta::new_readonly(
504                    spl_associated_token_account::ID,
505                    false,
506                ));
507                accounts.push(AccountMeta::new_readonly(
508                    solana_program::system_program::ID,
509                    false,
510                ));
511            }
512        }
513
514        Instruction {
515            program_id: Self::program_id(),
516            accounts,
517            data: RefundPayment {}.to_bytes(),
518        }
519    }
520
521    /// Create a submit delivery instruction
522    pub fn submit_delivery(
523        caller: Pubkey,
524        mint: Pubkey,
525        payment_uid: &str,
526        delivery_hash: [u8; 32],
527    ) -> Instruction {
528        let (bank_pda, _) = Self::bank_pda();
529        let (config_pda, _) = Self::config_pda();
530        let (escrow_pda, _) = Self::escrow_pda(mint);
531        let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
532
533        Instruction {
534            program_id: Self::program_id(),
535            accounts: vec![
536                AccountMeta::new(caller, true), // signer (seller or admin)
537                AccountMeta::new_readonly(bank_pda, false),
538                AccountMeta::new_readonly(config_pda, false),
539                AccountMeta::new_readonly(escrow_pda, false),
540                AccountMeta::new(payment_pda, false),
541            ],
542            data: SubmitDelivery { delivery_hash }.to_bytes(),
543        }
544    }
545
546    /// Create a close payment instruction
547    pub fn close_payment(
548        caller: Pubkey,
549        buyer: Pubkey,
550        mint: Pubkey,
551        payment_uid: &str,
552    ) -> Instruction {
553        let (bank_pda, _) = Self::bank_pda();
554        let (config_pda, _) = Self::config_pda();
555        let (escrow_pda, _) = Self::escrow_pda(mint);
556        let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
557
558        Instruction {
559            program_id: Self::program_id(),
560            accounts: vec![
561                AccountMeta::new(caller, true), // signer (buyer or admin)
562                AccountMeta::new(buyer, false), // rent receiver
563                AccountMeta::new_readonly(bank_pda, false),
564                AccountMeta::new_readonly(config_pda, false),
565                AccountMeta::new(escrow_pda, false),
566                AccountMeta::new(payment_pda, false),
567                AccountMeta::new_readonly(solana_program::system_program::ID, false),
568            ],
569            data: ClosePayment {}.to_bytes(),
570        }
571    }
572
573    /// Create a withdraw fees instruction (supports both SOL and SPL tokens)
574    /// For SOL: mint should be Pubkey::default()
575    /// For SPL tokens: mint should be the actual token mint
576    pub fn withdraw_fees(
577        authority: Pubkey,
578        beneficiary: Pubkey,
579        mint: Pubkey,
580        amount: u64,
581    ) -> Instruction {
582        Self::withdraw_fees_with_token_program(authority, beneficiary, mint, amount, spl_token::ID)
583    }
584
585    /// Create a withdraw fees instruction with an explicit token program.
586    pub fn withdraw_fees_with_token_program(
587        authority: Pubkey,
588        beneficiary: Pubkey,
589        mint: Pubkey,
590        amount: u64,
591        token_program: Pubkey,
592    ) -> Instruction {
593        let (bank_pda, _) = Self::bank_pda();
594        let (config_pda, _) = Self::config_pda();
595        let (escrow_pda, _) = Self::escrow_pda(mint);
596
597        let is_sol = mint == Pubkey::default();
598
599        let mut accounts = vec![
600            AccountMeta::new(authority, true),
601            AccountMeta::new_readonly(bank_pda, false),
602            AccountMeta::new_readonly(config_pda, false),
603            AccountMeta::new(escrow_pda, false),
604            AccountMeta::new(beneficiary, false),
605        ];
606
607        if is_sol {
608            // SOL accounts: [authority, bank, config, escrow, beneficiary, sol_storage_pda, system_program]
609            let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
610            accounts.push(AccountMeta::new(sol_storage_pda, false));
611            accounts.push(AccountMeta::new_readonly(
612                solana_program::system_program::ID,
613                false,
614            ));
615        } else {
616            // SPL Token accounts: [authority, bank, config, escrow, escrow_tokens, beneficiary_tokens, mint, token_program]
617            let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
618            let beneficiary_tokens =
619                Self::associated_token_account_with_program(beneficiary, mint, token_program);
620            // Replace beneficiary with escrow_tokens, then add beneficiary_tokens
621            accounts[4] = AccountMeta::new(escrow_tokens, false);
622            accounts.push(AccountMeta::new(beneficiary_tokens, false));
623            accounts.push(AccountMeta::new_readonly(mint, false));
624            accounts.push(AccountMeta::new_readonly(token_program, false));
625        }
626
627        Instruction {
628            program_id: Self::program_id(),
629            accounts,
630            data: WithdrawFees {
631                amount: amount.to_le_bytes(),
632            }
633            .to_bytes(),
634        }
635    }
636
637    /// Create an extend payment TTL instruction
638    pub fn extend_payment_ttl(
639        caller: Pubkey,
640        mint: Pubkey,
641        payment_uid: &str,
642        additional_seconds: i64,
643    ) -> Instruction {
644        let (bank_pda, _) = Self::bank_pda();
645        let (config_pda, _) = Self::config_pda();
646        let (escrow_pda, _) = Self::escrow_pda(mint);
647        let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
648
649        Instruction {
650            program_id: Self::program_id(),
651            accounts: vec![
652                AccountMeta::new(caller, true),
653                AccountMeta::new_readonly(bank_pda, false), // Bank is writable in program
654                AccountMeta::new_readonly(config_pda, false),
655                AccountMeta::new_readonly(escrow_pda, false),
656                AccountMeta::new(payment_pda, false), // Payment is writable in program
657                AccountMeta::new_readonly(solana_program::system_program::ID, false),
658                AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
659            ],
660            data: ExtendPaymentTTL {
661                additional_seconds: additional_seconds.to_le_bytes(),
662            }
663            .to_bytes(),
664        }
665    }
666
667    /// Create a close escrow instruction
668    pub fn close_escrow(authority: Pubkey, recipient: Pubkey, mint: Pubkey) -> Instruction {
669        Self::close_escrow_with_token_program(authority, recipient, mint, spl_token::ID)
670    }
671
672    /// Create a close escrow instruction with an explicit token program.
673    pub fn close_escrow_with_token_program(
674        authority: Pubkey,
675        recipient: Pubkey,
676        mint: Pubkey,
677        token_program: Pubkey,
678    ) -> Instruction {
679        let (bank_pda, _) = Self::bank_pda();
680        let (escrow_pda, _) = Self::escrow_pda(mint);
681        let escrow_tokens = if mint == Pubkey::default() {
682            let (sol_storage_pda, _) = Self::sol_storage_pda(mint, bank_pda, escrow_pda);
683            sol_storage_pda
684        } else {
685            Self::escrow_token_account_with_program(mint, token_program)
686        };
687
688        Instruction {
689            program_id: Self::program_id(),
690            accounts: vec![
691                AccountMeta::new(authority, true),
692                AccountMeta::new_readonly(bank_pda, false),
693                AccountMeta::new(escrow_pda, false),
694                AccountMeta::new(escrow_tokens, false),
695                AccountMeta::new_readonly(mint, false),
696                AccountMeta::new_readonly(token_program, false),
697                AccountMeta::new(recipient, false),
698            ],
699            data: CloseEscrow {}.to_bytes(),
700        }
701    }
702
703    /// Create an update escrow settings instruction
704    pub fn update_escrow_settings(
705        authority: Pubkey,
706        escrow: Pubkey,
707        fee_bps: u16,
708        min_payment_amount: u64,
709        max_payment_amount: u64,
710        min_fee_amount: u64,
711        oracle_fee_bps: u16,
712    ) -> Instruction {
713        let (bank_pda, _) = Self::bank_pda();
714        let (config_pda, _) = Self::config_pda();
715
716        Instruction {
717            program_id: Self::program_id(),
718            accounts: vec![
719                AccountMeta::new(authority, true),
720                AccountMeta::new_readonly(bank_pda, false), // Bank is writable in program
721                AccountMeta::new_readonly(config_pda, false),
722                AccountMeta::new(escrow, false), // Escrow is writable in program
723                AccountMeta::new_readonly(solana_program::system_program::ID, false),
724                AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
725            ],
726            data: UpdateEscrowSettings {
727                min_payment_amount: min_payment_amount.to_le_bytes(),
728                max_payment_amount: max_payment_amount.to_le_bytes(),
729                min_fee_amount: min_fee_amount.to_le_bytes(),
730                new_fee_bps: fee_bps.to_le_bytes(),
731                new_oracle_fee_bps: oracle_fee_bps.to_le_bytes(),
732                _padding: [0; 4],
733            }
734            .to_bytes(),
735        }
736    }
737
738    /// Create a pause escrow instruction
739    pub fn pause_escrow(authority: Pubkey, mint: Pubkey, pause: bool) -> Instruction {
740        let (bank_pda, _) = Self::bank_pda();
741        let (config_pda, _) = Self::config_pda();
742        let (escrow_pda, _) = Self::escrow_pda(mint);
743
744        Instruction {
745            program_id: Self::program_id(),
746            accounts: vec![
747                AccountMeta::new(authority, true),
748                AccountMeta::new_readonly(bank_pda, false), // Bank is writable in program
749                AccountMeta::new_readonly(config_pda, false),
750                AccountMeta::new(escrow_pda, false), // Escrow is writable in program
751                AccountMeta::new_readonly(solana_program::system_program::ID, false),
752                AccountMeta::new_readonly(solana_program::sysvar::rent::ID, false),
753            ],
754            data: PauseEscrow {
755                pause: if pause { 1 } else { 0 },
756            }
757            .to_bytes(),
758        }
759    }
760
761    /// Create an update authority instruction (Step 1: Propose)
762    pub fn update_authority(current_authority: Pubkey, new_authority: Pubkey) -> Instruction {
763        let (bank_pda, _) = Self::bank_pda();
764        let (transfer_pda, _) = Self::authority_transfer_pda(bank_pda);
765
766        Instruction {
767            program_id: Self::program_id(),
768            accounts: vec![
769                AccountMeta::new(current_authority, true),
770                AccountMeta::new_readonly(bank_pda, false),
771                AccountMeta::new(transfer_pda, false),
772                AccountMeta::new_readonly(solana_program::system_program::ID, false),
773            ],
774            data: UpdateAuthority { new_authority }.to_bytes(),
775        }
776    }
777
778    /// Create an accept authority instruction (Step 2: Accept)
779    pub fn accept_authority(new_authority: Pubkey) -> Instruction {
780        let (bank_pda, _) = Self::bank_pda();
781        let (transfer_pda, _) = Self::authority_transfer_pda(bank_pda);
782
783        Instruction {
784            program_id: Self::program_id(),
785            accounts: vec![
786                AccountMeta::new(new_authority, true), // signer, writable
787                AccountMeta::new(bank_pda, false),     // writable
788                AccountMeta::new(transfer_pda, false), // writable
789            ],
790            data: AcceptAuthority {}.to_bytes(),
791        }
792    }
793
794    /// Create a cancel authority proposal instruction
795    pub fn cancel_authority_proposal(current_authority: Pubkey) -> Instruction {
796        let (bank_pda, _) = Self::bank_pda();
797        let (transfer_pda, _) = Self::authority_transfer_pda(bank_pda);
798
799        Instruction {
800            program_id: Self::program_id(),
801            accounts: vec![
802                AccountMeta::new(current_authority, true), // signer, writable
803                AccountMeta::new_readonly(bank_pda, false),
804                AccountMeta::new(transfer_pda, false), // writable
805            ],
806            data: CancelAuthorityProposal {}.to_bytes(),
807        }
808    }
809
810    /// Create an update config instruction (admin only).
811    ///
812    /// Tunes global, program-wide policy knobs that are snapshotted into every
813    /// newly funded [`Payment`] via [`EscrowSdk::fund_payment`]. Existing
814    /// payments retain the values captured at funding time.
815    pub fn update_config(
816        admin: Pubkey,
817        closure_delay_seconds: i64,
818        refund_cooldown_seconds: i64,
819        delivery_cutoff_seconds: i64,
820    ) -> Instruction {
821        let (bank_pda, _) = Self::bank_pda();
822        let (config_pda, _) = Self::config_pda();
823
824        Instruction {
825            program_id: Self::program_id(),
826            accounts: vec![
827                AccountMeta::new(admin, true),
828                AccountMeta::new_readonly(bank_pda, false),
829                AccountMeta::new(config_pda, false),
830            ],
831            data: UpdateConfig {
832                closure_delay_seconds: closure_delay_seconds.to_le_bytes(),
833                refund_cooldown_seconds: refund_cooldown_seconds.to_le_bytes(),
834                delivery_cutoff_seconds: delivery_cutoff_seconds.to_le_bytes(),
835            }
836            .to_bytes(),
837        }
838    }
839
840    /// Create a confirm oracle instruction (oracle confirms fulfillment)
841    /// Build a `ConfirmOracle` instruction.
842    ///
843    /// `resolution_hash` is an opaque 32-byte attestation digest chosen by the
844    /// oracle (e.g. SHA-256 of the oracle's signed evidence bundle). It is
845    /// stored on the [`Payment`] and emitted in [`PaymentOracleConfirmedEvent`]
846    /// for auditors / multi-oracle aggregators / insurance consumers. Pass
847    /// `[0u8; 32]` when no attestation digest is available.
848    pub fn confirm_oracle(
849        oracle_authority: Pubkey,
850        mint: Pubkey,
851        payment_uid: &str,
852        delivery_hash: [u8; 32],
853        resolution_hash: [u8; 32],
854        resolution_state: u8,
855        resolution_reason: u16,
856    ) -> Instruction {
857        let (bank_pda, _) = Self::bank_pda();
858        let (config_pda, _) = Self::config_pda();
859        let (escrow_pda, _) = Self::escrow_pda(mint);
860        let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
861
862        Instruction {
863            program_id: Self::program_id(),
864            accounts: vec![
865                AccountMeta::new(oracle_authority, true), // Oracle must sign
866                AccountMeta::new_readonly(bank_pda, false),
867                AccountMeta::new_readonly(config_pda, false),
868                AccountMeta::new_readonly(escrow_pda, false),
869                AccountMeta::new(payment_pda, false), // Payment is writable
870            ],
871            data: ConfirmOracle {
872                delivery_hash,
873                resolution_hash,
874                resolution_reason: resolution_reason.to_le_bytes(),
875                resolution_state,
876                _padding: [0; 5],
877            }
878            .to_bytes(),
879        }
880    }
881
882    // ============================================================================
883    // VALIDATION HELPERS
884    // ============================================================================
885
886    /// Validate that a payment is in the correct state for funding
887    pub fn validate_payment_for_funding(payment: &Payment) -> Result<(), EscrowError> {
888        if payment.state != PAYMENT_STATE_FUNDED {
889            return Err(EscrowError::InvalidPaymentState);
890        }
891        Ok(())
892    }
893
894    /// Validate that a payment is in the correct state for release
895    pub fn validate_payment_for_release(payment: &Payment) -> Result<(), EscrowError> {
896        if payment.state != PAYMENT_STATE_FUNDED {
897            return Err(EscrowError::InvalidPaymentState);
898        }
899        Ok(())
900    }
901
902    /// Validate that a payment is in the correct state for refund
903    pub fn validate_payment_for_refund(payment: &Payment) -> Result<(), EscrowError> {
904        if payment.state != PAYMENT_STATE_FUNDED {
905            return Err(EscrowError::InvalidPaymentState);
906        }
907        Ok(())
908    }
909
910    // ============================================================================
911    // UTILITY FUNCTIONS
912    // ============================================================================
913
914    /// Calculate fee amount based on payment amount, fee basis points, and minimum fee
915    pub fn calculate_fee(amount: u64, fee_bps: u16, min_fee_amount: u64) -> u64 {
916        let fee_bps = fee_bps as u128;
917        let amount_128 = amount as u128;
918        let calculated_fee = ((amount_128 * fee_bps) / BASIS_POINTS_DENOMINATOR) as u64;
919        // FIX(#8): Apply min_fee_amount floor and amount ceiling, matching on-chain logic
920        calculated_fee.max(min_fee_amount).min(amount)
921    }
922
923    /// Calculate payout amount after deducting fees
924    pub fn calculate_payout(amount: u64, fee_bps: u16, min_fee_amount: u64) -> u64 {
925        let fee = Self::calculate_fee(amount, fee_bps, min_fee_amount);
926        amount.saturating_sub(fee)
927    }
928
929    /// Check if a payment has expired
930    pub fn is_payment_expired(payment: &Payment) -> bool {
931        let clock = solana_program::clock::Clock::get().unwrap_or_default();
932        clock.unix_timestamp > payment.expires_at
933    }
934
935    /// Get payment state as string
936    pub fn get_payment_state_string(state: u8) -> &'static str {
937        match state {
938            PAYMENT_STATE_FUNDED => "Funded",
939            PAYMENT_STATE_RELEASED => "Released",
940            PAYMENT_STATE_REFUNDED => "Refunded",
941            _ => "Unknown",
942        }
943    }
944
945    /// Get escrow state as string
946    pub fn get_escrow_state_string(paused: u8) -> &'static str {
947        match paused {
948            0 => "Active",
949            1 => "Paused",
950            _ => "Unknown",
951        }
952    }
953
954    // ============================================================================
955    // ACCOUNT DERIVATION HELPERS
956    // ============================================================================
957
958    /// Get all required accounts for a payment operation
959    pub fn get_payment_accounts(
960        buyer: Pubkey,
961        seller: Pubkey,
962        mint: Pubkey,
963        payment_uid: &str,
964    ) -> PaymentAccounts {
965        Self::get_payment_accounts_with_token_program(
966            buyer,
967            seller,
968            mint,
969            payment_uid,
970            spl_token::ID,
971        )
972    }
973
974    /// Get all required accounts for a payment operation with an explicit token program.
975    pub fn get_payment_accounts_with_token_program(
976        buyer: Pubkey,
977        seller: Pubkey,
978        mint: Pubkey,
979        payment_uid: &str,
980        token_program: Pubkey,
981    ) -> PaymentAccounts {
982        let (bank_pda, _) = Self::bank_pda();
983        let (escrow_pda, _) = Self::escrow_pda(mint);
984        let (payment_pda, _) = Self::payment_pda(payment_uid, bank_pda);
985        let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
986
987        PaymentAccounts {
988            buyer,
989            seller,
990            mint,
991            bank: bank_pda,
992            escrow: escrow_pda,
993            payment: payment_pda,
994            escrow_tokens,
995        }
996    }
997
998    /// Get all required accounts for an escrow operation
999    pub fn get_escrow_accounts(mint: Pubkey) -> EscrowAccounts {
1000        Self::get_escrow_accounts_with_token_program(mint, spl_token::ID)
1001    }
1002
1003    /// Get all required accounts for an escrow operation with an explicit token program.
1004    pub fn get_escrow_accounts_with_token_program(
1005        mint: Pubkey,
1006        token_program: Pubkey,
1007    ) -> EscrowAccounts {
1008        let (bank_pda, _) = Self::bank_pda();
1009        let (escrow_pda, _) = Self::escrow_pda(mint);
1010        let escrow_tokens = Self::escrow_token_account_with_program(mint, token_program);
1011
1012        EscrowAccounts {
1013            mint,
1014            bank: bank_pda,
1015            escrow: escrow_pda,
1016            escrow_tokens,
1017        }
1018    }
1019}
1020
1021// ============================================================================
1022// CONVENIENCE STRUCTS FOR COMMON OPERATIONS
1023// ============================================================================
1024
1025/// Accounts required for payment operations
1026#[derive(Debug, Clone)]
1027pub struct PaymentAccounts {
1028    pub buyer: Pubkey,
1029    pub seller: Pubkey,
1030    pub mint: Pubkey,
1031    pub bank: Pubkey,
1032    pub escrow: Pubkey,
1033    pub payment: Pubkey,
1034    pub escrow_tokens: Pubkey,
1035}
1036
1037/// Accounts required for escrow operations
1038#[derive(Debug, Clone)]
1039pub struct EscrowAccounts {
1040    pub mint: Pubkey,
1041    pub bank: Pubkey,
1042    pub escrow: Pubkey,
1043    pub escrow_tokens: Pubkey,
1044}
1045
1046/// Builder for [`EscrowSdk::fund_payment`].
1047///
1048/// The CLI calls [`EscrowSdk::fund_payment`] with explicit `clap` arguments so every flag maps
1049/// directly to user input and help text. External agents and SDKs may prefer this builder for
1050/// composition and defaults.
1051pub struct PaymentBuilder {
1052    buyer: Option<Pubkey>,
1053    buyer_tokens: Option<Pubkey>,
1054    seller: Option<Pubkey>,
1055    mint: Option<Pubkey>,
1056    token_program: Option<Pubkey>,
1057    amount: Option<u64>,
1058    ttl_seconds: Option<i64>,
1059    payment_uid: Option<String>,
1060    sla_hash: Option<[u8; 32]>,
1061    oracle_authority: Option<Pubkey>,
1062}
1063
1064impl PaymentBuilder {
1065    pub fn new() -> Self {
1066        Self {
1067            buyer: None,
1068            buyer_tokens: None,
1069            seller: None,
1070            mint: None,
1071            token_program: None,
1072            amount: None,
1073            ttl_seconds: None,
1074            payment_uid: None,
1075            sla_hash: None,
1076            oracle_authority: None,
1077        }
1078    }
1079
1080    pub fn buyer(mut self, buyer: Pubkey) -> Self {
1081        self.buyer = Some(buyer);
1082        self
1083    }
1084
1085    pub fn buyer_tokens(mut self, buyer_tokens: Pubkey) -> Self {
1086        self.buyer_tokens = Some(buyer_tokens);
1087        self
1088    }
1089
1090    pub fn seller(mut self, seller: Pubkey) -> Self {
1091        self.seller = Some(seller);
1092        self
1093    }
1094
1095    pub fn mint(mut self, mint: Pubkey) -> Self {
1096        self.mint = Some(mint);
1097        self
1098    }
1099
1100    pub fn token_program(mut self, token_program: Pubkey) -> Self {
1101        self.token_program = Some(token_program);
1102        self
1103    }
1104
1105    pub fn amount(mut self, amount: u64) -> Self {
1106        self.amount = Some(amount);
1107        self
1108    }
1109
1110    pub fn ttl_seconds(mut self, ttl_seconds: i64) -> Self {
1111        self.ttl_seconds = Some(ttl_seconds);
1112        self
1113    }
1114
1115    pub fn payment_uid(mut self, payment_uid: &str) -> Self {
1116        self.payment_uid = Some(payment_uid.to_string());
1117        self
1118    }
1119
1120    pub fn sla_hash(mut self, sla_hash: [u8; 32]) -> Self {
1121        self.sla_hash = Some(sla_hash);
1122        self
1123    }
1124
1125    pub fn oracle_authority(mut self, oracle_authority: Pubkey) -> Self {
1126        self.oracle_authority = Some(oracle_authority);
1127        self
1128    }
1129
1130    pub fn build(self) -> Result<Instruction, EscrowError> {
1131        let buyer = self.buyer.ok_or(EscrowError::MissingRequiredField)?;
1132        let buyer_tokens = self.buyer_tokens; // Now optional
1133        let seller = self.seller.ok_or(EscrowError::MissingRequiredField)?;
1134        let mint = self.mint.ok_or(EscrowError::MissingRequiredField)?;
1135        let token_program = self.token_program.unwrap_or(spl_token::ID);
1136        let amount = self.amount.ok_or(EscrowError::MissingRequiredField)?;
1137        let ttl_seconds = self.ttl_seconds.ok_or(EscrowError::MissingRequiredField)?;
1138        let payment_uid = self.payment_uid.ok_or(EscrowError::MissingRequiredField)?;
1139        let sla_hash = self.sla_hash.unwrap_or([0; 32]);
1140        let oracle_authority = self.oracle_authority.unwrap_or_default();
1141
1142        Ok(EscrowSdk::fund_payment_with_token_program(
1143            buyer,
1144            buyer_tokens,
1145            seller,
1146            mint,
1147            amount,
1148            ttl_seconds,
1149            &payment_uid,
1150            sla_hash,
1151            oracle_authority,
1152            token_program,
1153        ))
1154    }
1155}
1156
1157impl Default for PaymentBuilder {
1158    fn default() -> Self {
1159        Self::new()
1160    }
1161}
1162
1163/// Builder for admin escrow instructions ([`EscrowSdk::open_escrow`], [`EscrowSdk::pause_escrow`]).
1164///
1165/// Same rationale as [`PaymentBuilder`]: the CLI uses [`EscrowSdk`] directly; this type is for
1166/// programmatic callers.
1167pub struct EscrowBuilder {
1168    authority: Option<Pubkey>,
1169    funder: Option<Pubkey>,
1170    mint: Option<Pubkey>,
1171    token_program: Option<Pubkey>,
1172    min_payment_amount: Option<u64>,
1173    max_payment_amount: Option<u64>,
1174    min_fee_amount: Option<u64>,
1175    fee_bps: Option<u16>,
1176    oracle_fee_bps: Option<u16>,
1177}
1178
1179impl EscrowBuilder {
1180    pub fn new() -> Self {
1181        Self {
1182            authority: None,
1183            funder: None,
1184            mint: None,
1185            token_program: None,
1186            min_payment_amount: None,
1187            max_payment_amount: None,
1188            min_fee_amount: None,
1189            fee_bps: None,
1190            oracle_fee_bps: None,
1191        }
1192    }
1193
1194    pub fn authority(mut self, authority: Pubkey) -> Self {
1195        self.authority = Some(authority);
1196        self
1197    }
1198
1199    pub fn funder(mut self, funder: Pubkey) -> Self {
1200        self.funder = Some(funder);
1201        self
1202    }
1203
1204    pub fn mint(mut self, mint: Pubkey) -> Self {
1205        self.mint = Some(mint);
1206        self
1207    }
1208
1209    pub fn token_program(mut self, token_program: Pubkey) -> Self {
1210        self.token_program = Some(token_program);
1211        self
1212    }
1213
1214    pub fn min_payment_amount(mut self, min_payment_amount: u64) -> Self {
1215        self.min_payment_amount = Some(min_payment_amount);
1216        self
1217    }
1218
1219    pub fn max_payment_amount(mut self, max_payment_amount: u64) -> Self {
1220        self.max_payment_amount = Some(max_payment_amount);
1221        self
1222    }
1223
1224    pub fn min_fee_amount(mut self, min_fee_amount: u64) -> Self {
1225        self.min_fee_amount = Some(min_fee_amount);
1226        self
1227    }
1228
1229    pub fn fee_bps(mut self, fee_bps: u16) -> Self {
1230        self.fee_bps = Some(fee_bps);
1231        self
1232    }
1233
1234    pub fn oracle_fee_bps(mut self, oracle_fee_bps: u16) -> Self {
1235        self.oracle_fee_bps = Some(oracle_fee_bps);
1236        self
1237    }
1238
1239    pub fn create_escrow(self) -> Result<Instruction, EscrowError> {
1240        let authority = self.authority.ok_or(EscrowError::MissingRequiredField)?;
1241        let funder = self.funder.ok_or(EscrowError::MissingRequiredField)?;
1242        let mint = self.mint.ok_or(EscrowError::MissingRequiredField)?;
1243        let token_program = self.token_program.unwrap_or(spl_token::ID);
1244        let min_payment_amount = self
1245            .min_payment_amount
1246            .unwrap_or(crate::consts::DEFAULT_MIN_PAYMENT_RAW_USDC_DECIMALS_6);
1247        let max_payment_amount = self.max_payment_amount.unwrap_or(u64::MAX); // Default: no limit
1248        let min_fee_amount = self
1249            .min_fee_amount
1250            .unwrap_or(crate::consts::DEFAULT_MIN_FEE_RAW_USDC_DECIMALS_6);
1251        // Match CLI `open-escrow`: `None` / omitted → `u16::MAX` = use bank default on-chain.
1252        let fee_bps = self.fee_bps.unwrap_or(u16::MAX);
1253        let oracle_fee_bps = self.oracle_fee_bps.unwrap_or(0); // Default: no oracle tip
1254
1255        Ok(EscrowSdk::open_escrow_with_token_program(
1256            authority,
1257            funder,
1258            mint,
1259            min_payment_amount,
1260            max_payment_amount,
1261            min_fee_amount,
1262            fee_bps,
1263            oracle_fee_bps,
1264            token_program,
1265        ))
1266    }
1267
1268    pub fn pause_escrow(self, paused: bool) -> Result<Instruction, EscrowError> {
1269        let authority = self.authority.ok_or(EscrowError::MissingRequiredField)?;
1270        let mint = self.mint.ok_or(EscrowError::MissingRequiredField)?;
1271
1272        Ok(EscrowSdk::pause_escrow(authority, mint, paused))
1273    }
1274}
1275
1276impl Default for EscrowBuilder {
1277    fn default() -> Self {
1278        Self::new()
1279    }
1280}
1281
1282#[cfg(test)]
1283mod tests {
1284    use super::*;
1285
1286    #[test]
1287    fn token_program_aware_ata_derivation_changes_address() {
1288        let wallet = Pubkey::new_unique();
1289        let mint = Pubkey::new_unique();
1290
1291        let legacy = EscrowSdk::associated_token_account(wallet, mint);
1292        let token2022 = EscrowSdk::associated_token_account_with_program(
1293            wallet,
1294            mint,
1295            crate::consts::TOKEN_2022_PROGRAM_ID,
1296        );
1297
1298        assert_ne!(legacy, token2022);
1299    }
1300
1301    #[test]
1302    fn close_escrow_uses_supplied_token_program() {
1303        let authority = Pubkey::new_unique();
1304        let recipient = Pubkey::new_unique();
1305        let mint = Pubkey::new_unique();
1306
1307        let instruction = EscrowSdk::close_escrow_with_token_program(
1308            authority,
1309            recipient,
1310            mint,
1311            crate::consts::TOKEN_2022_PROGRAM_ID,
1312        );
1313
1314        assert_eq!(
1315            instruction.accounts[3].pubkey,
1316            EscrowSdk::escrow_token_account_with_program(
1317                mint,
1318                crate::consts::TOKEN_2022_PROGRAM_ID,
1319            )
1320        );
1321        assert_eq!(
1322            instruction.accounts[5].pubkey,
1323            crate::consts::TOKEN_2022_PROGRAM_ID
1324        );
1325    }
1326}
1327
1328// ============================================================================
1329// CONSTANTS AND HELPER FUNCTIONS
1330// ============================================================================
1331
1332/// Common token mint addresses
1333pub mod tokens {
1334    use solana_program::pubkey::Pubkey;
1335
1336    /// USDC mint address
1337    pub const USDC: Pubkey =
1338        solana_program::pubkey!("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");
1339
1340    /// USDT mint address
1341    pub const USDT: Pubkey =
1342        solana_program::pubkey!("Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB");
1343
1344    /// WSOL mint address
1345    pub const WSOL: Pubkey = solana_program::pubkey!("So11111111111111111111111111111111111111112");
1346
1347    /// MARS mint address
1348    pub const MARS: Pubkey =
1349        solana_program::pubkey!("7RAV5UPRTzxn46kLeA8MiJsdNy9VKc5fip8FWEgTpTHh");
1350
1351    /// MIRACLE mint address
1352    pub const MIRACLE: Pubkey =
1353        solana_program::pubkey!("Mirab4SFVff6sCuK48PPnSUj7PNpDDrBWY6FkJmuifG");
1354
1355    /// TESTCOIN mint address
1356    pub const TESTCOIN: Pubkey =
1357        solana_program::pubkey!("2gNCDGj8Xi9Zs7LNQTPWf4pfZvAM7UHusY4xhKNYg6W6");
1358}
1359
1360/// Payment states
1361pub mod payment_states {
1362    use crate::consts::{PAYMENT_STATE_FUNDED, PAYMENT_STATE_REFUNDED, PAYMENT_STATE_RELEASED};
1363
1364    pub const FUNDED: u8 = PAYMENT_STATE_FUNDED;
1365    pub const RELEASED: u8 = PAYMENT_STATE_RELEASED;
1366    pub const REFUNDED: u8 = PAYMENT_STATE_REFUNDED;
1367}
1368
1369/// Escrow states
1370pub mod escrow_states {
1371    pub const ACTIVE: u8 = 0;
1372    pub const PAUSED: u8 = 1;
1373}