Since this program is extended from the original Escrow Program, I assumed you have gone through the original blog post at least once.
However, there is one major difference between this exmaple and the original Escrow program: Instead of letting initializer create a token account to be reset to a PDA authority, we create a token account Vault
that has both a PDA key and a PDA authority.
Initializer
can send a transaction to the escrow program to initialize the Vault. In this transaction, two new accounts: Vault
and EscrowAccount
, will be created and tokens (Token A) to be exchanged will be transfered from Initializer
to Vault
.
Initializer
can also send a transaction to the escrow program to cancel the demand of escrow. The tokens will be transfered back to the Initialzer
and both Vault
and EscrowAccount
will be closed in this case.
Taker
can send a transaction to the escrow to exchange Token B for Token A. First, tokens (Token B) will be transfered from Taker
to Initializer
. Afterward, the tokens (Token A) kept in the Vault will be transfered to Taker
. Finally, both Vault
and EscrowAccount
will be closed.
Let's run the test once to see what happens.
First, install dependencies:
$ npm install
Make sure you have your local solana validator running if you want to deploy the program locally:
$ solana-test-validator
If you are on Apple Sillicon M1 chip, you will have to build Solana from the source. See this document for more details
Next, we will build and deploy the program via Anchor.
Install Anchor:
$ cargo install --git https://github.com/project-serum/anchor --tag v0.18.0 anchor-cli --locked
First, let's build the program:
$ anchor build
Deploy the program:
$ anchor deploy
Finally, run the test:
$ anchor test
Maker sure to terminate the
solana-test-validator
before you run thetest
command
Before we start, here are some materials that I strongly recommend all the readers to go through at least once.
// Processor (unimplemented)
#[program]
pub mod escrow {
use super::*;
pub fn initialize_escrow(
ctx: Context<InitializeEscrow>,
initializer_amount: u64,
taker_amount: u64,
) -> ProgramResult {
// TODO
Ok(())
}
pub fn cancel_escrow(ctx: Context<CancelEscrow>) -> ProgramResult {
// TODO
Ok(())
}
pub fn exchange(ctx: Context<Exchange>) -> ProgramResult {
// TODO
Ok(())
}
}
InitializeEscrow
for Context<T>
generic. InitializeEscrow
can be considered as a wrapper for instructions. This wrapper is enhanced by Anchor via derived macro (#[deive(account)]
)
// Instructions (unimplemented)
#[derive(Accounts)]
pub struct InitializeEscrow<'info> {
// TODO
}
#[derive(Accounts)]
pub struct Exchange<'info> {
// TODO
}
#[derive(Accounts)]
pub struct CancelEscrow<'info> {
// TODO
}
Field | Description |
---|---|
initializer_key |
To authorize the actions properly |
initializer_amount |
To record how much token should the initializer transfer to taker |
taker_amount |
To record how much token should the initializer receive from the taker |
initializer_deposit_token_account |
To record the deposit account of initialzer |
initializer_receive_token_account |
To record the receiving account of initializer |
// ProgramAccount (fully implemented)
#[account]
pub struct EscrowAccount {
pub initializer_key: Pubkey,
pub initializer_deposit_token_account: Pubkey,
pub initializer_receive_token_account: Pubkey,
pub initializer_amount: u64,
pub taker_amount: u64,
}
EscrowAccount
, we need the following accounts to initialize it.InitialEscrow
function:Field | Description |
---|---|
initializer | Signer of InitialEscrow instruction. To be stored in EscrowAccount |
initializer_deposit_token_account | The account of token account for token exchange. To be stored in EscrowAccount |
initializer_receive_token_account | The account of token account for token exchange. To be stored in EscrowAccount |
token_program | The account of TokenProgram |
escrow_account | The account of EscrowAccount |
vault_account | The account of Vault , which is created by Anchor via constraints. (Will be explained in part 3) |
mint | - |
system_program | - |
rent | - |
CancelEscrow
instruction:Field | Description |
---|---|
initializer | The initializer of EscrowAccount |
initializer_deposit_token_account | The address of token account for token exchange |
vault_account | The program derived address |
vault_authority | The program derived address |
escrow_account | The address of EscrowAccount . Have to check if the EscrowAccount follows certain constraints. |
token_program | The address of TokenProgram |
Exchange
instruction:Field | Description |
---|---|
taker | Singer of Exchange instruction |
taker_deposit_token_account | Token account for token exchange |
taker_receive_token_account | Token account for token exchange |
initializer_deposit_token_account | Token account for token exchange |
initializer_receive_token_account | Token account for token exchange |
initializer_main_account | To be used in constraints. (Will explain in part 3) |
escrow_account | The address of EscrowAccount . Have to check if the EscrowAccount follows certain constraints. |
vault_account | The program derived address |
vault_authority | The program derived address |
token_program | The address of TokenProgram |
// Instructions (partially implemented)
#[derive(Accounts)]
#[instruction(initializer_amount: u64)]
pub struct InitializeEscrow<'info> {
pub initializer: AccountInfo<'info>,
pub mint: Account<'info, Mint>,
pub vault_account: Account<'info, TokenAccount>,
pub initializer_deposit_token_account: Account<'info, TokenAccount>,
pub initializer_receive_token_account: Account<'info, TokenAccount>,
pub escrow_account: ProgramAccount<'info, EscrowAccount>,
pub system_program: AccountInfo<'info>,
pub rent: Sysvar<'info, Rent>,
pub token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct CancelEscrow<'info> {
pub initializer: AccountInfo<'info>,
pub initializer_deposit_token_account: Account<'info, TokenAccount>,
pub vault_account: Account<'info, TokenAccount>,
pub vault_authoitiy: AccountInfo<'info>,
pub escrow_account: ProgramAccount<'info, EscrowAccount>,
pub token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct Exchange<'info> {
pub taker: AccountInfo<'info>,
pub taker_deposit_token_account: Account<'info, TokenAccount>,
pub taker_receive_token_account: Account<'info, TokenAccount>,
pub initializer_deposit_token_account: Account<'info, TokenAccount>,
pub initializer_receive_token_account: Account<'info, TokenAccount>,
pub initializer_main_account: AccountInfo<'info>,
pub escrow_account: ProgramAccount<'info, EscrowAccount>,
pub vault_account: Account<'info, TokenAccount>,
pub vault_authority: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
}
// Processor (fully implenmented)
#[program]
pub mod escrow {
use super::*;
const ESCROW_PDA_SEED: &[u8] = b"escrow";
pub fn initialize_escrow(
ctx: Context<InitializeEscrow>,
_vault_account_bump: u8,
initializer_amount: u64,
taker_amount: u64,
) -> ProgramResult {
ctx.accounts.escrow_account.initializer_key = *ctx.accounts.initializer.key;
ctx.accounts
.escrow_account
.initializer_deposit_token_account = *ctx
.accounts
.initializer_deposit_token_account
.to_account_info()
.key;
ctx.accounts
.escrow_account
.initializer_receive_token_account = *ctx
.accounts
.initializer_receive_token_account
.to_account_info()
.key;
ctx.accounts.escrow_account.initializer_amount = initializer_amount;
ctx.accounts.escrow_account.taker_amount = taker_amount;
let (vault_authority, _vault_authority_bump) =
Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
token::set_authority(
ctx.accounts.into_set_authority_context(),
AuthorityType::AccountOwner,
Some(vault_authority),
)?;
token::transfer(
ctx.accounts.into_transfer_to_pda_context(),
ctx.accounts.escrow_account.initializer_amount,
)?;
Ok(())
}
pub fn cancel_escrow(ctx: Context<CancelEscrow>) -> ProgramResult {
let (_vault_authority, vault_authority_bump) =
Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
let authority_seeds = &[&ESCROW_PDA_SEED[..], &[vault_authority_bump]];
token::transfer(
ctx.accounts
.into_transfer_to_initializer_context()
.with_signer(&[&authority_seeds[..]]),
ctx.accounts.escrow_account.initializer_amount,
)?;
token::close_account(
ctx.accounts
.into_close_context()
.with_signer(&[&authority_seeds[..]]),
)?;
Ok(())
}
pub fn exchange(ctx: Context<Exchange>) -> ProgramResult {
// Transferring from initializer to taker
let (_vault_authority, vault_authority_bump) =
Pubkey::find_program_address(&[ESCROW_PDA_SEED], ctx.program_id);
let authority_seeds = &[&ESCROW_PDA_SEED[..], &[vault_authority_bump]];
token::transfer(
ctx.accounts.into_transfer_to_initializer_context(),
ctx.accounts.escrow_account.taker_amount,
)?;
token::transfer(
ctx.accounts
.into_transfer_to_taker_context()
.with_signer(&[&authority_seeds[..]]),
ctx.accounts.escrow_account.initializer_amount,
)?;
token::close_account(
ctx.accounts
.into_close_context()
.with_signer(&[&authority_seeds[..]]),
)?;
Ok(())
}
}
initialize_escrow
, what happens is that the input accouns are assigned to EscrowAccount
fields one by one.initializer_deposit_token_account
.cancel_escrow
, it just simply reset the authority from PDA back to the initializer.exchange
, 3 things happen: First, token A gets tranfered from pda_deposit_token_account
to taker_receive_token_account
. Next, token B gets transfered from taker_deposit_token_account
to initializer_receive_token_account
. Finally, the authority of pda_deposit_token_account
gets set back to the initializer
.tokens::transfer
, token::close_account
and token::set_authority
// Utils (fully implemented)
impl<'info> InitializeEscrow<'info> {
fn into_transfer_to_pda_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let cpi_accounts = Transfer {
from: self
.initializer_deposit_token_account
.to_account_info()
.clone(),
to: self.vault_account.to_account_info().clone(),
authority: self.initializer.clone(),
};
CpiContext::new(self.token_program.clone(), cpi_accounts)
}
fn into_set_authority_context(&self) -> CpiContext<'_, '_, '_, 'info, SetAuthority<'info>> {
let cpi_accounts = SetAuthority {
account_or_mint: self.vault_account.to_account_info().clone(),
current_authority: self.initializer.clone(),
};
CpiContext::new(self.token_program.clone(), cpi_accounts)
}
}
impl<'info> CancelEscrow<'info> {
fn into_transfer_to_initializer_context(
&self,
) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let cpi_accounts = Transfer {
from: self.vault_account.to_account_info().clone(),
to: self
.initializer_deposit_token_account
.to_account_info()
.clone(),
authority: self.vault_authority.clone(),
};
CpiContext::new(self.token_program.clone(), cpi_accounts)
}
fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> {
let cpi_accounts = CloseAccount {
account: self.vault_account.to_account_info().clone(),
dest: self.initializer.clone(),
authority: self.vault_authority.clone(),
};
CpiContext::new(self.token_program.clone(), cpi_accounts)
}
}
impl<'info> Exchange<'info> {
fn into_transfer_to_initializer_context(
&self,
) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let cpi_accounts = Transfer {
from: self.taker_deposit_token_account.to_account_info().clone(),
to: self
.initializer_receive_token_account
.to_account_info()
.clone(),
authority: self.taker.clone(),
};
CpiContext::new(self.token_program.clone(), cpi_accounts)
}
fn into_transfer_to_taker_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
let cpi_accounts = Transfer {
from: self.vault_account.to_account_info().clone(),
to: self.taker_receive_token_account.to_account_info().clone(),
authority: self.vault_authority.clone(),
};
CpiContext::new(self.token_program.clone(), cpi_accounts)
}
fn into_close_context(&self) -> CpiContext<'_, '_, '_, 'info, CloseAccount<'info>> {
let cpi_accounts = CloseAccount {
account: self.vault_account.to_account_info().clone(),
dest: self.initializer.clone(),
authority: self.vault_authority.clone(),
};
CpiContext::new(self.token_program.clone(), cpi_accounts)
}
}
// Instructions (fully implementated)
#[derive(Accounts)]
#[instruction(vault_account_bump: u8, initializer_amount: u64)]
pub struct InitializeEscrow<'info> {
#[account(mut, signer)]
pub initializer: AccountInfo<'info>,
pub mint: Account<'info, Mint>,
#[account(
init,
seeds = [b"token-seed".as_ref()],
bump = vault_account_bump,
payer = initializer,
token::mint = mint,
token::authority = initializer,
)]
pub vault_account: Account<'info, TokenAccount>,
#[account(
mut,
constraint = initializer_deposit_token_account.amount >= initializer_amount
)]
pub initializer_deposit_token_account: Account<'info, TokenAccount>,
pub initializer_receive_token_account: Account<'info, TokenAccount>,
#[account(zero)]
pub escrow_account: ProgramAccount<'info, EscrowAccount>,
pub system_program: AccountInfo<'info>,
pub rent: Sysvar<'info, Rent>,
pub token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct CancelEscrow<'info> {
#[account(mut, signer)]
pub initializer: AccountInfo<'info>,
#[account(mut)]
pub vault_account: Account<'info, TokenAccount>,
pub vault_authority: AccountInfo<'info>,
#[account(mut)]
pub initializer_deposit_token_account: Account<'info, TokenAccount>,
#[account(
mut,
constraint = escrow_account.initializer_key == *initializer.key,
constraint = escrow_account.initializer_deposit_token_account == *initializer_deposit_token_account.to_account_info().key,
close = initializer
)]
pub escrow_account: ProgramAccount<'info, EscrowAccount>,
pub token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct Exchange<'info> {
#[account(signer)]
pub taker: AccountInfo<'info>,
#[account(mut)]
pub taker_deposit_token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub taker_receive_token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub initializer_deposit_token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub initializer_receive_token_account: Account<'info, TokenAccount>,
#[account(mut)]
pub initializer: AccountInfo<'info>,
#[account(
mut,
constraint = escrow_account.taker_amount <= taker_deposit_token_account.amount,
constraint = escrow_account.initializer_deposit_token_account == *initializer_deposit_token_account.to_account_info().key,
constraint = escrow_account.initializer_receive_token_account == *initializer_receive_token_account.to_account_info().key,
constraint = escrow_account.initializer_key == *initializer.key,
close = initializer
)]
pub escrow_account: ProgramAccount<'info, EscrowAccount>,
#[account(mut)]
pub vault_account: Account<'info, TokenAccount>,
pub vault_authority: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
}
Create an token account that has a PDA key
#[derive(Accounts)]
#[instruction(token_bump: u8)]
pub struct TestTokenSeedsInit<'info> {
#[account(
init,
seeds = [b"my-token-seed".as_ref()],
bump = token_bump,
payer = authority,
token::mint = mint,
token::authority = authority,
)]
pub my_pda: Account<'info, TokenAccount>,
pub mint: Account<'info, Mint>,
pub authority: AccountInfo<'info>,
pub system_program: AccountInfo<'info>,
pub rent: Sysvar<'info, Rent>,
pub token_program: AccountInfo<'info>,
}
Here, we can see a few new attributes, such as:
Attribute | Description |
---|---|
#[account(signer)] |
Checks the given account signed the transaction |
#[account(mut)] |
Marks the account as mutable and persists the state transition |
#[account(constraint = <expression\>)] |
Executes the given code as a constraint. The expression should evaluate to a boolean |
#[account(close = <target\>)] |
Marks the account as being closed at the end of the instruction’s execution, sending the rent exemption lamports to the specified |
Check the official document for more constraints.
And that's it! We have walk through the escrow program essentially step by step.
solana
anchor