anchor_spl crate and example (#24)

This commit is contained in:
Armani Ferrante 2021-01-15 19:16:17 -08:00 committed by GitHub
parent b5ca210696
commit 11272e3677
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 414 additions and 18 deletions

View File

@ -16,6 +16,9 @@ _examples: &examples
- nvm install $NODE_VERSION
- npm install -g mocha
- npm install -g @project-serum/anchor
- npm install -g @project-serum/serum
- npm install -g @project-serum/common
- npm install -g @solana/spl-token
- sudo apt-get install -y pkg-config build-essential libudev-dev
- sh -c "$(curl -sSfL https://release.solana.com/v1.5.0/install)"
- export PATH="/home/travis/.local/share/solana/install/active_release/bin:$PATH"
@ -42,6 +45,7 @@ jobs:
script:
- pushd examples/sysvars && anchor test && popd
- pushd examples/composite && anchor test && popd
- pushd examples/spl/token-proxy && 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

9
Cargo.lock generated
View File

@ -121,6 +121,15 @@ dependencies = [
"thiserror",
]
[[package]]
name = "anchor-spl"
version = "0.0.0-alpha.0"
dependencies = [
"anchor-lang",
"solana-program",
"spl-token 3.0.1",
]
[[package]]
name = "anchor-syn"
version = "0.0.0-alpha.0"

View File

@ -27,4 +27,5 @@ members = [
"syn",
"attribute/*",
"derive/*",
"spl",
]

View File

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

View File

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

View File

@ -0,0 +1,20 @@
[package]
name = "token-proxy"
version = "0.1.0"
description = "Created with Anchor"
edition = "2018"
[lib]
crate-type = ["cdylib", "lib"]
name = "token_proxy"
[features]
no-entrypoint = []
cpi = ["no-entrypoint"]
[dependencies]
anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
anchor-spl = { git = "https://github.com/project-serum/anchor", features = ["derive"] }
serum-borsh = { version = "0.8.0-serum.1", features = ["serum-program"] }
solana-program = "1.4.3"
solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] }

View File

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

View File

@ -0,0 +1,96 @@
//! This example demonstrates the use of the `anchor_spl::token` CPI client.
#![feature(proc_macro_hygiene)]
use anchor_lang::prelude::*;
use anchor_spl::token::{self, Burn, MintTo, Transfer};
#[program]
mod token_proxy {
use super::*;
pub fn proxy_transfer(ctx: Context<ProxyTransfer>, amount: u64) -> ProgramResult {
token::transfer(ctx.accounts.into(), amount)
}
pub fn proxy_mint_to(ctx: Context<ProxyMintTo>, amount: u64) -> ProgramResult {
token::mint_to(ctx.accounts.into(), amount)
}
pub fn proxy_burn(ctx: Context<ProxyBurn>, amount: u64) -> ProgramResult {
token::burn(ctx.accounts.into(), amount)
}
}
#[derive(Accounts)]
pub struct ProxyTransfer<'info> {
#[account(signer)]
pub authority: AccountInfo<'info>,
#[account(mut)]
pub from: AccountInfo<'info>,
#[account(mut)]
pub to: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct ProxyMintTo<'info> {
#[account(signer)]
pub authority: AccountInfo<'info>,
#[account(mut)]
pub mint: AccountInfo<'info>,
#[account(mut)]
pub to: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct ProxyBurn<'info> {
#[account(signer)]
pub authority: AccountInfo<'info>,
#[account(mut)]
pub mint: AccountInfo<'info>,
#[account(mut)]
pub to: AccountInfo<'info>,
pub token_program: AccountInfo<'info>,
}
impl<'a, 'b, 'c, 'info> From<&mut ProxyTransfer<'info>>
for CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>
{
fn from(accounts: &mut ProxyTransfer<'info>) -> CpiContext<'a, 'b, 'c, 'info, Transfer<'info>> {
let cpi_accounts = Transfer {
from: accounts.from.clone(),
to: accounts.to.clone(),
authority: accounts.authority.clone(),
};
let cpi_program = accounts.token_program.clone();
CpiContext::new(cpi_program, cpi_accounts)
}
}
impl<'a, 'b, 'c, 'info> From<&mut ProxyMintTo<'info>>
for CpiContext<'a, 'b, 'c, 'info, MintTo<'info>>
{
fn from(accounts: &mut ProxyMintTo<'info>) -> CpiContext<'a, 'b, 'c, 'info, MintTo<'info>> {
let cpi_accounts = MintTo {
mint: accounts.mint.clone(),
to: accounts.to.clone(),
authority: accounts.authority.clone(),
};
let cpi_program = accounts.token_program.clone();
CpiContext::new(cpi_program, cpi_accounts)
}
}
impl<'a, 'b, 'c, 'info> From<&mut ProxyBurn<'info>> for CpiContext<'a, 'b, 'c, 'info, Burn<'info>> {
fn from(accounts: &mut ProxyBurn<'info>) -> CpiContext<'a, 'b, 'c, 'info, Burn<'info>> {
let cpi_accounts = Burn {
mint: accounts.mint.clone(),
to: accounts.to.clone(),
authority: accounts.authority.clone(),
};
let cpi_program = accounts.token_program.clone();
CpiContext::new(cpi_program, cpi_accounts)
}
}

View File

@ -0,0 +1,150 @@
const anchor = require("@project-serum/anchor");
const assert = require("assert");
describe("token", () => {
const provider = anchor.Provider.local();
// Configure the client to use the local cluster.
anchor.setProvider(provider);
const program = anchor.workspace.TokenProxy;
let mint = null;
let from = null;
let to = null;
it("Initializes test state", async () => {
mint = await createMint(provider);
from = await createTokenAccount(provider, mint, provider.wallet.publicKey);
to = await createTokenAccount(provider, mint, provider.wallet.publicKey);
});
it("Mints a token", async () => {
await program.rpc.proxyMintTo(new anchor.BN(1000), {
accounts: {
authority: provider.wallet.publicKey,
mint,
to: from,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
},
});
const fromAccount = await getTokenAccount(provider, from);
assert.ok(fromAccount.amount.eq(new anchor.BN(1000)));
});
it("Transfers a token", async () => {
await program.rpc.proxyTransfer(new anchor.BN(500), {
accounts: {
authority: provider.wallet.publicKey,
to,
from,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
},
});
const fromAccount = await getTokenAccount(provider, from);
const toAccount = await getTokenAccount(provider, to);
assert.ok(fromAccount.amount.eq(new anchor.BN(500)));
assert.ok(fromAccount.amount.eq(new anchor.BN(500)));
});
it("Burns a token", async () => {
await program.rpc.proxyBurn(new anchor.BN(499), {
accounts: {
authority: provider.wallet.publicKey,
mint,
to,
tokenProgram: TokenInstructions.TOKEN_PROGRAM_ID,
},
});
const toAccount = await getTokenAccount(provider, to);
assert.ok(toAccount.amount.eq(new anchor.BN(1)));
});
});
// SPL token client boilerplate for test initialization. Everything below here is
// mostly irrelevant to the point of the example.
const serumCmn = require("@project-serum/common");
const TokenInstructions = require("@project-serum/serum").TokenInstructions;
async function getTokenAccount(provider, addr) {
return await serumCmn.getTokenAccount(provider, addr);
}
async function createMint(provider, authority) {
if (authority === undefined) {
authority = provider.wallet.publicKey;
}
const mint = new anchor.web3.Account();
const instructions = await createMintInstructions(
provider,
authority,
mint.publicKey
);
const tx = new anchor.web3.Transaction();
tx.add(...instructions);
await provider.send(tx, [mint]);
return mint.publicKey;
}
async function createMintInstructions(provider, authority, mint) {
let instructions = [
anchor.web3.SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey: mint,
space: 82,
lamports: await provider.connection.getMinimumBalanceForRentExemption(82),
programId: TokenInstructions.TOKEN_PROGRAM_ID,
}),
TokenInstructions.initializeMint({
mint,
decimals: 0,
mintAuthority: authority,
}),
];
return instructions;
}
async function createTokenAccount(provider, mint, owner) {
const vault = new anchor.web3.Account();
const tx = new anchor.web3.Transaction();
tx.add(
...(await createTokenAccountInstrs(provider, vault.publicKey, mint, owner))
);
await provider.send(tx, [vault]);
return vault.publicKey;
}
async function createTokenAccountInstrs(
provider,
newAccountPubkey,
mint,
owner,
lamports
) {
if (lamports === undefined) {
lamports = await provider.connection.getMinimumBalanceForRentExemption(165);
}
return [
anchor.web3.SystemProgram.createAccount({
fromPubkey: provider.wallet.publicKey,
newAccountPubkey,
space: 165,
lamports,
programId: TokenInstructions.TOKEN_PROGRAM_ID,
}),
TokenInstructions.initializeAccount({
account: newAccountPubkey,
mint,
owner,
}),
];
}

10
spl/Cargo.toml Normal file
View File

@ -0,0 +1,10 @@
[package]
name = "anchor-spl"
version = "0.0.0-alpha.0"
authors = ["Armani Ferrante <armaniferrante@gmail.com>"]
edition = "2018"
[dependencies]
anchor-lang = { path = "../", features = ["derive"] }
solana-program = "1.4.3"
spl-token = { version = "3.0.1", features = ["no-entrypoint"] }

1
spl/src/lib.rs Normal file
View File

@ -0,0 +1 @@
pub mod token;

96
spl/src/token.rs Normal file
View File

@ -0,0 +1,96 @@
use anchor_lang::{Accounts, CpiContext};
use solana_program::account_info::AccountInfo;
use solana_program::entrypoint::ProgramResult;
pub fn transfer<'a, 'b, 'c, 'info>(
ctx: CpiContext<'a, 'b, 'c, 'info, Transfer<'info>>,
amount: u64,
) -> ProgramResult {
let ix = spl_token::instruction::transfer(
&spl_token::ID,
ctx.accounts.from.key,
ctx.accounts.to.key,
ctx.accounts.authority.key,
&[],
amount,
)?;
solana_program::program::invoke_signed(
&ix,
&[
ctx.accounts.from.clone(),
ctx.accounts.to.clone(),
ctx.accounts.authority.clone(),
ctx.program.clone(),
],
ctx.signer_seeds,
)
}
pub fn mint_to<'a, 'b, 'c, 'info>(
ctx: CpiContext<'a, 'b, 'c, 'info, MintTo<'info>>,
amount: u64,
) -> ProgramResult {
let ix = spl_token::instruction::mint_to(
&spl_token::ID,
ctx.accounts.mint.key,
ctx.accounts.to.key,
ctx.accounts.authority.key,
&[],
amount,
)?;
solana_program::program::invoke_signed(
&ix,
&[
ctx.accounts.mint.clone(),
ctx.accounts.to.clone(),
ctx.accounts.authority.clone(),
ctx.program.clone(),
],
ctx.signer_seeds,
)
}
pub fn burn<'a, 'b, 'c, 'info>(
ctx: CpiContext<'a, 'b, 'c, 'info, Burn<'info>>,
amount: u64,
) -> ProgramResult {
let ix = spl_token::instruction::burn(
&spl_token::ID,
ctx.accounts.to.key,
ctx.accounts.mint.key,
ctx.accounts.authority.key,
&[],
amount,
)?;
solana_program::program::invoke_signed(
&ix,
&[
ctx.accounts.to.clone(),
ctx.accounts.mint.clone(),
ctx.accounts.authority.clone(),
ctx.program.clone(),
],
ctx.signer_seeds,
)
}
#[derive(Accounts)]
pub struct Transfer<'info> {
pub from: AccountInfo<'info>,
pub to: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct MintTo<'info> {
pub mint: AccountInfo<'info>,
pub to: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
}
#[derive(Accounts)]
pub struct Burn<'info> {
pub mint: AccountInfo<'info>,
pub to: AccountInfo<'info>,
pub authority: AccountInfo<'info>,
}

View File

@ -76,7 +76,7 @@ where
T: AccountSerialize + AccountDeserialize + Clone,
{
fn try_accounts_init(
program_id: &Pubkey,
_program_id: &Pubkey,
accounts: &mut &[AccountInfo<'info>],
) -> Result<Self, ProgramError> {
if accounts.len() == 0 {

View File

@ -81,22 +81,23 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
AccountField::Field(f) => {
let ident = &f.ident;
let info = match f.ty {
Ty::AccountInfo => quote! { #ident },
// Only ProgramAccounts are automatically saved (when
// marked `#[account(mut)]`).
Ty::ProgramAccount(_) => quote! { #ident.to_account_info() },
_ => return quote! {},
};
match f.is_mut {
false => quote! {},
true => quote! {
// Only persist the change if the account is owned by the
// current program.
if program_id == self.#info.owner {
let info = self.#info;
let mut data = info.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut cursor = std::io::Cursor::new(dst);
self.#ident.try_serialize(&mut cursor)?;
}
// Only persist the change if the account is owned by the
// current program.
if program_id == self.#info.owner {
let info = self.#info;
let mut data = info.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut cursor = std::io::Cursor::new(dst);
self.#ident.try_serialize(&mut cursor)?;
}
},
}
}
@ -144,8 +145,8 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
};
quote! {
impl#combined_generics Accounts#trait_generics for #name#strct_generics {
fn try_accounts(program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>]) -> Result<Self, ProgramError> {
impl#combined_generics anchor_lang::Accounts#trait_generics for #name#strct_generics {
fn try_accounts(program_id: &solana_program::pubkey::Pubkey, accounts: &mut &[solana_program::account_info::AccountInfo<'info>]) -> Result<Self, solana_program::program_error::ProgramError> {
// Deserialize each account.
#(#deser_fields)*
@ -159,8 +160,8 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
}
}
impl#combined_generics ToAccountInfos#trait_generics for #name#strct_generics {
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
impl#combined_generics anchor_lang::ToAccountInfos#trait_generics for #name#strct_generics {
fn to_account_infos(&self) -> Vec<solana_program::account_info::AccountInfo<'info>> {
let mut account_infos = vec![];
#(#to_acc_infos)*
@ -169,8 +170,8 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
}
}
impl#combined_generics ToAccountMetas for #name#strct_generics {
fn to_account_metas(&self) -> Vec<AccountMeta> {
impl#combined_generics anchor_lang::ToAccountMetas for #name#strct_generics {
fn to_account_metas(&self) -> Vec<solana_program::instruction::AccountMeta> {
let mut account_metas = vec![];
#(#to_acc_metas)*
@ -181,7 +182,7 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream {
}
impl#strct_generics #name#strct_generics {
pub fn exit(&self, program_id: &Pubkey) -> ProgramResult {
pub fn exit(&self, program_id: &solana_program::pubkey::Pubkey) -> solana_program::entrypoint::ProgramResult {
#(#on_save)*
Ok(())
}