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