Skip to content

Commit

Permalink
[spl-token-2022-js]: Add SetTransferFee instruction (#6678)
Browse files Browse the repository at this point in the history
* Add SetTransferFee instruction

* Update token/js/src/extensions/transferFee/instructions.ts

---------

Co-authored-by: Joe C <[email protected]>
  • Loading branch information
nasjuice and buffalojoec authored May 4, 2024
1 parent d9d9ddf commit f0b0ced
Show file tree
Hide file tree
Showing 3 changed files with 225 additions and 0 deletions.
43 changes: 43 additions & 0 deletions token/js/src/extensions/transferFee/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { getSigners } from '../../actions/internal.js';
import { TOKEN_2022_PROGRAM_ID } from '../../constants.js';
import {
createHarvestWithheldTokensToMintInstruction,
createSetTransferFeeInstruction,
createTransferCheckedWithFeeInstruction,
createWithdrawWithheldTokensFromAccountsInstruction,
createWithdrawWithheldTokensFromMintInstruction,
Expand Down Expand Up @@ -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);
}
155 changes: 155 additions & 0 deletions token/js/src/extensions/transferFee/instructions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
};
}
27 changes: 27 additions & 0 deletions token/js/test/e2e-2022/transferFee.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
transferCheckedWithFee,
withdrawWithheldTokensFromAccounts,
withdrawWithheldTokensFromMint,
setTransferFee,
} from '../../src/extensions/transferFee/index';

import { TEST_PROGRAM_ID, newAccountWithLamports, getConnection } from '../common';
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit f0b0ced

Please sign in to comment.