From 34a3474663bc9bb8e27b38ca87658396a70a413b Mon Sep 17 00:00:00 2001 From: Armani Ferrante Date: Thu, 14 Jan 2021 22:35:50 -0800 Subject: [PATCH] Composable Accounts derivations (#21) --- .travis.yml | 1 + Cargo.lock | 28 +-- Cargo.toml | 2 +- README.md | 4 +- attribute/account/src/lib.rs | 4 +- cli/src/template.rs | 4 +- examples/composite/Anchor.toml | 2 + examples/composite/Cargo.toml | 4 + .../composite/programs/composite/Cargo.toml | 19 ++ .../composite/programs/composite/Xargo.toml | 2 + .../composite/programs/composite/src/lib.rs | 65 +++++ examples/composite/tests/composite.js | 59 +++++ examples/sysvars/programs/sysvars/Cargo.toml | 2 +- .../basic-0/programs/basic-0/Cargo.toml | 2 +- .../basic-1/programs/basic-1/Cargo.toml | 2 +- .../basic-2/programs/basic-2/Cargo.toml | 2 +- .../basic-2/programs/basic-2/src/lib.rs | 125 +++++----- examples/tutorial/basic-2/tests/basic-2.js | 113 ++++++++- .../basic-3/programs/puppet-master/Cargo.toml | 2 +- .../basic-3/programs/puppet/Cargo.toml | 2 +- src/account_info.rs | 41 ++++ src/context.rs | 56 +++++ src/cpi_account.rs | 90 +++++++ src/lib.rs | 221 ++++------------- src/program_account.rs | 131 ++++++++++ src/sysvar.rs | 69 ++++++ syn/src/codegen/accounts.rs | 223 +++++++----------- syn/src/codegen/program.rs | 2 +- syn/src/idl.rs | 13 +- syn/src/lib.rs | 135 ++++++++++- syn/src/parser/accounts.rs | 64 +++-- syn/src/parser/file.rs | 16 +- ts/package.json | 2 +- ts/src/coder.ts | 15 +- ts/src/idl.ts | 5 + 35 files changed, 1075 insertions(+), 452 deletions(-) create mode 100644 examples/composite/Anchor.toml create mode 100644 examples/composite/Cargo.toml create mode 100644 examples/composite/programs/composite/Cargo.toml create mode 100644 examples/composite/programs/composite/Xargo.toml create mode 100644 examples/composite/programs/composite/src/lib.rs create mode 100644 examples/composite/tests/composite.js create mode 100644 src/account_info.rs create mode 100644 src/context.rs create mode 100644 src/cpi_account.rs create mode 100644 src/program_account.rs create mode 100644 src/sysvar.rs diff --git a/.travis.yml b/.travis.yml index 3c32f08d..dc4f93c4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,6 +41,7 @@ jobs: name: Runs the examples script: - pushd examples/sysvars && anchor test && popd + - pushd examples/composite && 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 7e83d8f2..f7b57b0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,20 +43,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "anchor" -version = "0.1.0" -dependencies = [ - "anchor-attribute-access-control", - "anchor-attribute-account", - "anchor-attribute-program", - "anchor-derive-accounts", - "borsh", - "solana-program", - "solana-sdk", - "thiserror", -] - [[package]] name = "anchor-attribute-access-control" version = "0.1.0" @@ -121,6 +107,20 @@ dependencies = [ "syn 1.0.54", ] +[[package]] +name = "anchor-lang" +version = "0.1.0" +dependencies = [ + "anchor-attribute-access-control", + "anchor-attribute-account", + "anchor-attribute-program", + "anchor-derive-accounts", + "borsh", + "solana-program", + "solana-sdk", + "thiserror", +] + [[package]] name = "anchor-syn" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 709c5271..6ed4a565 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "anchor" +name = "anchor-lang" version = "0.1.0" description = "" repository = "https://github.com/project-serum/serum-dex" diff --git a/README.md b/README.md index ffc6c68b..1474d740 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ mod basic_1 { pub struct Initialize<'info> { #[account(init)] pub my_account: ProgramAccount<'info, MyAccount>, - pub rent: Rent, + pub rent: Sysvar<'info, Rent>, } #[derive(Accounts)] @@ -95,7 +95,7 @@ purposes of the Accounts macro) that can be specified on a struct deriving `Acco | `#[account(mut)]` | On `ProgramAccount` structs. | Marks the account as mutable and persists the state transition. | | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. | | `#[account(belongs_to = )]` | On `ProgramAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. | -| `#[account(owner = program \| skip)]` | On `ProgramAccount` and `AccountInfo` structs | Checks the owner of the account is the current program or skips the check. | +| `#[account(owner = program \| skip)]` | On `AccountInfo` structs | Checks the owner of the account is the current program or skips the check. | | `#[account("")]` | On `ProgramAccount` structs | Executes the given code literal as a constraint. The literal should evaluate to a boolean. | | `#[account(rent_exempt = )]` | On `AccountInfo` or `ProgramAccount` structs | Optional attribute to skip the rent exemption check. By default, all accounts marked with `#[account(init)]` will be rent exempt. Similarly, omitting `= skip` will mark the account rent exempt. | diff --git a/attribute/account/src/lib.rs b/attribute/account/src/lib.rs index be5b0541..997a010f 100644 --- a/attribute/account/src/lib.rs +++ b/attribute/account/src/lib.rs @@ -17,7 +17,7 @@ pub fn account( let discriminator_preimage = format!("account:{}", account_name.to_string()); let coder = quote! { - impl anchor::AccountSerialize for #account_name { + impl anchor_lang::AccountSerialize for #account_name { fn try_serialize(&self, writer: &mut W) -> Result<(), ProgramError> { // TODO: we shouldn't have to hash at runtime. However, rust // is not happy when trying to include solana-sdk from @@ -39,7 +39,7 @@ pub fn account( } } - impl anchor::AccountDeserialize for #account_name { + impl anchor_lang::AccountDeserialize for #account_name { fn try_deserialize(buf: &mut &[u8]) -> Result { let mut discriminator = [0u8; 8]; discriminator.copy_from_slice( diff --git a/cli/src/template.rs b/cli/src/template.rs index e946186c..24915839 100644 --- a/cli/src/template.rs +++ b/cli/src/template.rs @@ -30,7 +30,7 @@ cpi = ["no-entrypoint"] borsh = {{ git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] }} solana-program = "1.4.3" solana-sdk = {{ version = "1.3.14", default-features = false, features = ["program"] }} -anchor = {{ git = "https://github.com/project-serum/anchor", features = ["derive"] }} +anchor-lang = {{ git = "https://github.com/project-serum/anchor", features = ["derive"] }} "#, name, name.to_snake_case(), @@ -47,7 +47,7 @@ pub fn lib_rs(name: &str) -> String { format!( r#"#![feature(proc_macro_hygiene)] -use anchor::prelude::*; +use anchor_lang::prelude::*; #[program] mod {} {{ diff --git a/examples/composite/Anchor.toml b/examples/composite/Anchor.toml new file mode 100644 index 00000000..6e90c3dd --- /dev/null +++ b/examples/composite/Anchor.toml @@ -0,0 +1,2 @@ +cluster = "localnet" +wallet = "/home/armaniferrante/.config/solana/id.json" diff --git a/examples/composite/Cargo.toml b/examples/composite/Cargo.toml new file mode 100644 index 00000000..a60de986 --- /dev/null +++ b/examples/composite/Cargo.toml @@ -0,0 +1,4 @@ +[workspace] +members = [ + "programs/*" +] diff --git a/examples/composite/programs/composite/Cargo.toml b/examples/composite/programs/composite/Cargo.toml new file mode 100644 index 00000000..600ceb04 --- /dev/null +++ b/examples/composite/programs/composite/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "composite" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "composite" + +[features] +no-entrypoint = [] +cpi = ["no-entrypoint"] + +[dependencies] +borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] } +solana-program = "1.4.3" +solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] } +anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] } diff --git a/examples/composite/programs/composite/Xargo.toml b/examples/composite/programs/composite/Xargo.toml new file mode 100644 index 00000000..1744f098 --- /dev/null +++ b/examples/composite/programs/composite/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] \ No newline at end of file diff --git a/examples/composite/programs/composite/src/lib.rs b/examples/composite/programs/composite/src/lib.rs new file mode 100644 index 00000000..2cb5e2ff --- /dev/null +++ b/examples/composite/programs/composite/src/lib.rs @@ -0,0 +1,65 @@ +//! This example demonstrates the ability to compose together multiple +//! structs deriving `Accounts`. See `CompositeUpdate`, below. + +#![feature(proc_macro_hygiene)] + +use anchor_lang::prelude::*; + +#[program] +mod composite { + use super::*; + pub fn initialize(ctx: Context) -> ProgramResult { + Ok(()) + } + + pub fn composite_update( + ctx: Context, + dummy_a: u64, + dummy_b: String, + ) -> ProgramResult { + let a = &mut ctx.accounts.foo.dummy_a; + let b = &mut ctx.accounts.bar.dummy_b; + + a.data = dummy_a; + b.data = dummy_b; + + Ok(()) + } +} + +#[derive(Accounts)] +pub struct Initialize<'info> { + #[account(init)] + pub dummy_a: ProgramAccount<'info, DummyA>, + #[account(init)] + pub dummy_b: ProgramAccount<'info, DummyB>, + pub rent: Sysvar<'info, Rent>, +} + +#[derive(Accounts)] +pub struct CompositeUpdate<'info> { + foo: Foo<'info>, + bar: Bar<'info>, +} + +#[derive(Accounts)] +pub struct Foo<'info> { + #[account(mut)] + pub dummy_a: ProgramAccount<'info, DummyA>, +} + +#[derive(Accounts)] +pub struct Bar<'info> { + #[account(mut)] + pub dummy_b: ProgramAccount<'info, DummyB>, +} + +#[account] +pub struct DummyA { + pub data: u64, +} + +#[account] +pub struct DummyB { + pub data: String, +} diff --git a/examples/composite/tests/composite.js b/examples/composite/tests/composite.js new file mode 100644 index 00000000..f1c965e3 --- /dev/null +++ b/examples/composite/tests/composite.js @@ -0,0 +1,59 @@ +const assert = require('assert'); +const anchor = require('@project-serum/anchor'); + +describe('composite', () => { + + const provider = anchor.Provider.local(); + + // Configure the client to use the local cluster. + anchor.setProvider(provider); + + it('Is initialized!', async () => { + const program = anchor.workspace.Composite; + + const dummyA = new anchor.web3.Account(); + const dummyB = new anchor.web3.Account(); + + const tx = await program.rpc.initialize({ + accounts: { + dummyA: dummyA.publicKey, + dummyB: dummyB.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [dummyA, dummyB], + instructions: [ + anchor.web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: dummyA.publicKey, + space: 8 + 8, + lamports: await provider.connection.getMinimumBalanceForRentExemption( + 8 + 8 + ), + programId: program.programId, + }), + anchor.web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: dummyB.publicKey, + space: 8 + 100, + lamports: await provider.connection.getMinimumBalanceForRentExemption( + 8 + 100 + ), + programId: program.programId, + }), + ], + }); + + await program.rpc.compositeUpdate(new anchor.BN(1234), 'hello', { + accounts: { + dummyA: dummyA.publicKey, + dummyB: dummyB.publicKey, + }, + }); + + const dummyAAccount = await program.account.dummyA(dummyA.publicKey); + const dummyBAccount = await program.account.dummyB(dummyB.publicKey); + + assert.ok(dummyAAccount.data.eq(new anchor.BN(1234))); + assert.ok(dummyBAccount.data === 'hello'); + }); +}); diff --git a/examples/sysvars/programs/sysvars/Cargo.toml b/examples/sysvars/programs/sysvars/Cargo.toml index 8f5fac8a..7dba0c30 100644 --- a/examples/sysvars/programs/sysvars/Cargo.toml +++ b/examples/sysvars/programs/sysvars/Cargo.toml @@ -16,4 +16,4 @@ cpi = ["no-entrypoint"] borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] } solana-program = "1.4.3" solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] } -anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] } +anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] } diff --git a/examples/tutorial/basic-0/programs/basic-0/Cargo.toml b/examples/tutorial/basic-0/programs/basic-0/Cargo.toml index 23a3a59a..420bedb1 100644 --- a/examples/tutorial/basic-0/programs/basic-0/Cargo.toml +++ b/examples/tutorial/basic-0/programs/basic-0/Cargo.toml @@ -16,4 +16,4 @@ cpi = ["no-entrypoint"] borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] } solana-program = "1.4.3" solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] } -anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] } +anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] } diff --git a/examples/tutorial/basic-1/programs/basic-1/Cargo.toml b/examples/tutorial/basic-1/programs/basic-1/Cargo.toml index 0989465f..c71ad662 100644 --- a/examples/tutorial/basic-1/programs/basic-1/Cargo.toml +++ b/examples/tutorial/basic-1/programs/basic-1/Cargo.toml @@ -16,4 +16,4 @@ cpi = ["no-entrypoint"] borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] } solana-program = "1.4.3" solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] } -anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] } +anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] } diff --git a/examples/tutorial/basic-2/programs/basic-2/Cargo.toml b/examples/tutorial/basic-2/programs/basic-2/Cargo.toml index f0be6a22..ef08b405 100644 --- a/examples/tutorial/basic-2/programs/basic-2/Cargo.toml +++ b/examples/tutorial/basic-2/programs/basic-2/Cargo.toml @@ -16,4 +16,4 @@ cpi = ["no-entrypoint"] borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] } solana-program = "1.4.3" solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] } -anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] } +anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] } diff --git a/examples/tutorial/basic-2/programs/basic-2/src/lib.rs b/examples/tutorial/basic-2/programs/basic-2/src/lib.rs index 2be11e42..65c389e3 100644 --- a/examples/tutorial/basic-2/programs/basic-2/src/lib.rs +++ b/examples/tutorial/basic-2/programs/basic-2/src/lib.rs @@ -1,44 +1,49 @@ #![feature(proc_macro_hygiene)] -use anchor::prelude::*; +use anchor_lang::prelude::*; -// Define the program's RPC handlers. +// Define the program's instruction handlers. #[program] mod basic_2 { use super::*; - #[access_control(not_zero(authority))] - pub fn create_root(ctx: Context, authority: Pubkey, data: u64) -> ProgramResult { - let root = &mut ctx.accounts.root; - root.authority = authority; - root.data = data; - Ok(()) - } - - pub fn update_root(ctx: Context, data: u64) -> ProgramResult { - let root = &mut ctx.accounts.root; - root.data = data; - Ok(()) - } - - pub fn create_leaf(ctx: Context, data: u64, custom: MyCustomType) -> ProgramResult { - let leaf = &mut ctx.accounts.leaf; - leaf.root = *ctx.accounts.root.to_account_info().key; - leaf.data = data; - leaf.custom = custom; - Ok(()) - } - - pub fn update_leaf( - ctx: Context, - data: u64, - custom: Option, + pub fn create_author( + ctx: Context, + authority: Pubkey, + name: String, ) -> ProgramResult { - let leaf = &mut ctx.accounts.leaf; - leaf.data = data; - if let Some(custom) = custom { - leaf.custom = custom; + let author = &mut ctx.accounts.author; + author.authority = authority; + author.name = name; + Ok(()) + } + + pub fn update_author(ctx: Context, name: String) -> ProgramResult { + let author = &mut ctx.accounts.author; + author.name = name; + Ok(()) + } + + pub fn create_book(ctx: Context, title: String, pages: Vec) -> ProgramResult { + let book = &mut ctx.accounts.book; + book.author = *ctx.accounts.author.to_account_info().key; + book.title = title; + book.pages = pages; + Ok(()) + } + + pub fn update_book( + ctx: Context, + title: Option, + pages: Option>, + ) -> ProgramResult { + let book = &mut ctx.accounts.book; + if let Some(title) = title { + book.title = title; + } + if let Some(pages) = pages { + book.pages = pages; } Ok(()) } @@ -47,66 +52,60 @@ mod basic_2 { // Define the validated accounts for each handler. #[derive(Accounts)] -pub struct CreateRoot<'info> { +pub struct CreateAuthor<'info> { #[account(init)] - pub root: ProgramAccount<'info, Root>, + pub author: ProgramAccount<'info, Author>, pub rent: Sysvar<'info, Rent>, } #[derive(Accounts)] -pub struct UpdateRoot<'info> { +pub struct UpdateAuthor<'info> { #[account(signer)] pub authority: AccountInfo<'info>, - #[account(mut, "&root.authority == authority.key")] - pub root: ProgramAccount<'info, Root>, + #[account(mut, "&author.authority == authority.key")] + pub author: ProgramAccount<'info, Author>, } #[derive(Accounts)] -pub struct CreateLeaf<'info> { - pub root: ProgramAccount<'info, Root>, +pub struct CreateBook<'info> { + #[account(signer)] + pub authority: AccountInfo<'info>, + #[account("&author.authority == authority.key")] + pub author: ProgramAccount<'info, Author>, #[account(init)] - pub leaf: ProgramAccount<'info, Leaf>, + pub book: ProgramAccount<'info, Book>, pub rent: Sysvar<'info, Rent>, } #[derive(Accounts)] -pub struct UpdateLeaf<'info> { +pub struct UpdateBook<'info> { #[account(signer)] pub authority: AccountInfo<'info>, - #[account("&root.authority == authority.key")] - pub root: ProgramAccount<'info, Root>, - #[account(mut, belongs_to = root)] - pub leaf: ProgramAccount<'info, Leaf>, + #[account("&author.authority == authority.key")] + pub author: ProgramAccount<'info, Author>, + #[account(mut, belongs_to = author)] + pub book: ProgramAccount<'info, Book>, } // Define the program owned accounts. #[account] -pub struct Root { +pub struct Author { pub authority: Pubkey, - pub data: u64, + pub name: String, } #[account] -pub struct Leaf { - pub root: Pubkey, - pub data: u64, - pub custom: MyCustomType, +pub struct Book { + pub author: Pubkey, + pub title: String, + pub pages: Vec, } // Define custom types. #[derive(AnchorSerialize, AnchorDeserialize, Clone)] -pub struct MyCustomType { - pub my_data: u64, - pub key: Pubkey, -} - -// Define any auxiliary access control checks. - -fn not_zero(authority: Pubkey) -> ProgramResult { - if authority == Pubkey::new_from_array([0; 32]) { - return Err(ProgramError::InvalidInstructionData); - } - Ok(()) +pub struct Page { + pub content: String, + pub footnote: String, } diff --git a/examples/tutorial/basic-2/tests/basic-2.js b/examples/tutorial/basic-2/tests/basic-2.js index af6a6188..767c923d 100644 --- a/examples/tutorial/basic-2/tests/basic-2.js +++ b/examples/tutorial/basic-2/tests/basic-2.js @@ -1,11 +1,114 @@ -const anchor = require('@project-serum/anchor'); +const assert = require("assert"); +//const anchor = require('@project-serum/anchor'); +const anchor = require("/home/armaniferrante/Documents/code/src/github.com/project-serum/anchor/ts"); -describe('basic-2', () => { +describe("basic-2", () => { + const provider = anchor.Provider.local(); // Configure the client to use the local cluster. - anchor.setProvider(anchor.Provider.local()); + anchor.setProvider(provider); - it('Applies constraints and access control', async () => { - const program = anchor.workspace.Basic2; + // Author for the tests. + const author = new anchor.web3.Account(); + + // Program for the tests. + const program = anchor.workspace.Basic2; + + it("Creates an author", async () => { + await program.rpc.createAuthor(provider.wallet.publicKey, "Ghost", { + accounts: { + author: author.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [author], + instructions: [ + anchor.web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: author.publicKey, + space: 8 + 1000, + lamports: await provider.connection.getMinimumBalanceForRentExemption( + 8 + 1000 + ), + programId: program.programId, + }), + ], + }); + + let authorAccount = await program.account.author(author.publicKey); + + assert.ok(authorAccount.authority.equals(provider.wallet.publicKey)); + assert.ok(authorAccount.name === "Ghost"); + }); + + it("Updates an author", async () => { + await program.rpc.updateAuthor("Updated author", { + accounts: { + author: author.publicKey, + authority: provider.wallet.publicKey, + }, + }); + + authorAccount = await program.account.author(author.publicKey); + + assert.ok(authorAccount.authority.equals(provider.wallet.publicKey)); + assert.ok(authorAccount.name === "Updated author"); + }); + + // Book params to use accross tests. + const book = new anchor.web3.Account(); + const pages = [ + { + content: "first page", + footnote: "first footnote", + }, + { + content: "second page", + footnote: "second footnote", + }, + ]; + + it("Creates a book", async () => { + await program.rpc.createBook("Book title", pages, { + accounts: { + authority: provider.wallet.publicKey, + author: author.publicKey, + book: book.publicKey, + rent: anchor.web3.SYSVAR_RENT_PUBKEY, + }, + signers: [book], + instructions: [ + anchor.web3.SystemProgram.createAccount({ + fromPubkey: provider.wallet.publicKey, + newAccountPubkey: book.publicKey, + space: 8 + 1000, + lamports: await provider.connection.getMinimumBalanceForRentExemption( + 8 + 1000 + ), + programId: program.programId, + }), + ], + }); + + const bookAccount = await program.account.book(book.publicKey); + + assert.ok(bookAccount.author.equals(author.publicKey)); + assert.ok(bookAccount.title === "Book title"); + assert.deepEqual(bookAccount.pages, pages); + }); + + it("Updates a book", async () => { + await program.rpc.updateBook("New book title", null, { + accounts: { + authority: provider.wallet.publicKey, + author: author.publicKey, + book: book.publicKey, + }, + }); + + const bookAccount = await program.account.book(book.publicKey); + + assert.ok(bookAccount.author.equals(author.publicKey)); + assert.ok(bookAccount.title === "New book title"); + assert.deepEqual(bookAccount.pages, pages); }); }); diff --git a/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml b/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml index 54de2bb3..727dfb74 100644 --- a/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml +++ b/examples/tutorial/basic-3/programs/puppet-master/Cargo.toml @@ -16,5 +16,5 @@ cpi = ["no-entrypoint"] borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] } solana-program = "1.4.3" solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] } -anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] } +anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] } puppet = { path = "../puppet", features = ["cpi"] } diff --git a/examples/tutorial/basic-3/programs/puppet/Cargo.toml b/examples/tutorial/basic-3/programs/puppet/Cargo.toml index 575f57cc..6ab1d3d2 100644 --- a/examples/tutorial/basic-3/programs/puppet/Cargo.toml +++ b/examples/tutorial/basic-3/programs/puppet/Cargo.toml @@ -16,4 +16,4 @@ cpi = ["no-entrypoint"] borsh = { git = "https://github.com/project-serum/borsh", branch = "serum", features = ["serum-program"] } solana-program = "1.4.3" solana-sdk = { version = "1.3.14", default-features = false, features = ["program"] } -anchor = { git = "https://github.com/project-serum/anchor", features = ["derive"] } +anchor-lang = { git = "https://github.com/project-serum/anchor", features = ["derive"] } diff --git a/src/account_info.rs b/src/account_info.rs new file mode 100644 index 00000000..1dcbe3e2 --- /dev/null +++ b/src/account_info.rs @@ -0,0 +1,41 @@ +use crate::{Accounts, ToAccountInfo, ToAccountInfos, ToAccountMetas}; +use solana_sdk::account_info::AccountInfo; +use solana_sdk::instruction::AccountMeta; +use solana_sdk::program_error::ProgramError; +use solana_sdk::pubkey::Pubkey; + +impl<'info> Accounts<'info> for AccountInfo<'info> { + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + ) -> Result { + if accounts.len() == 0 { + return Err(ProgramError::NotEnoughAccountKeys); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + Ok(account.clone()) + } +} + +impl<'info> ToAccountMetas for AccountInfo<'info> { + fn to_account_metas(&self) -> Vec { + let meta = match self.is_writable { + false => AccountMeta::new_readonly(*self.key, self.is_signer), + true => AccountMeta::new(*self.key, self.is_signer), + }; + vec![meta] + } +} + +impl<'info> ToAccountInfos<'info> for AccountInfo<'info> { + fn to_account_infos(&self) -> Vec> { + vec![self.clone()] + } +} + +impl<'info> ToAccountInfo<'info> for AccountInfo<'info> { + fn to_account_info(&self) -> AccountInfo<'info> { + self.clone() + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 00000000..c3e63e83 --- /dev/null +++ b/src/context.rs @@ -0,0 +1,56 @@ +use crate::Accounts; +use solana_sdk::account_info::AccountInfo; +use solana_sdk::pubkey::Pubkey; + +/// Provides non-argument inputs to the program. +pub struct Context<'a, 'b, 'c, 'info, T> { + /// Currently executing program id. + pub program_id: &'a Pubkey, + /// Deserialized accounts. + pub accounts: &'b mut T, + /// Remaining accounts given but not deserialized or validated. + pub remaining_accounts: &'c [AccountInfo<'info>], +} + +impl<'a, 'b, 'c, 'info, T> Context<'a, 'b, 'c, 'info, T> { + pub fn new( + program_id: &'a Pubkey, + accounts: &'b mut T, + remaining_accounts: &'c [AccountInfo<'info>], + ) -> Self { + Self { + accounts, + program_id, + remaining_accounts, + } + } +} + +/// Context speciying non-argument inputs for cross-program-invocations. +pub struct CpiContext<'a, 'b, 'c, 'info, T: Accounts<'info>> { + pub accounts: T, + pub program: AccountInfo<'info>, + pub signer_seeds: &'a [&'b [&'c [u8]]], +} + +impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiContext<'a, 'b, 'c, 'info, T> { + pub fn new(program: AccountInfo<'info>, accounts: T) -> Self { + Self { + accounts, + program, + signer_seeds: &[], + } + } + + pub fn new_with_signer( + accounts: T, + program: AccountInfo<'info>, + signer_seeds: &'a [&'b [&'c [u8]]], + ) -> Self { + Self { + accounts, + program, + signer_seeds, + } + } +} diff --git a/src/cpi_account.rs b/src/cpi_account.rs new file mode 100644 index 00000000..15637969 --- /dev/null +++ b/src/cpi_account.rs @@ -0,0 +1,90 @@ +use crate::{ + AccountDeserialize, AccountSerialize, Accounts, ToAccountInfo, ToAccountInfos, ToAccountMetas, +}; +use solana_sdk::account_info::AccountInfo; +use solana_sdk::instruction::AccountMeta; +use solana_sdk::program_error::ProgramError; +use solana_sdk::pubkey::Pubkey; +use std::ops::{Deref, DerefMut}; + +/// Container for any account *not* owned by the current program. +#[derive(Clone)] +pub struct CpiAccount<'a, T: AccountSerialize + AccountDeserialize + Clone> { + info: AccountInfo<'a>, + account: T, +} + +impl<'a, T: AccountSerialize + AccountDeserialize + Clone> CpiAccount<'a, T> { + pub fn new(info: AccountInfo<'a>, account: T) -> CpiAccount<'a, T> { + Self { info, account } + } + + /// Deserializes the given `info` into a `CpiAccount`. + pub fn try_from(info: &AccountInfo<'a>) -> Result, ProgramError> { + let mut data: &[u8] = &info.try_borrow_data()?; + Ok(CpiAccount::new( + info.clone(), + T::try_deserialize(&mut data)?, + )) + } +} + +impl<'info, T> Accounts<'info> for CpiAccount<'info, T> +where + T: AccountSerialize + AccountDeserialize + Clone, +{ + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + ) -> Result { + if accounts.len() == 0 { + return Err(ProgramError::NotEnoughAccountKeys); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + let pa = CpiAccount::try_from(account)?; + Ok(pa) + } +} + +impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas + for CpiAccount<'info, T> +{ + fn to_account_metas(&self) -> Vec { + let meta = match self.info.is_writable { + false => AccountMeta::new_readonly(*self.info.key, self.info.is_signer), + true => AccountMeta::new(*self.info.key, self.info.is_signer), + }; + vec![meta] + } +} + +impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'info> + for CpiAccount<'info, T> +{ + fn to_account_infos(&self) -> Vec> { + vec![self.info.clone()] + } +} + +impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'info> + for CpiAccount<'info, T> +{ + fn to_account_info(&self) -> AccountInfo<'info> { + self.info.clone() + } +} + +impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for CpiAccount<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.account + } +} + +impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for CpiAccount<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.account + } +} diff --git a/src/lib.rs b/src/lib.rs index 7f4becef..0f6eb375 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,42 @@ +//! Anchor ⚓ is a framework for Solana's Sealevel runtime providing several +//! convenient developer tools. +//! +//! - Rust eDSL for writing safe, secure, and high level Solana programs +//! - [IDL](https://en.wikipedia.org/wiki/Interface_description_language) specification +//! - TypeScript package for generating clients from IDL +//! - CLI and workspace management for developing complete applications +//! +//! If you're familiar with developing in Ethereum's +//! [Solidity](https://docs.soliditylang.org/en/v0.7.4/), +//! [Truffle](https://www.trufflesuite.com/), +//! [web3.js](https://github.com/ethereum/web3.js) or Parity's +//! [Ink!](https://github.com/paritytech/ink), then the experience will be +//! familiar. Although the syntax and semantics are targeted at Solana, the high +//! level workflow of writing RPC request handlers, emitting an IDL, and +//! generating clients from IDL is the same. +//! +//! For detailed tutorials and examples on how to use Anchor, see the guided +//! [tutorials](https://project-serum.github.io/anchor) or examples in the GitHub +//! [repository](https://github.com/project-serum/anchor). +//! +//! Presented here are the Rust primitives for building on Solana. + use solana_sdk::account_info::AccountInfo; use solana_sdk::instruction::AccountMeta; use solana_sdk::program_error::ProgramError; use solana_sdk::pubkey::Pubkey; use std::io::Write; -use std::ops::{Deref, DerefMut}; +mod account_info; +mod context; +mod cpi_account; +mod program_account; +mod sysvar; + +pub use crate::context::{Context, CpiContext}; +pub use crate::cpi_account::CpiAccount; +pub use crate::program_account::ProgramAccount; +pub use crate::sysvar::Sysvar; pub use anchor_attribute_access_control::access_control; pub use anchor_attribute_account::account; pub use anchor_attribute_program::program; @@ -12,7 +44,7 @@ pub use anchor_derive_accounts::Accounts; /// Default serialization format for anchor instructions and accounts. pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorSerialize}; -/// A data structure of Solana accounts that can be deserialized from the input +/// A data structure of accounts that can be deserialized from the input /// of a Solana program. Due to the freewheeling nature of the accounts array, /// implementations of this trait should perform any and all constraint checks /// (in addition to any done within `AccountDeserialize`) on accounts to ensure @@ -21,7 +53,18 @@ pub use borsh::{BorshDeserialize as AnchorDeserialize, BorshSerialize as AnchorS pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized { fn try_accounts( program_id: &Pubkey, - from: &mut &[AccountInfo<'info>], + accounts: &mut &[AccountInfo<'info>], + ) -> Result; +} + +/// A data structure of accounts providing a one time deserialization upon +/// initialization, i.e., when the data array for a given account is zeroed. +/// For all subsequent deserializations, it's expected that +/// [Accounts](trait.Accounts.html) is used. +pub trait AccountsInit<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized { + fn try_accounts_init( + program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], ) -> Result; } @@ -67,177 +110,13 @@ pub trait AccountDeserialize: Sized { fn try_deserialize_unchecked(buf: &mut &[u8]) -> Result; } -/// Container for a serializable `account`. Use this to reference any account -/// owned by the currently executing program. -#[derive(Clone)] -pub struct ProgramAccount<'a, T: AccountSerialize + AccountDeserialize + Clone> { - info: AccountInfo<'a>, - account: T, -} - -impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> { - pub fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> { - Self { info, account } - } - - /// Deserializes the given `info` into a `ProgramAccount`. - pub fn try_from(info: &AccountInfo<'a>) -> Result, ProgramError> { - let mut data: &[u8] = &info.try_borrow_data()?; - Ok(ProgramAccount::new( - info.clone(), - T::try_deserialize(&mut data)?, - )) - } - - /// Deserializes the zero-initialized `info` into a `ProgramAccount` without - /// checking the account type. This should only be used upon program account - /// initialization (since the entire account data array is zeroed and thus - /// no account type is set). - pub fn try_from_init(info: &AccountInfo<'a>) -> Result, ProgramError> { - let mut data: &[u8] = &info.try_borrow_data()?; - - // The discriminator should be zero, since we're initializing. - let mut disc_bytes = [0u8; 8]; - disc_bytes.copy_from_slice(&data[..8]); - let discriminator = u64::from_le_bytes(disc_bytes); - if discriminator != 0 { - return Err(ProgramError::InvalidAccountData); - } - - Ok(ProgramAccount::new( - info.clone(), - T::try_deserialize_unchecked(&mut data)?, - )) - } -} - -impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'info> - for ProgramAccount<'info, T> -{ - fn to_account_info(&self) -> AccountInfo<'info> { - self.info.clone() - } -} - -impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for ProgramAccount<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.account - } -} - -impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for ProgramAccount<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.account - } -} - -/// Similar to `ProgramAccount`, but to reference any account *not* owned by -/// the current program. -pub type CpiAccount<'a, T> = ProgramAccount<'a, T>; - -/// Container for a Solana sysvar. -pub struct Sysvar<'info, T: solana_sdk::sysvar::Sysvar> { - info: AccountInfo<'info>, - account: T, -} - -impl<'info, T: solana_sdk::sysvar::Sysvar> Sysvar<'info, T> { - pub fn from_account_info( - acc_info: &AccountInfo<'info>, - ) -> Result, ProgramError> { - Ok(Sysvar { - info: acc_info.clone(), - account: T::from_account_info(&acc_info)?, - }) - } -} - -impl<'a, T: solana_sdk::sysvar::Sysvar> Deref for Sysvar<'a, T> { - type Target = T; - - fn deref(&self) -> &Self::Target { - &self.account - } -} - -impl<'a, T: solana_sdk::sysvar::Sysvar> DerefMut for Sysvar<'a, T> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.account - } -} - -impl<'info, T: solana_sdk::sysvar::Sysvar> ToAccountInfo<'info> for Sysvar<'info, T> { - fn to_account_info(&self) -> AccountInfo<'info> { - self.info.clone() - } -} - -impl<'info> ToAccountInfo<'info> for AccountInfo<'info> { - fn to_account_info(&self) -> AccountInfo<'info> { - self.clone() - } -} - -/// Provides non-argument inputs to the program. -pub struct Context<'a, 'b, 'c, 'info, T> { - /// Deserialized accounts. - pub accounts: &'a mut T, - /// Currently executing program id. - pub program_id: &'b Pubkey, - /// Remaining accounts given but not deserialized or validated. - pub remaining_accounts: &'c [AccountInfo<'info>], -} - -impl<'a, 'b, 'c, 'info, T> Context<'a, 'b, 'c, 'info, T> { - pub fn new( - accounts: &'a mut T, - program_id: &'b Pubkey, - remaining_accounts: &'c [AccountInfo<'info>], - ) -> Self { - Self { - accounts, - program_id, - remaining_accounts, - } - } -} - -/// Context speciying non-argument inputs for cross-program-invocations. -pub struct CpiContext<'a, 'b, 'c, 'info, T: Accounts<'info>> { - pub accounts: T, - pub program: AccountInfo<'info>, - pub signer_seeds: &'a [&'b [&'c [u8]]], -} - -impl<'a, 'b, 'c, 'info, T: Accounts<'info>> CpiContext<'a, 'b, 'c, 'info, T> { - pub fn new(program: AccountInfo<'info>, accounts: T) -> Self { - Self { - accounts, - program, - signer_seeds: &[], - } - } - - pub fn new_with_signer( - accounts: T, - program: AccountInfo<'info>, - signer_seeds: &'a [&'b [&'c [u8]]], - ) -> Self { - Self { - accounts, - program, - signer_seeds, - } - } -} - +/// The prelude contains all commonly used components of the crate. +/// All programs should include it via `anchor_lang::prelude::*;`. pub mod prelude { pub use super::{ access_control, account, program, AccountDeserialize, AccountSerialize, Accounts, - AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext, ProgramAccount, - Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas, + AccountsInit, AnchorDeserialize, AnchorSerialize, Context, CpiAccount, CpiContext, + ProgramAccount, Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas, }; pub use solana_program::msg; diff --git a/src/program_account.rs b/src/program_account.rs new file mode 100644 index 00000000..b62a416c --- /dev/null +++ b/src/program_account.rs @@ -0,0 +1,131 @@ +use crate::{ + AccountDeserialize, AccountSerialize, Accounts, AccountsInit, ToAccountInfo, ToAccountInfos, + ToAccountMetas, +}; +use solana_sdk::account_info::AccountInfo; +use solana_sdk::instruction::AccountMeta; +use solana_sdk::program_error::ProgramError; +use solana_sdk::pubkey::Pubkey; +use std::ops::{Deref, DerefMut}; + +/// Container for a serializable `account`. Use this to reference any account +/// owned by the currently executing program. +#[derive(Clone)] +pub struct ProgramAccount<'a, T: AccountSerialize + AccountDeserialize + Clone> { + info: AccountInfo<'a>, + account: T, +} + +impl<'a, T: AccountSerialize + AccountDeserialize + Clone> ProgramAccount<'a, T> { + pub fn new(info: AccountInfo<'a>, account: T) -> ProgramAccount<'a, T> { + Self { info, account } + } + + /// Deserializes the given `info` into a `ProgramAccount`. + pub fn try_from(info: &AccountInfo<'a>) -> Result, ProgramError> { + let mut data: &[u8] = &info.try_borrow_data()?; + Ok(ProgramAccount::new( + info.clone(), + T::try_deserialize(&mut data)?, + )) + } + + /// Deserializes the zero-initialized `info` into a `ProgramAccount` without + /// checking the account type. This should only be used upon program account + /// initialization (since the entire account data array is zeroed and thus + /// no account type is set). + pub fn try_from_init(info: &AccountInfo<'a>) -> Result, ProgramError> { + let mut data: &[u8] = &info.try_borrow_data()?; + + // The discriminator should be zero, since we're initializing. + let mut disc_bytes = [0u8; 8]; + disc_bytes.copy_from_slice(&data[..8]); + let discriminator = u64::from_le_bytes(disc_bytes); + if discriminator != 0 { + return Err(ProgramError::InvalidAccountData); + } + + Ok(ProgramAccount::new( + info.clone(), + T::try_deserialize_unchecked(&mut data)?, + )) + } +} + +impl<'info, T> Accounts<'info> for ProgramAccount<'info, T> +where + T: AccountSerialize + AccountDeserialize + Clone, +{ + fn try_accounts( + program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + ) -> Result { + if accounts.len() == 0 { + return Err(ProgramError::NotEnoughAccountKeys); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + let pa = ProgramAccount::try_from(account)?; + if pa.info.owner != program_id {} + Ok(pa) + } +} + +impl<'info, T> AccountsInit<'info> for ProgramAccount<'info, T> +where + T: AccountSerialize + AccountDeserialize + Clone, +{ + fn try_accounts_init( + program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + ) -> Result { + if accounts.len() == 0 { + return Err(ProgramError::NotEnoughAccountKeys); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + ProgramAccount::try_from_init(account) + } +} + +impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas + for ProgramAccount<'info, T> +{ + fn to_account_metas(&self) -> Vec { + let meta = match self.info.is_writable { + false => AccountMeta::new_readonly(*self.info.key, self.info.is_signer), + true => AccountMeta::new(*self.info.key, self.info.is_signer), + }; + vec![meta] + } +} + +impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfos<'info> + for ProgramAccount<'info, T> +{ + fn to_account_infos(&self) -> Vec> { + vec![self.info.clone()] + } +} + +impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountInfo<'info> + for ProgramAccount<'info, T> +{ + fn to_account_info(&self) -> AccountInfo<'info> { + self.info.clone() + } +} + +impl<'a, T: AccountSerialize + AccountDeserialize + Clone> Deref for ProgramAccount<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.account + } +} + +impl<'a, T: AccountSerialize + AccountDeserialize + Clone> DerefMut for ProgramAccount<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.account + } +} diff --git a/src/sysvar.rs b/src/sysvar.rs new file mode 100644 index 00000000..ce86418b --- /dev/null +++ b/src/sysvar.rs @@ -0,0 +1,69 @@ +use crate::{Accounts, ToAccountInfo, ToAccountInfos, ToAccountMetas}; +use solana_sdk::account_info::AccountInfo; +use solana_sdk::instruction::AccountMeta; +use solana_sdk::program_error::ProgramError; +use solana_sdk::pubkey::Pubkey; +use std::ops::{Deref, DerefMut}; + +/// Container for sysvars. +pub struct Sysvar<'info, T: solana_sdk::sysvar::Sysvar> { + info: AccountInfo<'info>, + account: T, +} + +impl<'info, T: solana_sdk::sysvar::Sysvar> Sysvar<'info, T> { + pub fn from_account_info( + acc_info: &AccountInfo<'info>, + ) -> Result, ProgramError> { + Ok(Sysvar { + info: acc_info.clone(), + account: T::from_account_info(&acc_info)?, + }) + } +} + +impl<'info, T: solana_sdk::sysvar::Sysvar> Accounts<'info> for Sysvar<'info, T> { + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + ) -> Result { + if accounts.len() == 0 { + return Err(ProgramError::NotEnoughAccountKeys); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + Sysvar::from_account_info(account) + } +} + +impl<'info, T: solana_sdk::sysvar::Sysvar> ToAccountMetas for Sysvar<'info, T> { + fn to_account_metas(&self) -> Vec { + vec![AccountMeta::new_readonly(*self.info.key, false)] + } +} + +impl<'info, T: solana_sdk::sysvar::Sysvar> ToAccountInfos<'info> for Sysvar<'info, T> { + fn to_account_infos(&self) -> Vec> { + vec![self.info.clone()] + } +} + +impl<'a, T: solana_sdk::sysvar::Sysvar> Deref for Sysvar<'a, T> { + type Target = T; + + fn deref(&self) -> &Self::Target { + &self.account + } +} + +impl<'a, T: solana_sdk::sysvar::Sysvar> DerefMut for Sysvar<'a, T> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.account + } +} + +impl<'info, T: solana_sdk::sysvar::Sysvar> ToAccountInfo<'info> for Sysvar<'info, T> { + fn to_account_info(&self) -> AccountInfo<'info> { + self.info.clone() + } +} diff --git a/syn/src/codegen/accounts.rs b/syn/src/codegen/accounts.rs index 639a7fae..2ca3b940 100644 --- a/syn/src/codegen/accounts.rs +++ b/syn/src/codegen/accounts.rs @@ -1,39 +1,44 @@ use crate::{ - AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, ConstraintOwner, - ConstraintRentExempt, ConstraintSigner, Field, SysvarTy, Ty, + AccountField, AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, + ConstraintOwner, ConstraintRentExempt, ConstraintSigner, Field, Ty, }; use quote::quote; pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { - // Extract out each account info. - let acc_infos: Vec = accs - .fields - .iter() - .map(|f: &Field| { - let name = &f.ident; - quote! { - let #name = next_account_info(acc_infos)?; - } - }) - .collect(); - let acc_infos_len = { - let acc_infos_len = acc_infos.len(); - quote! { - #acc_infos_len - } - }; - // Deserialization for each field. let deser_fields: Vec = accs .fields .iter() - .map(generate_field_deserialization) + .map(|af: &AccountField| match af { + AccountField::AccountsStruct(s) => { + let name = &s.ident; + quote! { + let #name = Accounts::try_accounts(program_id, accounts)?; + } + } + AccountField::Field(f) => { + let name = f.typed_ident(); + match f.is_init { + false => quote! { + let #name = Accounts::try_accounts(program_id, accounts)?; + }, + true => quote! { + let #name = AccountsInit::try_accounts_init(program_id, accounts)?; + }, + } + } + }) .collect(); // Constraint checks for each account fields. let access_checks: Vec = accs .fields .iter() + // TODO: allow constraints on composite fields. + .filter_map(|af: &AccountField| match af { + AccountField::AccountsStruct(_) => None, + AccountField::Field(f) => Some(f), + }) .map(|f: &Field| { let checks: Vec = f .constraints @@ -50,8 +55,11 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { let return_tys: Vec = accs .fields .iter() - .map(|f: &Field| { - let name = &f.ident; + .map(|f: &AccountField| { + let name = match f { + AccountField::AccountsStruct(s) => &s.ident, + AccountField::Field(f) => &f.ident, + }; quote! { #name } @@ -62,26 +70,36 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { let on_save: Vec = accs .fields .iter() - .map(|f: &Field| { - let ident = &f.ident; - let info = match f.ty { - Ty::AccountInfo => quote! { #ident }, - 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)?; + .map(|af: &AccountField| { + match af { + AccountField::AccountsStruct(s) => { + let name = &s.ident; + quote! { + self.#name.exit(program_id); } - }, + } + AccountField::Field(f) => { + let ident = &f.ident; + let info = match f.ty { + Ty::AccountInfo => quote! { #ident }, + 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)?; + } + }, + } + } } }) .collect(); @@ -90,10 +108,13 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { let to_acc_infos: Vec = accs .fields .iter() - .map(|f: &Field| { - let name = &f.ident; + .map(|f: &AccountField| { + let name = match f { + AccountField::AccountsStruct(s) => &s.ident, + AccountField::Field(f) => &f.ident, + }; quote! { - self.#name.to_account_info() + account_infos.extend(self.#name.to_account_infos()); } }) .collect(); @@ -102,19 +123,13 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { let to_acc_metas: Vec = accs .fields .iter() - .map(|f: &Field| { - let name = &f.ident; - let is_signer = match f.is_signer { - false => quote! { false }, - true => quote! { true }, + .map(|f: &AccountField| { + let name = match f { + AccountField::AccountsStruct(s) => &s.ident, + AccountField::Field(f) => &f.ident, }; - match f.is_mut { - false => quote! { - AccountMeta::new_readonly(*self.#name.to_account_info().key, #is_signer) - }, - true => quote! { - AccountMeta::new(*self.#name.to_account_info().key, #is_signer) - }, + quote! { + account_metas.extend(self.#name.to_account_metas()); } }) .collect(); @@ -130,15 +145,7 @@ 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, remaining_accounts: &mut &[AccountInfo<'info>]) -> Result { - let acc_infos = &mut remaining_accounts.iter(); - - // Pull out each account info from the `accounts` slice. - #(#acc_infos)* - - // Move the remaining_accounts cursor to the iterator end. - *remaining_accounts = &remaining_accounts[#acc_infos_len..]; - + fn try_accounts(program_id: &Pubkey, accounts: &mut &[AccountInfo<'info>]) -> Result { // Deserialize each account. #(#deser_fields)* @@ -154,17 +161,22 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { impl#combined_generics ToAccountInfos#trait_generics for #name#strct_generics { fn to_account_infos(&self) -> Vec> { - vec![ - #(#to_acc_infos),* - ] + let mut account_infos = vec![]; + + #(#to_acc_infos)* + + account_infos } } impl#combined_generics ToAccountMetas for #name#strct_generics { fn to_account_metas(&self) -> Vec { - vec![ - #(#to_acc_metas),* - ] + let mut account_metas = vec![]; + + #(#to_acc_metas)* + + + account_metas } } @@ -177,73 +189,6 @@ pub fn generate(accs: AccountsStruct) -> proc_macro2::TokenStream { } } -pub fn generate_field_deserialization(f: &Field) -> proc_macro2::TokenStream { - let ident = &f.ident; - let assign_ty = match &f.ty { - Ty::AccountInfo => quote! { - let #ident = #ident.clone(); - }, - Ty::ProgramAccount(acc) => { - let account_struct = &acc.account_ident; - match f.is_init { - false => quote! { - let #ident: ProgramAccount<#account_struct> = ProgramAccount::try_from(#ident)?; - }, - true => quote! { - let #ident: ProgramAccount<#account_struct> = ProgramAccount::try_from_init(#ident)?; - }, - } - } - Ty::CpiAccount(acc) => { - let account_struct = &acc.account_ident; - match f.is_init { - false => quote! { - let #ident: CpiAccount<#account_struct> = CpiAccount::try_from(#ident)?; - }, - true => quote! { - let #ident: CpiAccount<#account_struct> = CpiAccount::try_from_init(#ident)?; - }, - } - } - Ty::Sysvar(sysvar) => match sysvar { - SysvarTy::Clock => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::Rent => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::EpochSchedule => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::Fees => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::RecentBlockHashes => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::SlotHashes => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::SlotHistory => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::StakeHistory => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::Instructions => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - SysvarTy::Rewards => quote! { - let #ident: Sysvar = Sysvar::from_account_info(#ident)?; - }, - }, - }; - - quote! { - #assign_ty - } -} - pub fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream { match c { Constraint::BelongsTo(c) => generate_constraint_belongs_to(f, c), diff --git a/syn/src/codegen/program.rs b/syn/src/codegen/program.rs index 24b948f9..635fe425 100644 --- a/syn/src/codegen/program.rs +++ b/syn/src/codegen/program.rs @@ -49,7 +49,7 @@ pub fn generate_dispatch(program: &Program) -> proc_macro2::TokenStream { let mut remaining_accounts: &[AccountInfo] = accounts; let mut accounts = #anchor::try_accounts(program_id, &mut remaining_accounts)?; #program_name::#rpc_name( - Context::new(&mut accounts, program_id, remaining_accounts), + Context::new(program_id, &mut accounts, remaining_accounts), #(#rpc_arg_names),* )?; accounts.exit(program_id) diff --git a/syn/src/idl.rs b/syn/src/idl.rs index b3519286..0e069445 100644 --- a/syn/src/idl.rs +++ b/syn/src/idl.rs @@ -80,6 +80,7 @@ pub enum IdlType { PublicKey, Defined(String), Option(Box), + Vec(Box), } #[derive(Debug, Serialize, Deserialize)] @@ -107,7 +108,17 @@ impl std::str::FromStr for IdlType { "String" => IdlType::String, "Pubkey" => IdlType::PublicKey, _ => match s.to_string().strip_prefix("Option<") { - None => IdlType::Defined(s.to_string()), + None => match s.to_string().strip_prefix("Vec<") { + None => IdlType::Defined(s.to_string()), + Some(inner) => { + let inner_ty = Self::from_str( + inner + .strip_suffix(">") + .ok_or(anyhow::anyhow!("Invalid option"))?, + )?; + IdlType::Vec(Box::new(inner_ty)) + } + }, Some(inner) => { let inner_ty = Self::from_str( inner diff --git a/syn/src/lib.rs b/syn/src/lib.rs index 76bef154..8a3c1502 100644 --- a/syn/src/lib.rs +++ b/syn/src/lib.rs @@ -1,5 +1,13 @@ //! DSL syntax tokens. +#[cfg(feature = "idl")] +use crate::idl::IdlAccount; +use anyhow::Result; +#[cfg(feature = "idl")] +use heck::MixedCase; +use quote::quote; +use std::collections::HashMap; + pub mod codegen; #[cfg(feature = "idl")] pub mod idl; @@ -27,17 +35,18 @@ pub struct RpcArg { pub raw_arg: syn::PatType, } +#[derive(Debug)] pub struct AccountsStruct { // Name of the accounts struct. pub ident: syn::Ident, // Generics + lifetimes on the accounts struct. pub generics: syn::Generics, // Fields on the accounts struct. - pub fields: Vec, + pub fields: Vec, } impl AccountsStruct { - pub fn new(strct: syn::ItemStruct, fields: Vec) -> Self { + pub fn new(strct: syn::ItemStruct, fields: Vec) -> Self { let ident = strct.ident.clone(); let generics = strct.generics.clone(); Self { @@ -48,18 +57,73 @@ impl AccountsStruct { } // Returns all program owned accounts in the Accounts struct. - pub fn account_tys(&self) -> Vec { + // + // `global_accs` is given to "link" account types that are embedded + // in each other. + pub fn account_tys( + &self, + global_accs: &HashMap, + ) -> Result> { + let mut tys = vec![]; + for f in &self.fields { + match f { + AccountField::Field(f) => { + if let Ty::ProgramAccount(pty) = &f.ty { + tys.push(pty.account_ident.to_string()); + } + } + AccountField::AccountsStruct(comp_f) => { + let accs = global_accs.get(&comp_f.symbol).ok_or(anyhow::format_err!( + "Invalid account type: {}", + comp_f.symbol + ))?; + tys.extend(accs.account_tys(global_accs)?); + } + } + } + Ok(tys) + } + + #[cfg(feature = "idl")] + pub fn idl_accounts(&self, global_accs: &HashMap) -> Vec { self.fields .iter() - .filter_map(|f| match &f.ty { - Ty::ProgramAccount(pty) => Some(pty.account_ident.to_string()), - _ => None, + .flat_map(|acc: &AccountField| match acc { + AccountField::AccountsStruct(comp_f) => { + let accs_strct = global_accs + .get(&comp_f.symbol) + .expect("Could not reslve Accounts symbol"); + accs_strct.idl_accounts(global_accs) + } + AccountField::Field(acc) => vec![IdlAccount { + name: acc.ident.to_string().to_mixed_case(), + is_mut: acc.is_mut, + is_signer: acc.is_signer, + }], }) .collect::>() } } +#[derive(Debug)] +pub enum AccountField { + // Use a `String` instead of the `AccountsStruct` because all + // accounts structs aren't visible to a single derive macro. + // + // When we need the global context, we fill in the String with the + // appropriate values. See, `account_tys` as an example. + AccountsStruct(CompositeField), // Composite + Field(Field), // Primitive +} + +#[derive(Debug)] +pub struct CompositeField { + pub ident: syn::Ident, + pub symbol: String, +} + // An account in the accounts struct. +#[derive(Debug)] pub struct Field { pub ident: syn::Ident, pub ty: Ty, @@ -69,16 +133,59 @@ pub struct Field { pub is_init: bool, } +impl Field { + pub fn typed_ident(&self) -> proc_macro2::TokenStream { + let name = &self.ident; + + let ty = match &self.ty { + Ty::AccountInfo => quote! { AccountInfo }, + Ty::ProgramAccount(ty) => { + let account = &ty.account_ident; + quote! { + ProgramAccount<#account> + } + } + Ty::CpiAccount(ty) => { + let account = &ty.account_ident; + quote! { + CpiAccount<#account> + } + } + Ty::Sysvar(ty) => { + let account = match ty { + SysvarTy::Clock => quote! {Clock}, + SysvarTy::Rent => quote! {Rent}, + SysvarTy::EpochSchedule => quote! {EpochSchedule}, + SysvarTy::Fees => quote! {Fees}, + SysvarTy::RecentBlockHashes => quote! {RecentBlockHashes}, + SysvarTy::SlotHashes => quote! {SlotHashes}, + SysvarTy::SlotHistory => quote! {SlotHistory}, + SysvarTy::StakeHistory => quote! {StakeHistory}, + SysvarTy::Instructions => quote! {Instructions}, + SysvarTy::Rewards => quote! {Rewards}, + }; + quote! { + Sysvar<#account> + } + } + }; + + quote! { + #name: #ty + } + } +} + // A type of an account field. -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub enum Ty { AccountInfo, ProgramAccount(ProgramAccountTy), - Sysvar(SysvarTy), CpiAccount(CpiAccountTy), + Sysvar(SysvarTy), } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub enum SysvarTy { Clock, Rent, @@ -92,19 +199,20 @@ pub enum SysvarTy { Rewards, } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub struct ProgramAccountTy { // The struct type of the account. pub account_ident: syn::Ident, } -#[derive(PartialEq)] +#[derive(Debug, PartialEq)] pub struct CpiAccountTy { // The struct type of the account. pub account_ident: syn::Ident, } // An access control constraint for an account. +#[derive(Debug)] pub enum Constraint { Signer(ConstraintSigner), BelongsTo(ConstraintBelongsTo), @@ -113,21 +221,26 @@ pub enum Constraint { RentExempt(ConstraintRentExempt), } +#[derive(Debug)] pub struct ConstraintBelongsTo { pub join_target: proc_macro2::Ident, } +#[derive(Debug)] pub struct ConstraintSigner {} +#[derive(Debug)] pub struct ConstraintLiteral { pub tokens: proc_macro2::TokenStream, } +#[derive(Debug)] pub enum ConstraintOwner { Program, Skip, } +#[derive(Debug)] pub enum ConstraintRentExempt { Enforce, Skip, diff --git a/syn/src/parser/accounts.rs b/syn/src/parser/accounts.rs index a78cd2e8..0d84082b 100644 --- a/syn/src/parser/accounts.rs +++ b/syn/src/parser/accounts.rs @@ -1,6 +1,7 @@ use crate::{ - AccountsStruct, Constraint, ConstraintBelongsTo, ConstraintLiteral, ConstraintOwner, - ConstraintRentExempt, ConstraintSigner, CpiAccountTy, Field, ProgramAccountTy, SysvarTy, Ty, + AccountField, AccountsStruct, CompositeField, Constraint, ConstraintBelongsTo, + ConstraintLiteral, ConstraintOwner, ConstraintRentExempt, ConstraintSigner, CpiAccountTy, + Field, ProgramAccountTy, SysvarTy, Ty, }; pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct { @@ -9,7 +10,7 @@ pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct { _ => panic!("invalid input"), }; - let fields: Vec = fields + let fields: Vec = fields .named .iter() .map(|f: &syn::Field| { @@ -40,21 +41,34 @@ pub fn parse(strct: &syn::ItemStruct) -> AccountsStruct { AccountsStruct::new(strct.clone(), fields) } -// Parses an inert #[anchor] attribute specifying the DSL. -fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> Field { +fn parse_field(f: &syn::Field, anchor: Option<&syn::Attribute>) -> AccountField { let ident = f.ident.clone().unwrap(); - let ty = parse_ty(f); - let (constraints, is_mut, is_signer, is_init) = match anchor { - None => (vec![], false, false, false), - Some(anchor) => parse_constraints(anchor, &ty), - }; - Field { - ident, - ty, - constraints, - is_mut, - is_signer, - is_init, + match is_field_primitive(f) { + true => { + let ty = parse_ty(f); + let (constraints, is_mut, is_signer, is_init) = match anchor { + None => (vec![], false, false, false), + Some(anchor) => parse_constraints(anchor, &ty), + }; + AccountField::Field(Field { + ident, + ty, + constraints, + is_mut, + is_signer, + is_init, + }) + } + false => AccountField::AccountsStruct(CompositeField { + ident, + symbol: ident_string(f), + }), + } +} +fn is_field_primitive(f: &syn::Field) -> bool { + match ident_string(f).as_str() { + "ProgramAccount" | "CpiAccount" | "Sysvar" | "AccountInfo" => true, + _ => false, } } @@ -63,10 +77,7 @@ fn parse_ty(f: &syn::Field) -> Ty { syn::Type::Path(ty_path) => ty_path.path.clone(), _ => panic!("invalid account syntax"), }; - // TODO: allow segmented paths. - assert!(path.segments.len() == 1); - let segments = &path.segments[0]; - match segments.ident.to_string().as_str() { + match ident_string(f).as_str() { "ProgramAccount" => Ty::ProgramAccount(parse_program_account(&path)), "CpiAccount" => Ty::CpiAccount(parse_cpi_account(&path)), "Sysvar" => Ty::Sysvar(parse_sysvar(&path)), @@ -75,6 +86,17 @@ fn parse_ty(f: &syn::Field) -> Ty { } } +fn ident_string(f: &syn::Field) -> String { + let path = match &f.ty { + syn::Type::Path(ty_path) => ty_path.path.clone(), + _ => panic!("invalid account syntax"), + }; + // TODO: allow segmented paths. + assert!(path.segments.len() == 1); + let segments = &path.segments[0]; + segments.ident.to_string() +} + fn parse_cpi_account(path: &syn::Path) -> CpiAccountTy { let account_ident = parse_account(path); CpiAccountTy { account_ident } diff --git a/syn/src/parser/file.rs b/syn/src/parser/file.rs index 389131fb..ee6bc949 100644 --- a/syn/src/parser/file.rs +++ b/syn/src/parser/file.rs @@ -29,7 +29,7 @@ pub fn parse(filename: impl AsRef) -> Result { let mut acc_names = HashSet::new(); for accs_strct in accs.values() { - for a in accs_strct.account_tys() { + for a in accs_strct.account_tys(&accs)? { acc_names.insert(a); } } @@ -56,15 +56,7 @@ pub fn parse(filename: impl AsRef) -> Result { .collect::>(); // todo: don't unwrap let accounts_strct = accs.get(&rpc.anchor_ident.to_string()).unwrap(); - let accounts = accounts_strct - .fields - .iter() - .map(|acc| IdlAccount { - name: acc.ident.to_string().to_mixed_case(), - is_mut: acc.is_mut, - is_signer: acc.is_signer, - }) - .collect::>(); + let accounts = accounts_strct.idl_accounts(&accs); IdlInstruction { name: rpc.ident.to_string().to_mixed_case(), accounts, @@ -125,7 +117,7 @@ fn parse_program_mod(f: &syn::File) -> syn::ItemMod { mods[0].clone() } -// Parse all structs deriving the `Accounts` macro. +// Parse all structs implementing the `Accounts` trait. fn parse_accounts(f: &syn::File) -> HashMap { f.items .iter() @@ -139,6 +131,8 @@ fn parse_accounts(f: &syn::File) -> HashMap { } None } + // TODO: parse manual implementations. Currently we only look + // for derives. _ => None, }) .collect() diff --git a/ts/package.json b/ts/package.json index 155ee7cd..882a9dbb 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "@project-serum/anchor", - "version": "0.0.0-alpha.3", + "version": "0.0.0-alpha.4", "description": "Anchor client", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/ts/src/coder.ts b/ts/src/coder.ts index 0348e6d2..95b3f844 100644 --- a/ts/src/coder.ts +++ b/ts/src/coder.ts @@ -126,7 +126,20 @@ class IdlCoder { // TODO: all the other types that need to be exported by the borsh package. default: { // @ts-ignore - if (field.type.option) { + if (field.type.vec) { + return borsh.vec( + IdlCoder.fieldLayout( + { + name: undefined, + // @ts-ignore + type: field.type.vec, + }, + types + ), + fieldName + ); + // @ts-ignore + } else if (field.type.option) { return borsh.option( IdlCoder.fieldLayout( { diff --git a/ts/src/idl.ts b/ts/src/idl.ts index a11503bb..96b71dbc 100644 --- a/ts/src/idl.ts +++ b/ts/src/idl.ts @@ -54,9 +54,14 @@ type IdlType = | "bytes" | "string" | "publicKey" + | IdlTypeVec | IdlTypeOption | IdlTypeDefined; +export type IdlTypeVec = { + vec: IdlType; +}; + export type IdlTypeOption = { option: IdlType; };