From f1db9221832ed724bcfa56f47953b7486845cdc3 Mon Sep 17 00:00:00 2001 From: Mike MacCana Date: Tue, 9 Jun 2026 12:46:35 -0400 Subject: [PATCH] refactor(finance/escrow): extract PDA-aware token transfer/close helpers Add an owning_pda_seeds option to transfer_tokens and a new close_token_account helper in shared.rs, then use them across make_offer, take_offer, and cancel_offer. This removes the duplicated CpiContext::new_with_signer + transfer_checked/close_account boilerplate that was hand-rolled in each handler. No behavior change: per-maker PDA seeds and has_one checks are unchanged. Verified with cargo test (5 tests pass). --- .../escrow/src/instructions/cancel_offer.rs | 58 ++++++---------- .../escrow/src/instructions/make_offer.rs | 3 +- .../escrow/src/instructions/shared.rs | 44 +++++++++++- .../escrow/src/instructions/take_offer.rs | 68 +++++++------------ 4 files changed, 87 insertions(+), 86 deletions(-) diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs b/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs index 85f1e2b6..92472a37 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/cancel_offer.rs @@ -2,14 +2,13 @@ use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, - token_interface::{ - close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface, - TransferChecked, - }, + token_interface::{Mint, TokenAccount, TokenInterface}, }; use crate::Offer; +use super::{close_token_account, transfer_tokens}; + // Cancel an outstanding offer. Without this handler, an abandoned offer would // keep the maker's token-A locked in the vault forever (and the offer // account's rent unclaimed). The maker signs, the vault tokens flow back to @@ -55,45 +54,28 @@ pub struct CancelOffer<'info> { pub fn handle_cancel_offer(context: Context) -> Result<()> { let maker_key = context.accounts.maker.key(); let id_bytes = context.accounts.offer.id.to_le_bytes(); - let seeds = &[ - b"offer".as_ref(), - maker_key.as_ref(), - id_bytes.as_ref(), - &[context.accounts.offer.bump], - ]; - let signer_seeds = [&seeds[..]]; + let bump = [context.accounts.offer.bump]; + let offer_seeds: &[&[u8]] = &[b"offer", maker_key.as_ref(), id_bytes.as_ref(), &bump]; // Move all tokens back from the vault to the maker. - let vault_amount = context.accounts.vault.amount; - let transfer_accounts = TransferChecked { - from: context.accounts.vault.to_account_info(), - mint: context.accounts.token_mint_a.to_account_info(), - to: context.accounts.maker_token_account_a.to_account_info(), - authority: context.accounts.offer.to_account_info(), - }; - let cpi_context = CpiContext::new_with_signer( - context.accounts.token_program.key(), - transfer_accounts, - &signer_seeds, - ); - transfer_checked( - cpi_context, - vault_amount, - context.accounts.token_mint_a.decimals, + transfer_tokens( + &context.accounts.vault, + &context.accounts.maker_token_account_a, + &context.accounts.vault.amount, + &context.accounts.token_mint_a, + &context.accounts.offer.to_account_info(), + &context.accounts.token_program, + Some(offer_seeds), )?; // Close the vault, sending its rent lamports back to the maker. - let close_accounts = CloseAccount { - account: context.accounts.vault.to_account_info(), - destination: context.accounts.maker.to_account_info(), - authority: context.accounts.offer.to_account_info(), - }; - let cpi_context = CpiContext::new_with_signer( - context.accounts.token_program.key(), - close_accounts, - &signer_seeds, - ); - close_account(cpi_context)?; + close_token_account( + &context.accounts.vault, + &context.accounts.maker.to_account_info(), + &context.accounts.offer.to_account_info(), + &context.accounts.token_program, + Some(offer_seeds), + )?; // The offer account itself is closed by the `close = maker` constraint // above, which refunds its rent to the maker. diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs b/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs index b94d2862..a40ac32f 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/make_offer.rs @@ -76,8 +76,9 @@ pub fn handle_send_offered_tokens_to_vault( &context.accounts.vault, &token_a_offered_amount, &context.accounts.token_mint_a, - &context.accounts.maker, + &context.accounts.maker.to_account_info(), &context.accounts.token_program, + None, ) } diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs b/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs index dcac2116..349c107b 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/shared.rs @@ -1,16 +1,21 @@ use anchor_lang::prelude::*; use anchor_spl::token_interface::{ - transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked, + close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface, + TransferChecked, }; +// Transfer tokens from one token account to another. +// When transferring out of a token account owned by a PDA, pass the PDA's +// signer seeds via owning_pda_seeds; otherwise pass None. pub fn transfer_tokens<'info>( from: &InterfaceAccount<'info, TokenAccount>, to: &InterfaceAccount<'info, TokenAccount>, amount: &u64, mint: &InterfaceAccount<'info, Mint>, - authority: &Signer<'info>, + authority: &AccountInfo<'info>, token_program: &Interface<'info, TokenInterface>, + owning_pda_seeds: Option<&[&[u8]]>, ) -> Result<()> { let transfer_accounts = TransferChecked { from: from.to_account_info(), @@ -19,7 +24,40 @@ pub fn transfer_tokens<'info>( authority: authority.to_account_info(), }; - let cpi_context = CpiContext::new(token_program.key(), transfer_accounts); + let signer_seeds = owning_pda_seeds.map(|seeds| [seeds]); + let cpi_context = match signer_seeds.as_ref() { + Some(signer_seeds) => { + CpiContext::new_with_signer(token_program.key(), transfer_accounts, signer_seeds) + } + None => CpiContext::new(token_program.key(), transfer_accounts), + }; transfer_checked(cpi_context, *amount, mint.decimals) } + +// Close a token account, sending its rent lamports to destination. +// When the token account is owned by a PDA, pass the PDA's signer seeds via +// owning_pda_seeds; otherwise pass None. +pub fn close_token_account<'info>( + token_account: &InterfaceAccount<'info, TokenAccount>, + destination: &AccountInfo<'info>, + authority: &AccountInfo<'info>, + token_program: &Interface<'info, TokenInterface>, + owning_pda_seeds: Option<&[&[u8]]>, +) -> Result<()> { + let close_accounts = CloseAccount { + account: token_account.to_account_info(), + destination: destination.to_account_info(), + authority: authority.to_account_info(), + }; + + let signer_seeds = owning_pda_seeds.map(|seeds| [seeds]); + let cpi_context = match signer_seeds.as_ref() { + Some(signer_seeds) => { + CpiContext::new_with_signer(token_program.key(), close_accounts, signer_seeds) + } + None => CpiContext::new(token_program.key(), close_accounts), + }; + + close_account(cpi_context) +} diff --git a/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs b/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs index 11acc687..e3813b65 100644 --- a/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs +++ b/finance/escrow/anchor/programs/escrow/src/instructions/take_offer.rs @@ -2,15 +2,12 @@ use anchor_lang::prelude::*; use anchor_spl::{ associated_token::AssociatedToken, - token_interface::{ - close_account, transfer_checked, CloseAccount, Mint, TokenAccount, TokenInterface, - TransferChecked, - }, + token_interface::{Mint, TokenAccount, TokenInterface}, }; use crate::Offer; -use super::transfer_tokens; +use super::{close_token_account, transfer_tokens}; #[derive(Accounts)] pub struct TakeOffer<'info> { @@ -82,50 +79,33 @@ pub fn handle_send_wanted_tokens_to_maker(context: &Context) -> Resul &context.accounts.maker_token_account_b, &context.accounts.offer.token_b_wanted_amount, &context.accounts.token_mint_b, - &context.accounts.taker, + &context.accounts.taker.to_account_info(), &context.accounts.token_program, + None, ) } pub fn handle_withdraw_and_close_vault(context: Context) -> Result<()> { - let seeds = &[ - b"offer", - context.accounts.maker.to_account_info().key.as_ref(), - &context.accounts.offer.id.to_le_bytes()[..], - &[context.accounts.offer.bump], - ]; - let signer_seeds = [&seeds[..]]; - - let accounts = TransferChecked { - from: context.accounts.vault.to_account_info(), - mint: context.accounts.token_mint_a.to_account_info(), - to: context.accounts.taker_token_account_a.to_account_info(), - authority: context.accounts.offer.to_account_info(), - }; - - let cpi_context = CpiContext::new_with_signer( - context.accounts.token_program.key(), - accounts, - &signer_seeds, - ); - - transfer_checked( - cpi_context, - context.accounts.vault.amount, - context.accounts.token_mint_a.decimals, - )?; - - let accounts = CloseAccount { - account: context.accounts.vault.to_account_info(), - destination: context.accounts.taker.to_account_info(), - authority: context.accounts.offer.to_account_info(), - }; + let maker_key = context.accounts.maker.key(); + let id_bytes = context.accounts.offer.id.to_le_bytes(); + let bump = [context.accounts.offer.bump]; + let offer_seeds: &[&[u8]] = &[b"offer", maker_key.as_ref(), id_bytes.as_ref(), &bump]; - let cpi_context = CpiContext::new_with_signer( - context.accounts.token_program.key(), - accounts, - &signer_seeds, - ); + transfer_tokens( + &context.accounts.vault, + &context.accounts.taker_token_account_a, + &context.accounts.vault.amount, + &context.accounts.token_mint_a, + &context.accounts.offer.to_account_info(), + &context.accounts.token_program, + Some(offer_seeds), + )?; - close_account(cpi_context) + close_token_account( + &context.accounts.vault, + &context.accounts.taker.to_account_info(), + &context.accounts.offer.to_account_info(), + &context.accounts.token_program, + Some(offer_seeds), + ) }