lang: Add `LazyAccount` (#3194)

This commit is contained in:
acheron 2024-09-01 03:16:38 +02:00 committed by GitHub
parent dd14b2cce2
commit 879601e632
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 1259 additions and 14 deletions

View File

@ -463,6 +463,8 @@ jobs:
path: tests/bench
- cmd: cd tests/idl && ./test.sh
path: tests/idl
- cmd: cd tests/lazy-account && anchor test
path: tests/lazy-account
# TODO: Enable when `solang` becomes compatible with the new IDL spec
# - cmd: cd tests/solang && anchor test
# path: tests/solang

View File

@ -45,6 +45,7 @@ The minor version will be incremented upon a breaking change and the patch versi
- cli: Make `clean` command also remove the `.anchor` directory ([#3192](https://github.com/coral-xyz/anchor/pull/3192)).
- lang: Deprecate `#[interface]` attribute ([#3195](https://github.com/coral-xyz/anchor/pull/3195)).
- ts: Include unresolved accounts in the resolution error message ([#3207](https://github.com/coral-xyz/anchor/pull/3207)).
- lang: Add `LazyAccount` ([#3194](https://github.com/coral-xyz/anchor/pull/3194)).
### Fixes

View File

@ -36,6 +36,7 @@ idl-build = [
]
init-if-needed = ["anchor-derive-accounts/init-if-needed"]
interface-instructions = ["anchor-attribute-program/interface-instructions"]
lazy-account = ["anchor-attribute-account/lazy-account", "anchor-derive-serde/lazy-account"]
[dependencies]
anchor-attribute-access-control = { path = "./attribute/access-control", version = "0.30.1" }

View File

@ -13,6 +13,7 @@ proc-macro = true
[features]
anchor-debug = ["anchor-syn/anchor-debug"]
idl-build = ["anchor-syn/idl-build"]
lazy-account = []
[dependencies]
anchor-syn = { path = "../../syn", version = "0.30.1", features = ["hash"] }

View File

@ -0,0 +1,302 @@
use proc_macro2::{Literal, TokenStream};
use quote::{format_ident, quote, ToTokens};
pub fn gen_lazy(strct: &syn::ItemStruct) -> syn::Result<TokenStream> {
let ident = &strct.ident;
let lazy_ident = format_ident!("Lazy{}", ident);
let load_common_ident = to_private_ident("load_common");
let initialize_fields = to_private_ident("initialize_fields");
let lazy_acc_ty = quote! { anchor_lang::accounts::lazy_account::LazyAccount };
let disc_len = quote! { <#ident as anchor_lang::Discriminator>::DISCRIMINATOR.len() };
let load_common_docs = quote! {
/// The deserialized value is cached for future uses i.e. all subsequent calls to this
/// method do not deserialize the data again, instead, they return the cached value.
///
/// To reload the data from the underlying account info (e.g. after a CPI call), run
/// [`LazyAccount::unload`] before running this method.
///
/// See [`LazyAccount`]'s documentation for more information.
};
let load_panic_docs = quote! {
/// # Panics
///
/// If there is an existing mutable reference crated by any of the `load_mut` methods.
};
let load_mut_panic_docs = quote! {
/// # Panics
///
/// If there is an existing reference (mutable or not) created by any of the `load` methods.
};
let (loader_signatures, loader_impls) = strct
.fields
.iter()
.enumerate()
.map(|(i, field)| {
let field_ident = to_field_ident(field, i);
let load_ident = format_ident!("load_{field_ident}");
let load_mut_ident = format_ident!("load_mut_{field_ident}");
let load_common_ident = to_private_ident(format!("load_common_{field_ident}"));
let offset_of_ident = to_private_ident(format!("offset_of_{field_ident}"));
let size_of_ident = to_private_ident(format!("size_of_{field_ident}"));
let offset = i.eq(&0).then(|| quote!(#disc_len)).unwrap_or_else(|| {
// Current offset is the previous field's offset + size
strct
.fields
.iter()
.nth(i - 1)
.map(|field| {
let field_ident = to_field_ident(field, i - 1);
let offset_of_ident = to_private_ident(format!("offset_of_{field_ident}"));
let size_of_ident = to_private_ident(format!("size_of_{field_ident}"));
quote! { self.#offset_of_ident() + self.#size_of_ident() }
})
.expect("Previous field should always exist when i > 0")
});
let ty = &field.ty;
let size = quote! {
<#ty as anchor_lang::__private::Lazy>::size_of(
&self.__info.data.borrow()[self.#offset_of_ident()..]
)
};
let signatures = quote! {
/// Load a reference to the field.
///
#load_common_docs
///
#load_panic_docs
fn #load_ident(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ty>>;
/// Load a mutable reference to the field.
///
#load_common_docs
///
#load_mut_panic_docs
fn #load_mut_ident(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ty>>;
#[doc(hidden)]
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R>;
#[doc(hidden)]
fn #offset_of_ident(&self) -> usize;
#[doc(hidden)]
fn #size_of_ident(&self) -> usize;
};
let impls = quote! {
fn #load_ident(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ty>> {
self.#load_common_ident(|| {
// SAFETY: The common load method makes sure the field is initialized.
::core::cell::Ref::map(self.__account.borrow(), |acc| unsafe {
&*::core::ptr::addr_of!((*acc.as_ptr()).#field_ident)
})
})
}
fn #load_mut_ident(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ty>> {
self.#load_common_ident(|| {
// SAFETY: The common load method makes sure the field is initialized.
::core::cell::RefMut::map(self.__account.borrow_mut(), |acc| unsafe {
&mut *::core::ptr::addr_of_mut!((*acc.as_mut_ptr()).#field_ident)
})
})
}
#[inline(never)]
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R> {
self.#initialize_fields();
// Return early if initialized
if self.__fields.borrow().as_ref().unwrap()[#i] {
return Ok(f());
}
// Deserialize and write
let offset = self.#offset_of_ident();
let size = self.#size_of_ident();
let data = self.__info.data.borrow();
let val = anchor_lang::AnchorDeserialize::try_from_slice(
&data[offset..offset + size]
)?;
unsafe {
::core::ptr::addr_of_mut!(
(*self.__account.borrow_mut().as_mut_ptr()).#field_ident
).write(val)
};
// Set initialized
self.__fields.borrow_mut().as_mut().unwrap()[#i] = true;
Ok(f())
}
// If this method gets inlined when there are >= 12 fields, compilation breaks with
// `LLVM ERROR: Branch target out of insn range`
#[inline(never)]
fn #offset_of_ident(&self) -> usize {
#offset
}
#[inline(always)]
fn #size_of_ident(&self) -> usize {
#size
}
};
Ok((signatures, impls))
})
.collect::<syn::Result<Vec<_>>>()?
.into_iter()
.unzip::<_, _, Vec<_>, Vec<_>>();
let load_idents = strct
.fields
.iter()
.enumerate()
.map(|(i, field)| to_field_ident(field, i))
.map(|field| format_ident!("load_{field}"));
let total_fields = strct.fields.len();
Ok(quote! {
pub trait #lazy_ident {
/// Load a reference to the entire account.
///
#load_common_docs
///
#load_panic_docs
fn load(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ident>>;
/// Load a mutable reference to the entire account.
///
#load_common_docs
///
#load_mut_panic_docs
fn load_mut(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ident>>;
#[doc(hidden)]
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R>;
#(#loader_signatures)*
#[doc(hidden)]
fn #initialize_fields(&self);
/// Run the exit routine of the account, similar to [`AccountsExit`] but implemented
/// as a regular method because we can't implement external traits for external structs.
fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()>;
}
impl<'info> #lazy_ident for #lazy_acc_ty<'info, #ident> {
fn load(&self) -> anchor_lang::Result<::core::cell::Ref<'_, #ident>> {
self.#load_common_ident(|| {
// SAFETY: The common load method makes sure all fields are initialized.
::core::cell::Ref::map(self.__account.borrow(), |acc| unsafe {
acc.assume_init_ref()
})
})
}
fn load_mut(&self) -> anchor_lang::Result<::core::cell::RefMut<'_, #ident>> {
self.#load_common_ident(|| {
// SAFETY: The common load method makes sure all fields are initialized.
::core::cell::RefMut::map(self.__account.borrow_mut(), |acc| unsafe {
acc.assume_init_mut()
})
})
}
#[inline(never)]
fn #load_common_ident<R>(&self, f: impl FnOnce() -> R) -> anchor_lang::Result<R> {
self.#initialize_fields();
// Create a scope to drop the `__fields` borrow
let all_uninit = {
// Return early if all fields are initialized
let fields = self.__fields.borrow();
let fields = fields.as_ref().unwrap();
if !fields.contains(&false) {
return Ok(f());
}
!fields.contains(&true)
};
if all_uninit {
// Nothing is initialized, initialize all
let offset = #disc_len;
let mut data = self.__info.data.borrow();
let val = anchor_lang::AnchorDeserialize::deserialize(&mut &data[offset..])?;
unsafe { self.__account.borrow_mut().as_mut_ptr().write(val) };
// Set fields to initialized
let mut fields = self.__fields.borrow_mut();
let fields = fields.as_mut().unwrap();
for field in fields {
*field = true;
}
} else {
// Only initialize uninitialized fields (`load` methods already do this).
//
// This is not exactly efficient because `load` methods have a bit of
// runtime ownership overhead. This could be optimized further, but it
// requires some refactoring and also makes the code harder to reason about.
//
// We can return back to this if benchmarks show this is a bottleneck.
#(self.#load_idents()?;)*
}
Ok(f())
}
#(#loader_impls)*
#[inline(always)]
fn #initialize_fields(&self) {
if self.__fields.borrow().is_none() {
*self.__fields.borrow_mut() = Some(vec![false; #total_fields]);
}
}
// TODO: This method can be optimized to *only* serialize the fields that we have
// initialized rather than deserializing the whole account, and then serializing it
// back, which consumes a lot more CUs than it should for most accounts.
fn exit(&self, program_id: &anchor_lang::prelude::Pubkey) -> anchor_lang::Result<()> {
// Only persist if the owner is the current program and the account is not closed
if &<#ident as anchor_lang::Owner>::owner() == program_id
&& !anchor_lang::__private::is_closed(self.__info)
{
// Make sure all fields are initialized
let acc = self.load()?;
let mut data = self.__info.try_borrow_mut_data()?;
let dst: &mut [u8] = &mut data;
let mut writer = anchor_lang::__private::BpfWriter::new(dst);
acc.try_serialize(&mut writer)?;
}
Ok(())
}
}
})
}
/// Get the field's ident and if the ident doesn't exist (e.g. for tuple structs), default to the
/// given index.
fn to_field_ident(field: &syn::Field, i: usize) -> TokenStream {
field
.ident
.as_ref()
.map(ToTokens::to_token_stream)
.unwrap_or_else(|| Literal::usize_unsuffixed(i).to_token_stream())
}
/// Convert to private ident.
///
/// This is used to indicate to the users that they shouldn't use this identifier.
fn to_private_ident<S: AsRef<str>>(ident: S) -> syn::Ident {
format_ident!("__{}", ident.as_ref())
}

View File

@ -12,6 +12,9 @@ use syn::{
mod id;
#[cfg(feature = "lazy-account")]
mod lazy;
/// An attribute for a data structure representing a Solana account.
///
/// `#[account]` generates trait implementations for the following traits:
@ -207,6 +210,17 @@ pub fn account(
#owner_impl
}
} else {
let lazy = {
#[cfg(feature = "lazy-account")]
match namespace.is_empty().then(|| lazy::gen_lazy(&account_strct)) {
Some(Ok(lazy)) => lazy,
// If lazy codegen fails for whatever reason, return empty tokenstream which
// will make the account unusable with `LazyAccount<T>`
_ => Default::default(),
}
#[cfg(not(feature = "lazy-account"))]
proc_macro2::TokenStream::default()
};
quote! {
#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
#account_strct
@ -251,6 +265,8 @@ pub fn account(
}
#owner_impl
#lazy
}
}
})

View File

@ -12,6 +12,7 @@ proc-macro = true
[features]
idl-build = ["anchor-syn/idl-build"]
lazy-account = []
[dependencies]
anchor-syn = { path = "../../syn", version = "0.30.1" }

View File

@ -0,0 +1,63 @@
use proc_macro2::Literal;
use quote::{format_ident, quote};
use syn::{spanned::Spanned, Fields, Item};
pub fn gen_lazy(input: proc_macro::TokenStream) -> syn::Result<proc_macro2::TokenStream> {
let item = syn::parse::<Item>(input)?;
let (name, generics, size) = match &item {
Item::Struct(strct) => (&strct.ident, &strct.generics, sum_fields(&strct.fields)),
Item::Enum(enm) => {
let arms = enm
.variants
.iter()
.map(|variant| sum_fields(&variant.fields))
.enumerate()
.map(|(i, size)| (Literal::usize_unsuffixed(i), size))
.map(|(i, size)| quote! { Some(#i) => { #size } });
(
&enm.ident,
&enm.generics,
quote! {
1 + match buf.first() {
#(#arms,)*
_ => unreachable!(),
}
},
)
}
Item::Union(_) => return Err(syn::Error::new(item.span(), "Unions are not supported")),
_ => unreachable!(),
};
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
Ok(quote! {
impl #impl_generics anchor_lang::__private::Lazy for #name #ty_generics #where_clause {
#[inline(always)]
fn size_of(buf: &[u8]) -> usize {
#size
}
}
})
}
fn sum_fields(fields: &Fields) -> proc_macro2::TokenStream {
let names = fields
.iter()
.enumerate()
.map(|(i, _)| format_ident!("s{i}"))
.collect::<Vec<_>>();
let declarations = fields.iter().enumerate().map(|(i, field)| {
let ty = &field.ty;
let name = &names[i];
let sum = &names[..i];
let buf = quote! { &buf[0 #(+ #sum)*..] };
quote! { let #name = <#ty as anchor_lang::__private::Lazy>::size_of(#buf) }
});
quote! {
#(#declarations;)*
0 #(+ #names)*
}
}

View File

@ -1,5 +1,8 @@
extern crate proc_macro;
#[cfg(feature = "lazy-account")]
mod lazy;
use borsh_derive_internal::*;
use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2};
@ -73,5 +76,24 @@ fn gen_borsh_deserialize(input: TokenStream) -> TokenStream2 {
#[proc_macro_derive(AnchorDeserialize, attributes(borsh_skip, borsh_init))]
pub fn borsh_deserialize(input: TokenStream) -> TokenStream {
TokenStream::from(gen_borsh_deserialize(input))
#[cfg(feature = "lazy-account")]
{
let deser = gen_borsh_deserialize(input.clone());
let lazy = lazy::gen_lazy(input).unwrap_or_else(|e| e.to_compile_error());
quote::quote! {
#deser
#lazy
}
.into()
}
#[cfg(not(feature = "lazy-account"))]
gen_borsh_deserialize(input).into()
}
#[cfg(feature = "lazy-account")]
#[proc_macro_derive(Lazy)]
pub fn lazy(input: TokenStream) -> TokenStream {
lazy::gen_lazy(input)
.unwrap_or_else(|e| e.to_compile_error())
.into()
}

View File

@ -0,0 +1,338 @@
//! Like [`Account`](crate::Account), but deserializes on-demand.
use std::{cell::RefCell, collections::BTreeSet, fmt, mem::MaybeUninit, rc::Rc};
use crate::{
error::{Error, ErrorCode},
AccountInfo, AccountMeta, AccountSerialize, Accounts, AccountsClose, Discriminator, Key, Owner,
Pubkey, Result, ToAccountInfo, ToAccountInfos, ToAccountMetas,
};
/// Deserialize account data lazily (on-demand).
///
/// Anchor uses [`borsh`] deserialization by default, which can be expensive for both memory and
/// compute units usage.
///
/// With the regular [`Account`] type, all account data gets deserialized, even the fields not used
/// by your instruction. On contrast, [`LazyAccount`] allows you to deserialize individual fields,
/// saving both memory and compute units.
///
/// # Table of contents
///
/// - [When to use](#when-to-use)
/// - [Features](#features)
/// - [Example](#example)
/// - [Safety](#safety)
/// - [Performance](#performance)
/// - [Memory](#memory)
/// - [Compute units](#compute-units)
///
/// # When to use
///
/// This is currently an experimental account type, and therefore should only be used when you're
/// running into performance issues.
///
/// It's best to use [`LazyAccount`] when you only need to deserialize some of the fields,
/// especially if the account is read-only.
///
/// Replacing [`Account`] (including `Box`ed) with [`LazyAccount`] *can* improve both stack memory
/// and compute unit usage. However, this is not guaranteed. For example, if you need to
/// deserialize the account fully, using [`LazyAccount`] will have additional overhead and
/// therefore use slightly more compute units.
///
/// Currently, using the `mut` constraint eventually results in the whole account getting
/// deserialized, meaning it won't use fewer compute units compared to [`Account`]. This might get
/// optimized in the future.
///
/// # Features
///
/// - Can be used as a replacement for [`Account`].
/// - Checks the account owner and its discriminator.
/// - Does **not** check the type layout matches the defined layout.
/// - All account data can be deserialized with `load` and `load_mut` methods. These methods are
/// non-inlined, meaning that they're less likely to cause stack violation errors.
/// - Each individual field can be deserialized with the generated `load_<field>` and
/// `load_mut_<field>` methods.
///
/// # Example
///
/// ```
/// use anchor_lang::prelude::*;
///
/// declare_id!("LazyAccount11111111111111111111111111111111");
///
/// #[program]
/// pub mod lazy_account {
/// use super::*;
///
/// pub fn init(ctx: Context<Init>) -> Result<()> {
/// let mut my_account = ctx.accounts.my_account.load_mut()?;
/// my_account.authority = ctx.accounts.authority.key();
///
/// // Fill the dynamic data
/// for _ in 0..MAX_DATA_LEN {
/// my_account.dynamic.push(ctx.accounts.authority.key());
/// }
///
/// Ok(())
/// }
///
/// pub fn read(ctx: Context<Read>) -> Result<()> {
/// // Cached load due to the `has_one` constraint
/// let authority = ctx.accounts.my_account.load_authority()?;
/// msg!("Authority: {}", authority);
/// Ok(())
/// }
///
/// pub fn write(ctx: Context<Write>, new_authority: Pubkey) -> Result<()> {
/// // Cached load due to the `has_one` constraint
/// *ctx.accounts.my_account.load_mut_authority()? = new_authority;
/// Ok(())
/// }
/// }
///
/// #[derive(Accounts)]
/// pub struct Init<'info> {
/// #[account(mut)]
/// pub authority: Signer<'info>,
/// #[account(
/// init,
/// payer = authority,
/// space = MyAccount::DISCRIMINATOR.len() + MyAccount::INIT_SPACE
/// )]
/// pub my_account: LazyAccount<'info, MyAccount>,
/// pub system_program: Program<'info, System>,
/// }
///
/// #[derive(Accounts)]
/// pub struct Read<'info> {
/// pub authority: Signer<'info>,
/// #[account(has_one = authority)]
/// pub my_account: LazyAccount<'info, MyAccount>,
/// }
///
/// #[derive(Accounts)]
/// pub struct Write<'info> {
/// pub authority: Signer<'info>,
/// #[account(mut, has_one = authority)]
/// pub my_account: LazyAccount<'info, MyAccount>,
/// }
///
/// const MAX_DATA_LEN: usize = 256;
///
/// #[account]
/// #[derive(InitSpace)]
/// pub struct MyAccount {
/// pub authority: Pubkey,
/// pub fixed: [Pubkey; 8],
/// // Dynamic sized data also works, unlike `AccountLoader`
/// #[max_len(MAX_DATA_LEN)]
/// pub dynamic: Vec<Pubkey>,
/// }
/// ```
///
/// # Safety
///
/// The safety checks are done using the account's discriminator and the account's owner (similar
/// to [`Account`]). However, you should be extra careful when deserializing individual fields if,
/// for example, the account needs to be migrated. Make sure the previously serialized data always
/// matches the account's type identically.
///
/// # Performance
///
/// ## Memory
///
/// All fields (including the inner account type) are heap allocated. It only uses 24 bytes (3x
/// pointer size) of stack memory in total.
///
/// It's worth noting that where the account is being deserialized matters. For example, the main
/// place where Anchor programs are likely to hit stack violation errors is a generated function
/// called `try_accounts` (you might be familiar with it from the mangled build logs). This is
/// where the instruction is deserialized and constraints are run. Although having everything at the
/// same place is convenient for using constraints, this also makes it very easy to use the fixed
/// amount of stack space (4096 bytes) SVM allocates just by increasing the number of accounts the
/// instruction has. In SVM, each function has its own stack frame, meaning that it's possible to
/// deserialize more accounts simply by deserializing them inside other functions (rather than in
/// `try_accounts` which is already quite heavy).
///
/// The mentioned stack limitation can be solved using dynamic stack frames, see [SIMD-0166].
///
/// ## Compute units
///
/// Compute is harder to formulate, as it varies based on the inner account's type. That being said,
/// there are a few things you can do to optimize compute units usage when using [`LazyAccount`]:
///
/// - Order account fields from fixed-size data (e.g. `u8`, `Pubkey`) to dynamic data (e.g. `Vec`).
/// - Order account fields based on how frequently the field is accessed (starting with the most
/// frequent).
/// - Reduce or limit dynamic fields.
///
/// [`borsh`]: crate::prelude::borsh
/// [`Account`]: crate::prelude::Account
/// [SIMD-0166]: https://github.com/solana-foundation/solana-improvement-documents/pull/166
pub struct LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
/// **INTERNAL FIELD DO NOT USE!**
#[doc(hidden)]
pub __info: &'info AccountInfo<'info>,
/// **INTERNAL FIELD DO NOT USE!**
#[doc(hidden)]
pub __account: Rc<RefCell<MaybeUninit<T>>>,
/// **INTERNAL FIELD DO NOT USE!**
#[doc(hidden)]
pub __fields: Rc<RefCell<Option<Vec<bool>>>>,
}
impl<'info, T> fmt::Debug for LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone + fmt::Debug,
{
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
f.debug_struct("LazyAccount")
.field("info", &self.__info)
.field("account", &self.__account)
.field("fields", &self.__fields)
.finish()
}
}
impl<'info, T> LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
fn new(info: &'info AccountInfo<'info>) -> LazyAccount<'info, T> {
Self {
__info: info,
__account: Rc::new(RefCell::new(MaybeUninit::uninit())),
__fields: Rc::new(RefCell::new(None)),
}
}
/// Check both the owner and the discriminator.
pub fn try_from(info: &'info AccountInfo<'info>) -> Result<LazyAccount<'info, T>> {
let data = &info.try_borrow_data()?;
let disc = T::DISCRIMINATOR;
if data.len() < disc.len() {
return Err(ErrorCode::AccountDiscriminatorNotFound.into());
}
let given_disc = &data[..disc.len()];
if given_disc != disc {
return Err(ErrorCode::AccountDiscriminatorMismatch.into());
}
Self::try_from_unchecked(info)
}
/// Check the owner but **not** the discriminator.
pub fn try_from_unchecked(info: &'info AccountInfo<'info>) -> Result<LazyAccount<'info, T>> {
if info.owner != &T::owner() {
return Err(Error::from(ErrorCode::AccountOwnedByWrongProgram)
.with_pubkeys((*info.owner, T::owner())));
}
Ok(LazyAccount::new(info))
}
/// Unload the deserialized account value by resetting the cache.
///
/// This is useful when observing side-effects of CPIs.
///
/// # Usage
///
/// ```ignore
/// // Load the initial value
/// let initial_value = ctx.accounts.my_account.load_field()?;
///
/// // Do CPI...
///
/// // We still have a reference to the account from `initial_value`, drop it before `unload`
/// drop(initial_value);
///
/// // Load the updated value
/// let updated_value = ctx.accounts.my_account.unload()?.load_field()?;
/// ```
///
/// # Panics
///
/// If there is an existing reference (mutable or not) created by any of the `load` methods.
pub fn unload(&self) -> Result<&Self> {
// TODO: Should we drop the initialized fields manually?
*self.__account.borrow_mut() = MaybeUninit::uninit();
*self.__fields.borrow_mut() = None;
Ok(self)
}
}
impl<'info, B, T> Accounts<'info, B> for LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
#[inline(never)]
fn try_accounts(
_program_id: &Pubkey,
accounts: &mut &'info [AccountInfo<'info>],
_ix_data: &[u8],
_bumps: &mut B,
_reallocs: &mut BTreeSet<Pubkey>,
) -> Result<Self> {
if accounts.is_empty() {
return Err(ErrorCode::AccountNotEnoughKeys.into());
}
let account = &accounts[0];
*accounts = &accounts[1..];
LazyAccount::try_from(account)
}
}
impl<'info, T> AccountsClose<'info> for LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
fn close(&self, sol_destination: AccountInfo<'info>) -> Result<()> {
crate::common::close(self.to_account_info(), sol_destination)
}
}
impl<'info, T> ToAccountMetas for LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
let is_signer = is_signer.unwrap_or(self.__info.is_signer);
let meta = match self.__info.is_writable {
false => AccountMeta::new_readonly(*self.__info.key, is_signer),
true => AccountMeta::new(*self.__info.key, is_signer),
};
vec![meta]
}
}
impl<'info, T> ToAccountInfos<'info> for LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
fn to_account_infos(&self) -> Vec<AccountInfo<'info>> {
vec![self.to_account_info()]
}
}
impl<'info, T> AsRef<AccountInfo<'info>> for LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
fn as_ref(&self) -> &AccountInfo<'info> {
self.__info
}
}
impl<'info, T> Key for LazyAccount<'info, T>
where
T: AccountSerialize + Discriminator + Owner + Clone,
{
fn key(&self) -> Pubkey {
*self.__info.key
}
}

View File

@ -12,3 +12,6 @@ pub mod signer;
pub mod system_account;
pub mod sysvar;
pub mod unchecked_account;
#[cfg(feature = "lazy-account")]
pub mod lazy_account;

202
lang/src/lazy.rs Normal file
View File

@ -0,0 +1,202 @@
use crate::{AnchorDeserialize, Pubkey};
/// A helper trait to make lazy deserialization work.
///
/// Currently this is only implemented for [`borsh`], as it's not necessary for zero copy via
/// [`bytemuck`]. However, the functionality can be extended when we support custom serialization
/// in the future.
///
/// # Note
///
/// You should avoid implementing this trait manually.
///
/// It's currently implemented automatically if you derive [`AnchorDeserialize`]:
///
/// ```ignore
/// #[derive(AnchorDeserialize)]
/// pub struct MyStruct {
/// field: u8,
/// }
/// ```
pub trait Lazy: AnchorDeserialize {
/// Get the serialized size of the type from the given buffer.
///
/// For performance reasons, this method does not verify the validity of the data, and should
/// never fail.
///
/// # Panics
///
/// If the given buffer cannot be used to deserialize the data e.g. it's shorter than the
/// expected data. However, this doesn't mean it will panic **whenever** there is an incorrect
/// data e.g. passing **any** data for `bool::size_of` works, even when the buffer is empty.
fn size_of(buf: &[u8]) -> usize;
}
macro_rules! impl_sized {
($ty: ty) => {
impl Lazy for $ty {
#[inline(always)]
fn size_of(_buf: &[u8]) -> usize {
::core::mem::size_of::<$ty>()
}
}
};
}
impl_sized!(bool);
impl_sized!(u8);
impl_sized!(u16);
impl_sized!(u32);
impl_sized!(u64);
impl_sized!(u128);
impl_sized!(i8);
impl_sized!(i16);
impl_sized!(i32);
impl_sized!(i64);
impl_sized!(i128);
impl_sized!(f32);
impl_sized!(f64);
impl_sized!(Pubkey);
impl<T: Lazy, const N: usize> Lazy for [T; N] {
#[inline(always)]
fn size_of(buf: &[u8]) -> usize {
N * T::size_of(buf)
}
}
impl Lazy for String {
#[inline(always)]
fn size_of(buf: &[u8]) -> usize {
LEN + get_len(buf)
}
}
impl<T: Lazy> Lazy for Option<T> {
#[inline(always)]
fn size_of(buf: &[u8]) -> usize {
1 + match buf.first() {
Some(0) => 0,
Some(1) => T::size_of(&buf[1..]),
_ => unreachable!(),
}
}
}
impl<T: Lazy> Lazy for Vec<T> {
#[inline(always)]
fn size_of(buf: &[u8]) -> usize {
(0..get_len(buf)).fold(LEN, |acc, _| acc + T::size_of(&buf[acc..]))
}
}
/// `borsh` length identifier of unsized types.
const LEN: usize = 4;
#[inline(always)]
fn get_len(buf: &[u8]) -> usize {
u32::from_le_bytes((buf[..LEN].try_into()).unwrap())
.try_into()
.unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::AnchorSerialize;
macro_rules! len {
($val: expr) => {
$val.try_to_vec().unwrap().len()
};
}
#[test]
fn sized() {
// Sized inputs don't care about the passed data
const EMPTY: &[u8] = &[];
assert_eq!(bool::size_of(EMPTY), len!(true));
assert_eq!(u8::size_of(EMPTY), len!(0u8));
assert_eq!(u16::size_of(EMPTY), len!(0u16));
assert_eq!(u32::size_of(EMPTY), len!(0u32));
assert_eq!(u64::size_of(EMPTY), len!(0u64));
assert_eq!(u128::size_of(EMPTY), len!(0u128));
assert_eq!(i8::size_of(EMPTY), len!(0i8));
assert_eq!(i16::size_of(EMPTY), len!(0i16));
assert_eq!(i32::size_of(EMPTY), len!(0i32));
assert_eq!(i64::size_of(EMPTY), len!(0i64));
assert_eq!(i128::size_of(EMPTY), len!(0i128));
assert_eq!(f32::size_of(EMPTY), len!(0f32));
assert_eq!(f64::size_of(EMPTY), len!(0f64));
assert_eq!(Pubkey::size_of(EMPTY), len!(Pubkey::default()));
assert_eq!(<[i32; 4]>::size_of(EMPTY), len!([0i32; 4]));
}
#[test]
fn r#unsized() {
assert_eq!(String::size_of(&[1, 0, 0, 0, 65]), len!(String::from("a")));
assert_eq!(<Option<u8>>::size_of(&[0]), len!(Option::<u8>::None));
assert_eq!(<Option<u8>>::size_of(&[1, 1]), len!(Some(1u8)));
assert_eq!(<Vec<u8>>::size_of(&[1, 0, 0, 0, 1]), len!(vec![1u8]));
assert_eq!(
<Vec<String>>::size_of(&[1, 0, 0, 0, 1, 0, 0, 0, 65]),
len!(vec![String::from("a")])
);
assert_eq!(
<Vec<String>>::size_of(&[2, 0, 0, 0, 1, 0, 0, 0, 65, 2, 0, 0, 0, 65, 66]),
len!(vec![String::from("a"), String::from("ab")])
);
}
#[test]
fn defined() {
// Struct
#[derive(AnchorSerialize, AnchorDeserialize)]
struct MyStruct {
a: u8,
b: Vec<u8>,
c: Option<String>,
}
assert_eq!(
MyStruct::size_of(&[1, 2, 0, 0, 0, 1, 2, 1, 1, 0, 0, 0, 65]),
len!(MyStruct {
a: 1,
b: vec![1u8, 2],
c: Some(String::from("a"))
})
);
// Enum
#[derive(AnchorSerialize, AnchorDeserialize)]
enum MyEnum {
Unit,
Named { a: u8 },
Unnamed(i16, i16),
}
assert_eq!(MyEnum::size_of(&[0]), len!(MyEnum::Unit));
assert_eq!(MyEnum::size_of(&[1, 23]), len!(MyEnum::Named { a: 1 }));
assert_eq!(
MyEnum::size_of(&[2, 1, 2, 1, 2]),
len!(MyEnum::Unnamed(1, 2))
);
}
#[test]
fn generic() {
#[derive(AnchorSerialize, AnchorDeserialize)]
struct GenericStruct<T: Lazy> {
t: T,
}
assert_eq!(
GenericStruct::<i64>::size_of(&[1, 2, 3, 4, 5, 6, 7, 8]),
len!(GenericStruct { t: 1i64 })
);
assert_eq!(
GenericStruct::<Vec<u8>>::size_of(&[8, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8]),
len!(GenericStruct { t: vec![0u8; 8] })
);
}
}

View File

@ -44,8 +44,11 @@ pub mod event;
#[doc(hidden)]
pub mod idl;
pub mod system_program;
mod vec;
#[cfg(feature = "lazy-account")]
mod lazy;
pub use crate::bpf_upgradeable_state::*;
pub use anchor_attribute_access_control::access_control;
pub use anchor_attribute_account::{account, declare_id, pubkey, zero_copy};
@ -440,19 +443,21 @@ pub mod prelude {
#[cfg(feature = "interface-instructions")]
pub use super::interface;
#[cfg(feature = "lazy-account")]
pub use super::accounts::lazy_account::LazyAccount;
}
/// Internal module used by macros and unstable apis.
#[doc(hidden)]
pub mod __private {
pub use anchor_attribute_account::ZeroCopyAccessor;
pub use anchor_attribute_event::EventIndex;
pub use base64;
pub use bytemuck;
pub use crate::{bpf_writer::BpfWriter, common::is_closed};
use solana_program::pubkey::Pubkey;
// Used to calculate the maximum between two expressions.
@ -478,6 +483,11 @@ pub mod __private {
input.to_bytes()
}
}
#[cfg(feature = "lazy-account")]
pub use crate::lazy::Lazy;
#[cfg(feature = "lazy-account")]
pub use anchor_derive_serde::Lazy;
}
/// Ensures a condition is true, otherwise returns with the given error.

View File

@ -1,4 +1,4 @@
use quote::quote;
use quote::{format_ident, quote};
use std::collections::HashSet;
use crate::*;
@ -260,6 +260,13 @@ pub fn generate_constraint_has_one(
Ty::AccountLoader(_) => quote! {#ident.load()?},
_ => quote! {#ident},
};
let my_key = match &f.ty {
Ty::LazyAccount(_) => {
let load_ident = format_ident!("load_{}", target.to_token_stream().to_string());
quote! { *#field.#load_ident()? }
}
_ => quote! { #field.#target },
};
let error = generate_custom_error(
ident,
&c.error,
@ -272,7 +279,7 @@ pub fn generate_constraint_has_one(
quote! {
{
#target_optional_check
let my_key = #field.#target;
let my_key = #my_key;
let target_key = #target.key();
if my_key != target_key {
return #error;

View File

@ -1,6 +1,6 @@
use crate::accounts_codegen::constraints::OptionalCheckScope;
use crate::codegen::accounts::{generics, ParsedGenerics};
use crate::{AccountField, AccountsStruct};
use crate::{AccountField, AccountsStruct, Ty};
use quote::quote;
// Generates the `Exit` trait implementation.
@ -46,9 +46,16 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
} else {
match f.constraints.is_mutable() {
false => quote! {},
true => quote! {
anchor_lang::AccountsExit::exit(&self.#ident, program_id)
.map_err(|e| e.with_account_name(#name_str))?;
true => match &f.ty {
// `LazyAccount` is special because it has a custom `exit` method.
Ty::LazyAccount(_) => quote! {
self.#ident.exit(program_id)
.map_err(|e| e.with_account_name(#name_str))?;
},
_ => quote! {
anchor_lang::AccountsExit::exit(&self.#ident, program_id)
.map_err(|e| e.with_account_name(#name_str))?;
},
},
}
}

View File

@ -59,6 +59,7 @@ pub fn gen_idl_build_impl_accounts_struct(accounts: &AccountsStruct) -> TokenStr
{
Some(&ty.account_type_path)
}
Ty::LazyAccount(ty) => Some(&ty.account_type_path),
Ty::AccountLoader(ty) => Some(&ty.account_type_path),
Ty::InterfaceAccount(ty) => Some(&ty.account_type_path),
_ => None,

View File

@ -261,6 +261,7 @@ impl AccountField {
let qualified_ty_name = match self {
AccountField::Field(field) => match &field.ty {
Ty::Account(account) => Some(parser::tts_to_string(&account.account_type_path)),
Ty::LazyAccount(account) => Some(parser::tts_to_string(&account.account_type_path)),
_ => None,
},
AccountField::CompositeField(field) => Some(field.symbol.clone()),
@ -404,6 +405,23 @@ impl Field {
stream
}
}
Ty::LazyAccount(_) => {
if checked {
quote! {
match #container_ty::try_from(&#field) {
Ok(val) => val,
Err(e) => return Err(e.with_account_name(#field_str))
}
}
} else {
quote! {
match #container_ty::try_from_unchecked(&#field) {
Ok(val) => val,
Err(e) => return Err(e.with_account_name(#field_str))
}
}
}
}
Ty::AccountLoader(_) => {
if checked {
quote! {
@ -446,6 +464,9 @@ impl Field {
Ty::Account(_) => quote! {
anchor_lang::accounts::account::Account
},
Ty::LazyAccount(_) => quote! {
anchor_lang::accounts::lazy_account::LazyAccount
},
Ty::AccountLoader(_) => quote! {
anchor_lang::accounts::account_loader::AccountLoader
},
@ -487,6 +508,12 @@ impl Field {
#ident
}
}
Ty::LazyAccount(ty) => {
let ident = &ty.account_type_path;
quote! {
#ident
}
}
Ty::InterfaceAccount(ty) => {
let ident = &ty.account_type_path;
quote! {
@ -545,6 +572,7 @@ pub enum Ty {
AccountLoader(AccountLoaderTy),
Sysvar(SysvarTy),
Account(AccountTy),
LazyAccount(LazyAccountTy),
Program(ProgramTy),
Interface(InterfaceTy),
InterfaceAccount(InterfaceAccountTy),
@ -581,6 +609,12 @@ pub struct AccountTy {
pub boxed: bool,
}
#[derive(Debug, PartialEq, Eq)]
pub struct LazyAccountTy {
// The struct type of the account.
pub account_type_path: TypePath,
}
#[derive(Debug, PartialEq, Eq)]
pub struct InterfaceAccountTy {
// The struct type of the account.

View File

@ -1177,7 +1177,10 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
// Require a known account type that implements the `Discriminator` trait so that we can
// get the discriminator length dynamically
if !matches!(&self.f_ty, Some(Ty::Account(_) | Ty::AccountLoader(_))) {
if !matches!(
&self.f_ty,
Some(Ty::Account(_) | Ty::LazyAccount(_) | Ty::AccountLoader(_))
) {
return Err(ParseError::new(
c.span(),
"`zero` constraint requires the type to implement the `Discriminator` trait",
@ -1189,11 +1192,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
fn add_realloc(&mut self, c: Context<ConstraintRealloc>) -> ParseResult<()> {
if !matches!(self.f_ty, Some(Ty::Account(_)))
&& !matches!(self.f_ty, Some(Ty::LazyAccount(_)))
&& !matches!(self.f_ty, Some(Ty::AccountLoader(_)))
{
return Err(ParseError::new(
c.span(),
"realloc must be on an Account or AccountLoader",
"realloc must be on an Account, LazyAccount or AccountLoader",
));
}
if self.mutable.is_none() {
@ -1239,11 +1243,12 @@ impl<'ty> ConstraintGroupBuilder<'ty> {
fn add_close(&mut self, c: Context<ConstraintClose>) -> ParseResult<()> {
if !matches!(self.f_ty, Some(Ty::Account(_)))
&& !matches!(self.f_ty, Some(Ty::LazyAccount(_)))
&& !matches!(self.f_ty, Some(Ty::AccountLoader(_)))
{
return Err(ParseError::new(
c.span(),
"close must be on an Account, AccountLoader",
"close must be on an Account, LazyAccount or AccountLoader",
));
}
if self.mutable.is_none() {

View File

@ -334,6 +334,7 @@ fn is_field_primitive(f: &syn::Field) -> ParseResult<bool> {
| "UncheckedAccount"
| "AccountLoader"
| "Account"
| "LazyAccount"
| "Program"
| "Interface"
| "InterfaceAccount"
@ -352,6 +353,7 @@ fn parse_ty(f: &syn::Field) -> ParseResult<(Ty, bool)> {
"UncheckedAccount" => Ty::UncheckedAccount,
"AccountLoader" => Ty::AccountLoader(parse_program_account_loader(&path)?),
"Account" => Ty::Account(parse_account_ty(&path)?),
"LazyAccount" => Ty::LazyAccount(parse_lazy_account_ty(&path)?),
"Program" => Ty::Program(parse_program_ty(&path)?),
"Interface" => Ty::Interface(parse_interface_ty(&path)?),
"InterfaceAccount" => Ty::InterfaceAccount(parse_interface_account_ty(&path)?),
@ -444,6 +446,11 @@ fn parse_account_ty(path: &syn::Path) -> ParseResult<AccountTy> {
})
}
fn parse_lazy_account_ty(path: &syn::Path) -> ParseResult<LazyAccountTy> {
let account_type_path = parse_account(path)?;
Ok(LazyAccountTy { account_type_path })
}
fn parse_interface_account_ty(path: &syn::Path) -> ParseResult<InterfaceAccountTy> {
let account_type_path = parse_account(path)?;
let boxed = parser::tts_to_string(path)

View File

@ -0,0 +1,9 @@
[programs.localnet]
lazy_account = "LazyAccount11111111111111111111111111111111"
[provider]
cluster = "localnet"
wallet = "~/.config/solana/id.json"
[scripts]
test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts"

View File

@ -0,0 +1,14 @@
[workspace]
members = [
"programs/*"
]
resolver = "2"
[profile.release]
overflow-checks = true
lto = "fat"
codegen-units = 1
[profile.release.build-override]
opt-level = 3
incremental = false
codegen-units = 1

View File

@ -0,0 +1,16 @@
{
"name": "lazy-account",
"version": "0.30.1",
"license": "(MIT OR Apache-2.0)",
"homepage": "https://github.com/coral-xyz/anchor#readme",
"bugs": {
"url": "https://github.com/coral-xyz/anchor/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/coral-xyz/anchor.git"
},
"engines": {
"node": ">=17"
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "lazy-account"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "lazy_account"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
idl-build = ["anchor-lang/idl-build"]
[dependencies]
anchor-lang = { path = "../../../../lang", features = ["lazy-account"] }

View File

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

View File

@ -0,0 +1,114 @@
//! Tests demonstraing the usage of [`LazyAccount`].
//!
//! The tests have been simplied by using a stack heavy account in order to demonstrate the usage
//! and its usefulness without adding excessive amount of accounts.
//!
//! See the individual instructions for more documentation: [`Init`], [`Read`], [`Write`].
use anchor_lang::prelude::*;
declare_id!("LazyAccount11111111111111111111111111111111");
#[program]
pub mod lazy_account {
use super::*;
pub fn init(ctx: Context<Init>) -> Result<()> {
let mut my_account = ctx.accounts.my_account.load_mut()?;
my_account.authority = ctx.accounts.authority.key();
for _ in 0..MAX_DATA_LEN {
my_account.dynamic.push(ctx.accounts.authority.key());
}
Ok(())
}
pub fn read(ctx: Context<Read>) -> Result<()> {
// Cached load due to the `has_one` constraint
let authority = ctx.accounts.my_account.load_authority()?;
msg!("Authority: {}", authority);
Ok(())
}
pub fn write(ctx: Context<Write>, new_authority: Pubkey) -> Result<()> {
// Cached load due to the `has_one` constraint
*ctx.accounts.my_account.load_mut_authority()? = new_authority;
Ok(())
}
}
#[derive(Accounts)]
pub struct Init<'info> {
#[account(mut)]
pub authority: Signer<'info>,
#[account(
init,
payer = authority,
space = MyAccount::DISCRIMINATOR.len() + MyAccount::INIT_SPACE,
seeds = [b"my_account"],
bump
)]
pub my_account: LazyAccount<'info, MyAccount>,
/// Using `Account` instead of `LazyAccount` would either make the instruction fail due to
/// access violation errors, or worse, it would cause undefined behavior instead.
///
/// Using `Account` with Solana v1.18.17 (`platform-tools` v1.41) results in a stack violation
/// error (without a compiler error/warning on build).
#[account(
init,
payer = authority,
space = StackHeavyAccount::DISCRIMINATOR.len() + StackHeavyAccount::INIT_SPACE,
seeds = [b"stack_heavy_account"],
bump
)]
pub stack_heavy_account: LazyAccount<'info, StackHeavyAccount>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Read<'info> {
pub authority: Signer<'info>,
/// Using `Account` or `Box<Account>` instead of `LazyAccount` would increase the compute
/// units usage by ~90k units due to the unnecessary deserialization of the unused fields.
#[account(seeds = [b"my_account"], bump, has_one = authority)]
pub my_account: LazyAccount<'info, MyAccount>,
/// This account imitates heavy stack usage in more complex programs
#[account(seeds = [b"stack_heavy_account"], bump)]
pub stack_heavy_account: Account<'info, StackHeavyAccount>,
}
#[derive(Accounts)]
pub struct Write<'info> {
pub authority: Signer<'info>,
/// Using `Account` instead of `LazyAccount` would either make the instruction fail due to stack
/// violation errors, or worse, it would cause undefined behavior instead.
///
/// Using `Account` with Solana v1.18.17 (`platform-tools` v1.41) results in undefined behavior
/// in this instruction, and the authority field gets corrupted when writing.
#[account(mut, seeds = [b"my_account"], bump, has_one = authority)]
pub my_account: LazyAccount<'info, MyAccount>,
/// This account imitates heavy stack usage in more complex programs
#[account(seeds = [b"stack_heavy_account"], bump)]
pub stack_heavy_account: Account<'info, StackHeavyAccount>,
}
const MAX_DATA_LEN: usize = 256;
#[account]
#[derive(InitSpace)]
pub struct MyAccount {
pub authority: Pubkey,
/// Fixed size data
pub fixed: [Pubkey; 8],
/// Dynamic sized data also works, unlike `AccountLoader`
#[max_len(MAX_DATA_LEN)]
pub dynamic: Vec<Pubkey>,
}
/// Stack heavy filler account that imitates heavy stack usage caused my many accounts
#[account]
#[derive(InitSpace)]
pub struct StackHeavyAccount {
pub data: [u8; 1600],
}

View File

@ -0,0 +1,36 @@
import * as anchor from "@coral-xyz/anchor";
import assert from "assert";
import type { LazyAccount } from "../target/types/lazy_account";
describe("lazy-account", () => {
anchor.setProvider(anchor.AnchorProvider.env());
const program: anchor.Program<LazyAccount> = anchor.workspace.lazyAccount;
it("Can init", async () => {
const { pubkeys, signature } = await program.methods.init().rpcAndKeys();
await program.provider.connection.confirmTransaction(
signature,
"confirmed"
);
const myAccount = await program.account.myAccount.fetch(pubkeys.myAccount);
assert(myAccount.authority.equals(program.provider.publicKey!));
});
it("Can read", async () => {
await program.methods.read().rpc();
});
it("Can write", async () => {
const newAuthority = anchor.web3.PublicKey.default;
const { pubkeys, signature } = await program.methods
.write(newAuthority)
.rpcAndKeys();
await program.provider.connection.confirmTransaction(
signature,
"confirmed"
);
const myAccount = await program.account.myAccount.fetch(pubkeys.myAccount);
assert(myAccount.authority.equals(newAuthority));
});
});

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"types": ["mocha", "chai"],
"lib": ["es2015"],
"module": "commonjs",
"target": "es6",
"esModuleInterop": true,
"strict": true,
"skipLibCheck": true
}
}

View File

@ -25,6 +25,7 @@
"idl",
"ido-pool",
"interface",
"lazy-account",
"lockup",
"misc",
"multisig",