diff --git a/token/js/src/extensions/transferFee/actions.ts b/token/js/src/extensions/transferFee/actions.ts index 0a19b63ef91..6a3ea68fac6 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,37 @@ 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); +} \ No newline at end of file diff --git a/token/js/src/extensions/transferFee/instructions.ts b/token/js/src/extensions/transferFee/instructions.ts index b2db4b230aa..78c31274aae 100644 --- a/token/js/src/extensions/transferFee/instructions.ts +++ b/token/js/src/extensions/transferFee/instructions.ts @@ -826,3 +826,166 @@ 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 InitializeTransferFeeConfig 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 + }, + }; +} \ No newline at end of file diff --git a/token/js/test/e2e-2022/transferFee.test.ts b/token/js/test/e2e-2022/transferFee.test.ts index 7a0944c13a6..b6d90cfb841 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'; @@ -32,7 +33,9 @@ const MINT_EXTENSIONS = [ExtensionType.TransferFeeConfig]; const MINT_AMOUNT = BigInt(1_000_000_000); const TRANSFER_AMOUNT = BigInt(1_000_000); const FEE_BASIS_POINTS = 100; +const UPDATED_FEE_BASIS_POINTS= 150; const MAX_FEE = BigInt(100_000); +const UPDATED_MAX_FEE = BigInt(150_000); const FEE = (TRANSFER_AMOUNT * BigInt(FEE_BASIS_POINTS)) / BigInt(10_000); describe('transferFee', () => { let connection: Connection; @@ -289,6 +292,29 @@ describe('transferFee', () => { expect(transferFeeConfig.withdrawWithheldAuthority).to.eql(PublicKey.default); } }); + it('setTransferFee', async () => { + 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', () => {