From f7f094efd8f6b03ebe126c19a60a4404bca3d3f6 Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Tue, 12 Mar 2024 13:59:57 +0100 Subject: [PATCH 1/3] Add 4 transfer hook examples --- .../anchor/TransferHookCounter/.gitignore | 8 + .../TransferHookCounter/.prettierignore | 8 + .../anchor/TransferHookCounter/Anchor.toml | 18 ++ .../anchor/TransferHookCounter/Cargo.toml | 13 + .../TransferHookCounter/migrations/deploy.ts | 12 + .../anchor/TransferHookCounter/package.json | 21 ++ .../programs/destination.json | 1 + .../programs/transfer-hook/Cargo.toml | 24 ++ .../programs/transfer-hook/Xargo.toml | 2 + .../programs/transfer-hook/src/lib.rs | 173 ++++++++++ .../tests/transfer-hook.ts | 212 ++++++++++++ .../anchor/TransferHookCounter/tsconfig.json | 11 + .../anchor/TransferHookHelloWorld/.gitignore | 8 + .../TransferHookHelloWorld/.prettierignore | 8 + .../anchor/TransferHookHelloWorld/Anchor.toml | 24 ++ .../anchor/TransferHookHelloWorld/Cargo.toml | 13 + .../migrations/deploy.ts | 12 + .../TransferHookHelloWorld/package.json | 20 ++ .../programs/transfer-hook/Cargo.toml | 24 ++ .../programs/transfer-hook/Xargo.toml | 2 + .../programs/transfer-hook/src/lib.rs | 136 ++++++++ .../tests/transfer-hook.ts | 198 ++++++++++++ .../TransferHookHelloWorld/tsconfig.json | 11 + .../TransferHookTransferCost/.gitignore | 8 + .../TransferHookTransferCost/.prettierignore | 8 + .../TransferHookTransferCost/Anchor.toml | 18 ++ .../TransferHookTransferCost/Cargo.toml | 13 + .../migrations/deploy.ts | 12 + .../TransferHookTransferCost/package.json | 20 ++ .../programs/transfer-hook/Cargo.toml | 24 ++ .../programs/transfer-hook/Xargo.toml | 2 + .../programs/transfer-hook/src/lib.rs | 261 +++++++++++++++ .../tests/transfer-hook.ts | 302 ++++++++++++++++++ .../TransferHookTransferCost/tsconfig.json | 11 + .../anchor/TransferHookWhitelist/.gitignore | 8 + .../TransferHookWhitelist/.prettierignore | 8 + .../anchor/TransferHookWhitelist/Anchor.toml | 24 ++ .../anchor/TransferHookWhitelist/Cargo.toml | 13 + .../migrations/deploy.ts | 12 + .../anchor/TransferHookWhitelist/package.json | 20 ++ .../programs/transfer-hook/Cargo.toml | 24 ++ .../programs/transfer-hook/Xargo.toml | 2 + .../programs/transfer-hook/src/lib.rs | 195 +++++++++++ .../tests/transfer-hook.ts | 229 +++++++++++++ .../TransferHookWhitelist/tsconfig.json | 11 + 45 files changed, 2184 insertions(+) create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.gitignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.prettierignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Anchor.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/migrations/deploy.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/package.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Xargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tsconfig.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.gitignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.prettierignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Anchor.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/migrations/deploy.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/package.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Xargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tests/transfer-hook.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tsconfig.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.gitignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.prettierignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Anchor.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/migrations/deploy.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/package.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Xargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tsconfig.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.gitignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.prettierignore create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Anchor.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/migrations/deploy.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/package.json create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Cargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Xargo.toml create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tests/transfer-hook.ts create mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tsconfig.json diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.gitignore b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.gitignore new file mode 100644 index 00000000..8d401163 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.gitignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.prettierignore b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.prettierignore new file mode 100644 index 00000000..c1a0b75f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/.prettierignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Anchor.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Anchor.toml new file mode 100644 index 00000000..653ce1fe --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Anchor.toml @@ -0,0 +1,18 @@ +[toolchain] + +[features] +seeds = false +skip-lint = false + +[programs.localnet] +transfer_hook = "DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Cargo.toml new file mode 100644 index 00000000..ef17a63c --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = [ + "programs/*" +] + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/migrations/deploy.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/migrations/deploy.ts new file mode 100644 index 00000000..82fb175f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/package.json b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/package.json new file mode 100644 index 00000000..fb779265 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/package.json @@ -0,0 +1,21 @@ +{ + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.29.0", + "@solana/spl-token": "^0.4.0", + "@solana/web3.js": "^1.89.1" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json new file mode 100644 index 00000000..14b4602e --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json @@ -0,0 +1 @@ +[190,198,254,219,195,198,188,248,91,58,36,89,114,143,221,246,55,38,153,95,43,99,203,221,16,226,83,243,224,154,57,42,97,126,109,17,158,56,137,34,19,105,170,85,64,116,76,234,202,180,168,197,104,240,143,161,175,114,99,93,98,123,237,240] \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Cargo.toml new file mode 100644 index 00000000..61f92194 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "transfer-hook" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "transfer_hook" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = {version = "0.29.0", features = ["init-if-needed"]} +anchor-spl = "0.29.0" +solana-program = "=1.17.17" + +spl-transfer-hook-interface = "0.5.0" +spl-tlv-account-resolution = "0.5.0" diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Xargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs new file mode 100644 index 00000000..1a89ec95 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs @@ -0,0 +1,173 @@ +use anchor_lang::{ + prelude::*, + system_program::{create_account, CreateAccount}, +}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use spl_tlv_account_resolution::{ + account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, +}; +use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction}; + +declare_id!("DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub"); + +#[error_code] +pub enum MyError { + #[msg("The amount is too big")] + AmountTooBig, +} + +#[program] +pub mod transfer_hook { + use super::*; + + pub fn initialize_extra_account_meta_list( + ctx: Context, + ) -> Result<()> { + + // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly + let account_metas = vec![ + ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "counter".as_bytes().to_vec(), + }], + false, // is_signer + true, // is_writable + )?, + ]; + + // calculate account size + let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64; + // calculate minimum required lamports + let lamports = Rent::get()?.minimum_balance(account_size as usize); + + let mint = ctx.accounts.mint.key(); + let signer_seeds: &[&[&[u8]]] = &[&[ + b"extra-account-metas", + &mint.as_ref(), + &[ctx.bumps.extra_account_meta_list], + ]]; + + // create ExtraAccountMetaList account + create_account( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + CreateAccount { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.extra_account_meta_list.to_account_info(), + }, + ) + .with_signer(signer_seeds), + lamports, + account_size, + ctx.program_id, + )?; + + // initialize ExtraAccountMetaList account with extra accounts + ExtraAccountMetaList::init::( + &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, + &account_metas, + )?; + + Ok(()) + } + + pub fn transfer_hook(ctx: Context, amount: u64) -> Result<()> { + + if amount > 50 { + msg!("The amount is too big {0}", amount); + // return err!(MyError::AmountTooBig); + } + + ctx.accounts.counter_account.counter.checked_add(1).unwrap(); + + msg!("This token has been transfered {0} times", ctx.accounts.counter_account.counter); + + Ok(()) + } + + // fallback instruction handler as workaround to anchor instruction discriminator check + pub fn fallback<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], + ) -> Result<()> { + let instruction = TransferHookInstruction::unpack(data)?; + + // match instruction discriminator to transfer hook interface execute instruction + // token2022 program CPIs this instruction on token transfer + match instruction { + TransferHookInstruction::Execute { amount } => { + let amount_bytes = amount.to_le_bytes(); + + // invoke custom transfer hook instruction on our program + __private::__global::transfer_hook(program_id, accounts, &amount_bytes) + } + _ => return Err(ProgramError::InvalidInstructionData.into()), + } + } +} + +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList<'info> { + #[account(mut)] + payer: Signer<'info>, + + /// CHECK: ExtraAccountMetaList Account, must use these seeds + #[account( + mut, + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: AccountInfo<'info>, + pub mint: InterfaceAccount<'info, Mint>, + #[account( + init_if_needed, + seeds = [b"counter"], + bump, + payer = payer, + space = 16 + )] + pub counter_account: Account<'info, CounterAccount>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} + +// Order of accounts matters for this struct. +// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner) +// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account +// These accounts are provided via CPI to this program from the token2022 program +#[derive(Accounts)] +pub struct TransferHook<'info> { + #[account( + token::mint = mint, + token::authority = owner, + )] + pub source_token: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account( + token::mint = mint, + )] + pub destination_token: InterfaceAccount<'info, TokenAccount>, + /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program + pub owner: UncheckedAccount<'info>, + /// CHECK: ExtraAccountMetaList Account, + #[account( + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: UncheckedAccount<'info>, + #[account( + seeds = [b"counter"], + bump + )] + pub counter_account: Account<'info, CounterAccount>, +} + +#[account] +pub struct CounterAccount { + counter: u64, +} diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts new file mode 100644 index 00000000..05e73939 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts @@ -0,0 +1,212 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { TransferHook } from "../target/types/transfer_hook"; +import { + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, + Keypair, +} from "@solana/web3.js"; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + getMintLen, + createInitializeMintInstruction, + createInitializeTransferHookInstruction, + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + createTransferCheckedWithTransferHookInstruction +} from "@solana/spl-token"; + +describe("transfer-hook", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = anchor.workspace.TransferHook as Program; + const wallet = provider.wallet as anchor.Wallet; + const connection = provider.connection; + + // Generate keypair to use as address for the transfer-hook enabled mint + const mint = new Keypair(); + const decimals = 9; + + // Sender token account address + const sourceTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + wallet.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // Recipient token account address + const recipient = Keypair.generate(); + const destinationTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + recipient.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // ExtraAccountMetaList address + // Store extra accounts required by the custom transfer hook instruction + const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + program.programId + ); + + const [counterPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + program.programId + ); + + it("Create Mint Account with Transfer Hook Extension", async () => { + const extensions = [ExtensionType.TransferHook]; + const mintLen = getMintLen(extensions); + const lamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports: lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferHookInstruction( + mint.publicKey, + wallet.publicKey, + program.programId, // Transfer Hook Program ID + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mint.publicKey, + decimals, + wallet.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer, mint], + { skipPreflight: true, commitment: "finalized"} + ); + + const txDetails = await program.provider.connection.getTransaction(txSig, { maxSupportedTransactionVersion: 0, commitment: 'confirmed'}); + console.log(txDetails.meta.logMessages); + + + console.log(`Transaction Signature: ${txSig}`); + }); + + // Create the two token accounts for the transfer-hook enabled mint + // Fund the sender token account with 100 tokens + it("Create Token Accounts and Mint Tokens", async () => { + // 100 tokens + const amount = 100 * 10 ** decimals; + + const transaction = new Transaction().add( + createAssociatedTokenAccountInstruction( + wallet.publicKey, + sourceTokenAccount, + wallet.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createAssociatedTokenAccountInstruction( + wallet.publicKey, + destinationTokenAccount, + recipient.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createMintToInstruction( + mint.publicKey, + sourceTokenAccount, + wallet.publicKey, + amount, + [], + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + + console.log(`Transaction Signature: ${txSig}`); + }); + + // Account to store extra accounts required by the transfer hook instruction + it("Create ExtraAccountMetaList Account", async () => { + const initializeExtraAccountMetaListInstruction = await program.methods + .initializeExtraAccountMetaList() + .accounts({ + mint: mint.publicKey, + extraAccountMetaList: extraAccountMetaListPDA, + counterAccount: counterPDA, + }) + .instruction(); + + const transaction = new Transaction().add( + initializeExtraAccountMetaListInstruction + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer], + { skipPreflight: true, commitment: "confirmed"} + ); + console.log("Transaction Signature:", txSig); + }); + + it("Transfer Hook with Extra Account Meta", async () => { + // 1 tokens + const amount = 1 * 10 ** decimals; + const amountBigInt = BigInt(amount); + + let transferInstructionWithHelper = await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + amountBigInt, + decimals, + [], + "confirmed", + TOKEN_2022_PROGRAM_ID, + ); + + console.log("Extra accounts meta: " + extraAccountMetaListPDA); + console.log("Counter PDa: " + counterPDA); + console.log("Transfer Instruction: " + JSON.stringify(transferInstructionWithHelper)); + + const transaction = new Transaction().add( + transferInstructionWithHelper + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + console.log("Transfer Signature:", txSig); + }); +}); diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tsconfig.json b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tsconfig.json new file mode 100644 index 00000000..558b83e5 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } + } + \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.gitignore b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.gitignore new file mode 100644 index 00000000..8d401163 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.gitignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.prettierignore b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.prettierignore new file mode 100644 index 00000000..c1a0b75f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/.prettierignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Anchor.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Anchor.toml new file mode 100644 index 00000000..5ea6666b --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Anchor.toml @@ -0,0 +1,24 @@ +[toolchain] + +[features] +seeds = false +skip-lint = false + +[programs.localnet] +transfer_hook = "DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + +[test.validator] +url = "https://api.devnet.solana.com" + +[[test.validator.clone]] +address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Cargo.toml new file mode 100644 index 00000000..ef17a63c --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = [ + "programs/*" +] + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/migrations/deploy.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/migrations/deploy.ts new file mode 100644 index 00000000..82fb175f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/package.json b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/package.json new file mode 100644 index 00000000..738d7f66 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.29.0", + "@solana/spl-token": "^0.4.0" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Cargo.toml new file mode 100644 index 00000000..7fbf8725 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "transfer-hook" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "transfer_hook" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = {version = "0.29.0", features = ["init-if-needed"]} +anchor-spl = "0.29.0" +solana-program = "1.17.17" + +spl-transfer-hook-interface = "0.5.0" +spl-tlv-account-resolution = "0.5.0" diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Xargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs new file mode 100644 index 00000000..09f5c8fe --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs @@ -0,0 +1,136 @@ +use anchor_lang::{ + prelude::*, + system_program::{create_account, CreateAccount}, +}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{Mint, TokenAccount, TokenInterface}, +}; +use spl_tlv_account_resolution::{ + state::ExtraAccountMetaList, +}; +use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction}; + +declare_id!("DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub"); + +#[program] +pub mod transfer_hook { + use super::*; + + pub fn initialize_extra_account_meta_list( + ctx: Context, + ) -> Result<()> { + + // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly + let account_metas = vec![ + + ]; + + // calculate account size + let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64; + // calculate minimum required lamports + let lamports = Rent::get()?.minimum_balance(account_size as usize); + + let mint = ctx.accounts.mint.key(); + let signer_seeds: &[&[&[u8]]] = &[&[ + b"extra-account-metas", + &mint.as_ref(), + &[ctx.bumps.extra_account_meta_list], + ]]; + + // create ExtraAccountMetaList account + create_account( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + CreateAccount { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.extra_account_meta_list.to_account_info(), + }, + ) + .with_signer(signer_seeds), + lamports, + account_size, + ctx.program_id, + )?; + + // initialize ExtraAccountMetaList account with extra accounts + ExtraAccountMetaList::init::( + &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, + &account_metas, + )?; + + Ok(()) + } + + pub fn transfer_hook(ctx: Context, amount: u64) -> Result<()> { + + msg!("Hello Transfer Hook!"); + + Ok(()) + } + + // fallback instruction handler as workaround to anchor instruction discriminator check + pub fn fallback<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], + ) -> Result<()> { + let instruction = TransferHookInstruction::unpack(data)?; + + // match instruction discriminator to transfer hook interface execute instruction + // token2022 program CPIs this instruction on token transfer + match instruction { + TransferHookInstruction::Execute { amount } => { + let amount_bytes = amount.to_le_bytes(); + + // invoke custom transfer hook instruction on our program + __private::__global::transfer_hook(program_id, accounts, &amount_bytes) + } + _ => return Err(ProgramError::InvalidInstructionData.into()), + } + } +} + +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList<'info> { + #[account(mut)] + payer: Signer<'info>, + + /// CHECK: ExtraAccountMetaList Account, must use these seeds + #[account( + mut, + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: AccountInfo<'info>, + pub mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} + +// Order of accounts matters for this struct. +// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner) +// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account +// These accounts are provided via CPI to this program from the token2022 program +#[derive(Accounts)] +pub struct TransferHook<'info> { + #[account( + token::mint = mint, + token::authority = owner, + )] + pub source_token: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account( + token::mint = mint, + )] + pub destination_token: InterfaceAccount<'info, TokenAccount>, + /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program + pub owner: UncheckedAccount<'info>, + /// CHECK: ExtraAccountMetaList Account, + #[account( + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: UncheckedAccount<'info>, +} diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tests/transfer-hook.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tests/transfer-hook.ts new file mode 100644 index 00000000..40a29d19 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tests/transfer-hook.ts @@ -0,0 +1,198 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { TransferHook } from "../target/types/transfer_hook"; +import { + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, + Keypair, +} from "@solana/web3.js"; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + getMintLen, + createInitializeMintInstruction, + createInitializeTransferHookInstruction, + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createMintToInstruction, + createTransferCheckedInstruction, + getAssociatedTokenAddressSync, + createTransferCheckedWithTransferHookInstruction, +} from "@solana/spl-token"; + +describe("transfer-hook", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = anchor.workspace.TransferHook as Program; + const wallet = provider.wallet as anchor.Wallet; + const connection = provider.connection; + + // Generate keypair to use as address for the transfer-hook enabled mint + const mint = new Keypair(); + const decimals = 9; + + // Sender token account address + const sourceTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + wallet.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // Recipient token account address + const recipient = Keypair.generate(); + const destinationTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + recipient.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // ExtraAccountMetaList address + // Store extra accounts required by the custom transfer hook instruction + const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + program.programId + ); + + it("Create Mint Account with Transfer Hook Extension", async () => { + const extensions = [ExtensionType.TransferHook]; + const mintLen = getMintLen(extensions); + const lamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports: lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferHookInstruction( + mint.publicKey, + wallet.publicKey, + program.programId, // Transfer Hook Program ID + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mint.publicKey, + decimals, + wallet.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer, mint] + ); + console.log(`Transaction Signature: ${txSig}`); + }); + + // Create the two token accounts for the transfer-hook enabled mint + // Fund the sender token account with 100 tokens + it("Create Token Accounts and Mint Tokens", async () => { + // 100 tokens + const amount = 100 * 10 ** decimals; + + const transaction = new Transaction().add( + createAssociatedTokenAccountInstruction( + wallet.publicKey, + sourceTokenAccount, + wallet.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createAssociatedTokenAccountInstruction( + wallet.publicKey, + destinationTokenAccount, + recipient.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createMintToInstruction( + mint.publicKey, + sourceTokenAccount, + wallet.publicKey, + amount, + [], + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + + console.log(`Transaction Signature: ${txSig}`); + }); + + // Account to store extra accounts required by the transfer hook instruction + it("Create ExtraAccountMetaList Account", async () => { + const initializeExtraAccountMetaListInstruction = await program.methods + .initializeExtraAccountMetaList() + .accounts({ + mint: mint.publicKey, + extraAccountMetaList: extraAccountMetaListPDA, + }) + .instruction(); + + const transaction = new Transaction().add( + initializeExtraAccountMetaListInstruction + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer], + { skipPreflight: true, commitment: "confirmed" } + ); + console.log("Transaction Signature:", txSig); + }); + + it("Transfer Hook with Extra Account Meta", async () => { + // 1 tokens + const amount = 1 * 10 ** decimals; + const bigIntAmount = BigInt(amount); + + // Standard token transfer instruction + const transferInstruction = await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + bigIntAmount, + decimals, + [], + "confirmed", + TOKEN_2022_PROGRAM_ID + ); + + const transaction = new Transaction().add( + transferInstruction + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + console.log("Transfer Signature:", txSig); + }); +}); diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tsconfig.json b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tsconfig.json new file mode 100644 index 00000000..558b83e5 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } + } + \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.gitignore b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.gitignore new file mode 100644 index 00000000..8d401163 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.gitignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.prettierignore b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.prettierignore new file mode 100644 index 00000000..c1a0b75f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/.prettierignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Anchor.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Anchor.toml new file mode 100644 index 00000000..653ce1fe --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Anchor.toml @@ -0,0 +1,18 @@ +[toolchain] + +[features] +seeds = false +skip-lint = false + +[programs.localnet] +transfer_hook = "DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Cargo.toml new file mode 100644 index 00000000..ef17a63c --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = [ + "programs/*" +] + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/migrations/deploy.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/migrations/deploy.ts new file mode 100644 index 00000000..82fb175f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/package.json b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/package.json new file mode 100644 index 00000000..738d7f66 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.29.0", + "@solana/spl-token": "^0.4.0" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Cargo.toml new file mode 100644 index 00000000..1b85bdef --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "transfer-hook" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "transfer_hook" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = "0.29.0" +anchor-spl = "0.29.0" +solana-program = "1.17.13" + +spl-transfer-hook-interface = "0.4.1" +spl-tlv-account-resolution = "0.5.0" diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Xargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs new file mode 100644 index 00000000..c42841a7 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs @@ -0,0 +1,261 @@ +use anchor_lang::{ + prelude::*, + system_program::{create_account, CreateAccount}, +}; +use anchor_spl::{ + associated_token::AssociatedToken, + token_interface::{transfer_checked, Mint, TokenAccount, TokenInterface, TransferChecked}, +}; +use spl_tlv_account_resolution::{ + account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList, +}; +use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction}; + +// transfer-hook program that charges a SOL fee on token transfer +// use a delegate and wrapped SOL because signers from initial transfer are not accessible + +declare_id!("DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub"); + +#[error_code] +pub enum MyError { + #[msg("Amount Too big")] + AmountTooBig, +} + +#[program] +pub mod transfer_hook { + use super::*; + + pub fn initialize_extra_account_meta_list( + ctx: Context, + ) -> Result<()> { + // index 0-3 are the accounts required for token transfer (source, mint, destination, owner) + // index 4 is address of ExtraAccountMetaList account + // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly + let account_metas = vec![ + // index 5, wrapped SOL mint + ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?, + // index 6, token program + ExtraAccountMeta::new_with_pubkey(&ctx.accounts.token_program.key(), false, false)?, + // index 7, associated token program + ExtraAccountMeta::new_with_pubkey( + &ctx.accounts.associated_token_program.key(), + false, + false, + )?, + // index 8, delegate PDA + ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "delegate".as_bytes().to_vec(), + }], + false, // is_signer + true, // is_writable + )?, + // index 9, delegate wrapped SOL token account + ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 8 }, // owner index (delegate PDA) + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable + )?, + // index 10, sender wrapped SOL token account + ExtraAccountMeta::new_external_pda_with_seeds( + 7, // associated token program index + &[ + Seed::AccountKey { index: 3 }, // owner index + Seed::AccountKey { index: 6 }, // token program index + Seed::AccountKey { index: 5 }, // wsol mint index + ], + false, // is_signer + true, // is_writable + )?, + ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "counter".as_bytes().to_vec(), + }], + false, // is_signer + true, // is_writable + )?, + ]; + + // calculate account size + let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64; + // calculate minimum required lamports + let lamports = Rent::get()?.minimum_balance(account_size as usize); + + let mint = ctx.accounts.mint.key(); + let signer_seeds: &[&[&[u8]]] = &[&[ + b"extra-account-metas", + &mint.as_ref(), + &[ctx.bumps.extra_account_meta_list], + ]]; + + // create ExtraAccountMetaList account + create_account( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + CreateAccount { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.extra_account_meta_list.to_account_info(), + }, + ) + .with_signer(signer_seeds), + lamports, + account_size, + ctx.program_id, + )?; + + // initialize ExtraAccountMetaList account with extra accounts + ExtraAccountMetaList::init::( + &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, + &account_metas, + )?; + + Ok(()) + } + + pub fn transfer_hook(ctx: Context, amount: u64) -> Result<()> { + + if amount > 50 { + //msg!("The amount is too big {0}", amount); + //return err!(MyError::AmountTooBig); + } + + ctx.accounts.counter_account.counter += 1; + + msg!("This token has been transferred {0} times", ctx.accounts.counter_account.counter); + + // All accounts are non writable so you can not burn any of them for example here + msg!("Is writable mint {0}", ctx.accounts.mint.to_account_info().is_writable); + msg!("Is destination mint {0}", ctx.accounts.destination_token.to_account_info().is_writable); + msg!("Is source mint {0}", ctx.accounts.source_token.to_account_info().is_writable); + + let signer_seeds: &[&[&[u8]]] = &[&[b"delegate", &[ctx.bumps.delegate]]]; + + // Transfer WSOL from sender to delegate token account using delegate PDA + transfer_checked( + CpiContext::new( + ctx.accounts.token_program.to_account_info(), + TransferChecked { + from: ctx.accounts.sender_wsol_token_account.to_account_info(), + mint: ctx.accounts.wsol_mint.to_account_info(), + to: ctx.accounts.delegate_wsol_token_account.to_account_info(), + authority: ctx.accounts.delegate.to_account_info(), + }, + ) + .with_signer(signer_seeds), + amount / 2, + ctx.accounts.wsol_mint.decimals, + )?; + Ok(()) + } + + // fallback instruction handler as workaround to anchor instruction discriminator check + pub fn fallback<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], + ) -> Result<()> { + let instruction = TransferHookInstruction::unpack(data)?; + + // match instruction discriminator to transfer hook interface execute instruction + // token2022 program CPIs this instruction on token transfer + match instruction { + TransferHookInstruction::Execute { amount } => { + let amount_bytes = amount.to_le_bytes(); + + // invoke custom transfer hook instruction on our program + __private::__global::transfer_hook(program_id, accounts, &amount_bytes) + } + _ => Err(ProgramError::InvalidInstructionData.into()), + } + } +} + +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList<'info> { + #[account(mut)] + payer: Signer<'info>, + + /// CHECK: ExtraAccountMetaList Account, must use these seeds + #[account( + mut, + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: AccountInfo<'info>, + pub mint: InterfaceAccount<'info, Mint>, + pub wsol_mint: InterfaceAccount<'info, Mint>, + #[account( + init, + seeds = [b"counter"], + bump, + payer = payer, + space = 9 + )] + pub counter_account: Account<'info, CounterAccount>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, +} + +// Order of accounts matters for this struct. +// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner) +// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account +// These accounts are provided via CPI to this program from the token2022 program +#[derive(Accounts)] +pub struct TransferHook<'info> { + #[account( + token::mint = mint, + token::authority = owner, + )] + pub source_token: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account( + token::mint = mint, + )] + pub destination_token: InterfaceAccount<'info, TokenAccount>, + /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program + pub owner: UncheckedAccount<'info>, + /// CHECK: ExtraAccountMetaList Account, + #[account( + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: UncheckedAccount<'info>, + pub wsol_mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + #[account( + mut, + seeds = [b"delegate"], + bump + )] + pub delegate: SystemAccount<'info>, + #[account( + mut, + token::mint = wsol_mint, + token::authority = delegate, + )] + pub delegate_wsol_token_account: InterfaceAccount<'info, TokenAccount>, + #[account( + mut, + token::mint = wsol_mint, + token::authority = owner, + )] + pub sender_wsol_token_account: InterfaceAccount<'info, TokenAccount>, + #[account( + seeds = [b"counter"], + bump + )] + pub counter_account: Account<'info, CounterAccount>, +} + +#[account] +pub struct CounterAccount { + counter: u8 +} diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts new file mode 100644 index 00000000..62bc9e54 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts @@ -0,0 +1,302 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { TransferHook } from "../target/types/transfer_hook"; +import { + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, + Keypair, +} from "@solana/web3.js"; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + getMintLen, + createInitializeMintInstruction, + createInitializeTransferHookInstruction, + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createMintToInstruction, + getAssociatedTokenAddressSync, + createApproveInstruction, + createSyncNativeInstruction, + NATIVE_MINT, + TOKEN_PROGRAM_ID, + getAccount, + getOrCreateAssociatedTokenAccount, + createTransferCheckedWithTransferHookInstruction, + getMint, + getTransferHook, + getExtraAccountMetaAddress, + getExtraAccountMetas, +} from "@solana/spl-token"; +import assert from "assert"; + +describe("transfer-hook", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = anchor.workspace.TransferHook as Program; + const wallet = provider.wallet as anchor.Wallet; + const connection = provider.connection; + + // Generate keypair to use as address for the transfer-hook enabled mint + const mint = new Keypair(); + const decimals = 9; + + // Sender token account address + const sourceTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + wallet.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // Recipient token account address + const recipient = Keypair.generate(); + const destinationTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + recipient.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // ExtraAccountMetaList address + // Store extra accounts required by the custom transfer hook instruction + const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + program.programId + ); + + const [counterPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("counter")], + program.programId + ); + + // PDA delegate to transfer wSOL tokens from sender + const [delegatePDA] = PublicKey.findProgramAddressSync( + [Buffer.from("delegate")], + program.programId + ); + + // Sender wSOL token account address + const senderWSolTokenAccount = getAssociatedTokenAddressSync( + NATIVE_MINT, // mint + wallet.publicKey // owner + ); + + // Delegate PDA wSOL token account address, to receive wSOL tokens from sender + const delegateWSolTokenAccount = getAssociatedTokenAddressSync( + NATIVE_MINT, // mint + delegatePDA, // owner + true // allowOwnerOffCurve + ); + + // Create the two WSol token accounts as part of setup + before(async () => { + // WSol Token Account for sender + await getOrCreateAssociatedTokenAccount( + connection, + wallet.payer, + NATIVE_MINT, + wallet.publicKey + ); + + // WSol Token Account for delegate PDA + await getOrCreateAssociatedTokenAccount( + connection, + wallet.payer, + NATIVE_MINT, + delegatePDA, + true + ); + }); + + it("Create Mint Account with Transfer Hook Extension", async () => { + const extensions = [ExtensionType.TransferHook]; + const mintLen = getMintLen(extensions); + const lamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports: lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferHookInstruction( + mint.publicKey, + wallet.publicKey, + program.programId, // Transfer Hook Program ID + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mint.publicKey, + decimals, + wallet.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer, mint] + ); + console.log(`Transaction Signature: ${txSig}`); + }); + + // Create the two token accounts for the transfer-hook enabled mint + // Fund the sender token account with 100 tokens + it("Create Token Accounts and Mint Tokens", async () => { + // 100 tokens + const amount = 100 * 10 ** decimals; + + const transaction = new Transaction().add( + createAssociatedTokenAccountInstruction( + wallet.publicKey, + sourceTokenAccount, + wallet.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createAssociatedTokenAccountInstruction( + wallet.publicKey, + destinationTokenAccount, + recipient.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createMintToInstruction( + mint.publicKey, + sourceTokenAccount, + wallet.publicKey, + amount, + [], + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + + console.log(`Transaction Signature: ${txSig}`); + }); + + // Account to store extra accounts required by the transfer hook instruction + it("Create ExtraAccountMetaList Account", async () => { + const initializeExtraAccountMetaListInstruction = await program.methods + .initializeExtraAccountMetaList() + .accounts({ + payer: wallet.publicKey, + extraAccountMetaList: extraAccountMetaListPDA, + mint: mint.publicKey, + wsolMint: NATIVE_MINT, + counterAccount: counterPDA, + tokenProgram: TOKEN_PROGRAM_ID, + associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID, + }) + .instruction(); + + const transaction = new Transaction().add( + initializeExtraAccountMetaListInstruction + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer], + { skipPreflight: true, commitment : "confirmed"} + ); + console.log("Transaction Signature:", txSig); + }); + + it("Transfer Hook with Extra Account Meta", async () => { + // 1 tokens + const amount = 1 * 10 ** decimals; + const bigIntAmount = BigInt(amount); + + // Instruction for sender to fund their WSol token account + const solTransferInstruction = SystemProgram.transfer({ + fromPubkey: wallet.publicKey, + toPubkey: senderWSolTokenAccount, + lamports: amount, + }); + + // Approve delegate PDA to transfer WSol tokens from sender WSol token account + const approveInstruction = createApproveInstruction( + senderWSolTokenAccount, + delegatePDA, + wallet.publicKey, + amount, + [], + TOKEN_PROGRAM_ID + ); + + // Sync sender WSol token account + const syncWrappedSolInstruction = createSyncNativeInstruction( + senderWSolTokenAccount + ); + + const mintInfo = await getMint(connection, mint.publicKey, "confirmed", TOKEN_2022_PROGRAM_ID); + const transferHook = getTransferHook(mintInfo); + if (transferHook != null) { + console.log("Transfer hook not found" + JSON.stringify(transferHook)); + } + + const extraAccountsAccount = getExtraAccountMetaAddress(mint.publicKey, transferHook.programId); + const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, "confirmed"); + const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo); + + for (const extraAccountMeta of extraAccountMetas) { + console.log("Extra account meta: " + JSON.stringify(extraAccountMeta)); + } + + // Standard token transfer instruction + const transferInstruction = await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + bigIntAmount, + decimals, + [], + "confirmed", + TOKEN_2022_PROGRAM_ID + ); + + console.log("Pushed keys:", JSON.stringify(transferInstruction.keys)); + + const transaction = new Transaction().add( + solTransferInstruction, + syncWrappedSolInstruction, + approveInstruction, + transferInstruction + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + console.log("Transfer Signature:", txSig); + + const tokenAccount = await getAccount(connection, delegateWSolTokenAccount); + + assert.equal(Number(tokenAccount.amount), amount / 2); + }); +}); diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tsconfig.json b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tsconfig.json new file mode 100644 index 00000000..558b83e5 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } + } + \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.gitignore b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.gitignore new file mode 100644 index 00000000..8d401163 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.gitignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +**/*.rs.bk +node_modules +test-ledger +.yarn diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.prettierignore b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.prettierignore new file mode 100644 index 00000000..c1a0b75f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/.prettierignore @@ -0,0 +1,8 @@ + +.anchor +.DS_Store +target +node_modules +dist +build +test-ledger diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Anchor.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Anchor.toml new file mode 100644 index 00000000..5ea6666b --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Anchor.toml @@ -0,0 +1,24 @@ +[toolchain] + +[features] +seeds = false +skip-lint = false + +[programs.localnet] +transfer_hook = "DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub" + +[registry] +url = "https://api.apr.dev" + +[provider] +cluster = "Localnet" +wallet = "~/.config/solana/id.json" + +[scripts] +test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + +[test.validator] +url = "https://api.devnet.solana.com" + +[[test.validator.clone]] +address = "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Cargo.toml new file mode 100644 index 00000000..ef17a63c --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/Cargo.toml @@ -0,0 +1,13 @@ +[workspace] +members = [ + "programs/*" +] + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/migrations/deploy.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/migrations/deploy.ts new file mode 100644 index 00000000..82fb175f --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/migrations/deploy.ts @@ -0,0 +1,12 @@ +// Migrations are an early feature. Currently, they're nothing more than this +// single deploy script that's invoked from the CLI, injecting a provider +// configured from the workspace's Anchor.toml. + +const anchor = require("@coral-xyz/anchor"); + +module.exports = async function (provider) { + // Configure client to use the provider. + anchor.setProvider(provider); + + // Add your deploy script here. +}; diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/package.json b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/package.json new file mode 100644 index 00000000..738d7f66 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/package.json @@ -0,0 +1,20 @@ +{ + "scripts": { + "lint:fix": "prettier */*.js \"*/**/*{.js,.ts}\" -w", + "lint": "prettier */*.js \"*/**/*{.js,.ts}\" --check" + }, + "dependencies": { + "@coral-xyz/anchor": "^0.29.0", + "@solana/spl-token": "^0.4.0" + }, + "devDependencies": { + "@types/bn.js": "^5.1.0", + "@types/chai": "^4.3.0", + "@types/mocha": "^9.0.0", + "chai": "^4.3.4", + "mocha": "^9.0.3", + "prettier": "^2.6.2", + "ts-mocha": "^10.0.0", + "typescript": "^4.3.5" + } +} diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Cargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Cargo.toml new file mode 100644 index 00000000..138c8047 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "transfer-hook" +version = "0.1.0" +description = "Created with Anchor" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "transfer_hook" + +[features] +no-entrypoint = [] +no-idl = [] +no-log-ix-name = [] +cpi = ["no-entrypoint"] +default = [] + +[dependencies] +anchor-lang = {version = "0.29.0", features = ["init-if-needed"]} +anchor-spl = "0.29.0" +solana-program = "1.17.13" + +spl-transfer-hook-interface = "0.4.1" +spl-tlv-account-resolution = "0.5.0" diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Xargo.toml b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Xargo.toml new file mode 100644 index 00000000..475fb71e --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/Xargo.toml @@ -0,0 +1,2 @@ +[target.bpfel-unknown-unknown.dependencies.std] +features = [] diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs new file mode 100644 index 00000000..991dc884 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs @@ -0,0 +1,195 @@ +use anchor_lang::{ + prelude::*, + system_program::{create_account, CreateAccount}, +}; +use anchor_spl::{ + associated_token::AssociatedToken, token_interface::{Mint, TokenAccount, TokenInterface} +}; +use spl_tlv_account_resolution::state::ExtraAccountMetaList; +use spl_transfer_hook_interface::instruction::{ExecuteInstruction, TransferHookInstruction}; + +declare_id!("DrWbQtYJGtsoRwzKqAbHKHKsCJJfpysudF39GBVFSxub"); + +#[program] +pub mod transfer_hook { + + use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed}; + + use super::*; + + pub fn initialize_extra_account_meta_list( + ctx: Context, + ) -> Result<()> { + + // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly + let account_metas = vec![ + ExtraAccountMeta::new_with_seeds( + &[Seed::Literal { + bytes: "white_list".as_bytes().to_vec(), + }], // owner index (delegate PDA) + false, // is_signer + true, // is_writable + )?, + ]; + + ctx.accounts.white_list.authority = ctx.accounts.payer.key(); + + // calculate account size + let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64; + // calculate minimum required lamports + let lamports = Rent::get()?.minimum_balance(account_size as usize); + + let mint = ctx.accounts.mint.key(); + let signer_seeds: &[&[&[u8]]] = &[&[ + b"extra-account-metas", + &mint.as_ref(), + &[ctx.bumps.extra_account_meta_list], + ]]; + + // create ExtraAccountMetaList account + create_account( + CpiContext::new( + ctx.accounts.system_program.to_account_info(), + CreateAccount { + from: ctx.accounts.payer.to_account_info(), + to: ctx.accounts.extra_account_meta_list.to_account_info(), + }, + ) + .with_signer(signer_seeds), + lamports, + account_size, + ctx.program_id, + )?; + + // initialize ExtraAccountMetaList account with extra accounts + ExtraAccountMetaList::init::( + &mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?, + &account_metas, + )?; + + Ok(()) + } + + pub fn transfer_hook(ctx: Context, _amount: u64) -> Result<()> { + + if !ctx.accounts.white_list.white_list.contains(&ctx.accounts.destination_token.key()) { + panic!("Account not in white list!"); + } + + msg!("Account in white list, all good!"); + + Ok(()) + } + + pub fn add_to_whitelist(ctx: Context) -> Result<()> { + + if ctx.accounts.white_list.authority != ctx.accounts.signer.key() { + panic!("Only the authority can add to the white list!"); + } + + ctx.accounts.white_list.white_list.push(ctx.accounts.new_account.key()); + msg!("New account white listed! {0}", ctx.accounts.new_account.key().to_string()); + msg!("White list length! {0}", ctx.accounts.white_list.white_list.len()); + + Ok(()) + } + + // fallback instruction handler as workaround to anchor instruction discriminator check + pub fn fallback<'info>( + program_id: &Pubkey, + accounts: &'info [AccountInfo<'info>], + data: &[u8], + ) -> Result<()> { + let instruction = TransferHookInstruction::unpack(data)?; + + // match instruction discriminator to transfer hook interface execute instruction + // token2022 program CPIs this instruction on token transfer + match instruction { + TransferHookInstruction::Execute { amount } => { + let amount_bytes = amount.to_le_bytes(); + + // invoke custom transfer hook instruction on our program + __private::__global::transfer_hook(program_id, accounts, &amount_bytes) + } + _ => return Err(ProgramError::InvalidInstructionData.into()), + } + } +} + +#[derive(Accounts)] +pub struct InitializeExtraAccountMetaList<'info> { + #[account(mut)] + payer: Signer<'info>, + + /// CHECK: ExtraAccountMetaList Account, must use these seeds + #[account( + mut, + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: AccountInfo<'info>, + pub mint: InterfaceAccount<'info, Mint>, + pub token_program: Interface<'info, TokenInterface>, + pub associated_token_program: Program<'info, AssociatedToken>, + pub system_program: Program<'info, System>, + #[account( + init_if_needed, + seeds = [b"white_list"], + bump, + payer = payer, + space = 400 + )] + pub white_list: Account<'info, WhiteList>, +} + +// Order of accounts matters for this struct. +// The first 4 accounts are the accounts required for token transfer (source, mint, destination, owner) +// Remaining accounts are the extra accounts required from the ExtraAccountMetaList account +// These accounts are provided via CPI to this program from the token2022 program +#[derive(Accounts)] +pub struct TransferHook<'info> { + #[account( + token::mint = mint, + token::authority = owner, + )] + pub source_token: InterfaceAccount<'info, TokenAccount>, + pub mint: InterfaceAccount<'info, Mint>, + #[account( + token::mint = mint, + )] + pub destination_token: InterfaceAccount<'info, TokenAccount>, + /// CHECK: source token account owner, can be SystemAccount or PDA owned by another program + pub owner: UncheckedAccount<'info>, + /// CHECK: ExtraAccountMetaList Account, + #[account( + seeds = [b"extra-account-metas", mint.key().as_ref()], + bump + )] + pub extra_account_meta_list: UncheckedAccount<'info>, + #[account( + seeds = [b"white_list"], + bump + )] + pub white_list: Account<'info, WhiteList>, +} + +#[derive(Accounts)] +pub struct AddToWhiteList<'info> { + /// CHECK: New account to add to white list + #[account()] + pub new_account: AccountInfo<'info>, + #[account( + mut, + seeds = [b"white_list"], + bump + )] + pub white_list: Account<'info, WhiteList>, + #[account(mut)] + pub signer: Signer<'info>, +} + +#[account] +pub struct WhiteList { + pub authority: Pubkey, + pub white_list: Vec, +} \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tests/transfer-hook.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tests/transfer-hook.ts new file mode 100644 index 00000000..1e824857 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tests/transfer-hook.ts @@ -0,0 +1,229 @@ +import * as anchor from "@coral-xyz/anchor"; +import { Program } from "@coral-xyz/anchor"; +import { TransferHook } from "../target/types/transfer_hook"; +import { + PublicKey, + SystemProgram, + Transaction, + sendAndConfirmTransaction, + Keypair, +} from "@solana/web3.js"; +import { + ExtensionType, + TOKEN_2022_PROGRAM_ID, + getMintLen, + createInitializeMintInstruction, + createInitializeTransferHookInstruction, + ASSOCIATED_TOKEN_PROGRAM_ID, + createAssociatedTokenAccountInstruction, + createMintToInstruction, + createTransferCheckedInstruction, + getAssociatedTokenAddressSync, + createTransferCheckedWithTransferHookInstruction, +} from "@solana/spl-token"; + +describe("transfer-hook", () => { + // Configure the client to use the local cluster. + const provider = anchor.AnchorProvider.env(); + anchor.setProvider(provider); + + const program = anchor.workspace.TransferHook as Program; + const wallet = provider.wallet as anchor.Wallet; + const connection = provider.connection; + + // Generate keypair to use as address for the transfer-hook enabled mint + const mint = new Keypair(); + const decimals = 9; + + // Sender token account address + const sourceTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + wallet.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // Recipient token account address + const recipient = Keypair.generate(); + const destinationTokenAccount = getAssociatedTokenAddressSync( + mint.publicKey, + recipient.publicKey, + false, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ); + + // ExtraAccountMetaList address + // Store extra accounts required by the custom transfer hook instruction + const [extraAccountMetaListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], + program.programId + ); + + const [whiteListPDA] = PublicKey.findProgramAddressSync( + [Buffer.from("white_list")], + program.programId + ); + + it("Create Mint Account with Transfer Hook Extension", async () => { + const extensions = [ExtensionType.TransferHook]; + const mintLen = getMintLen(extensions); + const lamports = + await provider.connection.getMinimumBalanceForRentExemption(mintLen); + + const transaction = new Transaction().add( + SystemProgram.createAccount({ + fromPubkey: wallet.publicKey, + newAccountPubkey: mint.publicKey, + space: mintLen, + lamports: lamports, + programId: TOKEN_2022_PROGRAM_ID, + }), + createInitializeTransferHookInstruction( + mint.publicKey, + wallet.publicKey, + program.programId, // Transfer Hook Program ID + TOKEN_2022_PROGRAM_ID + ), + createInitializeMintInstruction( + mint.publicKey, + decimals, + wallet.publicKey, + null, + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer, mint] + ); + console.log(`Transaction Signature: ${txSig}`); + }); + + // Create the two token accounts for the transfer-hook enabled mint + // Fund the sender token account with 100 tokens + it("Create Token Accounts and Mint Tokens", async () => { + // 100 tokens + const amount = 100 * 10 ** decimals; + + const transaction = new Transaction().add( + createAssociatedTokenAccountInstruction( + wallet.publicKey, + sourceTokenAccount, + wallet.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createAssociatedTokenAccountInstruction( + wallet.publicKey, + destinationTokenAccount, + recipient.publicKey, + mint.publicKey, + TOKEN_2022_PROGRAM_ID, + ASSOCIATED_TOKEN_PROGRAM_ID + ), + createMintToInstruction( + mint.publicKey, + sourceTokenAccount, + wallet.publicKey, + amount, + [], + TOKEN_2022_PROGRAM_ID + ) + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + + console.log(`Transaction Signature: ${txSig}`); + }); + + // Account to store extra accounts required by the transfer hook instruction + it("Create ExtraAccountMetaList Account", async () => { + const initializeExtraAccountMetaListInstruction = await program.methods + .initializeExtraAccountMetaList() + .accounts({ + mint: mint.publicKey, + extraAccountMetaList: extraAccountMetaListPDA, + whiteList: whiteListPDA, + }) + .instruction(); + + const transaction = new Transaction().add( + initializeExtraAccountMetaListInstruction + ); + + const txSig = await sendAndConfirmTransaction( + provider.connection, + transaction, + [wallet.payer], + { skipPreflight: true, commitment: "confirmed" } + ); + + console.log("Transaction Signature:", txSig); + }); + + it("Add account to white list", async () => { + + const addAccountToWhiteListInstruction = await program.methods + .addToWhitelist() + .accounts({ + newAccount: destinationTokenAccount, + signer: wallet.publicKey, + whiteList: whiteListPDA + }) + .instruction(); + + const transaction = new Transaction().add( + addAccountToWhiteListInstruction + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + console.log("White Listed:", txSig); + }); + + it("Transfer Hook with Extra Account Meta", async () => { + // 1 tokens + const amount = 1 * 10 ** decimals; + const bigIntAmount = BigInt(amount); + + // Standard token transfer instruction + const transferInstruction = await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + bigIntAmount, + decimals, + [], + "confirmed", + TOKEN_2022_PROGRAM_ID + ); + + const transaction = new Transaction().add( + transferInstruction + ); + + const txSig = await sendAndConfirmTransaction( + connection, + transaction, + [wallet.payer], + { skipPreflight: true } + ); + console.log("Transfer Checked:", txSig); + }); +}); diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tsconfig.json b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tsconfig.json new file mode 100644 index 00000000..558b83e5 --- /dev/null +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "types": ["mocha", "chai"], + "typeRoots": ["./node_modules/@types"], + "lib": ["es2015"], + "module": "commonjs", + "target": "es6", + "esModuleInterop": true + } + } + \ No newline at end of file From 8e79654489d4da44206bac29ff499d6f118b23bd Mon Sep 17 00:00:00 2001 From: Jonas Hahn Date: Tue, 12 Mar 2024 14:10:22 +0100 Subject: [PATCH 2/3] remove unused destination keypair --- .../anchor/TransferHookCounter/programs/destination.json | 1 - .../TransferHookCounter/programs/transfer-hook/src/lib.rs | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json deleted file mode 100644 index 14b4602e..00000000 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/destination.json +++ /dev/null @@ -1 +0,0 @@ -[190,198,254,219,195,198,188,248,91,58,36,89,114,143,221,246,55,38,153,95,43,99,203,221,16,226,83,243,224,154,57,42,97,126,109,17,158,56,137,34,19,105,170,85,64,116,76,234,202,180,168,197,104,240,143,161,175,114,99,93,98,123,237,240] \ No newline at end of file diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs index 1a89ec95..04876b8f 100644 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs @@ -78,12 +78,12 @@ pub mod transfer_hook { if amount > 50 { msg!("The amount is too big {0}", amount); - // return err!(MyError::AmountTooBig); + //return err!(MyError::AmountTooBig); } ctx.accounts.counter_account.counter.checked_add(1).unwrap(); - msg!("This token has been transfered {0} times", ctx.accounts.counter_account.counter); + msg!("This token has been transferred {0} times", ctx.accounts.counter_account.counter); Ok(()) } From 204e31bc2ad68dab67720afa6ce0484ee7f0c0cf Mon Sep 17 00:00:00 2001 From: John Date: Wed, 20 Mar 2024 16:08:26 -0500 Subject: [PATCH 3/3] minor console log formatting updates --- .../programs/transfer-hook/src/lib.rs | 1 - .../tests/transfer-hook.ts | 48 ++++++++------- .../programs/transfer-hook/src/lib.rs | 5 +- .../programs/transfer-hook/src/lib.rs | 1 - .../tests/transfer-hook.ts | 61 ++++++++++++------- .../programs/transfer-hook/src/lib.rs | 1 - 6 files changed, 67 insertions(+), 50 deletions(-) diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs index 04876b8f..83adfd2a 100644 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/programs/transfer-hook/src/lib.rs @@ -27,7 +27,6 @@ pub mod transfer_hook { ctx: Context, ) -> Result<()> { - // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly let account_metas = vec![ ExtraAccountMeta::new_with_seeds( &[Seed::Literal { diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts index 05e73939..1fdce7c2 100644 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookCounter/tests/transfer-hook.ts @@ -18,7 +18,7 @@ import { createAssociatedTokenAccountInstruction, createMintToInstruction, getAssociatedTokenAddressSync, - createTransferCheckedWithTransferHookInstruction + createTransferCheckedWithTransferHookInstruction, } from "@solana/spl-token"; describe("transfer-hook", () => { @@ -59,7 +59,7 @@ describe("transfer-hook", () => { [Buffer.from("extra-account-metas"), mint.publicKey.toBuffer()], program.programId ); - + const [counterPDA] = PublicKey.findProgramAddressSync( [Buffer.from("counter")], program.programId @@ -98,13 +98,15 @@ describe("transfer-hook", () => { provider.connection, transaction, [wallet.payer, mint], - { skipPreflight: true, commitment: "finalized"} + { skipPreflight: true, commitment: "finalized" } ); - const txDetails = await program.provider.connection.getTransaction(txSig, { maxSupportedTransactionVersion: 0, commitment: 'confirmed'}); + const txDetails = await program.provider.connection.getTransaction(txSig, { + maxSupportedTransactionVersion: 0, + commitment: "confirmed", + }); console.log(txDetails.meta.logMessages); - console.log(`Transaction Signature: ${txSig}`); }); @@ -170,7 +172,7 @@ describe("transfer-hook", () => { provider.connection, transaction, [wallet.payer], - { skipPreflight: true, commitment: "confirmed"} + { skipPreflight: true, commitment: "confirmed" } ); console.log("Transaction Signature:", txSig); }); @@ -180,27 +182,29 @@ describe("transfer-hook", () => { const amount = 1 * 10 ** decimals; const amountBigInt = BigInt(amount); - let transferInstructionWithHelper = await createTransferCheckedWithTransferHookInstruction( - connection, - sourceTokenAccount, - mint.publicKey, - destinationTokenAccount, - wallet.publicKey, - amountBigInt, - decimals, - [], - "confirmed", - TOKEN_2022_PROGRAM_ID, - ); + let transferInstructionWithHelper = + await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + amountBigInt, + decimals, + [], + "confirmed", + TOKEN_2022_PROGRAM_ID + ); console.log("Extra accounts meta: " + extraAccountMetaListPDA); console.log("Counter PDa: " + counterPDA); - console.log("Transfer Instruction: " + JSON.stringify(transferInstructionWithHelper)); - - const transaction = new Transaction().add( - transferInstructionWithHelper + console.log( + "Transfer Instruction: " + + JSON.stringify(transferInstructionWithHelper, null, 2) ); + const transaction = new Transaction().add(transferInstructionWithHelper); + const txSig = await sendAndConfirmTransaction( connection, transaction, diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs index 09f5c8fe..df9b4d67 100644 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookHelloWorld/programs/transfer-hook/src/lib.rs @@ -21,10 +21,7 @@ pub mod transfer_hook { ctx: Context, ) -> Result<()> { - // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly - let account_metas = vec![ - - ]; + let account_metas = vec![]; // calculate account size let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64; diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs index c42841a7..1097d455 100644 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/programs/transfer-hook/src/lib.rs @@ -31,7 +31,6 @@ pub mod transfer_hook { ) -> Result<()> { // index 0-3 are the accounts required for token transfer (source, mint, destination, owner) // index 4 is address of ExtraAccountMetaList account - // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly let account_metas = vec![ // index 5, wrapped SOL mint ExtraAccountMeta::new_with_pubkey(&ctx.accounts.wsol_mint.key(), false, false)?, diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts index 62bc9e54..fd879514 100644 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookTransferCost/tests/transfer-hook.ts @@ -218,7 +218,7 @@ describe("transfer-hook", () => { provider.connection, transaction, [wallet.payer], - { skipPreflight: true, commitment : "confirmed"} + { skipPreflight: true, commitment: "confirmed" } ); console.log("Transaction Signature:", txSig); }); @@ -250,35 +250,54 @@ describe("transfer-hook", () => { senderWSolTokenAccount ); - const mintInfo = await getMint(connection, mint.publicKey, "confirmed", TOKEN_2022_PROGRAM_ID); + const mintInfo = await getMint( + connection, + mint.publicKey, + "confirmed", + TOKEN_2022_PROGRAM_ID + ); const transferHook = getTransferHook(mintInfo); if (transferHook != null) { - console.log("Transfer hook not found" + JSON.stringify(transferHook)); + console.log( + "Transfer hook program found: " + JSON.stringify(transferHook, null, 2) + ); } - const extraAccountsAccount = getExtraAccountMetaAddress(mint.publicKey, transferHook.programId); - const extraAccountsInfo = await connection.getAccountInfo(extraAccountsAccount, "confirmed"); + const extraAccountsAccount = getExtraAccountMetaAddress( + mint.publicKey, + transferHook.programId + ); + const extraAccountsInfo = await connection.getAccountInfo( + extraAccountsAccount, + "confirmed" + ); const extraAccountMetas = getExtraAccountMetas(extraAccountsInfo); for (const extraAccountMeta of extraAccountMetas) { - console.log("Extra account meta: " + JSON.stringify(extraAccountMeta)); + console.log( + "Extra account meta: " + JSON.stringify(extraAccountMeta, null, 2) + ); } // Standard token transfer instruction - const transferInstruction = await createTransferCheckedWithTransferHookInstruction( - connection, - sourceTokenAccount, - mint.publicKey, - destinationTokenAccount, - wallet.publicKey, - bigIntAmount, - decimals, - [], - "confirmed", - TOKEN_2022_PROGRAM_ID - ); + const transferInstruction = + await createTransferCheckedWithTransferHookInstruction( + connection, + sourceTokenAccount, + mint.publicKey, + destinationTokenAccount, + wallet.publicKey, + bigIntAmount, + decimals, + [], + "confirmed", + TOKEN_2022_PROGRAM_ID + ); - console.log("Pushed keys:", JSON.stringify(transferInstruction.keys)); + console.log( + "Pushed keys:", + JSON.stringify(transferInstruction.keys, null, 2) + ); const transaction = new Transaction().add( solTransferInstruction, @@ -286,7 +305,7 @@ describe("transfer-hook", () => { approveInstruction, transferInstruction ); - + const txSig = await sendAndConfirmTransaction( connection, transaction, @@ -296,7 +315,7 @@ describe("transfer-hook", () => { console.log("Transfer Signature:", txSig); const tokenAccount = await getAccount(connection, delegateWSolTokenAccount); - + assert.equal(Number(tokenAccount.amount), amount / 2); }); }); diff --git a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs index 991dc884..4df87648 100644 --- a/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs +++ b/tokens/token-2022/transfer-hook/anchor/TransferHookWhitelist/programs/transfer-hook/src/lib.rs @@ -21,7 +21,6 @@ pub mod transfer_hook { ctx: Context, ) -> Result<()> { - // The `addExtraAccountsToInstruction` JS helper function resolving incorrectly let account_metas = vec![ ExtraAccountMeta::new_with_seeds( &[Seed::Literal {