diff --git a/.travis.yml b/.travis.yml index dc4f93c4..193957f2 100644 --- a/.travis.yml +++ b/.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 diff --git a/Cargo.lock b/Cargo.lock index 14e8c82a..78114c7a 100644 --- a/Cargo.lock +++ b/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" diff --git a/Cargo.toml b/Cargo.toml index 63801b5a..4d77bc98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,4 +27,5 @@ members = [ "syn", "attribute/*", "derive/*", + "spl", ] diff --git a/examples/spl/token-proxy/Anchor.toml b/examples/spl/token-proxy/Anchor.toml new file mode 100644 index 00000000..2ebd5af9 --- /dev/null +++ b/examples/spl/token-proxy/Anchor.toml @@ -0,0 +1,2 @@ +cluster = "localnet" +wallet = "~/.config/solana/id.json" diff --git a/examples/spl/token-proxy/Cargo.toml b/examples/spl/token-proxy/Cargo.toml new file mode 100644 index 00000000..a60de986 --- /dev/null +++ b/examples/spl/token-proxy/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/examples/spl/token-proxy/programs/token-proxy/Cargo.toml b/examples/spl/token-proxy/programs/token-proxy/Cargo.toml new file mode 100644 index 00000000..0b4ea805 --- /dev/null +++ b/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"] } diff --git a/examples/spl/token-proxy/programs/token-proxy/Xargo.toml b/examples/spl/token-proxy/programs/token-proxy/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/examples/spl/token-proxy/programs/token-proxy/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/examples/spl/token-proxy/programs/token-proxy/src/lib.rs b/examples/spl/token-proxy/programs/token-proxy/src/lib.rs new file mode 100644 index 00000000..def404ed --- /dev/null +++ b/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, amount: u64) -> ProgramResult { + token::transfer(ctx.accounts.into(), amount) + } + + pub fn proxy_mint_to(ctx: Context, amount: u64) -> ProgramResult { + token::mint_to(ctx.accounts.into(), amount) + } + + pub fn proxy_burn(ctx: Context, 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) + } +} diff --git a/examples/spl/token-proxy/tests/token-proxy.js b/examples/spl/token-proxy/tests/token-proxy.js new file mode 100644 index 00000000..25386e9d --- /dev/null +++ b/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, + }), + ]; +} diff --git a/spl/Cargo.toml b/spl/Cargo.toml new file mode 100644 index 00000000..b1536e5d --- /dev/null +++ b/spl/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "anchor-spl" +version = "0.0.0-alpha.0" +authors = ["Armani Ferrante "] +edition = "2018" + +[dependencies] +anchor-lang = { path = "../", features = ["derive"] } +solana-program = "1.4.3" +spl-token = { version = "3.0.1", features = ["no-entrypoint"] } diff --git a/spl/src/lib.rs b/spl/src/lib.rs new file mode 100644 index 00000000..79c66ba6 --- /dev/null +++ b/spl/src/lib.rs @@ -0,0 +1 @@ +pub mod token; diff --git a/spl/src/token.rs b/spl/src/token.rs new file mode 100644 index 00000000..0d5639d8 --- /dev/null +++ b/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>, +} diff --git a/src/program_account.rs b/src/program_account.rs index c18df164..df2dccd6 100644 --- a/src/program_account.rs +++ b/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 { if accounts.len() == 0 { diff --git a/syn/src/codegen/accounts.rs b/syn/src/codegen/accounts.rs index 3f8dcbbf..f6801613 100644 --- a/syn/src/codegen/accounts.rs +++ b/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 { + 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 { // 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> { + impl#combined_generics anchor_lang::ToAccountInfos#trait_generics for #name#strct_generics { + fn to_account_infos(&self) -> Vec> { 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 { + impl#combined_generics anchor_lang::ToAccountMetas for #name#strct_generics { + fn to_account_metas(&self) -> Vec { 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(()) }