Browse Source

anchor_spl crate and example (#24)

Armani Ferrante 4 years ago
parent
commit
11272e3677

+ 4 - 0
.travis.yml

@@ -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 - 0
Cargo.lock

@@ -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"

+ 1 - 0
Cargo.toml

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

+ 2 - 0
examples/spl/token-proxy/Anchor.toml

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

+ 4 - 0
examples/spl/token-proxy/Cargo.toml

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

+ 20 - 0
examples/spl/token-proxy/programs/token-proxy/Cargo.toml

@@ -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"] }

+ 2 - 0
examples/spl/token-proxy/programs/token-proxy/Xargo.toml

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

+ 96 - 0
examples/spl/token-proxy/programs/token-proxy/src/lib.rs

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

+ 150 - 0
examples/spl/token-proxy/tests/token-proxy.js

@@ -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 - 0
spl/Cargo.toml

@@ -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 - 0
spl/src/lib.rs

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

+ 96 - 0
spl/src/token.rs

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

+ 1 - 1
src/program_account.rs

@@ -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 {

+ 18 - 17
syn/src/codegen/accounts.rs

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