examples: Cashiers check
This commit is contained in:
parent
d3daddcca0
commit
f1d2404450
|
@ -52,6 +52,7 @@ jobs:
|
|||
- pushd examples/interface && anchor test && popd
|
||||
- pushd examples/lockup && 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-1 && anchor test && popd
|
||||
- pushd examples/tutorial/basic-2 && anchor test && popd
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
|
@ -0,0 +1,4 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
|
@ -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.
|
||||
}
|
|
@ -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" }
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -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(())
|
||||
}
|
|
@ -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)));
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue