Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[spl-token-2022-js]: Add SetTransferFee instruction #6678

Merged
merged 2 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading