|
@@ -0,0 +1,228 @@
|
|
|
+//! An example of an escrow program, inspired by PaulX tutorial seen here
|
|
|
+//! https://paulx.dev/blog/2021/01/14/programming-on-solana-an-introduction/
|
|
|
+//! This example has some changes to implementation, but more or less should be the same overall
|
|
|
+//! Also gives examples on how to use some newer anchor features and CPI
|
|
|
+//!
|
|
|
+//! User (Initializer) constructs an escrow deal:
|
|
|
+//! - SPL token (X) they will offer and amount
|
|
|
+//! - SPL token (Y) count they want in return and amount
|
|
|
+//! - Program will take ownership of initializer's token X account
|
|
|
+//!
|
|
|
+//! Once this escrow is initialised, either:
|
|
|
+//! 1. User (Taker) can call the exchange function to exchange their Y for X
|
|
|
+//! - This will close the escrow account and no longer be usable
|
|
|
+//! OR
|
|
|
+//! 2. If no one has exchanged, the initializer can close the escrow account
|
|
|
+//! - Initializer will get back ownership of their token X account
|
|
|
+
|
|
|
+use anchor_lang::prelude::*;
|
|
|
+use anchor_spl::token::{self, SetAuthority, TokenAccount, Transfer};
|
|
|
+use spl_token::instruction::AuthorityType;
|
|
|
+
|
|
|
+#[program]
|
|
|
+pub mod escrow {
|
|
|
+ use super::*;
|
|
|
+
|
|
|
+ pub fn initialize_escrow(
|
|
|
+ ctx: Context<InitializeEscrow>,
|
|
|
+ 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 (pda, _bump_seed) = Pubkey::find_program_address(&[b"escrow"], ctx.program_id);
|
|
|
+ token::set_authority(ctx.accounts.into(), AuthorityType::AccountOwner, Some(pda))?;
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn cancel_escrow(ctx: Context<CancelEscrow>) -> ProgramResult {
|
|
|
+ let (_pda, bump_seed) = Pubkey::find_program_address(&[b"escrow"], ctx.program_id);
|
|
|
+ let seeds = &[&b"escrow"[..], &[bump_seed]];
|
|
|
+
|
|
|
+ token::set_authority(
|
|
|
+ ctx.accounts
|
|
|
+ .into_set_authority_context()
|
|
|
+ .with_signer(&[&seeds[..]]),
|
|
|
+ AuthorityType::AccountOwner,
|
|
|
+ Some(ctx.accounts.escrow_account.initializer_key),
|
|
|
+ )?;
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn exchange(ctx: Context<Exchange>) -> ProgramResult {
|
|
|
+ // Transferring from initializer to taker
|
|
|
+ let (_pda, bump_seed) = Pubkey::find_program_address(&[b"escrow"], ctx.program_id);
|
|
|
+ let seeds = &[&b"escrow"[..], &[bump_seed]];
|
|
|
+
|
|
|
+ token::transfer(
|
|
|
+ ctx.accounts
|
|
|
+ .into_transfer_to_taker_context()
|
|
|
+ .with_signer(&[&seeds[..]]),
|
|
|
+ ctx.accounts.escrow_account.initializer_amount,
|
|
|
+ )?;
|
|
|
+
|
|
|
+ token::transfer(
|
|
|
+ ctx.accounts.into_transfer_to_initializer_context(),
|
|
|
+ ctx.accounts.escrow_account.taker_amount,
|
|
|
+ )?;
|
|
|
+
|
|
|
+ token::set_authority(
|
|
|
+ ctx.accounts
|
|
|
+ .into_set_authority_context()
|
|
|
+ .with_signer(&[&seeds[..]]),
|
|
|
+ AuthorityType::AccountOwner,
|
|
|
+ Some(ctx.accounts.escrow_account.initializer_key),
|
|
|
+ )?;
|
|
|
+
|
|
|
+ Ok(())
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Accounts)]
|
|
|
+#[instruction(initializer_amount: u64)]
|
|
|
+pub struct InitializeEscrow<'info> {
|
|
|
+ #[account(signer)]
|
|
|
+ pub initializer: AccountInfo<'info>,
|
|
|
+ #[account(
|
|
|
+ mut,
|
|
|
+ constraint = initializer_deposit_token_account.amount >= initializer_amount
|
|
|
+ )]
|
|
|
+ pub initializer_deposit_token_account: CpiAccount<'info, TokenAccount>,
|
|
|
+ pub initializer_receive_token_account: CpiAccount<'info, TokenAccount>,
|
|
|
+ #[account(init)]
|
|
|
+ pub escrow_account: ProgramAccount<'info, EscrowAccount>,
|
|
|
+ pub token_program: AccountInfo<'info>,
|
|
|
+ pub rent: Sysvar<'info, Rent>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Accounts)]
|
|
|
+pub struct Exchange<'info> {
|
|
|
+ #[account(signer)]
|
|
|
+ pub taker: AccountInfo<'info>,
|
|
|
+ #[account(mut)]
|
|
|
+ pub taker_deposit_token_account: CpiAccount<'info, TokenAccount>,
|
|
|
+ #[account(mut)]
|
|
|
+ pub taker_receive_token_account: CpiAccount<'info, TokenAccount>,
|
|
|
+ #[account(mut)]
|
|
|
+ pub pda_deposit_token_account: CpiAccount<'info, TokenAccount>,
|
|
|
+ #[account(mut)]
|
|
|
+ pub initializer_receive_token_account: CpiAccount<'info, TokenAccount>,
|
|
|
+ #[account(mut)]
|
|
|
+ pub initializer_main_account: AccountInfo<'info>,
|
|
|
+ #[account(
|
|
|
+ mut,
|
|
|
+ constraint = escrow_account.taker_amount <= taker_deposit_token_account.amount,
|
|
|
+ constraint = escrow_account.initializer_deposit_token_account == *pda_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_main_account.key,
|
|
|
+ close = initializer_main_account
|
|
|
+ )]
|
|
|
+ pub escrow_account: ProgramAccount<'info, EscrowAccount>,
|
|
|
+ pub pda_account: AccountInfo<'info>,
|
|
|
+ pub token_program: AccountInfo<'info>,
|
|
|
+}
|
|
|
+
|
|
|
+#[derive(Accounts)]
|
|
|
+pub struct CancelEscrow<'info> {
|
|
|
+ pub initializer: AccountInfo<'info>,
|
|
|
+ #[account(mut)]
|
|
|
+ pub pda_deposit_token_account: CpiAccount<'info, TokenAccount>,
|
|
|
+ pub pda_account: AccountInfo<'info>,
|
|
|
+ #[account(
|
|
|
+ mut,
|
|
|
+ constraint = escrow_account.initializer_key == *initializer.key,
|
|
|
+ constraint = escrow_account.initializer_deposit_token_account == *pda_deposit_token_account.to_account_info().key,
|
|
|
+ close = initializer
|
|
|
+ )]
|
|
|
+ pub escrow_account: ProgramAccount<'info, EscrowAccount>,
|
|
|
+ pub token_program: AccountInfo<'info>,
|
|
|
+}
|
|
|
+
|
|
|
+#[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,
|
|
|
+}
|
|
|
+
|
|
|
+impl<'info> From<&mut InitializeEscrow<'info>>
|
|
|
+ for CpiContext<'_, '_, '_, 'info, SetAuthority<'info>>
|
|
|
+{
|
|
|
+ fn from(accounts: &mut InitializeEscrow<'info>) -> Self {
|
|
|
+ let cpi_accounts = SetAuthority {
|
|
|
+ account_or_mint: accounts
|
|
|
+ .initializer_deposit_token_account
|
|
|
+ .to_account_info()
|
|
|
+ .clone(),
|
|
|
+ current_authority: accounts.initializer.clone(),
|
|
|
+ };
|
|
|
+ let cpi_program = accounts.token_program.clone();
|
|
|
+ CpiContext::new(cpi_program, cpi_accounts)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'info> CancelEscrow<'info> {
|
|
|
+ fn into_set_authority_context(&self) -> CpiContext<'_, '_, '_, 'info, SetAuthority<'info>> {
|
|
|
+ let cpi_accounts = SetAuthority {
|
|
|
+ account_or_mint: self.pda_deposit_token_account.to_account_info().clone(),
|
|
|
+ current_authority: self.pda_account.clone(),
|
|
|
+ };
|
|
|
+ CpiContext::new(self.token_program.clone(), cpi_accounts)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'info> Exchange<'info> {
|
|
|
+ fn into_set_authority_context(&self) -> CpiContext<'_, '_, '_, 'info, SetAuthority<'info>> {
|
|
|
+ let cpi_accounts = SetAuthority {
|
|
|
+ account_or_mint: self.pda_deposit_token_account.to_account_info().clone(),
|
|
|
+ current_authority: self.pda_account.clone(),
|
|
|
+ };
|
|
|
+ CpiContext::new(self.token_program.clone(), cpi_accounts)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+impl<'info> Exchange<'info> {
|
|
|
+ fn into_transfer_to_taker_context(&self) -> CpiContext<'_, '_, '_, 'info, Transfer<'info>> {
|
|
|
+ let cpi_accounts = Transfer {
|
|
|
+ from: self.pda_deposit_token_account.to_account_info().clone(),
|
|
|
+ to: self.taker_receive_token_account.to_account_info().clone(),
|
|
|
+ authority: self.pda_account.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)
|
|
|
+ }
|
|
|
+}
|