diff --git a/CHANGELOG.md b/CHANGELOG.md index b2af2696..5148fc18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ incremented for features. * cli: `target/types` directory now created on build to store a TypeScript types file for each program's IDL ([#795](https://github.com/project-serum/anchor/pull/795)). * ts: `Program` can now be typed with an IDL type ([#795](https://github.com/project-serum/anchor/pull/795)). * lang: Add `mint::freeze_authority` keyword for mint initialization within `#[derive(Accounts)]` ([#835](https://github.com/project-serum/anchor/pull/835)). +* lang: Add `AccountLoader` type for `zero_copy` accounts with support for CPI ([#792](https://github.com/project-serum/anchor/pull/792)). ### Breaking diff --git a/lang/src/lib.rs b/lang/src/lib.rs index b9d0574a..a3c063b4 100644 --- a/lang/src/lib.rs +++ b/lang/src/lib.rs @@ -44,6 +44,7 @@ mod error; #[doc(hidden)] pub mod idl; mod loader; +mod loader_account; mod program; mod program_account; mod signer; @@ -65,6 +66,7 @@ pub use crate::cpi_account::CpiAccount; #[allow(deprecated)] pub use crate::cpi_state::CpiState; pub use crate::loader::Loader; +pub use crate::loader_account::AccountLoader; pub use crate::program::Program; #[doc(hidden)] #[allow(deprecated)] @@ -246,10 +248,10 @@ impl Key for Pubkey { pub mod prelude { pub use super::{ access_control, account, declare_id, emit, error, event, interface, program, require, - state, zero_copy, Account, AccountDeserialize, AccountSerialize, Accounts, AccountsExit, - AnchorDeserialize, AnchorSerialize, Context, CpiContext, Id, Key, Loader, Owner, Program, - ProgramAccount, Signer, System, Sysvar, ToAccountInfo, ToAccountInfos, ToAccountMetas, - UncheckedAccount, + state, zero_copy, Account, AccountDeserialize, AccountLoader, AccountSerialize, Accounts, + AccountsExit, AnchorDeserialize, AnchorSerialize, Context, CpiContext, Id, Key, Loader, + Owner, Program, ProgramAccount, Signer, System, Sysvar, ToAccountInfo, ToAccountInfos, + ToAccountMetas, UncheckedAccount, }; #[allow(deprecated)] diff --git a/lang/src/loader_account.rs b/lang/src/loader_account.rs new file mode 100644 index 00000000..a48a04c0 --- /dev/null +++ b/lang/src/loader_account.rs @@ -0,0 +1,198 @@ +use crate::error::ErrorCode; +use crate::{ + Accounts, AccountsClose, AccountsExit, Key, Owner, ToAccountInfo, ToAccountInfos, + ToAccountMetas, ZeroCopy, +}; +use solana_program::account_info::AccountInfo; +use solana_program::entrypoint::ProgramResult; +use solana_program::instruction::AccountMeta; +use solana_program::program_error::ProgramError; +use solana_program::pubkey::Pubkey; +use std::cell::{Ref, RefMut}; +use std::io::Write; +use std::marker::PhantomData; +use std::ops::DerefMut; + +/// Account AccountLoader facilitating on demand zero copy deserialization. +/// Note that using accounts in this way is distinctly different from using, +/// for example, the [`ProgramAccount`](./struct.ProgramAccount.html). Namely, +/// one must call `load`, `load_mut`, or `load_init`, before reading or writing +/// to the account. For more details on zero-copy-deserialization, see the +/// [`account`](./attr.account.html) attribute. +/// +/// When using it's important to be mindful of any calls to `load` so as not to +/// induce a `RefCell` panic, especially when sharing accounts across CPI +/// boundaries. When in doubt, one should make sure all refs resulting from a +/// call to `load` are dropped before CPI. +#[derive(Clone)] +pub struct AccountLoader<'info, T: ZeroCopy + Owner> { + acc_info: AccountInfo<'info>, + phantom: PhantomData<&'info T>, +} + +impl<'info, T: ZeroCopy + Owner> AccountLoader<'info, T> { + fn new(acc_info: AccountInfo<'info>) -> AccountLoader<'info, T> { + Self { + acc_info, + phantom: PhantomData, + } + } + + /// Constructs a new `Loader` from a previously initialized account. + #[inline(never)] + pub fn try_from( + acc_info: &AccountInfo<'info>, + ) -> Result, ProgramError> { + if acc_info.owner != &T::owner() { + return Err(ErrorCode::AccountNotProgramOwned.into()); + } + let data: &[u8] = &acc_info.try_borrow_data()?; + // Discriminator must match. + let mut disc_bytes = [0u8; 8]; + disc_bytes.copy_from_slice(&data[..8]); + if disc_bytes != T::discriminator() { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + + Ok(AccountLoader::new(acc_info.clone())) + } + + /// Constructs a new `Loader` from an uninitialized account. + #[inline(never)] + pub fn try_from_unchecked( + _program_id: &Pubkey, + acc_info: &AccountInfo<'info>, + ) -> Result, ProgramError> { + if acc_info.owner != &T::owner() { + return Err(ErrorCode::AccountNotProgramOwned.into()); + } + Ok(AccountLoader::new(acc_info.clone())) + } + + /// Returns a Ref to the account data structure for reading. + pub fn load(&self) -> Result, ProgramError> { + let data = self.acc_info.try_borrow_data()?; + + let mut disc_bytes = [0u8; 8]; + disc_bytes.copy_from_slice(&data[..8]); + if disc_bytes != T::discriminator() { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + + Ok(Ref::map(data, |data| bytemuck::from_bytes(&data[8..]))) + } + + /// Returns a `RefMut` to the account data structure for reading or writing. + pub fn load_mut(&self) -> Result, ProgramError> { + // AccountInfo api allows you to borrow mut even if the account isn't + // writable, so add this check for a better dev experience. + if !self.acc_info.is_writable { + return Err(ErrorCode::AccountNotMutable.into()); + } + + let data = self.acc_info.try_borrow_mut_data()?; + + let mut disc_bytes = [0u8; 8]; + disc_bytes.copy_from_slice(&data[..8]); + if disc_bytes != T::discriminator() { + return Err(ErrorCode::AccountDiscriminatorMismatch.into()); + } + + Ok(RefMut::map(data, |data| { + bytemuck::from_bytes_mut(&mut data.deref_mut()[8..]) + })) + } + + /// Returns a `RefMut` to the account data structure for reading or writing. + /// Should only be called once, when the account is being initialized. + pub fn load_init(&self) -> Result, ProgramError> { + // AccountInfo api allows you to borrow mut even if the account isn't + // writable, so add this check for a better dev experience. + if !self.acc_info.is_writable { + return Err(ErrorCode::AccountNotMutable.into()); + } + + let data = self.acc_info.try_borrow_mut_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(ErrorCode::AccountDiscriminatorAlreadySet.into()); + } + + Ok(RefMut::map(data, |data| { + bytemuck::from_bytes_mut(&mut data.deref_mut()[8..]) + })) + } +} + +impl<'info, T: ZeroCopy + Owner> Accounts<'info> for AccountLoader<'info, T> { + #[inline(never)] + fn try_accounts( + _program_id: &Pubkey, + accounts: &mut &[AccountInfo<'info>], + _ix_data: &[u8], + ) -> Result { + if accounts.is_empty() { + return Err(ErrorCode::AccountNotEnoughKeys.into()); + } + let account = &accounts[0]; + *accounts = &accounts[1..]; + let l = AccountLoader::try_from(account)?; + Ok(l) + } +} + +impl<'info, T: ZeroCopy + Owner> AccountsExit<'info> for AccountLoader<'info, T> { + // The account *cannot* be loaded when this is called. + fn exit(&self, _program_id: &Pubkey) -> ProgramResult { + let mut data = self.acc_info.try_borrow_mut_data()?; + let dst: &mut [u8] = &mut data; + let mut cursor = std::io::Cursor::new(dst); + cursor.write_all(&T::discriminator()).unwrap(); + Ok(()) + } +} + +impl<'info, T: ZeroCopy + Owner> AccountsClose<'info> for AccountLoader<'info, T> { + fn close(&self, sol_destination: AccountInfo<'info>) -> ProgramResult { + crate::common::close(self.to_account_info(), sol_destination) + } +} + +impl<'info, T: ZeroCopy + Owner> ToAccountMetas for AccountLoader<'info, T> { + fn to_account_metas(&self, is_signer: Option) -> Vec { + let is_signer = is_signer.unwrap_or(self.acc_info.is_signer); + let meta = match self.acc_info.is_writable { + false => AccountMeta::new_readonly(*self.acc_info.key, is_signer), + true => AccountMeta::new(*self.acc_info.key, is_signer), + }; + vec![meta] + } +} + +impl<'info, T: ZeroCopy + Owner> AsRef> for AccountLoader<'info, T> { + fn as_ref(&self) -> &AccountInfo<'info> { + &self.acc_info + } +} + +impl<'info, T: ZeroCopy + Owner> ToAccountInfos<'info> for AccountLoader<'info, T> { + fn to_account_infos(&self) -> Vec> { + vec![self.acc_info.clone()] + } +} + +impl<'info, T: ZeroCopy + Owner> ToAccountInfo<'info> for AccountLoader<'info, T> { + fn to_account_info(&self) -> AccountInfo<'info> { + self.acc_info.clone() + } +} + +impl<'info, T: ZeroCopy + Owner> Key for AccountLoader<'info, T> { + fn key(&self) -> Pubkey { + *self.acc_info.key + } +} diff --git a/lang/syn/src/codegen/accounts/constraints.rs b/lang/syn/src/codegen/accounts/constraints.rs index d3310a54..bb1f38bf 100644 --- a/lang/syn/src/codegen/accounts/constraints.rs +++ b/lang/syn/src/codegen/accounts/constraints.rs @@ -187,6 +187,7 @@ pub fn generate_constraint_has_one(f: &Field, c: &ConstraintHasOne) -> proc_macr let ident = &f.ident; let field = match &f.ty { Ty::Loader(_) => quote! {#ident.load()?}, + Ty::AccountLoader(_) => quote! {#ident.load()?}, _ => quote! {#ident}, }; quote! { @@ -203,6 +204,7 @@ pub fn generate_constraint_signer(f: &Field, _c: &ConstraintSigner) -> proc_macr Ty::ProgramAccount(_) => quote! { #ident.to_account_info() }, Ty::Account(_) => quote! { #ident.to_account_info() }, Ty::Loader(_) => quote! { #ident.to_account_info() }, + Ty::AccountLoader(_) => quote! { #ident.to_account_info() }, Ty::CpiAccount(_) => quote! { #ident.to_account_info() }, _ => panic!("Invalid syntax: signer cannot be specified."), }; @@ -488,7 +490,7 @@ pub fn generate_init( // and take the length (with +8 for the discriminator.) None => { let account_ty = f.account_ty(); - match matches!(f.ty, Ty::Loader(_)) { + match matches!(f.ty, Ty::Loader(_) | Ty::AccountLoader(_)) { false => { quote! { let space = 8 + #account_ty::default().try_to_vec().unwrap().len(); diff --git a/lang/syn/src/lib.rs b/lang/syn/src/lib.rs index abfb0db9..72cd6ec1 100644 --- a/lang/syn/src/lib.rs +++ b/lang/syn/src/lib.rs @@ -278,6 +278,9 @@ impl Field { Ty::Account(_) => quote! { anchor_lang::Account }, + Ty::AccountLoader(_) => quote! { + anchor_lang::AccountLoader + }, Ty::Loader(_) => quote! { anchor_lang::Loader }, @@ -318,6 +321,12 @@ impl Field { #ident } } + Ty::AccountLoader(ty) => { + let ident = &ty.account_type_path; + quote! { + #ident + } + } Ty::Loader(ty) => { let ident = &ty.account_type_path; quote! { @@ -382,6 +391,7 @@ pub enum Ty { CpiState(CpiStateTy), ProgramAccount(ProgramAccountTy), Loader(LoaderTy), + AccountLoader(LoaderAccountTy), CpiAccount(CpiAccountTy), Sysvar(SysvarTy), Account(AccountTy), @@ -425,6 +435,12 @@ pub struct CpiAccountTy { pub account_type_path: TypePath, } +#[derive(Debug, PartialEq)] +pub struct LoaderAccountTy { + // The struct type of the account. + pub account_type_path: TypePath, +} + #[derive(Debug, PartialEq)] pub struct LoaderTy { // The struct type of the account. diff --git a/lang/syn/src/parser/accounts/constraints.rs b/lang/syn/src/parser/accounts/constraints.rs index 970bb275..177bc3c3 100644 --- a/lang/syn/src/parser/accounts/constraints.rs +++ b/lang/syn/src/parser/accounts/constraints.rs @@ -625,6 +625,7 @@ impl<'ty> ConstraintGroupBuilder<'ty> { if !matches!(self.f_ty, Some(Ty::ProgramAccount(_))) && !matches!(self.f_ty, Some(Ty::Account(_))) && !matches!(self.f_ty, Some(Ty::Loader(_))) + && !matches!(self.f_ty, Some(Ty::AccountLoader(_))) { return Err(ParseError::new( c.span(), diff --git a/lang/syn/src/parser/accounts/mod.rs b/lang/syn/src/parser/accounts/mod.rs index d28fe76d..ceed8b99 100644 --- a/lang/syn/src/parser/accounts/mod.rs +++ b/lang/syn/src/parser/accounts/mod.rs @@ -74,6 +74,7 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult { | "UncheckedAccount" | "CpiState" | "Loader" + | "AccountLoader" | "Account" | "Program" | "Signer" @@ -95,6 +96,7 @@ fn parse_ty(f: &syn::Field) -> ParseResult { "AccountInfo" => Ty::AccountInfo, "UncheckedAccount" => Ty::UncheckedAccount, "Loader" => Ty::Loader(parse_program_account_zero_copy(&path)?), + "AccountLoader" => Ty::AccountLoader(parse_program_loader_account(&path)?), "Account" => Ty::Account(parse_account_ty(&path)?), "Program" => Ty::Program(parse_program_ty(&path)?), "Signer" => Ty::Signer, @@ -161,6 +163,12 @@ fn parse_program_account_zero_copy(path: &syn::Path) -> ParseResult { account_type_path: account_ident, }) } +fn parse_program_loader_account(path: &syn::Path) -> ParseResult { + let account_ident = parse_account(path)?; + Ok(LoaderAccountTy { + account_type_path: account_ident, + }) +} fn parse_account_ty(path: &syn::Path) -> ParseResult { let account_type_path = parse_account(path)?; diff --git a/lang/tests/generics_test.rs b/lang/tests/generics_test.rs index a569a59f..989cb738 100644 --- a/lang/tests/generics_test.rs +++ b/lang/tests/generics_test.rs @@ -3,6 +3,7 @@ use anchor_lang::prelude::borsh::maybestd::io::Write; use anchor_lang::prelude::*; use borsh::{BorshDeserialize, BorshSerialize}; +use solana_program::pubkey::Pubkey; // Needed to declare accounts. declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); @@ -16,6 +17,7 @@ where pub non_generic: AccountInfo<'info>, pub generic: Account<'info, T>, pub const_generic: Loader<'info, FooAccount>, + pub const_generic_loader: AccountLoader<'info, FooAccount>, pub associated: Account<'info, Associated>, } @@ -45,3 +47,8 @@ impl BorshDeserialize for WrappedU8Array { todo!() } } +impl Owner for WrappedU8Array { + fn owner() -> Pubkey { + crate::ID + } +} diff --git a/tests/zero-copy/Anchor.toml b/tests/zero-copy/Anchor.toml index cfd93f02..13258ea1 100644 --- a/tests/zero-copy/Anchor.toml +++ b/tests/zero-copy/Anchor.toml @@ -3,7 +3,11 @@ cluster = "localnet" wallet = "~/.config/solana/id.json" [workspace] -members = ["programs/zero-copy"] +members = ["programs/zero-copy", "programs/zero-cpi"] [scripts] test = "mocha -t 1000000 tests/" + +[programs.localnet] +zero_cpi = "ErjUjtqKE5AGWUsjseSJCVLtddM6rhaMbDqmhzraF9h6" +zero_copy = "Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS" diff --git a/tests/zero-copy/programs/zero-copy/src/lib.rs b/tests/zero-copy/programs/zero-copy/src/lib.rs index 1f165243..14e0d191 100644 --- a/tests/zero-copy/programs/zero-copy/src/lib.rs +++ b/tests/zero-copy/programs/zero-copy/src/lib.rs @@ -11,43 +11,10 @@ declare_id!("Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS"); #[program] pub mod zero_copy { + use std::str::FromStr; + use super::*; - #[state(zero_copy)] - pub struct Globals { - pub authority: Pubkey, - // The solana runtime currently restricts how much one can resize an - // account on CPI to ~10240 bytes. State accounts are program derived - // addresses, which means its max size is limited by this restriction - // (i.e., this is not an Anchor specific issue). - // - // As a result, we only use 250 events here. - // - // For larger zero-copy data structures, one must use non-state anchor - // accounts, as is demonstrated below. - pub events: [Event; 250], - } - - impl Globals { - // Note that the `new` constructor is different from non-zero-copy - // state accounts. Namely, it takes in a `&mut self` parameter. - pub fn new(&mut self, ctx: Context) -> ProgramResult { - self.authority = *ctx.accounts.authority.key; - Ok(()) - } - - #[access_control(auth(&self, &ctx))] - pub fn set_event( - &mut self, - ctx: Context, - idx: u32, - event: RpcEvent, - ) -> ProgramResult { - self.events[idx as usize] = event.into(); - Ok(()) - } - } - pub fn create_foo(ctx: Context) -> ProgramResult { let foo = &mut ctx.accounts.foo.load_init()?; foo.authority = *ctx.accounts.authority.key; @@ -97,12 +64,6 @@ pub mod zero_copy { } } -#[derive(Accounts)] -pub struct New<'info> { - #[account(signer)] - authority: AccountInfo<'info>, -} - #[derive(Accounts)] pub struct SetEvent<'info> { #[account(signer)] @@ -112,7 +73,7 @@ pub struct SetEvent<'info> { #[derive(Accounts)] pub struct CreateFoo<'info> { #[account(zero)] - foo: Loader<'info, Foo>, + foo: AccountLoader<'info, Foo>, #[account(signer)] authority: AccountInfo<'info>, } @@ -120,7 +81,7 @@ pub struct CreateFoo<'info> { #[derive(Accounts)] pub struct UpdateFoo<'info> { #[account(mut, has_one = authority)] - foo: Loader<'info, Foo>, + foo: AccountLoader<'info, Foo>, #[account(signer)] authority: AccountInfo<'info>, } @@ -131,7 +92,7 @@ pub struct UpdateFooSecond<'info> { mut, constraint = &foo.load()?.get_second_authority() == second_authority.key, )] - foo: Loader<'info, Foo>, + foo: AccountLoader<'info, Foo>, #[account(signer)] second_authority: AccountInfo<'info>, } @@ -142,15 +103,14 @@ pub struct CreateBar<'info> { init, seeds = [authority.key().as_ref(), foo.key().as_ref()], bump, - payer = authority, + payer = authority, owner = *program_id )] - bar: Loader<'info, Bar>, + bar: AccountLoader<'info, Bar>, #[account(signer)] authority: AccountInfo<'info>, - foo: Loader<'info, Foo>, + foo: AccountLoader<'info, Foo>, system_program: AccountInfo<'info>, } - #[derive(Accounts)] pub struct UpdateBar<'info> { #[account( @@ -159,22 +119,22 @@ pub struct UpdateBar<'info> { seeds = [authority.key().as_ref(), foo.key().as_ref()], bump, )] - bar: Loader<'info, Bar>, + pub bar: AccountLoader<'info, Bar>, #[account(signer)] - authority: AccountInfo<'info>, - foo: Loader<'info, Foo>, + pub authority: AccountInfo<'info>, + pub foo: AccountLoader<'info, Foo>, } #[derive(Accounts)] pub struct CreateLargeAccount<'info> { #[account(zero)] - event_q: Loader<'info, EventQ>, + event_q: AccountLoader<'info, EventQ>, } #[derive(Accounts)] pub struct UpdateLargeAccount<'info> { #[account(mut)] - event_q: Loader<'info, EventQ>, + event_q: AccountLoader<'info, EventQ>, #[account(signer)] from: AccountInfo<'info>, } @@ -231,10 +191,3 @@ impl From for Event { } } } - -fn auth(globals: &Globals, ctx: &Context) -> ProgramResult { - if &globals.authority != ctx.accounts.authority.key { - return Err(ProgramError::Custom(1)); // Arbitrary error. - } - Ok(()) -} diff --git a/tests/zero-copy/programs/zero-cpi/Cargo.toml b/tests/zero-copy/programs/zero-cpi/Cargo.toml new file mode 100644 index 00000000..d750ecfd --- /dev/null +++ b/tests/zero-copy/programs/zero-cpi/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "zero-cpi" +version = "0.1.0" +description = "Created with Anchor" +edition = "2018" + +[lib] +crate-type = ["cdylib", "lib"] +name = "zero_cpi" + +[features] +no-entrypoint = [] +no-idl = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = { path = "../../../../lang" } +zero-copy = { path = "../zero-copy", features = ["cpi"] } diff --git a/tests/zero-copy/programs/zero-cpi/Xargo.toml b/tests/zero-copy/programs/zero-cpi/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/tests/zero-copy/programs/zero-cpi/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tests/zero-copy/programs/zero-cpi/src/lib.rs b/tests/zero-copy/programs/zero-cpi/src/lib.rs new file mode 100644 index 00000000..4ea23f18 --- /dev/null +++ b/tests/zero-copy/programs/zero-cpi/src/lib.rs @@ -0,0 +1,35 @@ +use anchor_lang::prelude::*; +use zero_copy::cpi::accounts::UpdateBar; +use zero_copy::program::ZeroCopy; +use zero_copy::{self, Bar, Foo}; + +declare_id!("ErjUjtqKE5AGWUsjseSJCVLtddM6rhaMbDqmhzraF9h6"); + +#[program] +pub mod zero_cpi { + use super::*; + pub fn check_cpi(ctx: Context, data: u64) -> ProgramResult { + let cpi_program = ctx.accounts.zero_copy_program.to_account_info(); + let cpi_accounts = UpdateBar { + authority: ctx.accounts.authority.clone(), + bar: ctx.accounts.bar.to_account_info(), + foo: ctx.accounts.foo.to_account_info(), + }; + let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); + zero_copy::cpi::update_bar(cpi_ctx, data); + Ok(()) + } +} + +#[derive(Accounts)] +pub struct CheckCpi<'info> { + #[account( + mut, + has_one = authority, + )] + bar: AccountLoader<'info, Bar>, + #[account(signer)] + authority: AccountInfo<'info>, + foo: AccountLoader<'info, Foo>, + zero_copy_program: Program<'info, ZeroCopy>, +} diff --git a/tests/zero-copy/tests/zero-copy.js b/tests/zero-copy/tests/zero-copy.js index 14c24612..fa86800c 100644 --- a/tests/zero-copy/tests/zero-copy.js +++ b/tests/zero-copy/tests/zero-copy.js @@ -1,56 +1,17 @@ -const anchor = require("@project-serum/anchor"); -const PublicKey = anchor.web3.PublicKey; -const BN = anchor.BN; -const assert = require("assert"); +const anchor = require('@project-serum/anchor') +const PublicKey = anchor.web3.PublicKey +const BN = anchor.BN +const assert = require('assert') -describe("zero-copy", () => { +describe('zero-copy', () => { // Configure the client to use the local cluster. - anchor.setProvider(anchor.Provider.env()); + anchor.setProvider(anchor.Provider.env()) - const program = anchor.workspace.ZeroCopy; + const program = anchor.workspace.ZeroCopy + const programCpi = anchor.workspace.ZeroCpi - const foo = anchor.web3.Keypair.generate(); - - it("Creates zero copy state", async () => { - await program.state.rpc.new({ - accounts: { - authority: program.provider.wallet.publicKey, - }, - }); - const state = await program.state.fetch(); - assert.ok(state.authority.equals(program.provider.wallet.publicKey)); - assert.ok(state.events.length === 250); - state.events.forEach((event, idx) => { - assert.ok(event.from.equals(PublicKey.default)); - assert.ok(event.data.toNumber() === 0); - }); - }); - - it("Updates zero copy state", async () => { - let event = { - from: PublicKey.default, - data: new BN(1234), - }; - await program.state.rpc.setEvent(5, event, { - accounts: { - authority: program.provider.wallet.publicKey, - }, - }); - const state = await program.state.fetch(); - assert.ok(state.authority.equals(program.provider.wallet.publicKey)); - assert.ok(state.events.length === 250); - state.events.forEach((event, idx) => { - if (idx === 5) { - assert.ok(event.from.equals(event.from)); - assert.ok(event.data.eq(event.data)); - } else { - assert.ok(event.from.equals(PublicKey.default)); - assert.ok(event.data.toNumber() === 0); - } - }); - }); - - it("Is creates a zero copy account", async () => { + const foo = anchor.web3.Keypair.generate() + it('Is creates a zero copy account', async () => { await program.rpc.createFoo({ accounts: { foo: foo.publicKey, @@ -59,73 +20,70 @@ describe("zero-copy", () => { }, instructions: [await program.account.foo.createInstruction(foo)], signers: [foo], - }); - const account = await program.account.foo.fetch(foo.publicKey); + }) + const account = await program.account.foo.fetch(foo.publicKey) assert.ok( JSON.stringify(account.authority.toBuffer()) === JSON.stringify(program.provider.wallet.publicKey.toBuffer()) - ); - assert.ok(account.data.toNumber() === 0); - assert.ok(account.secondData.toNumber() === 0); + ) + assert.ok(account.data.toNumber() === 0) + assert.ok(account.secondData.toNumber() === 0) assert.ok( JSON.stringify(account.secondAuthority) === JSON.stringify([...program.provider.wallet.publicKey.toBuffer()]) - ); - }); + ) + }) - it("Updates a zero copy account field", async () => { + it('Updates a zero copy account field', async () => { await program.rpc.updateFoo(new BN(1234), { accounts: { foo: foo.publicKey, authority: program.provider.wallet.publicKey, }, - }); + }) - const account = await program.account.foo.fetch(foo.publicKey); + const account = await program.account.foo.fetch(foo.publicKey) assert.ok( JSON.stringify(account.authority.toBuffer()) === JSON.stringify(program.provider.wallet.publicKey.toBuffer()) - ); - assert.ok(account.data.toNumber() === 1234); - assert.ok(account.secondData.toNumber() === 0); + ) + assert.ok(account.data.toNumber() === 1234) + assert.ok(account.secondData.toNumber() === 0) assert.ok( JSON.stringify(account.secondAuthority) === JSON.stringify([...program.provider.wallet.publicKey.toBuffer()]) - ); - }); + ) + }) - it("Updates a a second zero copy account field", async () => { + it('Updates a a second zero copy account field', async () => { await program.rpc.updateFooSecond(new BN(55), { accounts: { foo: foo.publicKey, secondAuthority: program.provider.wallet.publicKey, }, - }); + }) - const account = await program.account.foo.fetch(foo.publicKey); + const account = await program.account.foo.fetch(foo.publicKey) assert.ok( JSON.stringify(account.authority.toBuffer()) === JSON.stringify(program.provider.wallet.publicKey.toBuffer()) - ); - assert.ok(account.data.toNumber() === 1234); - assert.ok(account.secondData.toNumber() === 55); + ) + assert.ok(account.data.toNumber() === 1234) + assert.ok(account.secondData.toNumber() === 55) assert.ok( JSON.stringify(account.secondAuthority) === JSON.stringify([...program.provider.wallet.publicKey.toBuffer()]) - ); - }); + ) + }) - it("Creates an associated zero copy account", async () => { + it('Creates an associated zero copy account', async () => { await program.rpc.createBar({ accounts: { bar: ( await PublicKey.findProgramAddress( - [ - program.provider.wallet.publicKey.toBuffer(), - foo.publicKey.toBuffer(), - ], + [program.provider.wallet.publicKey.toBuffer(), foo.publicKey.toBuffer()], program.programId ) )[0], @@ -133,86 +91,90 @@ describe("zero-copy", () => { foo: foo.publicKey, systemProgram: anchor.web3.SystemProgram.programId, }, - }); + }) const bar = ( await PublicKey.findProgramAddress( - [ - program.provider.wallet.publicKey.toBuffer(), - foo.publicKey.toBuffer(), - ], + [program.provider.wallet.publicKey.toBuffer(), foo.publicKey.toBuffer()], program.programId ) - )[0]; - const barAccount = await program.account.bar.fetch(bar); - assert.ok(barAccount.authority.equals(program.provider.wallet.publicKey)); - assert.ok(barAccount.data.toNumber() === 0); - }); + )[0] + const barAccount = await program.account.bar.fetch(bar) + assert.ok(barAccount.authority.equals(program.provider.wallet.publicKey)) + assert.ok(barAccount.data.toNumber() === 0) + }) - it("Updates an associated zero copy account", async () => { + it('Updates an associated zero copy account', async () => { const bar = ( await PublicKey.findProgramAddress( - [ - program.provider.wallet.publicKey.toBuffer(), - foo.publicKey.toBuffer(), - ], + [program.provider.wallet.publicKey.toBuffer(), foo.publicKey.toBuffer()], program.programId ) - )[0]; + )[0] await program.rpc.updateBar(new BN(99), { accounts: { bar, authority: program.provider.wallet.publicKey, foo: foo.publicKey, }, - }); - const barAccount = await program.account.bar.fetch(bar); - assert.ok(barAccount.authority.equals(program.provider.wallet.publicKey)); - assert.ok(barAccount.data.toNumber() === 99); - }); + }) + const barAccount = await program.account.bar.fetch(bar) + assert.ok(barAccount.authority.equals(program.provider.wallet.publicKey)) + assert.ok(barAccount.data.toNumber() === 99) + // Check zero_copy CPI + await programCpi.rpc.checkCpi(new BN(1337), { + accounts: { + bar, + authority: program.provider.wallet.publicKey, + foo: foo.publicKey, + zeroCopyProgram: program.programId, + }, + }) + const barAccountAfterCpi = await program.account.bar.fetch(bar) + assert.ok(barAccountAfterCpi.authority.equals(program.provider.wallet.publicKey)) + assert.ok(barAccountAfterCpi.data.toNumber() === 1337) + }) - const eventQ = anchor.web3.Keypair.generate(); - const size = 1000000 + 8; // Account size in bytes. + const eventQ = anchor.web3.Keypair.generate() + const size = 1000000 + 8 // Account size in bytes. - it("Creates a large event queue", async () => { + it('Creates a large event queue', async () => { await program.rpc.createLargeAccount({ accounts: { eventQ: eventQ.publicKey, rent: anchor.web3.SYSVAR_RENT_PUBKEY, }, - instructions: [ - await program.account.eventQ.createInstruction(eventQ, size), - ], + instructions: [await program.account.eventQ.createInstruction(eventQ, size)], signers: [eventQ], - }); - const account = await program.account.eventQ.fetch(eventQ.publicKey); - assert.ok(account.events.length === 25000); + }) + const account = await program.account.eventQ.fetch(eventQ.publicKey) + assert.ok(account.events.length === 25000) account.events.forEach((event) => { - assert.ok(event.from.equals(PublicKey.default)); - assert.ok(event.data.toNumber() === 0); - }); - }); + assert.ok(event.from.equals(PublicKey.default)) + assert.ok(event.data.toNumber() === 0) + }) + }) - it("Updates a large event queue", async () => { + it('Updates a large event queue', async () => { // Set index 0. await program.rpc.updateLargeAccount(0, new BN(48), { accounts: { eventQ: eventQ.publicKey, from: program.provider.wallet.publicKey, }, - }); + }) // Verify update. - let account = await program.account.eventQ.fetch(eventQ.publicKey); - assert.ok(account.events.length === 25000); + let account = await program.account.eventQ.fetch(eventQ.publicKey) + assert.ok(account.events.length === 25000) account.events.forEach((event, idx) => { if (idx === 0) { - assert.ok(event.from.equals(program.provider.wallet.publicKey)); - assert.ok(event.data.toNumber() === 48); + assert.ok(event.from.equals(program.provider.wallet.publicKey)) + assert.ok(event.data.toNumber() === 48) } else { - assert.ok(event.from.equals(PublicKey.default)); - assert.ok(event.data.toNumber() === 0); + assert.ok(event.from.equals(PublicKey.default)) + assert.ok(event.data.toNumber() === 0) } - }); + }) // Set index 11111. await program.rpc.updateLargeAccount(11111, new BN(1234), { @@ -220,22 +182,22 @@ describe("zero-copy", () => { eventQ: eventQ.publicKey, from: program.provider.wallet.publicKey, }, - }); + }) // Verify update. - account = await program.account.eventQ.fetch(eventQ.publicKey); - assert.ok(account.events.length === 25000); + account = await program.account.eventQ.fetch(eventQ.publicKey) + assert.ok(account.events.length === 25000) account.events.forEach((event, idx) => { if (idx === 0) { - assert.ok(event.from.equals(program.provider.wallet.publicKey)); - assert.ok(event.data.toNumber() === 48); + assert.ok(event.from.equals(program.provider.wallet.publicKey)) + assert.ok(event.data.toNumber() === 48) } else if (idx === 11111) { - assert.ok(event.from.equals(program.provider.wallet.publicKey)); - assert.ok(event.data.toNumber() === 1234); + assert.ok(event.from.equals(program.provider.wallet.publicKey)) + assert.ok(event.data.toNumber() === 1234) } else { - assert.ok(event.from.equals(PublicKey.default)); - assert.ok(event.data.toNumber() === 0); + assert.ok(event.from.equals(PublicKey.default)) + assert.ok(event.data.toNumber() === 0) } - }); + }) // Set last index. await program.rpc.updateLargeAccount(24999, new BN(99), { @@ -243,28 +205,28 @@ describe("zero-copy", () => { eventQ: eventQ.publicKey, from: program.provider.wallet.publicKey, }, - }); + }) // Verify update. - account = await program.account.eventQ.fetch(eventQ.publicKey); - assert.ok(account.events.length === 25000); + account = await program.account.eventQ.fetch(eventQ.publicKey) + assert.ok(account.events.length === 25000) account.events.forEach((event, idx) => { if (idx === 0) { - assert.ok(event.from.equals(program.provider.wallet.publicKey)); - assert.ok(event.data.toNumber() === 48); + assert.ok(event.from.equals(program.provider.wallet.publicKey)) + assert.ok(event.data.toNumber() === 48) } else if (idx === 11111) { - assert.ok(event.from.equals(program.provider.wallet.publicKey)); - assert.ok(event.data.toNumber() === 1234); + assert.ok(event.from.equals(program.provider.wallet.publicKey)) + assert.ok(event.data.toNumber() === 1234) } else if (idx === 24999) { - assert.ok(event.from.equals(program.provider.wallet.publicKey)); - assert.ok(event.data.toNumber() === 99); + assert.ok(event.from.equals(program.provider.wallet.publicKey)) + assert.ok(event.data.toNumber() === 99) } else { - assert.ok(event.from.equals(PublicKey.default)); - assert.ok(event.data.toNumber() === 0); + assert.ok(event.from.equals(PublicKey.default)) + assert.ok(event.data.toNumber() === 0) } - }); - }); + }) + }) - it("Errors when setting an out of bounds index", async () => { + it('Errors when setting an out of bounds index', async () => { // Fail to set non existing index. await assert.rejects( async () => { @@ -273,12 +235,12 @@ describe("zero-copy", () => { eventQ: eventQ.publicKey, from: program.provider.wallet.publicKey, }, - }); + }) }, (err) => { - console.log("err", err); - return true; + console.log('err', err) + return true } - ); - }); -}); + ) + }) +})