Browse Source

examples: Cashiers check

Armani Ferrante 4 years ago
parent
commit
f1d2404450

+ 1 - 0
.travis.yml

@@ -52,6 +52,7 @@ jobs:
         - pushd examples/interface && anchor test && popd
         - pushd examples/interface && anchor test && popd
         - pushd examples/lockup && anchor test && popd
         - pushd examples/lockup && anchor test && popd
         - pushd examples/misc && anchor test && popd
         - pushd examples/misc && anchor test && popd
+        - pushd examples/cashiers-check && 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
         - pushd examples/tutorial/basic-2 && anchor test && popd
         - pushd examples/tutorial/basic-2 && anchor test && popd

+ 2 - 0
examples/cashiers-check/Anchor.toml

@@ -0,0 +1,2 @@
+cluster = "localnet"
+wallet = "~/.config/solana/id.json"

+ 4 - 0
examples/cashiers-check/Cargo.toml

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

+ 13 - 0
examples/cashiers-check/migrations/deploy.js

@@ -0,0 +1,13 @@
+
+// Migrations are an early feature. Currently, they're nothing more than this
+// single deploy script that's invoked from the CLI, injecting a provider
+// configured from the workspace's Anchor.toml.
+
+const anchor = require("@project-serum/anchor");
+
+module.exports = async function (provider) {
+  // Configure client to use the provider.
+  anchor.setProvider(provider);
+
+  // Add your deploy script here.
+}

+ 19 - 0
examples/cashiers-check/programs/cashiers-check/Cargo.toml

@@ -0,0 +1,19 @@
+[package]
+name = "cashiers-check"
+version = "0.1.0"
+description = "Created with Anchor"
+edition = "2018"
+
+[lib]
+crate-type = ["cdylib", "lib"]
+name = "cashiers_check"
+
+[features]
+no-entrypoint = []
+no-idl = []
+cpi = ["no-entrypoint"]
+default = []
+
+[dependencies]
+anchor-lang = { git = "https://github.com/project-serum/anchor" }
+anchor-spl = { git = "https://github.com/project-serum/anchor" }

+ 2 - 0
examples/cashiers-check/programs/cashiers-check/Xargo.toml

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

+ 183 - 0
examples/cashiers-check/programs/cashiers-check/src/lib.rs

@@ -0,0 +1,183 @@
+//! A cashiers check example. The funds are immediately withdrawn from a user's
+//! account and sent to a program controlled `Check` account, where the funds
+//! reside until they are "cashed" by the intended recipient. The creator of
+//! the check can cancel the check at any time to get back the funds.
+
+#![feature(proc_macro_hygiene)]
+
+use anchor_lang::prelude::*;
+use anchor_spl::token::{self, TokenAccount, Transfer};
+use std::convert::Into;
+
+#[program]
+pub mod cashiers_check {
+    use super::*;
+
+    #[access_control(CreateCheck::accounts(&ctx, nonce))]
+    pub fn create_check(
+        ctx: Context<CreateCheck>,
+        amount: u64,
+        memo: Option<String>,
+        nonce: u8,
+    ) -> Result<()> {
+        // Transfer funds to the check.
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.from.to_account_info().clone(),
+            to: ctx.accounts.vault.to_account_info().clone(),
+            authority: ctx.accounts.owner.clone(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);
+        token::transfer(cpi_ctx, amount)?;
+
+        // Print the check.
+        let check = &mut ctx.accounts.check;
+        check.amount = amount;
+        check.from = *ctx.accounts.from.to_account_info().key;
+        check.to = *ctx.accounts.to.to_account_info().key;
+        check.vault = *ctx.accounts.vault.to_account_info().key;
+        check.nonce = nonce;
+        check.memo = memo;
+
+        Ok(())
+    }
+
+    #[access_control(not_burned(&ctx.accounts.check))]
+    pub fn cash_check(ctx: Context<CashCheck>) -> Result<()> {
+        let seeds = &[
+            ctx.accounts.check.to_account_info().key.as_ref(),
+            &[ctx.accounts.check.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.vault.to_account_info().clone(),
+            to: ctx.accounts.to.to_account_info().clone(),
+            authority: ctx.accounts.check_signer.clone(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        token::transfer(cpi_ctx, ctx.accounts.check.amount)?;
+        // Burn the check for one time use.
+        ctx.accounts.check.burned = true;
+        Ok(())
+    }
+
+    #[access_control(not_burned(&ctx.accounts.check))]
+    pub fn cancel_check(ctx: Context<CancelCheck>) -> Result<()> {
+        let seeds = &[
+            ctx.accounts.check.to_account_info().key.as_ref(),
+            &[ctx.accounts.check.nonce],
+        ];
+        let signer = &[&seeds[..]];
+        let cpi_accounts = Transfer {
+            from: ctx.accounts.vault.to_account_info().clone(),
+            to: ctx.accounts.from.to_account_info().clone(),
+            authority: ctx.accounts.check_signer.clone(),
+        };
+        let cpi_program = ctx.accounts.token_program.clone();
+        let cpi_ctx = CpiContext::new_with_signer(cpi_program, cpi_accounts, signer);
+        token::transfer(cpi_ctx, ctx.accounts.check.amount)?;
+        ctx.accounts.check.burned = true;
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct CreateCheck<'info> {
+    // Check being created.
+    #[account(init)]
+    check: ProgramAccount<'info, Check>,
+    // Check's token vault.
+    #[account(mut, "&vault.owner == check_signer.key")]
+    vault: CpiAccount<'info, TokenAccount>,
+    // Program derived address for the check.
+    check_signer: AccountInfo<'info>,
+    // Token account the check is made from.
+    #[account(mut, has_one = owner)]
+    from: CpiAccount<'info, TokenAccount>,
+    // Token account the check is made to.
+    #[account("from.mint == to.mint")]
+    to: CpiAccount<'info, TokenAccount>,
+    // Owner of the `from` token account.
+    owner: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+    rent: Sysvar<'info, Rent>,
+}
+
+impl<'info> CreateCheck<'info> {
+    pub fn accounts(ctx: &Context<CreateCheck>, nonce: u8) -> Result<()> {
+        let signer = Pubkey::create_program_address(
+            &[ctx.accounts.check.to_account_info().key.as_ref(), &[nonce]],
+            ctx.program_id,
+        )
+        .map_err(|_| ErrorCode::InvalidCheckNonce)?;
+        if &signer != ctx.accounts.check_signer.to_account_info().key {
+            return Err(ErrorCode::InvalidCheckSigner.into());
+        }
+        Ok(())
+    }
+}
+
+#[derive(Accounts)]
+pub struct CashCheck<'info> {
+    #[account(mut, has_one = vault, has_one = to)]
+    check: ProgramAccount<'info, Check>,
+    #[account(mut)]
+    vault: AccountInfo<'info>,
+    #[account(seeds = [
+        check.to_account_info().key.as_ref(),
+        &[check.nonce],
+    ])]
+    check_signer: AccountInfo<'info>,
+    #[account(mut, has_one = owner)]
+    to: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    owner: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+}
+
+#[derive(Accounts)]
+pub struct CancelCheck<'info> {
+    #[account(mut, has_one = vault, has_one = from)]
+    check: ProgramAccount<'info, Check>,
+    #[account(mut)]
+    vault: AccountInfo<'info>,
+    #[account(seeds = [
+        check.to_account_info().key.as_ref(),
+        &[check.nonce],
+    ])]
+    check_signer: AccountInfo<'info>,
+    #[account(mut, has_one = owner)]
+    from: CpiAccount<'info, TokenAccount>,
+    #[account(signer)]
+    owner: AccountInfo<'info>,
+    token_program: AccountInfo<'info>,
+}
+
+#[account]
+pub struct Check {
+    from: Pubkey,
+    to: Pubkey,
+    amount: u64,
+    memo: Option<String>,
+    vault: Pubkey,
+    nonce: u8,
+    burned: bool,
+}
+
+#[error]
+pub enum ErrorCode {
+    #[msg("The given nonce does not create a valid program derived address.")]
+    InvalidCheckNonce,
+    #[msg("The derived check signer does not match that which was given.")]
+    InvalidCheckSigner,
+    #[msg("The given check has already been burned.")]
+    AlreadyBurned,
+}
+
+fn not_burned(check: &Check) -> Result<()> {
+    if check.burned {
+        return Err(ErrorCode::AlreadyBurned.into());
+    }
+    Ok(())
+}

+ 109 - 0
examples/cashiers-check/tests/cashiers-check.js

@@ -0,0 +1,109 @@
+const anchor = require("@project-serum/anchor");
+const serumCmn = require("@project-serum/common");
+const TokenInstructions = require("@project-serum/serum").TokenInstructions;
+const assert = require("assert");
+
+describe("cashiers-check", () => {
+  // Configure the client to use the local cluster.
+  anchor.setProvider(anchor.Provider.env());
+
+  const program = anchor.workspace.CashiersCheck;
+
+  let mint = null;
+  let god = null;
+  let receiver = null;
+
+  it("Sets up initial test state", async () => {
+    const [_mint, _god] = await serumCmn.createMintAndVault(
+      program.provider,
+      new anchor.BN(1000000)
+    );
+    mint = _mint;
+    god = _god;
+
+    receiver = await serumCmn.createTokenAccount(
+      program.provider,
+      mint,
+      program.provider.wallet.publicKey
+    );
+  });
+
+  const check = new anchor.web3.Account();
+  const vault = new anchor.web3.Account();
+
+  let checkSigner = null;
+
+  it("Creates a check!", async () => {
+    let [_checkSigner, nonce] = await anchor.web3.PublicKey.findProgramAddress(
+      [check.publicKey.toBuffer()],
+      program.programId
+    );
+    checkSigner = _checkSigner;
+
+    await program.rpc.createCheck(new anchor.BN(100), "Hello world", nonce, {
+      accounts: {
+        check: check.publicKey,
+        vault: vault.publicKey,
+        checkSigner,
+        from: god,
+        to: receiver,
+        owner: program.provider.wallet.publicKey,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+        rent: anchor.web3.SYSVAR_RENT_PUBKEY,
+      },
+      signers: [check, vault],
+      instructions: [
+        await program.account.check.createInstruction(check, 300),
+        ...(await serumCmn.createTokenAccountInstrs(
+          program.provider,
+          vault.publicKey,
+          mint,
+          checkSigner
+        )),
+      ],
+    });
+
+    const checkAccount = await program.account.check(check.publicKey);
+    assert.ok(checkAccount.from.equals(god));
+    assert.ok(checkAccount.to.equals(receiver));
+    assert.ok(checkAccount.amount.eq(new anchor.BN(100)));
+    assert.ok(checkAccount.memo === "Hello world");
+    assert.ok(checkAccount.vault.equals(vault.publicKey));
+    assert.ok(checkAccount.nonce === nonce);
+    assert.ok(checkAccount.burned === false);
+
+    let vaultAccount = await serumCmn.getTokenAccount(
+      program.provider,
+      checkAccount.vault
+    );
+    assert.ok(vaultAccount.amount.eq(new anchor.BN(100)));
+  });
+
+  it("Cashes a check", async () => {
+    await program.rpc.cashCheck({
+      accounts: {
+        check: check.publicKey,
+        vault: vault.publicKey,
+        checkSigner: checkSigner,
+        to: receiver,
+        owner: program.provider.wallet.publicKey,
+        tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
+      },
+    });
+
+    const checkAccount = await program.account.check(check.publicKey);
+    assert.ok(checkAccount.burned === true);
+
+    let vaultAccount = await serumCmn.getTokenAccount(
+      program.provider,
+      checkAccount.vault
+    );
+    assert.ok(vaultAccount.amount.eq(new anchor.BN(0)));
+
+    let receiverAccount = await serumCmn.getTokenAccount(
+      program.provider,
+      receiver
+    );
+    assert.ok(receiverAccount.amount.eq(new anchor.BN(100)));
+  });
+});