Rust
Pinocchio Escrow

Pinocchio Escrow

Take

The take instruction finalizes the swap:

  • Move Token A from the vault to the taker.

  • Close the now empty vault token account.

  • Move the agreed amount of Token B from the taker to the maker.

  • Close the escrow program account.

Required Accounts

Below are the accounts the context needs:

  • taker: the person that wants to take the deal. Must be a signer and mutable.

  • maker: the creator of the escrow. Must be mutable.

  • escrow: the escrow account that we're initializing. Must be mutable.

  • mint_a: the token we're depositing in the escrow

  • mint_b: the token we want to receive

  • vault: the associated token account owned by the escrow. Must be mutable

  • taker_ata_a: the associated token account owned by the taker for mint_a. Must be mutable

  • taker_ata_b: the associated token account owned by the taker for mint_b. Must be mutable

  • maker_ata_b: the associated token account owned by the maker for mint_b. Must be mutable

  • system_program: the system program. Must be executable

  • token_program: the token program. Must be executable

And we perform these checks on it:

rust
pub struct TakeAccounts<'a> {
  pub taker: &'a AccountInfo,
  pub maker: &'a AccountInfo,
  pub escrow: &'a AccountInfo,
  pub mint_a: &'a AccountInfo,
  pub mint_b: &'a AccountInfo,
  pub vault: &'a AccountInfo,
  pub taker_ata_a: &'a AccountInfo,
  pub taker_ata_b: &'a AccountInfo,
  pub maker_ata_b: &'a AccountInfo,
  pub system_program: &'a AccountInfo,
  pub token_program: &'a AccountInfo,
}

impl<'a> TryFrom<&'a [AccountInfo]> for TakeAccounts<'a> {
  type Error = ProgramError;

  fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
    let [taker, maker, escrow, mint_a, mint_b, vault, taker_ata_a, taker_ata_b, maker_ata_b, system_program, token_program, _] = accounts else {
      return Err(ProgramError::NotEnoughAccountKeys);
    };

    // Basic Accounts Checks
    SignerAccount::check(taker)?;
    ProgramAccount::check(escrow)?;
    MintInterface::check(mint_a)?;
    MintInterface::check(mint_b)?;
    AssociatedTokenAccount::check(taker_ata_b, taker, mint_b, token_program)?;
    AssociatedTokenAccount::check(vault, escrow, mint_a, token_program)?;

    // Return the accounts
    Ok(Self {
      taker,
      maker,
      escrow,
      mint_a,
      mint_b,
      taker_ata_a,
      taker_ata_b,
      maker_ata_b,
      vault,
      system_program,
      token_program,
    })
  }
}
Expand
[31 more lines]

Instruction Data

All the data that we need to perform the logic already lives in the Escrow account or on the accounts that we're deserializing. For this reason we don't need any instruction_data for this instruction.

Instruction Logic

We begin by initializing the required accounts in the TryFrom implementation, after we've deserialized the accounts.

For this step, we ensure both the taker's Token A account and the maker's Token B account are initialized using AssociatedTokenAccount::init_if_needed since we're not sure that they already exist.

rust
pub struct Take<'a> {
  pub accounts: TakeAccounts<'a>,
}

impl<'a> TryFrom<&'a [AccountInfo]> for Take<'a> {
  type Error = ProgramError;

  fn try_from(accounts: &'a [AccountInfo]) -> Result<Self, Self::Error> {
    let accounts = TakeAccounts::try_from(accounts)?;

    // Initialize necessary accounts
    AssociatedTokenAccount::init_if_needed(
      accounts.taker_ata_a,
      accounts.mint_a,
      accounts.taker,
      accounts.taker,
      accounts.system_program,
      accounts.token_program,
    )?;

    AssociatedTokenAccount::init_if_needed(
      accounts.maker_ata_b,
      accounts.mint_b,
      accounts.taker,
      accounts.maker,
      accounts.system_program,
      accounts.token_program,
    )?;

    Ok(Self {
      accounts,
    })
  }
}
Expand
[19 more lines]

We can now focus on the logic itself that will:

  • Transfer the tokens from the vault to the taker_ata_a.

  • Close the now empty vault token account and withdraw its rent.

  • Transfer the tokens from the taker_ata_b to the maker_ata_b.

  • Close the escrow program account once the swap is complete.

Rather than doing three separate token CPI calls to finalize the swap, we can take advantage of the batch instruction in p-token to efficiently perform all the token operations in a single CPI call.

rust
impl<'a> Take<'a> {
  pub const DISCRIMINATOR: &'a u8 = &1;

  const MAX_ACCOUNTS_LEN: usize = Transfer::MAX_ACCOUNTS_LEN * 2 + CloseAccount::MAX_ACCOUNTS_LEN;
  const MAX_DATA_LEN: usize = Batch::header_data_len(3) + Transfer::DATA_LEN * 2 + CloseAccount::DATA_LEN;

  pub fn process(&mut self) -> ProgramResult {
    let data = self.accounts.escrow.try_borrow_data()?;
    let escrow = Escrow::load(&data)?;

    // Check if the escrow is valid
    let escrow_key = create_program_address(&[b"escrow", self.accounts.maker.key(), &escrow.seed.to_le_bytes(), &escrow.bump], &crate::ID)?;
    if &escrow_key != self.accounts.escrow.key() {
      return Err(ProgramError::InvalidAccountOwner);
    }

    let seed_binding = escrow.seed.to_le_bytes();
    let bump_binding = escrow.bump;
    let escrow_seeds = [
      Seed::from(b"escrow"),
      Seed::from(self.accounts.maker.key().as_ref()),
      Seed::from(&seed_binding),
      Seed::from(&bump_binding),
    ];
    let signer = Signer::from(&escrow_seeds);

    let amount = TokenAccount::from_account_info(self.accounts.vault)?.amount();

    let batch_state = BatchState::new(Self::MAX_ACCOUNTS_LEN, Self::MAX_DATA_LEN);
    let mut batch = batch_state.as_batch()?;

    Transfer::new(
      self.accounts.vault,
      self.accounts.taker_ata_a,
      self.accounts.escrow,
      amount,
    )
    .into_batch(&mut batch)?;

    CloseAccount::new(
      self.accounts.vault,
      self.accounts.maker,
      self.accounts.escrow,
    )
    .into_batch(&mut batch)?;

    Transfer::new(
      self.accounts.taker_ata_b,
      self.accounts.maker_ata_b,
      self.accounts.taker,
      escrow.receive,
    )
    .into_batch(&mut batch)?;

    batch.invoke_signed(&[signer])?;

    // Close the Escrow program account
    drop(data);
    ProgramAccount::close(self.accounts.escrow, self.accounts.taker)?;

    Ok(())
  }
}
Expand
[48 more lines]

Make sure pinocchio-token is at least version 0.6.0 to support p-token batch operations.

Contents
View Source
Blueshift © 2026Commit: 3c44267