Skip to content

Commit

Permalink
Add SetTransferFee instruction
Browse files Browse the repository at this point in the history
  • Loading branch information
nasjuice committed May 3, 2024
1 parent 94d5a4f commit 079f88f
Show file tree
Hide file tree
Showing 3 changed files with 224 additions and 0 deletions.
35 changes: 35 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,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<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);
}
163 changes: 163 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,166 @@ 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 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
},
};
}
26 changes: 26 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 All @@ -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;
Expand Down Expand Up @@ -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', () => {
Expand Down

0 comments on commit 079f88f

Please sign in to comment.