Browse Source

examples: Add escrow (#410)

aac 4 years ago
parent
commit
fcb7a3e17b

+ 1 - 0
.travis.yml

@@ -67,6 +67,7 @@ jobs:
     - <<: *examples
     - <<: *examples
       name: Runs the examples 3
       name: Runs the examples 3
       script:
       script:
+        - pushd examples/escrow && yarn && anchor test && popd
         - pushd examples/pyth && yarn && anchor test && popd
         - pushd examples/pyth && yarn && anchor test && popd
         - pushd examples/tutorial/basic-0 && anchor test && popd
         - pushd examples/tutorial/basic-0 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd
         - pushd examples/tutorial/basic-1 && anchor test && popd

+ 3 - 0
examples/escrow/Anchor.toml

@@ -0,0 +1,3 @@
+[provider]
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/escrow/Cargo.toml

@@ -0,0 +1,4 @@
+[workspace]
+members = [
+    "programs/*"
+]

+ 11 - 0
examples/escrow/package.json

@@ -0,0 +1,11 @@
+{
+  "dependencies": {
+    "@project-serum/anchor": "^0.9.0",
+    "@project-serum/serum": "0.13.38",
+    "@solana/web3.js": "^1.18.0",
+    "@solana/spl-token": "^0.1.6"
+  },
+  "devDependencies": {
+    "ts-mocha": "^8.0.0"
+  }
+}

+ 20 - 0
examples/escrow/programs/escrow/Cargo.toml

@@ -0,0 +1,20 @@
+[package]
+name = "escrow"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "escrow"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = { path = "../../../../lang" }
+anchor-spl = { path = "../../../../spl" }
+spl-token = { version = "3.1.1", features = ["no-entrypoint"] }

+ 2 - 0
examples/escrow/programs/escrow/Xargo.toml

@@ -0,0 +1,2 @@
+[target.bpfel-unknown-unknown.dependencies.std]
+features = []

+ 228 - 0
examples/escrow/programs/escrow/src/lib.rs

@@ -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)
+    }
+}

+ 207 - 0
examples/escrow/tests/escrow.js

@@ -0,0 +1,207 @@
+const anchor = require("@project-serum/anchor");
+const { TOKEN_PROGRAM_ID, Token } = require("@solana/spl-token");
+const assert = require("assert");
+
+describe("escrow", () => {
+  const provider = anchor.Provider.env();
+  anchor.setProvider(provider);
+
+  const program = anchor.workspace.Escrow;
+
+  let mintA = null;
+  let mintB = null;
+  let initializerTokenAccountA = null;
+  let initializerTokenAccountB = null;
+  let takerTokenAccountA = null;
+  let takerTokenAccountB = null;
+  let pda = null;
+
+  const takerAmount = 1000;
+  const initializerAmount = 500;
+
+  const escrowAccount = anchor.web3.Keypair.generate();
+  const payer = anchor.web3.Keypair.generate();
+  const mintAuthority = anchor.web3.Keypair.generate();
+
+  it("Initialise escrow state", async () => {
+    // Airdropping tokens to a payer.
+    await provider.connection.confirmTransaction(
+      await provider.connection.requestAirdrop(payer.publicKey, 10000000000),
+      "confirmed"
+    );
+
+    mintA = await Token.createMint(
+      provider.connection,
+      payer,
+      mintAuthority.publicKey,
+      null,
+      0,
+      TOKEN_PROGRAM_ID
+    );
+
+    mintB = await Token.createMint(
+      provider.connection,
+      payer,
+      mintAuthority.publicKey,
+      null,
+      0,
+      TOKEN_PROGRAM_ID
+    );
+
+    initializerTokenAccountA = await mintA.createAccount(provider.wallet.publicKey);
+    takerTokenAccountA = await mintA.createAccount(provider.wallet.publicKey);
+
+    initializerTokenAccountB = await mintB.createAccount(provider.wallet.publicKey);
+    takerTokenAccountB = await mintB.createAccount(provider.wallet.publicKey);
+
+    await mintA.mintTo(
+      initializerTokenAccountA,
+      mintAuthority.publicKey,
+      [mintAuthority],
+      initializerAmount
+    );
+
+    await mintB.mintTo(
+      takerTokenAccountB,
+      mintAuthority.publicKey,
+      [mintAuthority],
+      takerAmount
+    );
+
+    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+    let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
+
+    assert.ok(_initializerTokenAccountA.amount.toNumber() == initializerAmount);
+    assert.ok(_takerTokenAccountB.amount.toNumber() == takerAmount);
+  });
+
+  it("Initialize escrow", async () => {
+    await program.rpc.initializeEscrow(
+      new anchor.BN(initializerAmount),
+      new anchor.BN(takerAmount),
+      {
+        accounts: {
+          initializer: provider.wallet.publicKey,
+          initializerDepositTokenAccount: initializerTokenAccountA,
+          initializerReceiveTokenAccount: initializerTokenAccountB,
+          escrowAccount: escrowAccount.publicKey,
+          tokenProgram: TOKEN_PROGRAM_ID,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        },
+        instructions: [
+          await program.account.escrowAccount.createInstruction(escrowAccount),
+        ],
+        signers: [escrowAccount],
+      }
+    );
+
+    // Get the PDA that is assigned authority to token account.
+    const [_pda, _nonce] = await anchor.web3.PublicKey.findProgramAddress(
+      [Buffer.from(anchor.utils.bytes.utf8.encode("escrow"))],
+      program.programId
+    );
+
+    pda = _pda;
+
+    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+
+    let _escrowAccount = await program.account.escrowAccount.fetch(
+      escrowAccount.publicKey
+    );
+
+    // Check that the new owner is the PDA.
+    assert.ok(_initializerTokenAccountA.owner.equals(pda));
+
+    // Check that the values in the escrow account match what we expect.
+    assert.ok(_escrowAccount.initializerKey.equals(provider.wallet.publicKey));
+    assert.ok(_escrowAccount.initializerAmount.toNumber() == initializerAmount);
+    assert.ok(_escrowAccount.takerAmount.toNumber() == takerAmount);
+    assert.ok(
+      _escrowAccount.initializerDepositTokenAccount.equals(initializerTokenAccountA)
+    );
+    assert.ok(
+      _escrowAccount.initializerReceiveTokenAccount.equals(initializerTokenAccountB)
+    );
+  });
+
+  it("Exchange escrow", async () => {
+    await program.rpc.exchange({
+      accounts: {
+        taker: provider.wallet.publicKey,
+        takerDepositTokenAccount: takerTokenAccountB,
+        takerReceiveTokenAccount: takerTokenAccountA,
+        pdaDepositTokenAccount: initializerTokenAccountA,
+        initializerReceiveTokenAccount: initializerTokenAccountB,
+        initializerMainAccount: provider.wallet.publicKey,
+        escrowAccount: escrowAccount.publicKey,
+        pdaAccount: pda,
+        tokenProgram: TOKEN_PROGRAM_ID,
+      },
+    });
+
+    let _takerTokenAccountA = await mintA.getAccountInfo(takerTokenAccountA);
+    let _takerTokenAccountB = await mintB.getAccountInfo(takerTokenAccountB);
+    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+    let _initializerTokenAccountB = await mintB.getAccountInfo(initializerTokenAccountB);
+
+    // Check that the initializer gets back ownership of their token account.
+    assert.ok(_takerTokenAccountA.owner.equals(provider.wallet.publicKey));
+
+    assert.ok(_takerTokenAccountA.amount.toNumber() == initializerAmount);
+    assert.ok(_initializerTokenAccountA.amount.toNumber() == 0);
+    assert.ok(_initializerTokenAccountB.amount.toNumber() == takerAmount);
+    assert.ok(_takerTokenAccountB.amount.toNumber() == 0);
+  });
+
+  let newEscrow = anchor.web3.Keypair.generate();
+
+  it("Initialize escrow and cancel escrow", async () => {
+    // Put back tokens into initializer token A account.
+    await mintA.mintTo(
+      initializerTokenAccountA,
+      mintAuthority.publicKey,
+      [mintAuthority],
+      initializerAmount
+    );
+
+    await program.rpc.initializeEscrow(
+      new anchor.BN(initializerAmount),
+      new anchor.BN(takerAmount),
+      {
+        accounts: {
+          initializer: provider.wallet.publicKey,
+          initializerDepositTokenAccount: initializerTokenAccountA,
+          initializerReceiveTokenAccount: initializerTokenAccountB,
+          escrowAccount: newEscrow.publicKey,
+          tokenProgram: TOKEN_PROGRAM_ID,
+          rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+        },
+        instructions: [await program.account.escrowAccount.createInstruction(newEscrow)],
+        signers: [newEscrow],
+      }
+    );
+
+    let _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+
+    // Check that the new owner is the PDA.
+    assert.ok(_initializerTokenAccountA.owner.equals(pda));
+
+    // Cancel the escrow.
+    await program.rpc.cancelEscrow({
+      accounts: {
+        initializer: provider.wallet.publicKey,
+        pdaDepositTokenAccount: initializerTokenAccountA,
+        pdaAccount: pda,
+        escrowAccount: newEscrow.publicKey,
+        tokenProgram: TOKEN_PROGRAM_ID,
+      },
+    });
+
+    // Check the final owner should be the provider public key.
+    _initializerTokenAccountA = await mintA.getAccountInfo(initializerTokenAccountA);
+    assert.ok(_initializerTokenAccountA.owner.equals(provider.wallet.publicKey));
+
+    // Check all the funds are still there.
+    assert.ok(_initializerTokenAccountA.amount.toNumber() == initializerAmount);
+  });
+});