Repository: solana-labs/solana-program-library
Pull request #6678 (2024-05-03 01:43:12)

[spl-token-2022-js]: Add SetTransferFee instruction
Problem
As pointed out at https://github.com/solana-labs/solana-program-library/issues/5451

Solution
Add SetTransferFee endpoint

Fixes https://github.com/solana-labs/solana-program-library/issues/5451


Patch:
diff --git a/token/js/src/extensions/transferFee/actions.ts b/token/js/src/extensions/transferFee/actions.ts
index 0a19b63ef..aaa67577c 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<TransactionSignature> {
+    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 b2db4b230..f2a74d85a 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<SetTransferFeeInstructionData>([
+    u8('instruction'),
+    u8('transferFeeInstruction'),
+    u16('transferFeeBasisPoints'),
+    u64('maximumFee'),
+]);
+
+/**
+ * Construct a SetTransferFeeInstruction instruction
+ *
+ * @param mint                      The token mint
+ * @param authority                 The authority of the transfer fee
+ * @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,
+        },
+    };
+}


Test patch:
diff --git a/token/js/test/e2e-2022/transferFee.test.ts b/token/js/test/e2e-2022/transferFee.test.ts
index 7a0944c13..e7315f653 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', () => {