lang, ts: Account close constraint (#371)
This commit is contained in:
parent
ba99c9c920
commit
df51a27a48
|
@ -15,6 +15,7 @@ incremented for features.
|
||||||
|
|
||||||
* cli: Add `--program-name` option for build command to build a single program at a time ([#362](https://github.com/project-serum/anchor/pull/362)).
|
* cli: Add `--program-name` option for build command to build a single program at a time ([#362](https://github.com/project-serum/anchor/pull/362)).
|
||||||
* cli, client: Parse custom cluster urls from str ([#369](https://github.com/project-serum/anchor/pull/369)).
|
* cli, client: Parse custom cluster urls from str ([#369](https://github.com/project-serum/anchor/pull/369)).
|
||||||
|
* lang: Add `#[account(close = <destination>)]` constraint for closing accounts and sending the rent exemption lamports to a specified destination account ([#371](https://github.com/project-serum/anchor/pull/371)).
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|
||||||
|
|
|
@ -163,6 +163,7 @@ dependencies = [
|
||||||
"solana-client",
|
"solana-client",
|
||||||
"solana-sdk",
|
"solana-sdk",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
|
"url",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
@ -82,6 +82,10 @@ pub mod misc {
|
||||||
ctx.accounts.data.data = data;
|
ctx.accounts.data.data = data;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn test_close(_ctx: Context<TestClose>) -> ProgramResult {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Accounts)]
|
#[derive(Accounts)]
|
||||||
|
@ -117,6 +121,13 @@ pub struct TestStateCpi<'info> {
|
||||||
misc2_program: AccountInfo<'info>,
|
misc2_program: AccountInfo<'info>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Accounts)]
|
||||||
|
pub struct TestClose<'info> {
|
||||||
|
#[account(mut, close = sol_dest)]
|
||||||
|
data: ProgramAccount<'info, Data>,
|
||||||
|
sol_dest: AccountInfo<'info>,
|
||||||
|
}
|
||||||
|
|
||||||
// `my_account` is the associated token account being created.
|
// `my_account` is the associated token account being created.
|
||||||
// `authority` must be a `mut` and `signer` since it will pay for the creation
|
// `authority` must be a `mut` and `signer` since it will pay for the creation
|
||||||
// of the associated token account. `state` is used as an association, i.e., one
|
// of the associated token account. `state` is used as an association, i.e., one
|
||||||
|
|
|
@ -257,7 +257,60 @@ describe("misc", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Can use base58 strings to fetch an account", async () => {
|
it("Can use base58 strings to fetch an account", async () => {
|
||||||
const dataAccount = await program.account.dataI16.fetch(dataPubkey.toString());
|
const dataAccount = await program.account.dataI16.fetch(
|
||||||
|
dataPubkey.toString()
|
||||||
|
);
|
||||||
assert.ok(dataAccount.data === -2048);
|
assert.ok(dataAccount.data === -2048);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("Should fail to close an account when sending lamports to itself", async () => {
|
||||||
|
try {
|
||||||
|
await program.rpc.testClose({
|
||||||
|
accounts: {
|
||||||
|
data: data.publicKey,
|
||||||
|
solDest: data.publicKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.ok(false);
|
||||||
|
} catch (err) {
|
||||||
|
const errMsg = "A close constraint was violated";
|
||||||
|
assert.equal(err.toString(), errMsg);
|
||||||
|
assert.equal(err.msg, errMsg);
|
||||||
|
assert.equal(err.code, 151);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Can close an account", async () => {
|
||||||
|
const openAccount = await program.provider.connection.getAccountInfo(
|
||||||
|
data.publicKey
|
||||||
|
);
|
||||||
|
assert.ok(openAccount !== null);
|
||||||
|
|
||||||
|
let beforeBalance = (
|
||||||
|
await program.provider.connection.getAccountInfo(
|
||||||
|
program.provider.wallet.publicKey
|
||||||
|
)
|
||||||
|
).lamports;
|
||||||
|
|
||||||
|
await program.rpc.testClose({
|
||||||
|
accounts: {
|
||||||
|
data: data.publicKey,
|
||||||
|
solDest: program.provider.wallet.publicKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let afterBalance = (
|
||||||
|
await program.provider.connection.getAccountInfo(
|
||||||
|
program.provider.wallet.publicKey
|
||||||
|
)
|
||||||
|
).lamports;
|
||||||
|
|
||||||
|
// Retrieved rent exemption sol.
|
||||||
|
assert.ok(afterBalance > beforeBalance);
|
||||||
|
|
||||||
|
const closedAccount = await program.provider.connection.getAccountInfo(
|
||||||
|
data.publicKey
|
||||||
|
);
|
||||||
|
assert.ok(closedAccount === null);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -40,6 +40,7 @@ use syn::parse_macro_input;
|
||||||
/// | `#[account(signer)]` | On raw `AccountInfo` structs. | Checks the given account signed the transaction. |
|
/// | `#[account(signer)]` | On raw `AccountInfo` structs. | Checks the given account signed the transaction. |
|
||||||
/// | `#[account(mut)]` | On `AccountInfo`, `ProgramAccount` or `CpiAccount` structs. | Marks the account as mutable and persists the state transition. |
|
/// | `#[account(mut)]` | On `AccountInfo`, `ProgramAccount` or `CpiAccount` 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. When using `init`, a `rent` `Sysvar` must be present in the `Accounts` struct. |
|
/// | `#[account(init)]` | On `ProgramAccount` structs. | Marks the account as being initialized, skipping the account discriminator check. When using `init`, a `rent` `Sysvar` must be present in the `Accounts` struct. |
|
||||||
|
/// | `#[account(close = <target>)]` | On `ProgramAccount` and `Loader` structs. | Marks the account as being closed at the end of the instruction's execution, sending the rent exemption lamports to the specified <target>. |
|
||||||
/// | `#[account(belongs_to = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
|
/// | `#[account(belongs_to = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Checks the `target` field on the account matches the `target` field in the struct deriving `Accounts`. |
|
||||||
/// | `#[account(has_one = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Semantically different, but otherwise the same as `belongs_to`. |
|
/// | `#[account(has_one = <target>)]` | On `ProgramAccount` or `CpiAccount` structs | Semantically different, but otherwise the same as `belongs_to`. |
|
||||||
/// | `#[account(seeds = [<seeds>])]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. |
|
/// | `#[account(seeds = [<seeds>])]` | On `AccountInfo` structs | Seeds for the program derived address an `AccountInfo` struct represents. |
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
use crate::error::ErrorCode;
|
||||||
|
use solana_program::account_info::AccountInfo;
|
||||||
|
use solana_program::entrypoint::ProgramResult;
|
||||||
|
use std::io::Write;
|
||||||
|
|
||||||
|
pub fn close<'info>(
|
||||||
|
info: AccountInfo<'info>,
|
||||||
|
sol_destination: AccountInfo<'info>,
|
||||||
|
) -> ProgramResult {
|
||||||
|
// Transfer tokens from the account to the sol_destination.
|
||||||
|
let dest_starting_lamports = sol_destination.lamports();
|
||||||
|
**sol_destination.lamports.borrow_mut() =
|
||||||
|
dest_starting_lamports.checked_add(info.lamports()).unwrap();
|
||||||
|
**info.lamports.borrow_mut() = 0;
|
||||||
|
|
||||||
|
// Mark the account discriminator as closed.
|
||||||
|
let mut data = info.try_borrow_mut_data()?;
|
||||||
|
let dst: &mut [u8] = &mut data;
|
||||||
|
let mut cursor = std::io::Cursor::new(dst);
|
||||||
|
cursor
|
||||||
|
.write_all(&crate::__private::CLOSED_ACCOUNT_DISCRIMINATOR)
|
||||||
|
.map_err(|_| ErrorCode::AccountDidNotSerialize)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -42,6 +42,8 @@ pub enum ErrorCode {
|
||||||
ConstraintAssociated,
|
ConstraintAssociated,
|
||||||
#[msg("An associated init constraint was violated")]
|
#[msg("An associated init constraint was violated")]
|
||||||
ConstraintAssociatedInit,
|
ConstraintAssociatedInit,
|
||||||
|
#[msg("A close constraint was violated")]
|
||||||
|
ConstraintClose,
|
||||||
|
|
||||||
// Accounts.
|
// Accounts.
|
||||||
#[msg("The account discriminator was already set on this account")]
|
#[msg("The account discriminator was already set on this account")]
|
||||||
|
|
|
@ -25,6 +25,7 @@ extern crate self as anchor_lang;
|
||||||
|
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
use solana_program::account_info::AccountInfo;
|
use solana_program::account_info::AccountInfo;
|
||||||
|
use solana_program::entrypoint::ProgramResult;
|
||||||
use solana_program::instruction::AccountMeta;
|
use solana_program::instruction::AccountMeta;
|
||||||
use solana_program::program_error::ProgramError;
|
use solana_program::program_error::ProgramError;
|
||||||
use solana_program::pubkey::Pubkey;
|
use solana_program::pubkey::Pubkey;
|
||||||
|
@ -32,6 +33,7 @@ use std::io::Write;
|
||||||
|
|
||||||
mod account_info;
|
mod account_info;
|
||||||
mod boxed;
|
mod boxed;
|
||||||
|
mod common;
|
||||||
mod context;
|
mod context;
|
||||||
mod cpi_account;
|
mod cpi_account;
|
||||||
mod cpi_state;
|
mod cpi_state;
|
||||||
|
@ -92,7 +94,13 @@ pub trait Accounts<'info>: ToAccountMetas + ToAccountInfos<'info> + Sized {
|
||||||
/// should be done here.
|
/// should be done here.
|
||||||
pub trait AccountsExit<'info>: ToAccountMetas + ToAccountInfos<'info> {
|
pub trait AccountsExit<'info>: ToAccountMetas + ToAccountInfos<'info> {
|
||||||
/// `program_id` is the currently executing program.
|
/// `program_id` is the currently executing program.
|
||||||
fn exit(&self, program_id: &Pubkey) -> solana_program::entrypoint::ProgramResult;
|
fn exit(&self, program_id: &Pubkey) -> ProgramResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The close procedure to initiate garabage collection of an account, allowing
|
||||||
|
/// one to retrieve the rent exemption.
|
||||||
|
pub trait AccountsClose<'info>: ToAccountInfos<'info> {
|
||||||
|
fn close(&self, sol_destination: AccountInfo<'info>) -> ProgramResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A data structure of accounts providing a one time deserialization upon
|
/// A data structure of accounts providing a one time deserialization upon
|
||||||
|
@ -275,4 +283,5 @@ pub mod __private {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub use crate::state::PROGRAM_STATE_SEED;
|
pub use crate::state::PROGRAM_STATE_SEED;
|
||||||
|
pub const CLOSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 255];
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::error::ErrorCode;
|
use crate::error::ErrorCode;
|
||||||
use crate::{
|
use crate::{
|
||||||
Accounts, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos, ToAccountMetas, ZeroCopy,
|
Accounts, AccountsClose, AccountsExit, AccountsInit, ToAccountInfo, ToAccountInfos,
|
||||||
|
ToAccountMetas, ZeroCopy,
|
||||||
};
|
};
|
||||||
use solana_program::account_info::AccountInfo;
|
use solana_program::account_info::AccountInfo;
|
||||||
use solana_program::entrypoint::ProgramResult;
|
use solana_program::entrypoint::ProgramResult;
|
||||||
|
@ -175,6 +176,12 @@ impl<'info, T: ZeroCopy> AccountsExit<'info> for Loader<'info, T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'info, T: ZeroCopy> AccountsClose<'info> for Loader<'info, T> {
|
||||||
|
fn close(&self, sol_destination: AccountInfo<'info>) -> ProgramResult {
|
||||||
|
crate::common::close(self.to_account_info(), sol_destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'info, T: ZeroCopy> ToAccountMetas for Loader<'info, T> {
|
impl<'info, T: ZeroCopy> ToAccountMetas for Loader<'info, T> {
|
||||||
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
|
fn to_account_metas(&self, is_signer: Option<bool>) -> Vec<AccountMeta> {
|
||||||
let is_signer = is_signer.unwrap_or(self.acc_info.is_signer);
|
let is_signer = is_signer.unwrap_or(self.acc_info.is_signer);
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use crate::error::ErrorCode;
|
use crate::error::ErrorCode;
|
||||||
use crate::{
|
use crate::{
|
||||||
AccountDeserialize, AccountSerialize, Accounts, AccountsExit, AccountsInit, CpiAccount,
|
AccountDeserialize, AccountSerialize, Accounts, AccountsClose, AccountsExit, AccountsInit,
|
||||||
ToAccountInfo, ToAccountInfos, ToAccountMetas,
|
CpiAccount, ToAccountInfo, ToAccountInfos, ToAccountMetas,
|
||||||
};
|
};
|
||||||
use solana_program::account_info::AccountInfo;
|
use solana_program::account_info::AccountInfo;
|
||||||
use solana_program::entrypoint::ProgramResult;
|
use solana_program::entrypoint::ProgramResult;
|
||||||
|
@ -120,6 +120,14 @@ impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsExit<'info
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl<'info, T: AccountSerialize + AccountDeserialize + Clone> AccountsClose<'info>
|
||||||
|
for ProgramAccount<'info, T>
|
||||||
|
{
|
||||||
|
fn close(&self, sol_destination: AccountInfo<'info>) -> ProgramResult {
|
||||||
|
crate::common::close(self.to_account_info(), sol_destination)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
|
impl<'info, T: AccountSerialize + AccountDeserialize + Clone> ToAccountMetas
|
||||||
for ProgramAccount<'info, T>
|
for ProgramAccount<'info, T>
|
||||||
{
|
{
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo,
|
CompositeField, Constraint, ConstraintAssociatedGroup, ConstraintBelongsTo, ConstraintClose,
|
||||||
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
|
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
|
||||||
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
|
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
|
||||||
ConstraintState, Field, Ty,
|
ConstraintState, Field, Ty,
|
||||||
|
@ -50,6 +50,7 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
|
||||||
executable,
|
executable,
|
||||||
state,
|
state,
|
||||||
associated,
|
associated,
|
||||||
|
close,
|
||||||
} = c_group.clone();
|
} = c_group.clone();
|
||||||
|
|
||||||
let mut constraints = Vec::new();
|
let mut constraints = Vec::new();
|
||||||
|
@ -94,6 +95,9 @@ pub fn linearize(c_group: &ConstraintGroup) -> Vec<Constraint> {
|
||||||
if let Some(c) = state {
|
if let Some(c) = state {
|
||||||
constraints.push(Constraint::State(c));
|
constraints.push(Constraint::State(c));
|
||||||
}
|
}
|
||||||
|
if let Some(c) = close {
|
||||||
|
constraints.push(Constraint::Close(c));
|
||||||
|
}
|
||||||
constraints
|
constraints
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +115,7 @@ fn generate_constraint(f: &Field, c: &Constraint) -> proc_macro2::TokenStream {
|
||||||
Constraint::Executable(c) => generate_constraint_executable(f, c),
|
Constraint::Executable(c) => generate_constraint_executable(f, c),
|
||||||
Constraint::State(c) => generate_constraint_state(f, c),
|
Constraint::State(c) => generate_constraint_state(f, c),
|
||||||
Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c),
|
Constraint::AssociatedGroup(c) => generate_constraint_associated(f, c),
|
||||||
|
Constraint::Close(c) => generate_constraint_close(f, c),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,6 +131,16 @@ pub fn generate_constraint_init(_f: &Field, _c: &ConstraintInit) -> proc_macro2:
|
||||||
quote! {}
|
quote! {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn generate_constraint_close(f: &Field, c: &ConstraintClose) -> proc_macro2::TokenStream {
|
||||||
|
let field = &f.ident;
|
||||||
|
let target = &c.sol_dest;
|
||||||
|
quote! {
|
||||||
|
if #field.to_account_info().key == #target.to_account_info().key {
|
||||||
|
return Err(anchor_lang::__private::ErrorCode::ConstraintClose.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn generate_constraint_mut(f: &Field, _c: &ConstraintMut) -> proc_macro2::TokenStream {
|
pub fn generate_constraint_mut(f: &Field, _c: &ConstraintMut) -> proc_macro2::TokenStream {
|
||||||
let ident = &f.ident;
|
let ident = &f.ident;
|
||||||
quote! {
|
quote! {
|
||||||
|
|
|
@ -19,11 +19,21 @@ pub fn generate(accs: &AccountsStruct) -> proc_macro2::TokenStream {
|
||||||
}
|
}
|
||||||
AccountField::Field(f) => {
|
AccountField::Field(f) => {
|
||||||
let ident = &f.ident;
|
let ident = &f.ident;
|
||||||
match f.constraints.is_mutable() {
|
if f.constraints.is_close() {
|
||||||
false => quote! {},
|
let close_target = &f.constraints.close.as_ref().unwrap().sol_dest;
|
||||||
true => quote! {
|
quote! {
|
||||||
anchor_lang::AccountsExit::exit(&self.#ident, program_id)?;
|
anchor_lang::AccountsClose::close(
|
||||||
},
|
&self.#ident,
|
||||||
|
self.#close_target.to_account_info(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match f.constraints.is_mutable() {
|
||||||
|
false => quote! {},
|
||||||
|
true => quote! {
|
||||||
|
anchor_lang::AccountsExit::exit(&self.#ident, program_id)?;
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -257,6 +257,7 @@ pub struct ConstraintGroup {
|
||||||
belongs_to: Vec<ConstraintBelongsTo>,
|
belongs_to: Vec<ConstraintBelongsTo>,
|
||||||
literal: Vec<ConstraintLiteral>,
|
literal: Vec<ConstraintLiteral>,
|
||||||
raw: Vec<ConstraintRaw>,
|
raw: Vec<ConstraintRaw>,
|
||||||
|
close: Option<ConstraintClose>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConstraintGroup {
|
impl ConstraintGroup {
|
||||||
|
@ -271,6 +272,10 @@ impl ConstraintGroup {
|
||||||
pub fn is_signer(&self) -> bool {
|
pub fn is_signer(&self) -> bool {
|
||||||
self.signer.is_some()
|
self.signer.is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn is_close(&self) -> bool {
|
||||||
|
self.close.is_some()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A single account constraint *after* merging all tokens into a well formed
|
// A single account constraint *after* merging all tokens into a well formed
|
||||||
|
@ -290,6 +295,7 @@ pub enum Constraint {
|
||||||
Executable(ConstraintExecutable),
|
Executable(ConstraintExecutable),
|
||||||
State(ConstraintState),
|
State(ConstraintState),
|
||||||
AssociatedGroup(ConstraintAssociatedGroup),
|
AssociatedGroup(ConstraintAssociatedGroup),
|
||||||
|
Close(ConstraintClose),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
|
// Constraint token is a single keyword in a `#[account(<TOKEN>)]` attribute.
|
||||||
|
@ -306,7 +312,7 @@ pub enum ConstraintToken {
|
||||||
Seeds(Context<ConstraintSeeds>),
|
Seeds(Context<ConstraintSeeds>),
|
||||||
Executable(Context<ConstraintExecutable>),
|
Executable(Context<ConstraintExecutable>),
|
||||||
State(Context<ConstraintState>),
|
State(Context<ConstraintState>),
|
||||||
AssociatedGroup(ConstraintAssociatedGroup),
|
Close(Context<ConstraintClose>),
|
||||||
Associated(Context<ConstraintAssociated>),
|
Associated(Context<ConstraintAssociated>),
|
||||||
AssociatedPayer(Context<ConstraintAssociatedPayer>),
|
AssociatedPayer(Context<ConstraintAssociatedPayer>),
|
||||||
AssociatedSpace(Context<ConstraintAssociatedSpace>),
|
AssociatedSpace(Context<ConstraintAssociatedSpace>),
|
||||||
|
@ -396,6 +402,11 @@ pub struct ConstraintAssociatedSpace {
|
||||||
pub space: LitInt,
|
pub space: LitInt,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct ConstraintClose {
|
||||||
|
pub sol_dest: Ident,
|
||||||
|
}
|
||||||
|
|
||||||
// Syntaxt context object for preserving metadata about the inner item.
|
// Syntaxt context object for preserving metadata about the inner item.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct Context<T> {
|
pub struct Context<T> {
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
|
ConstraintAssociated, ConstraintAssociatedGroup, ConstraintAssociatedPayer,
|
||||||
ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintExecutable,
|
ConstraintAssociatedSpace, ConstraintAssociatedWith, ConstraintBelongsTo, ConstraintClose,
|
||||||
ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut, ConstraintOwner,
|
ConstraintExecutable, ConstraintGroup, ConstraintInit, ConstraintLiteral, ConstraintMut,
|
||||||
ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner, ConstraintState,
|
ConstraintOwner, ConstraintRaw, ConstraintRentExempt, ConstraintSeeds, ConstraintSigner,
|
||||||
ConstraintToken, Context,
|
ConstraintState, ConstraintToken, Context, Ty,
|
||||||
};
|
};
|
||||||
use syn::ext::IdentExt;
|
use syn::ext::IdentExt;
|
||||||
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
use syn::parse::{Error as ParseError, Parse, ParseStream, Result as ParseResult};
|
||||||
|
@ -12,8 +12,8 @@ use syn::spanned::Spanned;
|
||||||
use syn::token::Comma;
|
use syn::token::Comma;
|
||||||
use syn::{bracketed, Expr, Ident, LitStr, Token};
|
use syn::{bracketed, Expr, Ident, LitStr, Token};
|
||||||
|
|
||||||
pub fn parse(f: &syn::Field) -> ParseResult<ConstraintGroup> {
|
pub fn parse(f: &syn::Field, f_ty: Option<&Ty>) -> ParseResult<ConstraintGroup> {
|
||||||
let mut constraints = ConstraintGroupBuilder::default();
|
let mut constraints = ConstraintGroupBuilder::new(f_ty);
|
||||||
for attr in f.attrs.iter().filter(is_account) {
|
for attr in f.attrs.iter().filter(is_account) {
|
||||||
for c in attr.parse_args_with(Punctuated::<ConstraintToken, Comma>::parse_terminated)? {
|
for c in attr.parse_args_with(Punctuated::<ConstraintToken, Comma>::parse_terminated)? {
|
||||||
constraints.add(c)?;
|
constraints.add(c)?;
|
||||||
|
@ -122,6 +122,12 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
|
||||||
raw: stream.parse()?,
|
raw: stream.parse()?,
|
||||||
},
|
},
|
||||||
)),
|
)),
|
||||||
|
"close" => ConstraintToken::Close(Context::new(
|
||||||
|
span,
|
||||||
|
ConstraintClose {
|
||||||
|
sol_dest: stream.parse()?,
|
||||||
|
},
|
||||||
|
)),
|
||||||
_ => Err(ParseError::new(ident.span(), "Invalid attribute"))?,
|
_ => Err(ParseError::new(ident.span(), "Invalid attribute"))?,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -131,7 +137,8 @@ pub fn parse_token(stream: ParseStream) -> ParseResult<ConstraintToken> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct ConstraintGroupBuilder {
|
pub struct ConstraintGroupBuilder<'ty> {
|
||||||
|
pub f_ty: Option<&'ty Ty>,
|
||||||
pub init: Option<Context<ConstraintInit>>,
|
pub init: Option<Context<ConstraintInit>>,
|
||||||
pub mutable: Option<Context<ConstraintMut>>,
|
pub mutable: Option<Context<ConstraintMut>>,
|
||||||
pub signer: Option<Context<ConstraintSigner>>,
|
pub signer: Option<Context<ConstraintSigner>>,
|
||||||
|
@ -147,9 +154,31 @@ pub struct ConstraintGroupBuilder {
|
||||||
pub associated_payer: Option<Context<ConstraintAssociatedPayer>>,
|
pub associated_payer: Option<Context<ConstraintAssociatedPayer>>,
|
||||||
pub associated_space: Option<Context<ConstraintAssociatedSpace>>,
|
pub associated_space: Option<Context<ConstraintAssociatedSpace>>,
|
||||||
pub associated_with: Vec<Context<ConstraintAssociatedWith>>,
|
pub associated_with: Vec<Context<ConstraintAssociatedWith>>,
|
||||||
|
pub close: Option<Context<ConstraintClose>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ConstraintGroupBuilder {
|
impl<'ty> ConstraintGroupBuilder<'ty> {
|
||||||
|
pub fn new(f_ty: Option<&'ty Ty>) -> Self {
|
||||||
|
Self {
|
||||||
|
f_ty,
|
||||||
|
init: None,
|
||||||
|
mutable: None,
|
||||||
|
signer: None,
|
||||||
|
belongs_to: Vec::new(),
|
||||||
|
literal: Vec::new(),
|
||||||
|
raw: Vec::new(),
|
||||||
|
owner: None,
|
||||||
|
rent_exempt: None,
|
||||||
|
seeds: None,
|
||||||
|
executable: None,
|
||||||
|
state: None,
|
||||||
|
associated: None,
|
||||||
|
associated_payer: None,
|
||||||
|
associated_space: None,
|
||||||
|
associated_with: Vec::new(),
|
||||||
|
close: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
|
pub fn build(mut self) -> ParseResult<ConstraintGroup> {
|
||||||
// Init implies mutable and rent exempt.
|
// Init implies mutable and rent exempt.
|
||||||
if let Some(i) = &self.init {
|
if let Some(i) = &self.init {
|
||||||
|
@ -171,6 +200,7 @@ impl ConstraintGroupBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
let ConstraintGroupBuilder {
|
let ConstraintGroupBuilder {
|
||||||
|
f_ty: _,
|
||||||
init,
|
init,
|
||||||
mutable,
|
mutable,
|
||||||
signer,
|
signer,
|
||||||
|
@ -186,6 +216,7 @@ impl ConstraintGroupBuilder {
|
||||||
associated_payer,
|
associated_payer,
|
||||||
associated_space,
|
associated_space,
|
||||||
associated_with,
|
associated_with,
|
||||||
|
close,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
// Converts Option<Context<T>> -> Option<T>.
|
// Converts Option<Context<T>> -> Option<T>.
|
||||||
|
@ -221,6 +252,7 @@ impl ConstraintGroupBuilder {
|
||||||
payer: associated_payer.map(|p| p.target.clone()),
|
payer: associated_payer.map(|p| p.target.clone()),
|
||||||
space: associated_space.map(|s| s.space.clone()),
|
space: associated_space.map(|s| s.space.clone()),
|
||||||
}),
|
}),
|
||||||
|
close: into_inner!(close),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -241,7 +273,7 @@ impl ConstraintGroupBuilder {
|
||||||
ConstraintToken::AssociatedPayer(c) => self.add_associated_payer(c),
|
ConstraintToken::AssociatedPayer(c) => self.add_associated_payer(c),
|
||||||
ConstraintToken::AssociatedSpace(c) => self.add_associated_space(c),
|
ConstraintToken::AssociatedSpace(c) => self.add_associated_space(c),
|
||||||
ConstraintToken::AssociatedWith(c) => self.add_associated_with(c),
|
ConstraintToken::AssociatedWith(c) => self.add_associated_with(c),
|
||||||
ConstraintToken::AssociatedGroup(_) => panic!("Invariant violation"),
|
ConstraintToken::Close(c) => self.add_close(c),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -253,6 +285,28 @@ impl ConstraintGroupBuilder {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_close(&mut self, c: Context<ConstraintClose>) -> ParseResult<()> {
|
||||||
|
if !matches!(self.f_ty, Some(Ty::ProgramAccount(_)))
|
||||||
|
&& !matches!(self.f_ty, Some(Ty::Loader(_)))
|
||||||
|
{
|
||||||
|
return Err(ParseError::new(
|
||||||
|
c.span(),
|
||||||
|
"close must be on a ProgramAccount",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.mutable.is_none() {
|
||||||
|
return Err(ParseError::new(
|
||||||
|
c.span(),
|
||||||
|
"mut must be provided before close",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if self.close.is_some() {
|
||||||
|
return Err(ParseError::new(c.span(), "close already provided"));
|
||||||
|
}
|
||||||
|
self.close.replace(c);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn add_mut(&mut self, c: Context<ConstraintMut>) -> ParseResult<()> {
|
fn add_mut(&mut self, c: Context<ConstraintMut>) -> ParseResult<()> {
|
||||||
if self.mutable.is_some() {
|
if self.mutable.is_some() {
|
||||||
return Err(ParseError::new(c.span(), "mut already provided"));
|
return Err(ParseError::new(c.span(), "mut already provided"));
|
||||||
|
|
|
@ -25,24 +25,26 @@ pub fn parse(strct: &syn::ItemStruct) -> ParseResult<AccountsStruct> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_account_field(f: &syn::Field) -> ParseResult<AccountField> {
|
pub fn parse_account_field(f: &syn::Field) -> ParseResult<AccountField> {
|
||||||
let constraints = constraints::parse(f)?;
|
|
||||||
|
|
||||||
let ident = f.ident.clone().unwrap();
|
let ident = f.ident.clone().unwrap();
|
||||||
let account_field = match is_field_primitive(f)? {
|
let account_field = match is_field_primitive(f)? {
|
||||||
true => {
|
true => {
|
||||||
let ty = parse_ty(f)?;
|
let ty = parse_ty(f)?;
|
||||||
|
let constraints = constraints::parse(f, Some(&ty))?;
|
||||||
AccountField::Field(Field {
|
AccountField::Field(Field {
|
||||||
ident,
|
ident,
|
||||||
ty,
|
ty,
|
||||||
constraints,
|
constraints,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
false => AccountField::CompositeField(CompositeField {
|
false => {
|
||||||
ident,
|
let constraints = constraints::parse(f, None)?;
|
||||||
constraints,
|
AccountField::CompositeField(CompositeField {
|
||||||
symbol: ident_string(f)?,
|
ident,
|
||||||
raw_field: f.clone(),
|
constraints,
|
||||||
}),
|
symbol: ident_string(f)?,
|
||||||
|
raw_field: f.clone(),
|
||||||
|
})
|
||||||
|
}
|
||||||
};
|
};
|
||||||
Ok(account_field)
|
Ok(account_field)
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,6 +68,7 @@ const LangErrorCode = {
|
||||||
ConstraintState: 148,
|
ConstraintState: 148,
|
||||||
ConstraintAssociated: 149,
|
ConstraintAssociated: 149,
|
||||||
ConstraintAssociatedInit: 150,
|
ConstraintAssociatedInit: 150,
|
||||||
|
ConstraintClose: 151,
|
||||||
|
|
||||||
// Accounts.
|
// Accounts.
|
||||||
AccountDiscriminatorAlreadySet: 160,
|
AccountDiscriminatorAlreadySet: 160,
|
||||||
|
@ -130,6 +131,10 @@ const LangErrorMessage = new Map([
|
||||||
LangErrorCode.ConstraintAssociatedInit,
|
LangErrorCode.ConstraintAssociatedInit,
|
||||||
"An associated init constraint was violated",
|
"An associated init constraint was violated",
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
LangErrorCode.ConstraintClose,
|
||||||
|
"A close constraint was violated"
|
||||||
|
],
|
||||||
|
|
||||||
// Accounts.
|
// Accounts.
|
||||||
[
|
[
|
||||||
|
|
Loading…
Reference in New Issue