From a39b8eb9652b47d1a96643d9b97126da5c46747e Mon Sep 17 00:00:00 2001 From: Joe Date: Wed, 10 Jan 2024 09:49:57 -0600 Subject: [PATCH 1/5] token js: create new offchain helper --- .../extensions/transferHook/instructions.ts | 158 +++- token/js/test/unit/transferHook.test.ts | 700 ++++++++++++++---- 2 files changed, 677 insertions(+), 181 deletions(-) diff --git a/token/js/src/extensions/transferHook/instructions.ts b/token/js/src/extensions/transferHook/instructions.ts index 9484a3053c0..41292d91b60 100644 --- a/token/js/src/extensions/transferHook/instructions.ts +++ b/token/js/src/extensions/transferHook/instructions.ts @@ -137,6 +137,8 @@ function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMe } /** + * @deprecated Deprecated since v0.4.0. Please use {@link addExtraAccountMetasForExecute} instead. + * * Add extra accounts needed for transfer hook to an instruction * * @param connection Connection to use @@ -190,6 +192,112 @@ export async function addExtraAccountsToInstruction( return new TransactionInstruction({ keys: accountMetas, programId, data: instruction.data }); } +/** + * Construct an `ExecuteInstruction` for a transfer hook program + * + * @param programId The program ID of the transfer hook program + * @param source The source account + * @param mint The mint account + * @param destination The destination account + * @param owner Owner of the source account + * @param validateStatePubkey The validate state pubkey + * @param amount The amount of tokens to transfer + * @returns Instruction to add to a transaction + */ +export function createExecuteInstruction( + programId: PublicKey, + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + authority: PublicKey, + validateStatePubkey: PublicKey, + amount: number | bigint +): TransactionInstruction { + const keys = [source, mint, destination, authority, validateStatePubkey].map((pubkey) => ({ + pubkey, + isSigner: false, + isWritable: false, + })); + + const data = Buffer.alloc(16); + data.set(Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]), 0); // `ExecuteInstruction` discriminator + data.writeBigUInt64LE(BigInt(amount), 8); + + return new TransactionInstruction({ keys, programId, data }); +} + +/** + * Adds all the extra accounts needed for a transfer hook to an instruction + * + * @param connection Connection to use + * @param instruction The instruction to add accounts to + * @param programId Transfer hook program ID + * @param source The source account + * @param mint The mint account + * @param destination The destination account + * @param authority Owner of the source account + * @param amount The amount of tokens to transfer + * @param commitment Commitment to use + * @returns A new instruction with the extra account metas added + */ +export async function addExtraAccountMetasForExecute( + connection: Connection, + instruction: TransactionInstruction, + programId: PublicKey, + source: PublicKey, + mint: PublicKey, + destination: PublicKey, + authority: PublicKey, + amount: number | bigint, + commitment?: Commitment +) { + const validateStatePubkey = getExtraAccountMetaAddress(mint, programId); + const validateStateAccount = await connection.getAccountInfo(validateStatePubkey, commitment); + if (validateStateAccount == null) { + return instruction; + } + const validateStateData = getExtraAccountMetas(validateStateAccount); + + // Check to make sure the provided keys are in the instruction + if (![source, mint, destination, authority].every((key) => instruction.keys.some((meta) => meta.pubkey === key))) { + throw new Error('Missing required account in instruction'); + } + + const executeInstruction = createExecuteInstruction( + programId, + source, + mint, + destination, + authority, + validateStatePubkey, + amount + ); + + for (const extraAccountMeta of validateStateData) { + executeInstruction.keys.push( + deEscalateAccountMeta( + await resolveExtraAccountMeta( + connection, + extraAccountMeta, + executeInstruction.keys, + executeInstruction.data, + executeInstruction.programId + ), + executeInstruction.keys + ) + ); + } + + // Add only the extra accounts resolved from the validation state + instruction.keys.push(...executeInstruction.keys.slice(5)); + + // Add the transfer hook program ID and the validation state account + instruction.keys.push({ pubkey: programId, isSigner: false, isWritable: false }); + instruction.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false }); + + return instruction; +} + /** * Construct an transferChecked instruction with extra accounts for transfer hook * @@ -197,7 +305,7 @@ export async function addExtraAccountsToInstruction( * @param source Source account * @param mint Mint to update * @param destination Destination account - * @param authority The mint's transfer hook authority + * @param authority Owner of the source account * @param amount The amount of tokens to transfer * @param decimals Number of decimals in transfer amount * @param multiSigners The signer account(s) for a multisig @@ -229,15 +337,22 @@ export async function createTransferCheckedWithTransferHookInstruction( programId ); - const hydratedInstruction = await addExtraAccountsToInstruction( - connection, - rawInstruction, - mint, - commitment, - programId - ); + const mintInfo = await getMint(connection, mint, commitment, programId); + const transferHook = getTransferHook(mintInfo); - return hydratedInstruction; + return transferHook + ? addExtraAccountMetasForExecute( + connection, + rawInstruction, + transferHook.programId, + source, + mint, + destination, + authority, + amount, + commitment + ) + : rawInstruction; } /** @@ -247,7 +362,7 @@ export async function createTransferCheckedWithTransferHookInstruction( * @param source Source account * @param mint Mint to update * @param destination Destination account - * @param authority The mint's transfer hook authority + * @param authority Owner of the source account * @param amount The amount of tokens to transfer * @param decimals Number of decimals in transfer amount * @param fee The calculated fee for the transfer fee extension @@ -282,13 +397,20 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction( programId ); - const hydratedInstruction = await addExtraAccountsToInstruction( - connection, - rawInstruction, - mint, - commitment, - programId - ); + const mintInfo = await getMint(connection, mint, commitment, programId); + const transferHook = getTransferHook(mintInfo); - return hydratedInstruction; + return transferHook + ? addExtraAccountMetasForExecute( + connection, + rawInstruction, + transferHook.programId, + source, + mint, + destination, + authority, + amount, + commitment + ) + : rawInstruction; } diff --git a/token/js/test/unit/transferHook.test.ts b/token/js/test/unit/transferHook.test.ts index f24c24747c1..03b1ea6ca34 100644 --- a/token/js/test/unit/transferHook.test.ts +++ b/token/js/test/unit/transferHook.test.ts @@ -1,173 +1,547 @@ -import { getExtraAccountMetas, resolveExtraAccountMeta } from '../../src'; +import type { ExtraAccountMeta, ExtraAccountMetaList } from '../../src'; +import { + ACCOUNT_SIZE, + ACCOUNT_TYPE_SIZE, + ExtensionType, + ExtraAccountMetaAccountDataLayout, + ExtraAccountMetaLayout, + LENGTH_SIZE, + MintLayout, + TOKEN_2022_PROGRAM_ID, + TRANSFER_HOOK_SIZE, + TYPE_SIZE, + TransferHookLayout, + addExtraAccountMetasForExecute, + createTransferCheckedWithTransferHookInstruction, + getExtraAccountMetaAddress, + getExtraAccountMetas, + resolveExtraAccountMeta, +} from '../../src'; import { expect } from 'chai'; import type { Connection } from '@solana/web3.js'; -import { PublicKey } from '@solana/web3.js'; +import { Keypair, PublicKey, TransactionInstruction } from '@solana/web3.js'; import { getConnection } from '../common'; -describe('transferHookExtraAccounts', () => { - let connection: Connection; - const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); - const instructionData = Buffer.from(Array.from(Array(32).keys())); - const plainAccount = new PublicKey('6c5q79ccBTWvZTEx3JkdHThtMa2eALba5bfvHGf8kA2c'); - const seeds = [Buffer.from('seed'), Buffer.from([4, 5, 6, 7]), plainAccount.toBuffer(), Buffer.from([2, 2, 2, 2])]; - const pdaPublicKey = PublicKey.findProgramAddressSync(seeds, testProgramId)[0]; - const pdaPublicKeyWithProgramId = PublicKey.findProgramAddressSync(seeds, plainAccount)[0]; - - const plainSeed = Buffer.concat([ - Buffer.from([1]), // u8 discriminator - Buffer.from([4]), // u8 length - Buffer.from('seed'), // 4 bytes seed - ]); - - const instructionDataSeed = Buffer.concat([ - Buffer.from([2]), // u8 discriminator - Buffer.from([4]), // u8 offset - Buffer.from([4]), // u8 length - ]); - - const accountKeySeed = Buffer.concat([ - Buffer.from([3]), // u8 discriminator - Buffer.from([0]), // u8 index - ]); - - const accountDataSeed = Buffer.concat([ - Buffer.from([4]), // u8 discriminator - Buffer.from([0]), // u8 account index - Buffer.from([2]), // u8 account data offset - Buffer.from([4]), // u8 account data length - ]); - - const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed, accountDataSeed], 32); - - const plainExtraAccountMeta = { - discriminator: 0, - addressConfig: plainAccount.toBuffer(), - isSigner: false, - isWritable: false, - }; - const plainExtraAccount = Buffer.concat([ - Buffer.from([0]), // u8 discriminator - plainAccount.toBuffer(), // 32 bytes address - Buffer.from([0]), // bool isSigner - Buffer.from([0]), // bool isWritable - ]); - - const pdaExtraAccountMeta = { - discriminator: 1, - addressConfig, - isSigner: true, - isWritable: false, - }; - const pdaExtraAccount = Buffer.concat([ - Buffer.from([1]), // u8 discriminator - addressConfig, // 32 bytes address config - Buffer.from([1]), // bool isSigner - Buffer.from([0]), // bool isWritable - ]); - - const pdaExtraAccountMetaWithProgramId = { - discriminator: 128, - addressConfig, - isSigner: false, - isWritable: true, - }; - const pdaExtraAccountWithProgramId = Buffer.concat([ - Buffer.from([128]), // u8 discriminator - addressConfig, // 32 bytes address config - Buffer.from([0]), // bool isSigner - Buffer.from([1]), // bool isWritable - ]); - - const extraAccountList = Buffer.concat([ - Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), // u64 accountDiscriminator - Buffer.from([0, 0, 0, 0]), // u32 length - Buffer.from([3, 0, 0, 0]), // u32 count - plainExtraAccount, - pdaExtraAccount, - pdaExtraAccountWithProgramId, - ]); - - before(async () => { - connection = await getConnection(); - connection.getAccountInfo = async ( - _publicKey: PublicKey, - _commitmentOrConfig?: Parameters<(typeof connection)['getAccountInfo']>[1] - ): ReturnType<(typeof connection)['getAccountInfo']> => ({ - data: Buffer.from([0, 0, 2, 2, 2, 2]), - owner: PublicKey.default, - executable: false, - lamports: 0, +describe('transferHook', () => { + describe('validation data', () => { + let connection: Connection; + const testProgramId = new PublicKey('7N4HggYEJAtCLJdnHGCtFqfxcB5rhQCsQTze3ftYstVj'); + const instructionData = Buffer.from(Array.from(Array(32).keys())); + const plainAccount = new PublicKey('6c5q79ccBTWvZTEx3JkdHThtMa2eALba5bfvHGf8kA2c'); + const seeds = [ + Buffer.from('seed'), + Buffer.from([4, 5, 6, 7]), + plainAccount.toBuffer(), + Buffer.from([2, 2, 2, 2]), + ]; + const pdaPublicKey = PublicKey.findProgramAddressSync(seeds, testProgramId)[0]; + const pdaPublicKeyWithProgramId = PublicKey.findProgramAddressSync(seeds, plainAccount)[0]; + + const plainSeed = Buffer.concat([ + Buffer.from([1]), // u8 discriminator + Buffer.from([4]), // u8 length + Buffer.from('seed'), // 4 bytes seed + ]); + + const instructionDataSeed = Buffer.concat([ + Buffer.from([2]), // u8 discriminator + Buffer.from([4]), // u8 offset + Buffer.from([4]), // u8 length + ]); + + const accountKeySeed = Buffer.concat([ + Buffer.from([3]), // u8 discriminator + Buffer.from([0]), // u8 index + ]); + + const accountDataSeed = Buffer.concat([ + Buffer.from([4]), // u8 discriminator + Buffer.from([0]), // u8 account index + Buffer.from([2]), // u8 account data offset + Buffer.from([4]), // u8 account data length + ]); + + const addressConfig = Buffer.concat([plainSeed, instructionDataSeed, accountKeySeed, accountDataSeed], 32); + + const plainExtraAccountMeta = { + discriminator: 0, + addressConfig: plainAccount.toBuffer(), + isSigner: false, + isWritable: false, + }; + const plainExtraAccount = Buffer.concat([ + Buffer.from([0]), // u8 discriminator + plainAccount.toBuffer(), // 32 bytes address + Buffer.from([0]), // bool isSigner + Buffer.from([0]), // bool isWritable + ]); + + const pdaExtraAccountMeta = { + discriminator: 1, + addressConfig, + isSigner: true, + isWritable: false, + }; + const pdaExtraAccount = Buffer.concat([ + Buffer.from([1]), // u8 discriminator + addressConfig, // 32 bytes address config + Buffer.from([1]), // bool isSigner + Buffer.from([0]), // bool isWritable + ]); + + const pdaExtraAccountMetaWithProgramId = { + discriminator: 128, + addressConfig, + isSigner: false, + isWritable: true, + }; + const pdaExtraAccountWithProgramId = Buffer.concat([ + Buffer.from([128]), // u8 discriminator + addressConfig, // 32 bytes address config + Buffer.from([0]), // bool isSigner + Buffer.from([1]), // bool isWritable + ]); + + const extraAccountList = Buffer.concat([ + Buffer.from([0, 0, 0, 0, 0, 0, 0, 0]), // u64 accountDiscriminator + Buffer.from([0, 0, 0, 0]), // u32 length + Buffer.from([3, 0, 0, 0]), // u32 count + plainExtraAccount, + pdaExtraAccount, + pdaExtraAccountWithProgramId, + ]); + + before(async () => { + connection = await getConnection(); + connection.getAccountInfo = async ( + _publicKey: PublicKey, + _commitmentOrConfig?: Parameters<(typeof connection)['getAccountInfo']>[1] + ): ReturnType<(typeof connection)['getAccountInfo']> => ({ + data: Buffer.from([0, 0, 2, 2, 2, 2]), + owner: PublicKey.default, + executable: false, + lamports: 0, + }); + }); + + it('can parse extra metas', () => { + const accountInfo = { + data: extraAccountList, + owner: PublicKey.default, + executable: false, + lamports: 0, + }; + const parsedExtraAccounts = getExtraAccountMetas(accountInfo); + expect(parsedExtraAccounts).to.not.be.null; + if (parsedExtraAccounts == null) { + return; + } + + expect(parsedExtraAccounts).to.have.length(3); + if (parsedExtraAccounts.length !== 3) { + return; + } + + expect(parsedExtraAccounts[0].discriminator).to.eql(0); + expect(parsedExtraAccounts[0].addressConfig).to.eql(plainAccount.toBuffer()); + expect(parsedExtraAccounts[0].isSigner).to.be.false; + expect(parsedExtraAccounts[0].isWritable).to.be.false; + + expect(parsedExtraAccounts[1].discriminator).to.eql(1); + expect(parsedExtraAccounts[1].addressConfig).to.eql(addressConfig); + expect(parsedExtraAccounts[1].isSigner).to.be.true; + expect(parsedExtraAccounts[1].isWritable).to.be.false; + + expect(parsedExtraAccounts[2].discriminator).to.eql(128); + expect(parsedExtraAccounts[2].addressConfig).to.eql(addressConfig); + expect(parsedExtraAccounts[2].isSigner).to.be.false; + expect(parsedExtraAccounts[2].isWritable).to.be.true; + }); + + it('can resolve extra metas', async () => { + const resolvedPlainAccount = await resolveExtraAccountMeta( + connection, + plainExtraAccountMeta, + [], + instructionData, + testProgramId + ); + + expect(resolvedPlainAccount.pubkey).to.eql(plainAccount); + expect(resolvedPlainAccount.isSigner).to.be.false; + expect(resolvedPlainAccount.isWritable).to.be.false; + + const resolvedPdaAccount = await resolveExtraAccountMeta( + connection, + pdaExtraAccountMeta, + [resolvedPlainAccount], + instructionData, + testProgramId + ); + + expect(resolvedPdaAccount.pubkey).to.eql(pdaPublicKey); + expect(resolvedPdaAccount.isSigner).to.be.true; + expect(resolvedPdaAccount.isWritable).to.be.false; + + const resolvedPdaAccountWithProgramId = await resolveExtraAccountMeta( + connection, + pdaExtraAccountMetaWithProgramId, + [resolvedPlainAccount], + instructionData, + testProgramId + ); + + expect(resolvedPdaAccountWithProgramId.pubkey).to.eql(pdaPublicKeyWithProgramId); + expect(resolvedPdaAccountWithProgramId.isSigner).to.be.false; + expect(resolvedPdaAccountWithProgramId.isWritable).to.be.true; }); }); - it('getExtraAccountMetas', () => { - const accountInfo = { - data: extraAccountList, - owner: PublicKey.default, - executable: false, - lamports: 0, + // prettier-ignore + describe('adding extra metas to instructions', () => { + const addressConfig = (data: Uint8Array) => { + const addressConfig = Buffer.alloc(32); + addressConfig.set(data, 0); + return addressConfig; }; - const parsedExtraAccounts = getExtraAccountMetas(accountInfo); - expect(parsedExtraAccounts).to.not.be.null; - if (parsedExtraAccounts == null) { - return; - } - - expect(parsedExtraAccounts).to.have.length(3); - if (parsedExtraAccounts.length !== 3) { - return; - } - - expect(parsedExtraAccounts[0].discriminator).to.eql(0); - expect(parsedExtraAccounts[0].addressConfig).to.eql(plainAccount.toBuffer()); - expect(parsedExtraAccounts[0].isSigner).to.be.false; - expect(parsedExtraAccounts[0].isWritable).to.be.false; - - expect(parsedExtraAccounts[1].discriminator).to.eql(1); - expect(parsedExtraAccounts[1].addressConfig).to.eql(addressConfig); - expect(parsedExtraAccounts[1].isSigner).to.be.true; - expect(parsedExtraAccounts[1].isWritable).to.be.false; - - expect(parsedExtraAccounts[2].discriminator).to.eql(128); - expect(parsedExtraAccounts[2].addressConfig).to.eql(addressConfig); - expect(parsedExtraAccounts[2].isSigner).to.be.false; - expect(parsedExtraAccounts[2].isWritable).to.be.true; - }); - it('resolveExtraAccountMeta', async () => { - const resolvedPlainAccount = await resolveExtraAccountMeta( - connection, - plainExtraAccountMeta, - [], - instructionData, - testProgramId - ); - - expect(resolvedPlainAccount.pubkey).to.eql(plainAccount); - expect(resolvedPlainAccount.isSigner).to.be.false; - expect(resolvedPlainAccount.isWritable).to.be.false; - - const resolvedPdaAccount = await resolveExtraAccountMeta( - connection, - pdaExtraAccountMeta, - [resolvedPlainAccount], - instructionData, - testProgramId - ); - - expect(resolvedPdaAccount.pubkey).to.eql(pdaPublicKey); - expect(resolvedPdaAccount.isSigner).to.be.true; - expect(resolvedPdaAccount.isWritable).to.be.false; - - const resolvedPdaAccountWithProgramId = await resolveExtraAccountMeta( - connection, - pdaExtraAccountMetaWithProgramId, - [resolvedPlainAccount], - instructionData, - testProgramId - ); - - expect(resolvedPdaAccountWithProgramId.pubkey).to.eql(pdaPublicKeyWithProgramId); - expect(resolvedPdaAccountWithProgramId.isSigner).to.be.false; - expect(resolvedPdaAccountWithProgramId.isWritable).to.be.true; + + const fixedAddress = (address: PublicKey, isSigner: boolean, isWritable: boolean) => ({ + discriminator: 0, + addressConfig: address.toBuffer(), + isSigner, + isWritable, + }); + + const pda = (seeds: number[], isSigner: boolean, isWritable: boolean) => ({ + discriminator: 1, + addressConfig: addressConfig(new Uint8Array(seeds)), + isSigner, + isWritable, + }); + + const externalPda = (programKeyIndex: number, seeds: number[], isSigner: boolean, isWritable: boolean) => ({ + discriminator: (1 << 7) + programKeyIndex, + addressConfig: addressConfig(new Uint8Array(seeds)), + isSigner, + isWritable, + }); + + it('can add extra account metas for execute', async () => { + const transferHookProgramId = Keypair.generate().publicKey; + + const sourcePubkey = Keypair.generate().publicKey; + const mintPubkey = Keypair.generate().publicKey; + const destinationPubkey = Keypair.generate().publicKey; + const authorityPubkey = Keypair.generate().publicKey; + const validateStatePubkey = getExtraAccountMetaAddress(mintPubkey, transferHookProgramId); + const amount = 100n; + const decimals = 0; + + const extraMeta1Pubkey = Keypair.generate().publicKey; + const extraMeta2Pubkey = Keypair.generate().publicKey; + const extraMeta3Pubkey = Keypair.generate().publicKey; + + const connection = await getConnection(); + connection.getAccountInfo = async function mockFetchAccountDataFn( + publicKey: PublicKey, + _commitmentOrConfig?: Parameters[1] + ): ReturnType { + // Mocked validate state + const extraAccounts: ExtraAccountMeta[] = [ + fixedAddress(extraMeta1Pubkey, false, false), + fixedAddress(extraMeta2Pubkey, false, false), + fixedAddress(extraMeta3Pubkey, false, false), + pda([ + 3, 0, // First seed: Account key at index 0 (2) + 3, 4, // Second seed: Account key at index 4 (2) + ], false, false), + pda([ + 3, 5, // First seed: Account key at index 5 (2) + 3, 6, // Second seed: Account key at index 6 (2) + ], false, false), + pda([ + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + ], false, false), + ]; + const extraAccountsList: ExtraAccountMetaList = { + count: extraAccounts.length, + extraAccounts, + } + const instructionDiscriminator = Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]).readBigUInt64LE(); + const data = Buffer.alloc(8 + 4 + 4 + ExtraAccountMetaLayout.span * extraAccounts.length); + ExtraAccountMetaAccountDataLayout.encode({ + instructionDiscriminator, + length: 4 + ExtraAccountMetaLayout.span * extraAccounts.length, + extraAccountsList, + }, data); + return { + data, + owner: transferHookProgramId, + executable: false, + lamports: 0, + }; + }; + + const amountInLeBytes = Buffer.alloc(8); + amountInLeBytes.writeBigUInt64LE(amount); + + const extraMeta4Pubkey = PublicKey.findProgramAddressSync( + [ + sourcePubkey.toBuffer(), + validateStatePubkey.toBuffer(), + ], + transferHookProgramId, + )[0]; + const extraMeta5Pubkey = PublicKey.findProgramAddressSync( + [ + extraMeta1Pubkey.toBuffer(), + extraMeta2Pubkey.toBuffer(), + ], + transferHookProgramId, + )[0]; + const extraMeta6Pubkey = PublicKey.findProgramAddressSync( + [ + Buffer.from("prefix"), + amountInLeBytes, // Instruction data 8..16 + ], + transferHookProgramId, + )[0]; + + // Fail missing key + const rawInstructionMissingKey = new TransactionInstruction({ + keys: [ + // source missing + { pubkey: mintPubkey, isSigner: false, isWritable: false }, + { pubkey: destinationPubkey, isSigner: false, isWritable: true }, + { pubkey: authorityPubkey, isSigner: true, isWritable: false }, + ], + programId: transferHookProgramId, + }); + await expect( + addExtraAccountMetasForExecute( + connection, + rawInstructionMissingKey, + transferHookProgramId, + sourcePubkey, + mintPubkey, + destinationPubkey, + authorityPubkey, + amount, + ) + ).to.be.rejectedWith("Missing required account in instruction"); + + const rawInstruction = new TransactionInstruction({ + keys: [ + { pubkey: sourcePubkey, isSigner: false, isWritable: true }, + { pubkey: mintPubkey, isSigner: false, isWritable: false }, + { pubkey: destinationPubkey, isSigner: false, isWritable: true }, + { pubkey: authorityPubkey, isSigner: true, isWritable: false }, + ], + programId: transferHookProgramId, + }); + + const hydratedInstruction = await addExtraAccountMetasForExecute( + connection, + rawInstruction, + transferHookProgramId, + sourcePubkey, + mintPubkey, + destinationPubkey, + authorityPubkey, + amount, + ); + + const checkMetas = [ + { pubkey: sourcePubkey, isSigner: false, isWritable: true }, + { pubkey: mintPubkey, isSigner: false, isWritable: false }, + { pubkey: destinationPubkey, isSigner: false, isWritable: true }, + { pubkey: authorityPubkey, isSigner: true, isWritable: false }, + { pubkey: extraMeta1Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta2Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta3Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta4Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta5Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta6Pubkey, isSigner: false, isWritable: false }, + { pubkey: transferHookProgramId, isSigner: false, isWritable: false }, + { pubkey: validateStatePubkey, isSigner: false, isWritable: false }, + ]; + + expect(hydratedInstruction.keys).to.eql(checkMetas); + }); + + it('can create a transfer instruction with extra metas', async () => { + const transferHookProgramId = Keypair.generate().publicKey; + + const sourcePubkey = Keypair.generate().publicKey; + const mintPubkey = Keypair.generate().publicKey; + const destinationPubkey = Keypair.generate().publicKey; + const authorityPubkey = Keypair.generate().publicKey; + const validateStatePubkey = getExtraAccountMetaAddress(mintPubkey, transferHookProgramId); + const amount = 100n; + const decimals = 0; + + // Arbitrary program ID included to test external PDAs + const arbitraryProgramId = Keypair.generate().publicKey; + + const connection = await getConnection(); + connection.getAccountInfo = async function mockFetchAccountDataFn( + publicKey: PublicKey, + _commitmentOrConfig?: Parameters[1] + ): ReturnType { + // Mocked mint state + if (publicKey.equals(mintPubkey)) { + const data = Buffer.alloc(ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE + TRANSFER_HOOK_SIZE); + MintLayout.encode({ + mintAuthorityOption: 0, + mintAuthority: PublicKey.default, + supply: 10000n, + decimals, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, data, 0); + data.writeUint8(1, ACCOUNT_SIZE); // Account type (1): Mint = 1 + data.writeUint16LE(ExtensionType.TransferHook, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE); + data.writeUint16LE(TRANSFER_HOOK_SIZE, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE); + TransferHookLayout.encode({ + authority: Keypair.generate().publicKey, + programId: transferHookProgramId, + }, data, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE); + return { + data, + owner: TOKEN_2022_PROGRAM_ID, + executable: false, + lamports: 0, + }; + } + + // Mocked validate state + if (publicKey.equals(validateStatePubkey)) { + const extraAccounts: ExtraAccountMeta[] = [ + pda([ + 3, 0, // First seed: Account key at index 0 (2) + 3, 1, // Second seed: Account key at index 1 (2) + ], false, false), + pda([ + 3, 4, // First seed: Account key at index 4 (2) + ], false, false), + pda([ + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + ], false, false), + fixedAddress(arbitraryProgramId, false, false), + externalPda(8, [ + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + ], false, false), + externalPda(8, [ + 1, 14, 97, 110, 111, 116, 104, 101, 114, 95, 112, 114, 101, 102, 105, + 120, // First seed: Literal "another_prefix" (16) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 3, 9, // Fourth seed: Account key at index 9 (2) + ], false, false), + ]; + const extraAccountsList: ExtraAccountMetaList = { + count: extraAccounts.length, + extraAccounts, + } + const instructionDiscriminator = Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]).readBigUInt64LE(); + const data = Buffer.alloc(8 + 4 + 4 + ExtraAccountMetaLayout.span * extraAccounts.length); + ExtraAccountMetaAccountDataLayout.encode({ + instructionDiscriminator, + length: 4 + ExtraAccountMetaLayout.span * extraAccounts.length, + extraAccountsList, + }, data); + return { + data, + owner: transferHookProgramId, + executable: false, + lamports: 0, + }; + } + + return { + data: Buffer.from([]), + owner: PublicKey.default, + executable: false, + lamports: 0, + }; + }; + + const amountInLeBytes = Buffer.alloc(8); + amountInLeBytes.writeBigUInt64LE(amount); + + const extraMeta1Pubkey = PublicKey.findProgramAddressSync( + [ + sourcePubkey.toBuffer(), // Account key at index 0 + mintPubkey.toBuffer(), // Account key at index 1 + ], + transferHookProgramId, + )[0]; + const extraMeta2Pubkey = PublicKey.findProgramAddressSync( + [ + validateStatePubkey.toBuffer(), // Account key at index 4 + ], + transferHookProgramId, + )[0]; + const extraMeta3Pubkey = PublicKey.findProgramAddressSync( + [ + Buffer.from("prefix"), + amountInLeBytes, // Instruction data 8..16 + ], + transferHookProgramId, + )[0]; + const extraMeta4Pubkey = arbitraryProgramId; + const extraMeta5Pubkey = PublicKey.findProgramAddressSync( + [ + Buffer.from("prefix"), + amountInLeBytes, // Instruction data 8..16 + extraMeta2Pubkey.toBuffer(), + ], + extraMeta4Pubkey, // PDA off of the arbitrary program ID + )[0]; + const extraMeta6Pubkey = PublicKey.findProgramAddressSync( + [ + Buffer.from("another_prefix"), + amountInLeBytes, // Instruction data 8..16 + extraMeta2Pubkey.toBuffer(), + extraMeta5Pubkey.toBuffer(), + ], + extraMeta4Pubkey, // PDA off of the arbitrary program ID + )[0]; + + const instruction = await createTransferCheckedWithTransferHookInstruction( + connection, + sourcePubkey, + mintPubkey, + destinationPubkey, + authorityPubkey, + amount, + decimals, + [], + undefined, + TOKEN_2022_PROGRAM_ID + ); + + const checkMetas = [ + { pubkey: sourcePubkey, isSigner: false, isWritable: true }, + { pubkey: mintPubkey, isSigner: false, isWritable: false }, + { pubkey: destinationPubkey, isSigner: false, isWritable: true }, + { pubkey: authorityPubkey, isSigner: true, isWritable: false }, + { pubkey: extraMeta1Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta2Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta3Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta4Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta5Pubkey, isSigner: false, isWritable: false }, + { pubkey: extraMeta6Pubkey, isSigner: false, isWritable: false }, + { pubkey: transferHookProgramId, isSigner: false, isWritable: false }, + { pubkey: validateStatePubkey, isSigner: false, isWritable: false }, + ]; + + expect(instruction.keys).to.eql(checkMetas); + }); }); }); From 7a3e1ed4db12b690bf68e7de7f1a6540cc706066 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 11 Jan 2024 10:09:33 -0600 Subject: [PATCH 2/5] refactor tests a bit --- token/js/test/unit/transferHook.test.ts | 355 ++++++++++++------------ 1 file changed, 173 insertions(+), 182 deletions(-) diff --git a/token/js/test/unit/transferHook.test.ts b/token/js/test/unit/transferHook.test.ts index 03b1ea6ca34..e6219f19c1a 100644 --- a/token/js/test/unit/transferHook.test.ts +++ b/token/js/test/unit/transferHook.test.ts @@ -197,8 +197,116 @@ describe('transferHook', () => { }); }); - // prettier-ignore describe('adding extra metas to instructions', () => { + let connection: Connection; + + let transferHookProgramId: PublicKey; + + let sourcePubkey: PublicKey; + let mintPubkey: PublicKey; + let destinationPubkey: PublicKey; + let authorityPubkey: PublicKey; + let validateStatePubkey: PublicKey; + + const amount = 100n; + const amountInLeBytes = Buffer.alloc(8); + amountInLeBytes.writeBigUInt64LE(amount); + const decimals = 0; + + // Arbitrary program ID included to test external PDAs + let arbitraryProgramId: PublicKey; + + beforeEach(async () => { + connection = await getConnection(); + + transferHookProgramId = Keypair.generate().publicKey; + + sourcePubkey = Keypair.generate().publicKey; + mintPubkey = Keypair.generate().publicKey; + destinationPubkey = Keypair.generate().publicKey; + authorityPubkey = Keypair.generate().publicKey; + validateStatePubkey = getExtraAccountMetaAddress(mintPubkey, transferHookProgramId); + + arbitraryProgramId = Keypair.generate().publicKey; + }); + + function createMockFetchAccountDataFn(extraAccounts: ExtraAccountMeta[]) { + return async function mockFetchAccountDataFn( + publicKey: PublicKey, + _commitmentOrConfig?: Parameters[1] + ): ReturnType { + // Mocked mint state + if (publicKey.equals(mintPubkey)) { + const data = Buffer.alloc( + ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE + TRANSFER_HOOK_SIZE + ); + MintLayout.encode( + { + mintAuthorityOption: 0, + mintAuthority: PublicKey.default, + supply: 10000n, + decimals, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + data, + 0 + ); + data.writeUint8(1, ACCOUNT_SIZE); // Account type (1): Mint = 1 + data.writeUint16LE(ExtensionType.TransferHook, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE); + data.writeUint16LE(TRANSFER_HOOK_SIZE, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE); + TransferHookLayout.encode( + { + authority: Keypair.generate().publicKey, + programId: transferHookProgramId, + }, + data, + ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE + ); + return { + data, + owner: TOKEN_2022_PROGRAM_ID, + executable: false, + lamports: 0, + }; + } + + // Mocked validate state + if (publicKey.equals(validateStatePubkey)) { + const extraAccountsList: ExtraAccountMetaList = { + count: extraAccounts.length, + extraAccounts, + }; + const instructionDiscriminator = Buffer.from([ + 105, 37, 101, 197, 75, 251, 102, 26, + ]).readBigUInt64LE(); + const data = Buffer.alloc(8 + 4 + 4 + ExtraAccountMetaLayout.span * extraAccounts.length); + ExtraAccountMetaAccountDataLayout.encode( + { + instructionDiscriminator, + length: 4 + ExtraAccountMetaLayout.span * extraAccounts.length, + extraAccountsList, + }, + data + ); + return { + data, + owner: transferHookProgramId, + executable: false, + lamports: 0, + }; + } + + return { + data: Buffer.from([]), + owner: PublicKey.default, + executable: false, + lamports: 0, + }; + }; + } + const addressConfig = (data: Uint8Array) => { const addressConfig = Buffer.alloc(32); addressConfig.set(data, 0); @@ -227,85 +335,43 @@ describe('transferHook', () => { }); it('can add extra account metas for execute', async () => { - const transferHookProgramId = Keypair.generate().publicKey; - - const sourcePubkey = Keypair.generate().publicKey; - const mintPubkey = Keypair.generate().publicKey; - const destinationPubkey = Keypair.generate().publicKey; - const authorityPubkey = Keypair.generate().publicKey; - const validateStatePubkey = getExtraAccountMetaAddress(mintPubkey, transferHookProgramId); - const amount = 100n; - const decimals = 0; - const extraMeta1Pubkey = Keypair.generate().publicKey; const extraMeta2Pubkey = Keypair.generate().publicKey; const extraMeta3Pubkey = Keypair.generate().publicKey; - const connection = await getConnection(); - connection.getAccountInfo = async function mockFetchAccountDataFn( - publicKey: PublicKey, - _commitmentOrConfig?: Parameters[1] - ): ReturnType { - // Mocked validate state - const extraAccounts: ExtraAccountMeta[] = [ - fixedAddress(extraMeta1Pubkey, false, false), - fixedAddress(extraMeta2Pubkey, false, false), - fixedAddress(extraMeta3Pubkey, false, false), - pda([ - 3, 0, // First seed: Account key at index 0 (2) - 3, 4, // Second seed: Account key at index 4 (2) - ], false, false), - pda([ - 3, 5, // First seed: Account key at index 5 (2) - 3, 6, // Second seed: Account key at index 6 (2) - ], false, false), - pda([ - 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - ], false, false), - ]; - const extraAccountsList: ExtraAccountMetaList = { - count: extraAccounts.length, - extraAccounts, - } - const instructionDiscriminator = Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]).readBigUInt64LE(); - const data = Buffer.alloc(8 + 4 + 4 + ExtraAccountMetaLayout.span * extraAccounts.length); - ExtraAccountMetaAccountDataLayout.encode({ - instructionDiscriminator, - length: 4 + ExtraAccountMetaLayout.span * extraAccounts.length, - extraAccountsList, - }, data); - return { - data, - owner: transferHookProgramId, - executable: false, - lamports: 0, - }; - }; - - const amountInLeBytes = Buffer.alloc(8); - amountInLeBytes.writeBigUInt64LE(amount); + // prettier-ignore + connection.getAccountInfo = createMockFetchAccountDataFn([ + fixedAddress(extraMeta1Pubkey, false, false), + fixedAddress(extraMeta2Pubkey, false, false), + fixedAddress(extraMeta3Pubkey, false, false), + pda([ + 3, 0, // First seed: Account key at index 0 (2) + 3, 4, // Second seed: Account key at index 4 (2) + ], false, false), + pda([ + 3, 5, // First seed: Account key at index 5 (2) + 3, 6, // Second seed: Account key at index 6 (2) + ], false, false), + pda([ + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + ], false, false), + ]); const extraMeta4Pubkey = PublicKey.findProgramAddressSync( - [ - sourcePubkey.toBuffer(), - validateStatePubkey.toBuffer(), - ], - transferHookProgramId, + [sourcePubkey.toBuffer(), validateStatePubkey.toBuffer()], + transferHookProgramId )[0]; const extraMeta5Pubkey = PublicKey.findProgramAddressSync( - [ - extraMeta1Pubkey.toBuffer(), - extraMeta2Pubkey.toBuffer(), - ], - transferHookProgramId, + [extraMeta1Pubkey.toBuffer(), extraMeta2Pubkey.toBuffer()], + transferHookProgramId )[0]; const extraMeta6Pubkey = PublicKey.findProgramAddressSync( [ - Buffer.from("prefix"), + Buffer.from('prefix'), amountInLeBytes, // Instruction data 8..16 ], - transferHookProgramId, + transferHookProgramId )[0]; // Fail missing key @@ -327,9 +393,9 @@ describe('transferHook', () => { mintPubkey, destinationPubkey, authorityPubkey, - amount, + amount ) - ).to.be.rejectedWith("Missing required account in instruction"); + ).to.be.rejectedWith('Missing required account in instruction'); const rawInstruction = new TransactionInstruction({ keys: [ @@ -349,7 +415,7 @@ describe('transferHook', () => { mintPubkey, destinationPubkey, authorityPubkey, - amount, + amount ); const checkMetas = [ @@ -371,146 +437,71 @@ describe('transferHook', () => { }); it('can create a transfer instruction with extra metas', async () => { - const transferHookProgramId = Keypair.generate().publicKey; - - const sourcePubkey = Keypair.generate().publicKey; - const mintPubkey = Keypair.generate().publicKey; - const destinationPubkey = Keypair.generate().publicKey; - const authorityPubkey = Keypair.generate().publicKey; - const validateStatePubkey = getExtraAccountMetaAddress(mintPubkey, transferHookProgramId); - const amount = 100n; - const decimals = 0; - - // Arbitrary program ID included to test external PDAs - const arbitraryProgramId = Keypair.generate().publicKey; - - const connection = await getConnection(); - connection.getAccountInfo = async function mockFetchAccountDataFn( - publicKey: PublicKey, - _commitmentOrConfig?: Parameters[1] - ): ReturnType { - // Mocked mint state - if (publicKey.equals(mintPubkey)) { - const data = Buffer.alloc(ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE + TRANSFER_HOOK_SIZE); - MintLayout.encode({ - mintAuthorityOption: 0, - mintAuthority: PublicKey.default, - supply: 10000n, - decimals, - isInitialized: true, - freezeAuthorityOption: 0, - freezeAuthority: PublicKey.default, - }, data, 0); - data.writeUint8(1, ACCOUNT_SIZE); // Account type (1): Mint = 1 - data.writeUint16LE(ExtensionType.TransferHook, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE); - data.writeUint16LE(TRANSFER_HOOK_SIZE, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE); - TransferHookLayout.encode({ - authority: Keypair.generate().publicKey, - programId: transferHookProgramId, - }, data, ACCOUNT_SIZE + ACCOUNT_TYPE_SIZE + TYPE_SIZE + LENGTH_SIZE); - return { - data, - owner: TOKEN_2022_PROGRAM_ID, - executable: false, - lamports: 0, - }; - } - - // Mocked validate state - if (publicKey.equals(validateStatePubkey)) { - const extraAccounts: ExtraAccountMeta[] = [ - pda([ - 3, 0, // First seed: Account key at index 0 (2) - 3, 1, // Second seed: Account key at index 1 (2) - ], false, false), - pda([ - 3, 4, // First seed: Account key at index 4 (2) - ], false, false), - pda([ - 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - ], false, false), - fixedAddress(arbitraryProgramId, false, false), - externalPda(8, [ - 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - 3, 6, // Third seed: Account key at index 6 (2) - ], false, false), - externalPda(8, [ - 1, 14, 97, 110, 111, 116, 104, 101, 114, 95, 112, 114, 101, 102, 105, - 120, // First seed: Literal "another_prefix" (16) - 2, 8, 8, // Second seed: Instruction data 8..16 (3) - 3, 6, // Third seed: Account key at index 6 (2) - 3, 9, // Fourth seed: Account key at index 9 (2) - ], false, false), - ]; - const extraAccountsList: ExtraAccountMetaList = { - count: extraAccounts.length, - extraAccounts, - } - const instructionDiscriminator = Buffer.from([105, 37, 101, 197, 75, 251, 102, 26]).readBigUInt64LE(); - const data = Buffer.alloc(8 + 4 + 4 + ExtraAccountMetaLayout.span * extraAccounts.length); - ExtraAccountMetaAccountDataLayout.encode({ - instructionDiscriminator, - length: 4 + ExtraAccountMetaLayout.span * extraAccounts.length, - extraAccountsList, - }, data); - return { - data, - owner: transferHookProgramId, - executable: false, - lamports: 0, - }; - } - - return { - data: Buffer.from([]), - owner: PublicKey.default, - executable: false, - lamports: 0, - }; - }; - - const amountInLeBytes = Buffer.alloc(8); - amountInLeBytes.writeBigUInt64LE(amount); + // prettier-ignore + connection.getAccountInfo = createMockFetchAccountDataFn([ + pda([ + 3, 0, // First seed: Account key at index 0 (2) + 3, 1, // Second seed: Account key at index 1 (2) + ], false, false), + pda([ + 3, 4, // First seed: Account key at index 4 (2) + ], false, false), + pda([ + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + ], false, false), + fixedAddress(arbitraryProgramId, false, false), + externalPda(8, [ + 1, 6, 112, 114, 101, 102, 105, 120, // First seed: Literal "prefix" (8) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + ], false, false), + externalPda(8, [ + 1, 14, 97, 110, 111, 116, 104, 101, 114, 95, 112, 114, 101, 102, 105, + 120, // First seed: Literal "another_prefix" (16) + 2, 8, 8, // Second seed: Instruction data 8..16 (3) + 3, 6, // Third seed: Account key at index 6 (2) + 3, 9, // Fourth seed: Account key at index 9 (2) + ], false, false), + ]); const extraMeta1Pubkey = PublicKey.findProgramAddressSync( [ sourcePubkey.toBuffer(), // Account key at index 0 - mintPubkey.toBuffer(), // Account key at index 1 + mintPubkey.toBuffer(), // Account key at index 1 ], - transferHookProgramId, + transferHookProgramId )[0]; const extraMeta2Pubkey = PublicKey.findProgramAddressSync( [ validateStatePubkey.toBuffer(), // Account key at index 4 ], - transferHookProgramId, + transferHookProgramId )[0]; const extraMeta3Pubkey = PublicKey.findProgramAddressSync( [ - Buffer.from("prefix"), + Buffer.from('prefix'), amountInLeBytes, // Instruction data 8..16 ], - transferHookProgramId, + transferHookProgramId )[0]; const extraMeta4Pubkey = arbitraryProgramId; const extraMeta5Pubkey = PublicKey.findProgramAddressSync( [ - Buffer.from("prefix"), + Buffer.from('prefix'), amountInLeBytes, // Instruction data 8..16 extraMeta2Pubkey.toBuffer(), ], - extraMeta4Pubkey, // PDA off of the arbitrary program ID + extraMeta4Pubkey // PDA off of the arbitrary program ID )[0]; const extraMeta6Pubkey = PublicKey.findProgramAddressSync( [ - Buffer.from("another_prefix"), + Buffer.from('another_prefix'), amountInLeBytes, // Instruction data 8..16 extraMeta2Pubkey.toBuffer(), extraMeta5Pubkey.toBuffer(), ], - extraMeta4Pubkey, // PDA off of the arbitrary program ID + extraMeta4Pubkey // PDA off of the arbitrary program ID )[0]; const instruction = await createTransferCheckedWithTransferHookInstruction( @@ -540,7 +531,7 @@ describe('transferHook', () => { { pubkey: transferHookProgramId, isSigner: false, isWritable: false }, { pubkey: validateStatePubkey, isSigner: false, isWritable: false }, ]; - + expect(instruction.keys).to.eql(checkMetas); }); }); From 367ae170f9b9a66ab1cfc6f277fb5dc078334c8e Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 11 Jan 2024 10:17:19 -0600 Subject: [PATCH 3/5] docs and parameter names --- .../extensions/transferHook/instructions.ts | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/token/js/src/extensions/transferHook/instructions.ts b/token/js/src/extensions/transferHook/instructions.ts index 41292d91b60..07bb80e2ed0 100644 --- a/token/js/src/extensions/transferHook/instructions.ts +++ b/token/js/src/extensions/transferHook/instructions.ts @@ -137,7 +137,7 @@ function deEscalateAccountMeta(accountMeta: AccountMeta, accountMetas: AccountMe } /** - * @deprecated Deprecated since v0.4.0. Please use {@link addExtraAccountMetasForExecute} instead. + * @deprecated Deprecated since v0.3.12. Please use {@link addExtraAccountMetasForExecute} instead. * * Add extra accounts needed for transfer hook to an instruction * @@ -193,7 +193,8 @@ export async function addExtraAccountsToInstruction( } /** - * Construct an `ExecuteInstruction` for a transfer hook program + * Construct an `ExecuteInstruction` for a transfer hook program, without the + * additional accounts * * @param programId The program ID of the transfer hook program * @param source The source account @@ -209,11 +210,11 @@ export function createExecuteInstruction( source: PublicKey, mint: PublicKey, destination: PublicKey, - authority: PublicKey, + owner: PublicKey, validateStatePubkey: PublicKey, amount: number | bigint ): TransactionInstruction { - const keys = [source, mint, destination, authority, validateStatePubkey].map((pubkey) => ({ + const keys = [source, mint, destination, owner, validateStatePubkey].map((pubkey) => ({ pubkey, isSigner: false, isWritable: false, @@ -235,7 +236,7 @@ export function createExecuteInstruction( * @param source The source account * @param mint The mint account * @param destination The destination account - * @param authority Owner of the source account + * @param owner Owner of the source account * @param amount The amount of tokens to transfer * @param commitment Commitment to use * @returns A new instruction with the extra account metas added @@ -247,7 +248,7 @@ export async function addExtraAccountMetasForExecute( source: PublicKey, mint: PublicKey, destination: PublicKey, - authority: PublicKey, + owner: PublicKey, amount: number | bigint, commitment?: Commitment ) { @@ -259,7 +260,7 @@ export async function addExtraAccountMetasForExecute( const validateStateData = getExtraAccountMetas(validateStateAccount); // Check to make sure the provided keys are in the instruction - if (![source, mint, destination, authority].every((key) => instruction.keys.some((meta) => meta.pubkey === key))) { + if (![source, mint, destination, owner].every((key) => instruction.keys.some((meta) => meta.pubkey === key))) { throw new Error('Missing required account in instruction'); } @@ -268,7 +269,7 @@ export async function addExtraAccountMetasForExecute( source, mint, destination, - authority, + owner, validateStatePubkey, amount ); @@ -305,7 +306,7 @@ export async function addExtraAccountMetasForExecute( * @param source Source account * @param mint Mint to update * @param destination Destination account - * @param authority Owner of the source account + * @param owner Owner of the source account * @param amount The amount of tokens to transfer * @param decimals Number of decimals in transfer amount * @param multiSigners The signer account(s) for a multisig @@ -319,7 +320,7 @@ export async function createTransferCheckedWithTransferHookInstruction( source: PublicKey, mint: PublicKey, destination: PublicKey, - authority: PublicKey, + owner: PublicKey, amount: bigint, decimals: number, multiSigners: (Signer | PublicKey)[] = [], @@ -330,7 +331,7 @@ export async function createTransferCheckedWithTransferHookInstruction( source, mint, destination, - authority, + owner, amount, decimals, multiSigners, @@ -348,7 +349,7 @@ export async function createTransferCheckedWithTransferHookInstruction( source, mint, destination, - authority, + owner, amount, commitment ) @@ -362,7 +363,7 @@ export async function createTransferCheckedWithTransferHookInstruction( * @param source Source account * @param mint Mint to update * @param destination Destination account - * @param authority Owner of the source account + * @param owner Owner of the source account * @param amount The amount of tokens to transfer * @param decimals Number of decimals in transfer amount * @param fee The calculated fee for the transfer fee extension @@ -377,7 +378,7 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction( source: PublicKey, mint: PublicKey, destination: PublicKey, - authority: PublicKey, + owner: PublicKey, amount: bigint, decimals: number, fee: bigint, @@ -389,7 +390,7 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction( source, mint, destination, - authority, + owner, amount, decimals, fee, @@ -408,7 +409,7 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction( source, mint, destination, - authority, + owner, amount, commitment ) From f847d4f04548e328dc4d3a75e990e3a2bf483977 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 11 Jan 2024 10:18:30 -0600 Subject: [PATCH 4/5] bigint for `createExecuteInstruction` --- token/js/src/extensions/transferHook/instructions.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/token/js/src/extensions/transferHook/instructions.ts b/token/js/src/extensions/transferHook/instructions.ts index 07bb80e2ed0..7ddb96fe37f 100644 --- a/token/js/src/extensions/transferHook/instructions.ts +++ b/token/js/src/extensions/transferHook/instructions.ts @@ -212,7 +212,7 @@ export function createExecuteInstruction( destination: PublicKey, owner: PublicKey, validateStatePubkey: PublicKey, - amount: number | bigint + amount: bigint ): TransactionInstruction { const keys = [source, mint, destination, owner, validateStatePubkey].map((pubkey) => ({ pubkey, @@ -271,7 +271,7 @@ export async function addExtraAccountMetasForExecute( destination, owner, validateStatePubkey, - amount + BigInt(amount) ); for (const extraAccountMeta of validateStateData) { From 07b2e6bf7b229658760bec4e73e033e851b53d03 Mon Sep 17 00:00:00 2001 From: Joe Date: Thu, 11 Jan 2024 10:24:53 -0600 Subject: [PATCH 5/5] make `addExtraAccountMetasForExecute` modify the instruction in-place --- .../extensions/transferHook/instructions.ts | 67 ++++++++++--------- token/js/test/unit/transferHook.test.ts | 8 +-- 2 files changed, 39 insertions(+), 36 deletions(-) diff --git a/token/js/src/extensions/transferHook/instructions.ts b/token/js/src/extensions/transferHook/instructions.ts index 7ddb96fe37f..1a5ffcfb669 100644 --- a/token/js/src/extensions/transferHook/instructions.ts +++ b/token/js/src/extensions/transferHook/instructions.ts @@ -228,7 +228,9 @@ export function createExecuteInstruction( } /** - * Adds all the extra accounts needed for a transfer hook to an instruction + * Adds all the extra accounts needed for a transfer hook to an instruction. + * + * Note this will modify the instruction passed in. * * @param connection Connection to use * @param instruction The instruction to add accounts to @@ -239,7 +241,6 @@ export function createExecuteInstruction( * @param owner Owner of the source account * @param amount The amount of tokens to transfer * @param commitment Commitment to use - * @returns A new instruction with the extra account metas added */ export async function addExtraAccountMetasForExecute( connection: Connection, @@ -295,8 +296,6 @@ export async function addExtraAccountMetasForExecute( // Add the transfer hook program ID and the validation state account instruction.keys.push({ pubkey: programId, isSigner: false, isWritable: false }); instruction.keys.push({ pubkey: validateStatePubkey, isSigner: false, isWritable: false }); - - return instruction; } /** @@ -327,7 +326,7 @@ export async function createTransferCheckedWithTransferHookInstruction( commitment?: Commitment, programId = TOKEN_PROGRAM_ID ) { - const rawInstruction = createTransferCheckedInstruction( + const instruction = createTransferCheckedInstruction( source, mint, destination, @@ -341,19 +340,21 @@ export async function createTransferCheckedWithTransferHookInstruction( const mintInfo = await getMint(connection, mint, commitment, programId); const transferHook = getTransferHook(mintInfo); - return transferHook - ? addExtraAccountMetasForExecute( - connection, - rawInstruction, - transferHook.programId, - source, - mint, - destination, - owner, - amount, - commitment - ) - : rawInstruction; + if (transferHook) { + await addExtraAccountMetasForExecute( + connection, + instruction, + transferHook.programId, + source, + mint, + destination, + owner, + amount, + commitment + ); + } + + return instruction; } /** @@ -386,7 +387,7 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction( commitment?: Commitment, programId = TOKEN_PROGRAM_ID ) { - const rawInstruction = createTransferCheckedWithFeeInstruction( + const instruction = createTransferCheckedWithFeeInstruction( source, mint, destination, @@ -401,17 +402,19 @@ export async function createTransferCheckedWithFeeAndTransferHookInstruction( const mintInfo = await getMint(connection, mint, commitment, programId); const transferHook = getTransferHook(mintInfo); - return transferHook - ? addExtraAccountMetasForExecute( - connection, - rawInstruction, - transferHook.programId, - source, - mint, - destination, - owner, - amount, - commitment - ) - : rawInstruction; + if (transferHook) { + await addExtraAccountMetasForExecute( + connection, + instruction, + transferHook.programId, + source, + mint, + destination, + owner, + amount, + commitment + ); + } + + return instruction; } diff --git a/token/js/test/unit/transferHook.test.ts b/token/js/test/unit/transferHook.test.ts index e6219f19c1a..7dc3dee30a6 100644 --- a/token/js/test/unit/transferHook.test.ts +++ b/token/js/test/unit/transferHook.test.ts @@ -397,7 +397,7 @@ describe('transferHook', () => { ) ).to.be.rejectedWith('Missing required account in instruction'); - const rawInstruction = new TransactionInstruction({ + const instruction = new TransactionInstruction({ keys: [ { pubkey: sourcePubkey, isSigner: false, isWritable: true }, { pubkey: mintPubkey, isSigner: false, isWritable: false }, @@ -407,9 +407,9 @@ describe('transferHook', () => { programId: transferHookProgramId, }); - const hydratedInstruction = await addExtraAccountMetasForExecute( + await addExtraAccountMetasForExecute( connection, - rawInstruction, + instruction, transferHookProgramId, sourcePubkey, mintPubkey, @@ -433,7 +433,7 @@ describe('transferHook', () => { { pubkey: validateStatePubkey, isSigner: false, isWritable: false }, ]; - expect(hydratedInstruction.keys).to.eql(checkMetas); + expect(instruction.keys).to.eql(checkMetas); }); it('can create a transfer instruction with extra metas', async () => {