lang: Add `#[interface]` attribute for overriding the default discriminator (#2728)
This commit is contained in:
parent
c2b5472d85
commit
13fc0bb915
|
@ -385,6 +385,8 @@ jobs:
|
|||
path: spl/token-proxy
|
||||
- cmd: cd tests/spl/token-wrapper && anchor test --skip-lint
|
||||
path: spl/token-wrapper
|
||||
- cmd: cd tests/spl/transfer-hook && anchor test --skip-lint
|
||||
path: spl/transfer-hook
|
||||
- cmd: cd tests/multisig && anchor test --skip-lint
|
||||
path: tests/multisig
|
||||
# - cmd: cd tests/lockup && anchor test --skip-lint
|
||||
|
|
|
@ -16,6 +16,8 @@ The minor version will be incremented upon a breaking change and the patch versi
|
|||
- cli: Add verifiable option when `deploy` ([#2705](https://github.com/coral-xyz/anchor/pull/2705)).
|
||||
- cli: Add support for passing arguments to the underlying `solana program deploy` command with `anchor deploy` ([#2709](https://github.com/coral-xyz/anchor/pull/2709)).
|
||||
- lang: Add `InstructionData::write_to` implementation ([#2733](https://github.com/coral-xyz/anchor/pull/2733)).
|
||||
- lang: Add `#[interface(..)]` attribute for instruction discriminator overrides ([#2728](https://github.com/coral-xyz/anchor/pull/2728)).
|
||||
- ts: Add `.interface(..)` method for instruction discriminator overrides ([#2728](https://github.com/coral-xyz/anchor/pull/2728)).
|
||||
|
||||
### Fixes
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ idl-build = [
|
|||
"anchor-syn/idl-build",
|
||||
]
|
||||
init-if-needed = ["anchor-derive-accounts/init-if-needed"]
|
||||
interface-instructions = ["anchor-attribute-program/interface-instructions"]
|
||||
|
||||
[dependencies]
|
||||
anchor-attribute-access-control = { path = "./attribute/access-control", version = "0.29.0" }
|
||||
|
|
|
@ -14,6 +14,7 @@ proc-macro = true
|
|||
[features]
|
||||
anchor-debug = ["anchor-syn/anchor-debug"]
|
||||
idl-build = ["anchor-syn/idl-build"]
|
||||
interface-instructions = ["anchor-syn/interface-instructions"]
|
||||
|
||||
[dependencies]
|
||||
anchor-syn = { path = "../../syn", version = "0.29.0" }
|
||||
|
|
|
@ -14,3 +14,49 @@ pub fn program(
|
|||
.to_token_stream()
|
||||
.into()
|
||||
}
|
||||
|
||||
/// The `#[interface]` attribute is used to mark an instruction as belonging
|
||||
/// to an interface implementation, thus transforming its discriminator to the
|
||||
/// proper bytes for that interface instruction.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust,ignore
|
||||
/// use anchor_lang::prelude::*;
|
||||
///
|
||||
/// // SPL Transfer Hook Interface: `Execute` instruction.
|
||||
/// //
|
||||
/// // This instruction is invoked by Token-2022 when a transfer occurs,
|
||||
/// // if a mint has specified this program as its transfer hook.
|
||||
/// #[interface(spl_transfer_hook_interface::execute)]
|
||||
/// pub fn execute_transfer(ctx: Context<Execute>, amount: u64) -> Result<()> {
|
||||
/// // Check that all extra accounts were provided
|
||||
/// let data = ctx.accounts.extra_metas_account.try_borrow_data()?;
|
||||
/// ExtraAccountMetaList::check_account_infos::<ExecuteInstruction>(
|
||||
/// &ctx.accounts.to_account_infos(),
|
||||
/// &TransferHookInstruction::Execute { amount }.pack(),
|
||||
/// &ctx.program_id,
|
||||
/// &data,
|
||||
/// )?;
|
||||
///
|
||||
/// // Or maybe perform some custom logic
|
||||
/// if ctx.accounts.token_metadata.mint != ctx.accounts.token_account.mint {
|
||||
/// return Err(ProgramError::IncorrectAccount);
|
||||
/// }
|
||||
///
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
#[cfg(feature = "interface-instructions")]
|
||||
#[proc_macro_attribute]
|
||||
pub fn interface(
|
||||
_args: proc_macro::TokenStream,
|
||||
input: proc_macro::TokenStream,
|
||||
) -> proc_macro::TokenStream {
|
||||
// This macro itself is a no-op, but must be defined as a proc-macro
|
||||
// attribute to be used on a function as the `#[interface]` attribute.
|
||||
//
|
||||
// The `#[program]` macro will detect this attribute and transform the
|
||||
// discriminator.
|
||||
input
|
||||
}
|
||||
|
|
|
@ -65,6 +65,9 @@ pub use anchor_attribute_event::{emit_cpi, event_cpi};
|
|||
#[cfg(feature = "idl-build")]
|
||||
pub use anchor_syn::{self, idl::build::IdlBuild};
|
||||
|
||||
#[cfg(feature = "interface-instructions")]
|
||||
pub use anchor_attribute_program::interface;
|
||||
|
||||
pub type Result<T> = std::result::Result<T, error::Error>;
|
||||
|
||||
/// A data structure of validated accounts that can be deserialized from the
|
||||
|
@ -418,6 +421,9 @@ pub mod prelude {
|
|||
|
||||
#[cfg(feature = "idl-build")]
|
||||
pub use super::IdlBuild;
|
||||
|
||||
#[cfg(feature = "interface-instructions")]
|
||||
pub use super::interface;
|
||||
}
|
||||
|
||||
/// Internal module used by macros and unstable apis.
|
||||
|
|
|
@ -17,6 +17,7 @@ idl-build = ["idl-parse", "idl-types"]
|
|||
idl-parse = ["idl-types"]
|
||||
idl-types = []
|
||||
init-if-needed = []
|
||||
interface-instructions = []
|
||||
seeds = []
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -22,7 +22,9 @@ pub fn generate(program: &Program) -> proc_macro2::TokenStream {
|
|||
})
|
||||
.collect();
|
||||
let ix_data_trait = {
|
||||
let sighash_arr = sighash(SIGHASH_GLOBAL_NAMESPACE, name);
|
||||
let sighash_arr = ix
|
||||
.interface_discriminator
|
||||
.unwrap_or(sighash(SIGHASH_GLOBAL_NAMESPACE, name));
|
||||
let sighash_tts: proc_macro2::TokenStream =
|
||||
format!("{sighash_arr:?}").parse().unwrap();
|
||||
quote! {
|
||||
|
|
|
@ -66,6 +66,8 @@ pub struct Ix {
|
|||
pub returns: IxReturn,
|
||||
// The ident for the struct deriving Accounts.
|
||||
pub anchor_ident: Ident,
|
||||
// The discriminator based on the `#[interface]` attribute.
|
||||
pub interface_discriminator: Option<[u8; 8]>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
|
@ -3,6 +3,7 @@ pub mod context;
|
|||
pub mod docs;
|
||||
pub mod error;
|
||||
pub mod program;
|
||||
pub mod spl_interface;
|
||||
|
||||
pub fn tts_to_string<T: quote::ToTokens>(item: T) -> String {
|
||||
let mut tts = proc_macro2::TokenStream::new();
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
use crate::parser::docs;
|
||||
use crate::parser::program::ctx_accounts_ident;
|
||||
use crate::parser::spl_interface;
|
||||
use crate::{FallbackFn, Ix, IxArg, IxReturn};
|
||||
use syn::parse::{Error as ParseError, Result as ParseResult};
|
||||
use syn::spanned::Spanned;
|
||||
|
@ -24,6 +25,7 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<(Vec<Ix>, Option<Fallbac
|
|||
})
|
||||
.map(|method: &syn::ItemFn| {
|
||||
let (ctx, args) = parse_args(method)?;
|
||||
let interface_discriminator = spl_interface::parse(&method.attrs);
|
||||
let docs = docs::parse(&method.attrs);
|
||||
let returns = parse_return(method)?;
|
||||
let anchor_ident = ctx_accounts_ident(&ctx.raw_arg)?;
|
||||
|
@ -34,6 +36,7 @@ pub fn parse(program_mod: &syn::ItemMod) -> ParseResult<(Vec<Ix>, Option<Fallbac
|
|||
args,
|
||||
anchor_ident,
|
||||
returns,
|
||||
interface_discriminator,
|
||||
})
|
||||
})
|
||||
.collect::<ParseResult<Vec<Ix>>>()?;
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
#[cfg(feature = "interface-instructions")]
|
||||
use syn::{Meta, NestedMeta, Path};
|
||||
|
||||
#[cfg(not(feature = "interface-instructions"))]
|
||||
pub fn parse(_attrs: &[syn::Attribute]) -> Option<[u8; 8]> {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface-instructions")]
|
||||
pub fn parse(attrs: &[syn::Attribute]) -> Option<[u8; 8]> {
|
||||
let interfaces: Vec<[u8; 8]> = attrs
|
||||
.iter()
|
||||
.filter_map(|attr| {
|
||||
if attr.path.is_ident("interface") {
|
||||
if let Ok(Meta::List(meta_list)) = attr.parse_meta() {
|
||||
if let Some(NestedMeta::Meta(Meta::Path(path))) = meta_list.nested.first() {
|
||||
return Some(parse_interface_instruction(path));
|
||||
}
|
||||
}
|
||||
panic!(
|
||||
"Failed to parse interface instruction:\n{}",
|
||||
quote::quote!(#attr)
|
||||
);
|
||||
}
|
||||
None
|
||||
})
|
||||
.collect();
|
||||
if interfaces.len() > 1 {
|
||||
panic!("An instruction can only implement one interface instruction");
|
||||
} else if interfaces.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(interfaces[0])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "interface-instructions")]
|
||||
fn parse_interface_instruction(path: &Path) -> [u8; 8] {
|
||||
if path.segments.len() != 2 {
|
||||
// All interface instruction args are expected to be in the form
|
||||
// <interface>::<instruction>
|
||||
panic!(
|
||||
"Invalid interface instruction: {}",
|
||||
path.segments
|
||||
.iter()
|
||||
.map(|segment| segment.ident.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("::")
|
||||
);
|
||||
}
|
||||
let interface = path.segments[0].ident.to_string();
|
||||
if interface == "spl_transfer_hook_interface" {
|
||||
let instruction = path.segments[1].ident.to_string();
|
||||
if instruction == "initialize_extra_account_meta_list" {
|
||||
return [43, 34, 13, 49, 167, 88, 235, 235]; // `InitializeExtraAccountMetaList`
|
||||
} else if instruction == "execute" {
|
||||
return [105, 37, 101, 197, 75, 251, 102, 26]; // `Execute`
|
||||
} else {
|
||||
panic!("Unsupported instruction: {}", instruction);
|
||||
}
|
||||
}
|
||||
panic!("Unsupported interface: {}", interface);
|
||||
}
|
|
@ -34,6 +34,7 @@
|
|||
"spl/metadata",
|
||||
"spl/token-proxy",
|
||||
"spl/token-wrapper",
|
||||
"spl/transfer-hook",
|
||||
"swap",
|
||||
"system-accounts",
|
||||
"sysvars",
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
[provider]
|
||||
cluster = "localnet"
|
||||
wallet = "~/.config/solana/id.json"
|
||||
|
||||
[programs.localnet]
|
||||
transfer_hook = "9vaEfNU4HquQJuNQ6HYrpJW518a3n4wNUt5mAMY2UUHW"
|
||||
|
||||
[scripts]
|
||||
test = "yarn run ts-mocha -t 1000000 tests/*.ts"
|
||||
|
||||
[features]
|
||||
|
||||
[test.validator]
|
||||
url = "https://api.mainnet-beta.solana.com"
|
||||
|
||||
[[test.validator.clone]]
|
||||
address = "TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
|
@ -0,0 +1,8 @@
|
|||
[workspace]
|
||||
members = [
|
||||
"programs/*"
|
||||
]
|
||||
resolver = "2"
|
||||
|
||||
[profile.release]
|
||||
overflow-checks = true
|
|
@ -0,0 +1,22 @@
|
|||
{
|
||||
"name": "transfer-hook",
|
||||
"version": "0.29.0",
|
||||
"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": ">=11"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "anchor test"
|
||||
},
|
||||
"dependencies": {
|
||||
"@solana/spl-token": "^0.3.9"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
[package]
|
||||
name = "transfer-hook"
|
||||
version = "0.1.0"
|
||||
description = "Created with Anchor"
|
||||
rust-version = "1.60"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib", "lib"]
|
||||
name = "transfer_hook"
|
||||
|
||||
[features]
|
||||
no-entrypoint = []
|
||||
no-idl = []
|
||||
cpi = ["no-entrypoint"]
|
||||
default = []
|
||||
|
||||
[dependencies]
|
||||
anchor-lang = { path = "../../../../../lang", features = ["interface-instructions"] }
|
||||
anchor-spl = { path = "../../../../../spl" }
|
||||
spl-tlv-account-resolution = "0.4.0"
|
||||
spl-transfer-hook-interface = "0.3.0"
|
|
@ -0,0 +1,2 @@
|
|||
[target.bpfel-unknown-unknown.dependencies.std]
|
||||
features = []
|
|
@ -0,0 +1,173 @@
|
|||
//! An example of a transfer hook program.
|
||||
//!
|
||||
//! This program is intended to implement the SPL Transfer Hook interface,
|
||||
//! thus allowing Token2022 to call into this program when a transfer occurs.
|
||||
//!
|
||||
//! <https://spl.solana.com/token-2022/extensions#transfer-hook>
|
||||
|
||||
use {
|
||||
anchor_lang::prelude::*,
|
||||
anchor_spl::{
|
||||
token_2022::{
|
||||
spl_token_2022::{
|
||||
extension::{
|
||||
transfer_hook::TransferHookAccount, BaseStateWithExtensions,
|
||||
StateWithExtensions,
|
||||
},
|
||||
state::Account as Token2022Account,
|
||||
},
|
||||
ID as TOKEN_2022_PROGRAM_ID,
|
||||
},
|
||||
token_interface::{Mint, TokenAccount},
|
||||
},
|
||||
spl_tlv_account_resolution::{account::ExtraAccountMeta, state::ExtraAccountMetaList},
|
||||
spl_transfer_hook_interface::{
|
||||
error::TransferHookError,
|
||||
instruction::{ExecuteInstruction, TransferHookInstruction},
|
||||
},
|
||||
};
|
||||
|
||||
declare_id!("9vaEfNU4HquQJuNQ6HYrpJW518a3n4wNUt5mAMY2UUHW");
|
||||
|
||||
fn check_token_account_is_transferring(account_data: &[u8]) -> Result<()> {
|
||||
let token_account = StateWithExtensions::<Token2022Account>::unpack(account_data)?;
|
||||
let extension = token_account.get_extension::<TransferHookAccount>()?;
|
||||
if bool::from(extension.transferring) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Into::<ProgramError>::into(
|
||||
TransferHookError::ProgramCalledOutsideOfTransfer,
|
||||
))?
|
||||
}
|
||||
}
|
||||
|
||||
#[program]
|
||||
pub mod transfer_hook {
|
||||
use super::*;
|
||||
|
||||
#[interface(spl_transfer_hook_interface::initialize_extra_account_meta_list)]
|
||||
pub fn initialize(ctx: Context<Initialize>, metas: Vec<AnchorExtraAccountMeta>) -> Result<()> {
|
||||
let extra_metas_account = &ctx.accounts.extra_metas_account;
|
||||
let mint = &ctx.accounts.mint;
|
||||
let mint_authority = &ctx.accounts.mint_authority;
|
||||
|
||||
if mint_authority.key()
|
||||
!= mint.mint_authority.ok_or(Into::<ProgramError>::into(
|
||||
TransferHookError::MintHasNoMintAuthority,
|
||||
))?
|
||||
{
|
||||
Err(Into::<ProgramError>::into(
|
||||
TransferHookError::IncorrectMintAuthority,
|
||||
))?;
|
||||
}
|
||||
|
||||
let metas: Vec<ExtraAccountMeta> = metas.into_iter().map(|meta| meta.into()).collect();
|
||||
let mut data = extra_metas_account.try_borrow_mut_data()?;
|
||||
ExtraAccountMetaList::init::<ExecuteInstruction>(&mut data, &metas)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[interface(spl_transfer_hook_interface::execute)]
|
||||
pub fn execute(ctx: Context<Execute>, amount: u64) -> Result<()> {
|
||||
let source_account = &ctx.accounts.source_account;
|
||||
let destination_account = &ctx.accounts.destination_account;
|
||||
|
||||
check_token_account_is_transferring(&source_account.to_account_info().try_borrow_data()?)?;
|
||||
check_token_account_is_transferring(
|
||||
&destination_account.to_account_info().try_borrow_data()?,
|
||||
)?;
|
||||
|
||||
let data = ctx.accounts.extra_metas_account.try_borrow_data()?;
|
||||
ExtraAccountMetaList::check_account_infos::<ExecuteInstruction>(
|
||||
&ctx.accounts.to_account_infos(),
|
||||
&TransferHookInstruction::Execute { amount }.pack(),
|
||||
&ctx.program_id,
|
||||
&data,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
#[instruction(metas: Vec<AnchorExtraAccountMeta>)]
|
||||
pub struct Initialize<'info> {
|
||||
/// CHECK: This account's data is a buffer of TLV data
|
||||
#[account(
|
||||
init,
|
||||
space = ExtraAccountMetaList::size_of(metas.len()).unwrap(),
|
||||
// space = 8 + 4 + 2 * 35,
|
||||
seeds = [b"extra-account-metas", mint.key().as_ref()],
|
||||
bump,
|
||||
payer = payer,
|
||||
)]
|
||||
pub extra_metas_account: UncheckedAccount<'info>,
|
||||
|
||||
#[account(
|
||||
mint::token_program = TOKEN_2022_PROGRAM_ID,
|
||||
)]
|
||||
pub mint: Box<InterfaceAccount<'info, Mint>>,
|
||||
|
||||
#[account(mut)]
|
||||
pub mint_authority: Signer<'info>,
|
||||
|
||||
pub system_program: Program<'info, System>,
|
||||
|
||||
#[account(mut)]
|
||||
pub payer: Signer<'info>,
|
||||
}
|
||||
|
||||
#[derive(Accounts)]
|
||||
pub struct Execute<'info> {
|
||||
#[account(
|
||||
token::mint = mint,
|
||||
token::authority = owner_delegate,
|
||||
token::token_program = TOKEN_2022_PROGRAM_ID,
|
||||
)]
|
||||
pub source_account: Box<InterfaceAccount<'info, TokenAccount>>,
|
||||
|
||||
#[account(
|
||||
mint::token_program = TOKEN_2022_PROGRAM_ID,
|
||||
)]
|
||||
pub mint: Box<InterfaceAccount<'info, Mint>>,
|
||||
|
||||
#[account(
|
||||
token::mint = mint,
|
||||
token::token_program = TOKEN_2022_PROGRAM_ID,
|
||||
)]
|
||||
pub destination_account: Box<InterfaceAccount<'info, TokenAccount>>,
|
||||
|
||||
pub owner_delegate: SystemAccount<'info>,
|
||||
|
||||
/// CHECK: This account's data is a buffer of TLV data
|
||||
#[account(
|
||||
seeds = [b"extra-account-metas", mint.key().as_ref()],
|
||||
bump,
|
||||
)]
|
||||
pub extra_metas_account: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Example extra PDA for transfer #1
|
||||
pub secondary_authority_1: UncheckedAccount<'info>,
|
||||
|
||||
/// CHECK: Example extra PDA for transfer #2
|
||||
pub secondary_authority_2: UncheckedAccount<'info>,
|
||||
}
|
||||
|
||||
#[derive(AnchorSerialize, AnchorDeserialize)]
|
||||
pub struct AnchorExtraAccountMeta {
|
||||
pub discriminator: u8,
|
||||
pub address_config: [u8; 32],
|
||||
pub is_signer: bool,
|
||||
pub is_writable: bool,
|
||||
}
|
||||
impl From<AnchorExtraAccountMeta> for ExtraAccountMeta {
|
||||
fn from(meta: AnchorExtraAccountMeta) -> Self {
|
||||
Self {
|
||||
discriminator: meta.discriminator,
|
||||
address_config: meta.address_config,
|
||||
is_signer: meta.is_signer.into(),
|
||||
is_writable: meta.is_writable.into(),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,304 @@
|
|||
import * as anchor from "@coral-xyz/anchor";
|
||||
import { Program } from "@coral-xyz/anchor";
|
||||
import {
|
||||
PublicKey,
|
||||
Keypair,
|
||||
SystemProgram,
|
||||
sendAndConfirmTransaction,
|
||||
Transaction,
|
||||
AccountInfo,
|
||||
} from "@solana/web3.js";
|
||||
import {
|
||||
getExtraAccountMetaAddress,
|
||||
ExtraAccountMeta,
|
||||
getMintLen,
|
||||
ExtensionType,
|
||||
createInitializeTransferHookInstruction,
|
||||
createInitializeMintInstruction,
|
||||
createAssociatedTokenAccountInstruction,
|
||||
getAssociatedTokenAddressSync,
|
||||
createMintToInstruction,
|
||||
createTransferCheckedInstruction,
|
||||
getAccount,
|
||||
addExtraAccountsToInstruction,
|
||||
} from "@solana/spl-token";
|
||||
import { assert } from "chai";
|
||||
import { TransferHook } from "../target/types/transfer_hook";
|
||||
|
||||
describe("transfer hook", () => {
|
||||
const provider = anchor.AnchorProvider.env();
|
||||
anchor.setProvider(provider);
|
||||
|
||||
const TOKEN_2022_PROGRAM_ID = new anchor.web3.PublicKey(
|
||||
"TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb"
|
||||
);
|
||||
const program = anchor.workspace.TransferHook as Program<TransferHook>;
|
||||
|
||||
const decimals = 2;
|
||||
const mintAmount = 100;
|
||||
const transferAmount = 10;
|
||||
|
||||
const payer = Keypair.generate();
|
||||
const mintAuthority = Keypair.generate();
|
||||
const mint = Keypair.generate();
|
||||
|
||||
const sourceAuthority = Keypair.generate();
|
||||
const destinationAuthority = Keypair.generate().publicKey;
|
||||
let source: PublicKey = null;
|
||||
let destination: PublicKey = null;
|
||||
|
||||
let extraMetasAddress: PublicKey = null;
|
||||
const validationLen = 8 + 4 + 4 + 2 * 35; // Discriminator, length, pod slice length, pod slice with 2 extra metas
|
||||
const extraMetas: ExtraAccountMeta[] = [
|
||||
{
|
||||
discriminator: 0,
|
||||
addressConfig: Keypair.generate().publicKey.toBuffer(),
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
},
|
||||
{
|
||||
discriminator: 0,
|
||||
addressConfig: Keypair.generate().publicKey.toBuffer(),
|
||||
isWritable: false,
|
||||
isSigner: false,
|
||||
},
|
||||
];
|
||||
|
||||
before(async () => {
|
||||
const { programId } = program;
|
||||
const extensions = [ExtensionType.TransferHook];
|
||||
const mintLen = getMintLen(extensions);
|
||||
const lamports =
|
||||
await provider.connection.getMinimumBalanceForRentExemption(mintLen);
|
||||
|
||||
source = getAssociatedTokenAddressSync(
|
||||
mint.publicKey,
|
||||
sourceAuthority.publicKey,
|
||||
false,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
);
|
||||
destination = getAssociatedTokenAddressSync(
|
||||
mint.publicKey,
|
||||
destinationAuthority,
|
||||
false,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
);
|
||||
|
||||
extraMetasAddress = getExtraAccountMetaAddress(mint.publicKey, programId);
|
||||
|
||||
const transaction = new Transaction().add(
|
||||
SystemProgram.createAccount({
|
||||
fromPubkey: payer.publicKey,
|
||||
newAccountPubkey: mint.publicKey,
|
||||
space: mintLen,
|
||||
lamports,
|
||||
programId: TOKEN_2022_PROGRAM_ID,
|
||||
}),
|
||||
createInitializeTransferHookInstruction(
|
||||
mint.publicKey,
|
||||
mintAuthority.publicKey,
|
||||
programId,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
),
|
||||
createInitializeMintInstruction(
|
||||
mint.publicKey,
|
||||
decimals,
|
||||
mintAuthority.publicKey,
|
||||
mintAuthority.publicKey,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
),
|
||||
createAssociatedTokenAccountInstruction(
|
||||
payer.publicKey,
|
||||
source,
|
||||
sourceAuthority.publicKey,
|
||||
mint.publicKey,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
),
|
||||
createAssociatedTokenAccountInstruction(
|
||||
payer.publicKey,
|
||||
destination,
|
||||
destinationAuthority,
|
||||
mint.publicKey,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
),
|
||||
createMintToInstruction(
|
||||
mint.publicKey,
|
||||
source,
|
||||
mintAuthority.publicKey,
|
||||
mintAmount,
|
||||
[],
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
)
|
||||
);
|
||||
|
||||
await provider.connection.confirmTransaction(
|
||||
await provider.connection.requestAirdrop(payer.publicKey, 10000000000),
|
||||
"confirmed"
|
||||
);
|
||||
|
||||
await sendAndConfirmTransaction(provider.connection, transaction, [
|
||||
payer,
|
||||
mint,
|
||||
mintAuthority,
|
||||
]);
|
||||
});
|
||||
|
||||
it("can create an `InitializeExtraAccountMetaList` instruction with the proper discriminator", async () => {
|
||||
const ix = await program.methods
|
||||
.initialize(extraMetas as any[])
|
||||
.interface("spl_transfer_hook_interface::initialize_extra_account_metas")
|
||||
.accounts({
|
||||
extraMetasAccount: extraMetasAddress,
|
||||
mint: mint.publicKey,
|
||||
mintAuthority: mintAuthority.publicKey,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.instruction();
|
||||
assert.equal(
|
||||
ix.data.subarray(0, 8).compare(
|
||||
Buffer.from([43, 34, 13, 49, 167, 88, 235, 235]) // SPL discriminator for `InitializeExtraAccountMetaList` from interface
|
||||
),
|
||||
0
|
||||
);
|
||||
const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode(
|
||||
ix.data,
|
||||
"hex",
|
||||
"initialize"
|
||||
);
|
||||
assert.equal(name, "initialize");
|
||||
assert.property(data, "metas");
|
||||
assert.isArray(data.metas);
|
||||
assert.equal(data.metas.length, extraMetas.length);
|
||||
});
|
||||
|
||||
it("can create an `Execute` instruction with the proper discriminator", async () => {
|
||||
const ix = await program.methods
|
||||
.execute(new anchor.BN(transferAmount))
|
||||
.interface("spl_transfer_hook_interface::execute")
|
||||
.accounts({
|
||||
sourceAccount: source,
|
||||
mint: mint.publicKey,
|
||||
destinationAccount: destination,
|
||||
ownerDelegate: sourceAuthority.publicKey,
|
||||
extraMetasAccount: extraMetasAddress,
|
||||
secondaryAuthority1: new PublicKey(extraMetas[0].addressConfig),
|
||||
secondaryAuthority2: new PublicKey(extraMetas[1].addressConfig),
|
||||
})
|
||||
.instruction();
|
||||
assert.equal(
|
||||
ix.data.subarray(0, 8).compare(
|
||||
Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]) // SPL discriminator for `Execute` from interface
|
||||
),
|
||||
0
|
||||
);
|
||||
const { name, data } = new anchor.BorshInstructionCoder(program.idl).decode(
|
||||
ix.data,
|
||||
"hex",
|
||||
"execute"
|
||||
);
|
||||
assert.equal(name, "execute");
|
||||
assert.property(data, "amount");
|
||||
assert.isTrue(anchor.BN.isBN(data.amount));
|
||||
assert.isTrue(data.amount.eq(new anchor.BN(transferAmount)));
|
||||
});
|
||||
|
||||
it("can transfer with extra account metas", async () => {
|
||||
// Initialize the extra metas
|
||||
await program.methods
|
||||
.initialize(extraMetas as any[])
|
||||
.interface("spl_transfer_hook_interface::initialize_extra_account_metas")
|
||||
.accounts({
|
||||
extraMetasAccount: extraMetasAddress,
|
||||
mint: mint.publicKey,
|
||||
mintAuthority: mintAuthority.publicKey,
|
||||
systemProgram: SystemProgram.programId,
|
||||
})
|
||||
.signers([mintAuthority])
|
||||
.rpc();
|
||||
|
||||
// Check the account data
|
||||
await provider.connection
|
||||
.getAccountInfo(extraMetasAddress)
|
||||
.then((account: AccountInfo<Buffer>) => {
|
||||
assert.equal(account.data.length, validationLen);
|
||||
assert.equal(
|
||||
account.data.subarray(0, 8).compare(
|
||||
Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]) // SPL discriminator for `Execute` from interface
|
||||
),
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
account.data.subarray(8, 12).compare(
|
||||
Buffer.from([74, 0, 0, 0]) // Little endian 74
|
||||
),
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
account.data.subarray(12, 16).compare(
|
||||
Buffer.from([2, 0, 0, 0]) // Little endian 2
|
||||
),
|
||||
0
|
||||
);
|
||||
const extraMetaToBuffer = (extraMeta: ExtraAccountMeta) => {
|
||||
const buf = Buffer.alloc(35);
|
||||
buf.set(extraMeta.addressConfig, 1);
|
||||
buf.writeUInt8(0, 33); // isSigner
|
||||
buf.writeUInt8(0, 34); // isWritable
|
||||
return buf;
|
||||
};
|
||||
assert.equal(
|
||||
account.data
|
||||
.subarray(16, 51)
|
||||
.compare(extraMetaToBuffer(extraMetas[0])),
|
||||
0
|
||||
);
|
||||
assert.equal(
|
||||
account.data
|
||||
.subarray(51, 86)
|
||||
.compare(extraMetaToBuffer(extraMetas[1])),
|
||||
0
|
||||
);
|
||||
});
|
||||
|
||||
const ix = await addExtraAccountsToInstruction(
|
||||
provider.connection,
|
||||
createTransferCheckedInstruction(
|
||||
source,
|
||||
mint.publicKey,
|
||||
destination,
|
||||
sourceAuthority.publicKey,
|
||||
transferAmount,
|
||||
decimals,
|
||||
undefined,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
),
|
||||
mint.publicKey,
|
||||
undefined,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
);
|
||||
|
||||
await sendAndConfirmTransaction(
|
||||
provider.connection,
|
||||
new Transaction().add(ix),
|
||||
[payer, sourceAuthority]
|
||||
);
|
||||
|
||||
// Check the resulting token balances
|
||||
await getAccount(
|
||||
provider.connection,
|
||||
source,
|
||||
undefined,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
).then((account) => {
|
||||
assert.equal(account.amount, BigInt(mintAmount - transferAmount));
|
||||
});
|
||||
await getAccount(
|
||||
provider.connection,
|
||||
destination,
|
||||
undefined,
|
||||
TOKEN_2022_PROGRAM_ID
|
||||
).then((account) => {
|
||||
assert.equal(account.amount, BigInt(transferAmount));
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"types": ["mocha", "chai", "node"],
|
||||
"typeRoots": ["./node_modules/@types"],
|
||||
"lib": ["es2015"],
|
||||
"module": "commonjs",
|
||||
"target": "es6",
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
|
@ -23,6 +23,13 @@
|
|||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@babel/runtime@^7.23.2":
|
||||
version "7.23.5"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.5.tgz#11edb98f8aeec529b82b211028177679144242db"
|
||||
integrity sha512-NdUTHcPe4C99WxPub+K9l9tK5/lV4UXIoaHSYgzco9BCyjKAAwzdBI+wWtYqHt7LJdbo74ZjRPJgzVweq1sz0w==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@metaplex-foundation/mpl-auction@^0.0.2":
|
||||
version "0.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@metaplex-foundation/mpl-auction/-/mpl-auction-0.0.2.tgz#3de3c982e88d6a88f0ef05be73453cf3cfaccf26"
|
||||
|
@ -175,6 +182,16 @@
|
|||
bn.js "^5.1.2"
|
||||
buffer-layout "^1.2.0"
|
||||
|
||||
"@solana/buffer-layout-utils@^0.2.0":
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@solana/buffer-layout-utils/-/buffer-layout-utils-0.2.0.tgz#b45a6cab3293a2eb7597cceb474f229889d875ca"
|
||||
integrity sha512-szG4sxgJGktbuZYDg2FfNmkMi0DYQoVjN2h7ta1W1hPrwzarcFLBq9UpX1UjNXsNpT9dn+chgprtWGioUAr4/g==
|
||||
dependencies:
|
||||
"@solana/buffer-layout" "^4.0.0"
|
||||
"@solana/web3.js" "^1.32.0"
|
||||
bigint-buffer "^1.1.5"
|
||||
bignumber.js "^9.0.1"
|
||||
|
||||
"@solana/buffer-layout@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.0.tgz#75b1b11adc487234821c81dfae3119b73a5fd734"
|
||||
|
@ -194,6 +211,15 @@
|
|||
buffer-layout "^1.2.0"
|
||||
dotenv "10.0.0"
|
||||
|
||||
"@solana/spl-token@^0.3.9":
|
||||
version "0.3.9"
|
||||
resolved "https://registry.yarnpkg.com/@solana/spl-token/-/spl-token-0.3.9.tgz#477e703c3638ffb17dd29b82a203c21c3e465851"
|
||||
integrity sha512-1EXHxKICMnab35MvvY/5DBc/K/uQAOJCYnDZXw83McCAYUAfi+rwq6qfd6MmITmSTEhcfBcl/zYxmW/OSN0RmA==
|
||||
dependencies:
|
||||
"@solana/buffer-layout" "^4.0.0"
|
||||
"@solana/buffer-layout-utils" "^0.2.0"
|
||||
buffer "^6.0.3"
|
||||
|
||||
"@solana/web3.js@^1.17.0", "@solana/web3.js@^1.21.0":
|
||||
version "1.64.0"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.64.0.tgz#b7f5a976976039a0161242e94d6e1224ab5d30f9"
|
||||
|
@ -236,6 +262,27 @@
|
|||
rpc-websockets "^7.5.1"
|
||||
superstruct "^0.14.2"
|
||||
|
||||
"@solana/web3.js@^1.32.0":
|
||||
version "1.87.6"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.87.6.tgz#6744cfc5f4fc81e0f58241c0a92648a7320bb3bf"
|
||||
integrity sha512-LkqsEBgTZztFiccZZXnawWa8qNCATEqE97/d0vIwjTclmVlc8pBpD1DmjfVHtZ1HS5fZorFlVhXfpwnCNDZfyg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@noble/curves" "^1.2.0"
|
||||
"@noble/hashes" "^1.3.1"
|
||||
"@solana/buffer-layout" "^4.0.0"
|
||||
agentkeepalive "^4.3.0"
|
||||
bigint-buffer "^1.1.5"
|
||||
bn.js "^5.2.1"
|
||||
borsh "^0.7.0"
|
||||
bs58 "^4.0.1"
|
||||
buffer "6.0.3"
|
||||
fast-stable-stringify "^1.0.0"
|
||||
jayson "^4.1.0"
|
||||
node-fetch "^2.6.12"
|
||||
rpc-websockets "^7.5.1"
|
||||
superstruct "^0.14.2"
|
||||
|
||||
"@solana/web3.js@^1.68.0":
|
||||
version "1.70.0"
|
||||
resolved "https://registry.yarnpkg.com/@solana/web3.js/-/web3.js-1.70.0.tgz#14ad207f431861397db85921aad8df4e8374e7c8"
|
||||
|
@ -442,6 +489,11 @@ bigint-buffer@^1.1.5:
|
|||
dependencies:
|
||||
bindings "^1.3.0"
|
||||
|
||||
bignumber.js@^9.0.1:
|
||||
version "9.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bignumber.js/-/bignumber.js-9.1.2.tgz#b7c4242259c008903b13707983b5f4bbd31eda0c"
|
||||
integrity sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==
|
||||
|
||||
binary-extensions@^2.0.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
|
|
|
@ -56,11 +56,15 @@ export class BorshInstructionCoder implements InstructionCoder {
|
|||
/**
|
||||
* Encodes a program instruction.
|
||||
*/
|
||||
public encode(ixName: string, ix: any): Buffer {
|
||||
return this._encode(SIGHASH_GLOBAL_NAMESPACE, ixName, ix);
|
||||
public encode(ixName: string, ix: any, discriminator?: Buffer): Buffer {
|
||||
return this._encode(
|
||||
ixName,
|
||||
ix,
|
||||
discriminator ?? sighash(SIGHASH_GLOBAL_NAMESPACE, ixName)
|
||||
);
|
||||
}
|
||||
|
||||
private _encode(nameSpace: string, ixName: string, ix: any): Buffer {
|
||||
private _encode(ixName: string, ix: any, discriminator: Buffer): Buffer {
|
||||
const buffer = Buffer.alloc(1000); // TODO: use a tighter buffer.
|
||||
const methodName = camelCase(ixName);
|
||||
const layout = this.ixLayout.get(methodName);
|
||||
|
@ -69,7 +73,7 @@ export class BorshInstructionCoder implements InstructionCoder {
|
|||
}
|
||||
const len = layout.encode(ix, buffer);
|
||||
const data = buffer.slice(0, len);
|
||||
return Buffer.concat([sighash(nameSpace, ixName), data]);
|
||||
return Buffer.concat([discriminator, data]);
|
||||
}
|
||||
|
||||
private static parseIxLayout(idl: Idl): Map<string, Layout> {
|
||||
|
@ -92,14 +96,21 @@ export class BorshInstructionCoder implements InstructionCoder {
|
|||
*/
|
||||
public decode(
|
||||
ix: Buffer | string,
|
||||
encoding: "hex" | "base58" = "hex"
|
||||
encoding: "hex" | "base58" = "hex",
|
||||
ixName?: string
|
||||
): Instruction | null {
|
||||
if (typeof ix === "string") {
|
||||
ix = encoding === "hex" ? Buffer.from(ix, "hex") : bs58.decode(ix);
|
||||
}
|
||||
let sighash = bs58.encode(ix.slice(0, 8));
|
||||
// Use the provided method name to get the sighash, ignoring the
|
||||
// discriminator in the instruction data.
|
||||
// This is useful for decoding instructions that have been encoded with a
|
||||
// different namespace, such as an SPL interface.
|
||||
let sighashKey = bs58.encode(
|
||||
ixName ? sighash(SIGHASH_GLOBAL_NAMESPACE, ixName) : ix.slice(0, 8)
|
||||
);
|
||||
let data = ix.slice(8);
|
||||
const decoder = this.sighashLayouts.get(sighash);
|
||||
const decoder = this.sighashLayouts.get(sighashKey);
|
||||
if (!decoder) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ export interface AccountsCoder<A extends string = string> {
|
|||
}
|
||||
|
||||
export interface InstructionCoder {
|
||||
encode(ixName: string, ix: any): Buffer;
|
||||
encode(ixName: string, ix: any, discriminator?: Buffer): Buffer;
|
||||
}
|
||||
|
||||
export interface EventCoder {
|
||||
|
|
|
@ -50,6 +50,11 @@ export type Context<A extends Accounts = Accounts> = {
|
|||
* Commitment parameters to use for a transaction.
|
||||
*/
|
||||
options?: ConfirmOptions;
|
||||
|
||||
/**
|
||||
* An optional override for the default instruction discriminator.
|
||||
*/
|
||||
discriminator?: Buffer;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
@ -60,7 +60,8 @@ export default class NamespaceFactory {
|
|||
idl.instructions.forEach((idlIx) => {
|
||||
const ixItem = InstructionFactory.build<IDL, typeof idlIx>(
|
||||
idlIx,
|
||||
(ixName, ix) => coder.instruction.encode(ixName, ix),
|
||||
(ixName, ix, discriminator) =>
|
||||
coder.instruction.encode(ixName, ix, discriminator),
|
||||
programId
|
||||
);
|
||||
const txItem = TransactionFactory.build(idlIx, ixItem);
|
||||
|
|
|
@ -41,6 +41,7 @@ export default class InstructionNamespaceFactory {
|
|||
...args: InstructionContextFnArgs<IDL, I>
|
||||
): TransactionInstruction => {
|
||||
const [ixArgs, ctx] = splitArgsAndCtx(idlIx, [...args]);
|
||||
const { discriminator } = ctx;
|
||||
validateAccounts(idlIx.accounts, ctx.accounts);
|
||||
validateInstruction(idlIx, ...args);
|
||||
|
||||
|
@ -57,7 +58,11 @@ export default class InstructionNamespaceFactory {
|
|||
return new TransactionInstruction({
|
||||
keys,
|
||||
programId,
|
||||
data: encodeFn(idlIx.name, toInstruction(idlIx, ...ixArgs)),
|
||||
data: encodeFn(
|
||||
idlIx.name,
|
||||
toInstruction(idlIx, ...ixArgs),
|
||||
discriminator
|
||||
),
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -191,7 +196,8 @@ type IxProps<A extends Accounts> = {
|
|||
|
||||
export type InstructionEncodeFn<I extends IdlInstruction = IdlInstruction> = (
|
||||
ixName: I["name"],
|
||||
ix: any
|
||||
ix: any,
|
||||
discriminator?: Buffer
|
||||
) => Buffer;
|
||||
|
||||
// Throws error if any argument required for the `ix` is not given.
|
||||
|
|
|
@ -108,6 +108,10 @@ export function flattenPartialAccounts<A extends IdlAccountItem>(
|
|||
return toReturn;
|
||||
}
|
||||
|
||||
type SplInterface =
|
||||
| "spl_transfer_hook_interface::initialize_extra_account_metas"
|
||||
| "spl_transfer_hook_interface::execute";
|
||||
|
||||
export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
||||
private readonly _accounts: AccountsGeneric = {};
|
||||
private _remainingAccounts: Array<AccountMeta> = [];
|
||||
|
@ -117,6 +121,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
private _accountsResolver: AccountsResolver<IDL>;
|
||||
private _autoResolveAccounts: boolean = true;
|
||||
private _args: Array<any>;
|
||||
private _discriminator?: Buffer;
|
||||
|
||||
constructor(
|
||||
_args: Array<any>,
|
||||
|
@ -161,6 +166,20 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
>;
|
||||
}
|
||||
|
||||
public interface(splInterface: SplInterface): MethodsBuilder<IDL, I> {
|
||||
if (
|
||||
splInterface ===
|
||||
"spl_transfer_hook_interface::initialize_extra_account_metas"
|
||||
) {
|
||||
this._discriminator = Buffer.from([43, 34, 13, 49, 167, 88, 235, 235]);
|
||||
} else if (splInterface === "spl_transfer_hook_interface::execute") {
|
||||
this._discriminator = Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]);
|
||||
} else {
|
||||
throw new Error(`Unsupported interface: ${splInterface}`);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public accounts(
|
||||
accounts: PartialAccounts<I["accounts"][number]>
|
||||
): MethodsBuilder<IDL, I> {
|
||||
|
@ -216,6 +235,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
preInstructions: this._preInstructions,
|
||||
postInstructions: this._postInstructions,
|
||||
options: options,
|
||||
discriminator: this._discriminator,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -280,6 +300,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
remainingAccounts: this._remainingAccounts,
|
||||
preInstructions: this._preInstructions,
|
||||
postInstructions: this._postInstructions,
|
||||
discriminator: this._discriminator,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -311,6 +332,7 @@ export class MethodsBuilder<IDL extends Idl, I extends AllInstructions<IDL>> {
|
|||
remainingAccounts: this._remainingAccounts,
|
||||
preInstructions: this._preInstructions,
|
||||
postInstructions: this._postInstructions,
|
||||
discriminator: this._discriminator,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue