From 3193d0c7ef118124da44a89da5032f46057530a1 Mon Sep 17 00:00:00 2001 From: Naasir Jusab Date: Thu, 2 May 2024 21:38:43 -0400 Subject: [PATCH 1/2] Add SetTransferFee instruction --- .../js/src/extensions/transferFee/actions.ts | 43 +++++ .../extensions/transferFee/instructions.ts | 155 ++++++++++++++++++ token/js/test/e2e-2022/transferFee.test.ts | 27 +++ 3 files changed, 225 insertions(+) diff --git a/token/js/src/extensions/transferFee/actions.ts b/token/js/src/extensions/transferFee/actions.ts index 0a19b63ef91..aaa67577c90 100644 --- a/token/js/src/extensions/transferFee/actions.ts +++ b/token/js/src/extensions/transferFee/actions.ts @@ -4,6 +4,7 @@ import { getSigners } from '../../actions/internal.js'; import { TOKEN_2022_PROGRAM_ID } from '../../constants.js'; import { createHarvestWithheldTokensToMintInstruction, + createSetTransferFeeInstruction, createTransferCheckedWithFeeInstruction, createWithdrawWithheldTokensFromAccountsInstruction, createWithdrawWithheldTokensFromMintInstruction, @@ -158,3 +159,45 @@ export async function harvestWithheldTokensToMint( return await sendAndConfirmTransaction(connection, transaction, [payer], confirmOptions); } + +/** + * Update transfer fee and maximum fee + * + * @param connection Connection to use + * @param payer Payer of the transaction fees + * @param mint The token mint + * @param authority The authority of the transfer fee + * @param multiSigners Signing accounts if `owner` is a multisig + * @param transferFeeBasisPoints Amount of transfer collected as fees, expressed as basis points of the transfer amount + * @param maximumFee Maximum fee assessed on transfers + * @param confirmOptions Options for confirming the transaction + * @param programId SPL Token program account + * + * @return Signature of the confirmed transaction + */ +export async function setTransferFee( + connection: Connection, + payer: Signer, + mint: PublicKey, + authority: Signer | PublicKey, + multiSigners: Signer[], + transferFeeBasisPoints: number, + maximumFee: bigint, + confirmOptions?: ConfirmOptions, + programId = TOKEN_2022_PROGRAM_ID +): Promise { + const [authorityPublicKey, signers] = getSigners(authority, multiSigners); + + const transaction = new Transaction().add( + createSetTransferFeeInstruction( + mint, + authorityPublicKey, + signers, + transferFeeBasisPoints, + maximumFee, + programId + ) + ); + + return await sendAndConfirmTransaction(connection, transaction, [payer, ...signers], confirmOptions); +} diff --git a/token/js/src/extensions/transferFee/instructions.ts b/token/js/src/extensions/transferFee/instructions.ts index b2db4b230aa..29f5f40a953 100644 --- a/token/js/src/extensions/transferFee/instructions.ts +++ b/token/js/src/extensions/transferFee/instructions.ts @@ -826,3 +826,158 @@ export function decodeHarvestWithheldTokensToMintInstructionUnchecked({ }, }; } + +// SetTransferFee + +export interface SetTransferFeeInstructionData { + instruction: TokenInstruction.TransferFeeExtension; + transferFeeInstruction: TransferFeeInstruction.SetTransferFee; + transferFeeBasisPoints: number; + maximumFee: bigint; +} + +export const setTransferFeeInstructionData = struct([ + u8('instruction'), + u8('transferFeeInstruction'), + u16('transferFeeBasisPoints'), + u64('maximumFee'), +]); + +/** + * Construct a SetTransferFeeInstruction instruction + * + * @param mint The token mint + * @param authority The source account's owner/delegate + * @param signers The signer account(s) + * @param transferFeeBasisPoints Amount of transfer collected as fees, expressed as basis points of the transfer amount + * @param maximumFee Maximum fee assessed on transfers + * @param programID SPL Token program account + * + * @return Instruction to add to a transaction + */ +export function createSetTransferFeeInstruction( + mint: PublicKey, + authority: PublicKey, + signers: (Signer | PublicKey)[], + transferFeeBasisPoints: number, + maximumFee: bigint, + programId = TOKEN_2022_PROGRAM_ID +): TransactionInstruction { + if (!programSupportsExtensions(programId)) { + throw new TokenUnsupportedInstructionError(); + } + + const data = Buffer.alloc(setTransferFeeInstructionData.span); + setTransferFeeInstructionData.encode( + { + instruction: TokenInstruction.TransferFeeExtension, + transferFeeInstruction: TransferFeeInstruction.SetTransferFee, + transferFeeBasisPoints: transferFeeBasisPoints, + maximumFee: maximumFee, + }, + data + ); + const keys = addSigners([{ pubkey: mint, isSigner: false, isWritable: true }], authority, signers); + + return new TransactionInstruction({ keys, programId, data }); +} + +/** A decoded, valid SetTransferFee instruction */ +export interface DecodedSetTransferFeeInstruction { + programId: PublicKey; + keys: { + mint: AccountMeta; + authority: AccountMeta; + signers: AccountMeta[] | null; + }; + data: { + instruction: TokenInstruction.TransferFeeExtension; + transferFeeInstruction: TransferFeeInstruction.SetTransferFee; + transferFeeBasisPoints: number; + maximumFee: bigint; + }; +} + +/** + * Decode an SetTransferFee instruction and validate it + * + * @param instruction Transaction instruction to decode + * @param programId SPL Token program account + * + * @return Decoded, valid instruction + */ +export function decodeSetTransferFeeInstruction( + instruction: TransactionInstruction, + programId: PublicKey +): DecodedSetTransferFeeInstruction { + if (!instruction.programId.equals(programId)) throw new TokenInvalidInstructionProgramError(); + if (instruction.data.length !== setTransferFeeInstructionData.span) throw new TokenInvalidInstructionDataError(); + + const { + keys: { mint, authority, signers }, + data, + } = decodeSetTransferFeeInstructionUnchecked(instruction); + if ( + data.instruction !== TokenInstruction.TransferFeeExtension || + data.transferFeeInstruction !== TransferFeeInstruction.SetTransferFee + ) + throw new TokenInvalidInstructionTypeError(); + if (!mint) throw new TokenInvalidInstructionKeysError(); + + return { + programId, + keys: { + mint, + authority, + signers: signers ? signers : null, + }, + data, + }; +} + +/** A decoded, valid SetTransferFee instruction */ +export interface DecodedSetTransferFeeInstructionUnchecked { + programId: PublicKey; + keys: { + mint: AccountMeta; + authority: AccountMeta; + signers: AccountMeta[] | undefined; + }; + data: { + instruction: TokenInstruction.TransferFeeExtension; + transferFeeInstruction: TransferFeeInstruction.SetTransferFee; + transferFeeBasisPoints: number; + maximumFee: bigint; + }; +} + +/** + * Decode a SetTransferFee instruction without validating it + * + * @param instruction Transaction instruction to decode + * + * @return Decoded, non-validated instruction + */ +export function decodeSetTransferFeeInstructionUnchecked({ + programId, + keys: [mint, authority, ...signers], + data, +}: TransactionInstruction): DecodedSetTransferFeeInstructionUnchecked { + const { instruction, transferFeeInstruction, transferFeeBasisPoints, maximumFee } = + setTransferFeeInstructionData.decode(data); + + return { + programId, + keys: { + mint, + authority, + signers, + }, + data: { + instruction, + transferFeeInstruction, + transferFeeBasisPoints, + maximumFee, + }, + }; +} diff --git a/token/js/test/e2e-2022/transferFee.test.ts b/token/js/test/e2e-2022/transferFee.test.ts index 7a0944c13a6..e7315f6538f 100644 --- a/token/js/test/e2e-2022/transferFee.test.ts +++ b/token/js/test/e2e-2022/transferFee.test.ts @@ -24,6 +24,7 @@ import { transferCheckedWithFee, withdrawWithheldTokensFromAccounts, withdrawWithheldTokensFromMint, + setTransferFee, } from '../../src/extensions/transferFee/index'; import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common'; @@ -289,6 +290,32 @@ describe('transferFee', () => { expect(transferFeeConfig.withdrawWithheldAuthority).to.eql(PublicKey.default); } }); + it('setTransferFee', async () => { + const UPDATED_FEE_BASIS_POINTS = 150; + const UPDATED_MAX_FEE = BigInt(150_000); + + await setTransferFee( + connection, + payer, + mint, + transferFeeConfigAuthority, + [], + UPDATED_FEE_BASIS_POINTS, + UPDATED_MAX_FEE, + undefined, + TEST_PROGRAM_ID + ); + const mintInfo = await getMint(connection, mint, undefined, TEST_PROGRAM_ID); + const transferFeeConfig = getTransferFeeConfig(mintInfo); + expect(transferFeeConfig).to.not.be.null; + if (transferFeeConfig !== null) { + expect(transferFeeConfig.transferFeeConfigAuthority).to.eql(transferFeeConfigAuthority.publicKey); + expect(transferFeeConfig.olderTransferFee.transferFeeBasisPoints).to.eql(FEE_BASIS_POINTS); + expect(transferFeeConfig.olderTransferFee.maximumFee).to.eql(MAX_FEE); + expect(transferFeeConfig.newerTransferFee.transferFeeBasisPoints).to.eql(UPDATED_FEE_BASIS_POINTS); + expect(transferFeeConfig.newerTransferFee.maximumFee).to.eql(UPDATED_MAX_FEE); + } + }); }); describe('with null authorities', () => { From c6e36a34bf6f353217168320cbbd3281441a2e53 Mon Sep 17 00:00:00 2001 From: Joe C Date: Fri, 3 May 2024 23:17:10 -0500 Subject: [PATCH 2/2] Update token/js/src/extensions/transferFee/instructions.ts --- token/js/src/extensions/transferFee/instructions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/token/js/src/extensions/transferFee/instructions.ts b/token/js/src/extensions/transferFee/instructions.ts index 29f5f40a953..f2a74d85a7c 100644 --- a/token/js/src/extensions/transferFee/instructions.ts +++ b/token/js/src/extensions/transferFee/instructions.ts @@ -847,7 +847,7 @@ export const setTransferFeeInstructionData = struct