lang: Add `#[interface]` attribute for overriding the default discriminator (#2728)

This commit is contained in:
Joe C 2023-12-17 17:57:57 -05:00 committed by GitHub
parent c2b5472d85
commit 13fc0bb915
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 799 additions and 12 deletions

View File

@ -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

View File

@ -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

View File

@ -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" }

View File

@ -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" }

View File

@ -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
}

View File

@ -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.

View File

@ -17,6 +17,7 @@ idl-build = ["idl-parse", "idl-types"]
idl-parse = ["idl-types"]
idl-types = []
init-if-needed = []
interface-instructions = []
seeds = []
[dependencies]

View File

@ -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! {

View File

@ -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)]

View File

@ -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();

View File

@ -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>>>()?;

View File

@ -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);
}

View File

@ -34,6 +34,7 @@
"spl/metadata",
"spl/token-proxy",
"spl/token-wrapper",
"spl/transfer-hook",
"swap",
"system-accounts",
"sysvars",

View File

@ -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"

View File

@ -0,0 +1,8 @@
[workspace]
members = [
"programs/*"
]
resolver = "2"
[profile.release]
overflow-checks = true

View File

@ -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"
}
}

View File

@ -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"

View File

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

View File

@ -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(),
}
}
}

View File

@ -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));
});
});
});

View File

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

View File

@ -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"

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
};
/**

View File

@ -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);

View File

@ -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.

View File

@ -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,
});
}
}