전송 수수료

전송 수수료란 무엇인가요?

Token Extensions Program의 TransferFeeConfig 민트 확장은 해당 민트의 각 전송에 수수료를 적용합니다.

전송 시점에:

  • 전송 금액이 구성된 수수료만큼 감소됩니다
  • 보류된 수수료가 대상 토큰 계정에 추적됩니다
  • 보류된 수수료는 토큰 계정에서 직접 인출하거나 민트로 수집할 수 있습니다
  • withdraw_withheld_authority는 수집된 수수료를 수수료 수령자 토큰 계정으로 이동시킬 수 있습니다

토큰 계정 확장

_rsTransferFeeConfig_가 있는 민트에 대해 토큰 계정이 초기화되면, 토큰 계정은 _rsTransferFeeAmount_와 함께 초기화됩니다. Associated Token Program을 통해 토큰 계정이 생성되면, 필요한 계정 크기가 계산되고 토큰 계정은 필요한 크기와 rent 면제 lamport로 생성됩니다.

전송 수수료 민트를 생성하는 방법

전송 수수료 민트를 생성하려면:

  1. 민트와 TransferFeeConfig 확장에 필요한 민트 계정 크기와 rent를 계산합니다.
  2. *rsCreateAccount*로 민트 계정을 생성하고, *rsTransferFeeConfig*를 초기화한 다음, *rsInitializeMint*로 민트를 초기화합니다.
  3. 민트에 대한 토큰 계정을 생성합니다. *rsTransferFeeAmount*는 토큰 계정에 대해 자동으로 활성화됩니다.
  4. *rsTransferCheckedWithFee*를 사용하여 토큰을 전송하고 예상 수수료를 확인합니다. *rsTransferChecked*도 작동하며 대상 토큰 계정에서 구성된 수수료를 자동으로 보류합니다.
  5. *rsWithdrawWithheldTokensFromAccounts*를 사용하여 토큰 계정에서 수수료 수령자 토큰 계정으로 보류된 수수료를 직접 이동시킵니다. 이는 민트의 withdraw_withheld_authority가 서명합니다.
  6. 권한이 필요 없는 *rsHarvestWithheldTokensToMint*를 사용하여 토큰 계정에서 민트 계정으로 보류된 수수료를 이동시킨 다음, *rsWithdrawWithheldTokensFromMint*를 사용하여 민트 계정의 보류된 수수료를 수수료 수령자 토큰 계정으로 인출합니다. 이는 민트의 withdraw_withheld_authority가 서명합니다.
  7. *rsSetTransferFee*를 사용하여 다음 전송 수수료 구성을 업데이트합니다. 이는 두 epoch 이후부터 적용됩니다.

계정 크기 계산

기본 민트에 대한 민트 계정 크기와 TransferFeeConfig 확장을 계산합니다. 이는 *rsCreateAccount*에서 사용되는 크기입니다.

rent 계산

민트에 필요한 크기와 TransferFeeConfig 확장을 사용하여 rent를 계산합니다.

민트 계정 생성

계산된 공간과 lamport로 민트 계정을 생성합니다.

TransferFeeConfig 초기화

민트에서 TransferFeeConfig 확장을 초기화합니다.

민트 초기화

동일한 트랜잭션에서 *rsInitializeMint*로 민트를 초기화합니다.

토큰 계정 생성 및 토큰 발행

소스, 수신자 및 수수료 수령자를 위한 token account를 생성한 다음 소스 token account에 토큰을 발행합니다.

TransferCheckedWithFee로 전송

*rsTransferCheckedWithFee*로 토큰을 전송하고 예상 수수료를 확인합니다.

TransferChecked로 전송

*rsTransferChecked*로 토큰을 전송합니다. 설정된 수수료는 여전히 대상 token account에서 원천징수됩니다.

token account에서 원천징수된 수수료 인출

*rsWithdrawWithheldTokensFromAccounts*를 사용하여 token account에서 수수료 수령자 token account로 원천징수된 수수료를 직접 이동합니다.

mint로 원천징수된 수수료 수집

*rsHarvestWithheldTokensToMint*를 사용하여 token account에서 mint account의 원천징수 누적기로 원천징수된 수수료를 이동합니다.

mint에서 원천징수된 수수료 인출

*rsWithdrawWithheldTokensFromMint*를 사용하여 mint account에서 수수료 수령자 token account로 수집된 수수료를 이동합니다.

다음 전송 수수료 업데이트

*rsSetTransferFee*를 사용하여 다음 전송 수수료 구성을 업데이트합니다.

계정 크기 계산

기본 민트에 대한 민트 계정 크기와 TransferFeeConfig 확장을 계산합니다. 이는 *rsCreateAccount*에서 사용되는 크기입니다.

rent 계산

민트에 필요한 크기와 TransferFeeConfig 확장을 사용하여 rent를 계산합니다.

민트 계정 생성

계산된 공간과 lamport로 민트 계정을 생성합니다.

TransferFeeConfig 초기화

민트에서 TransferFeeConfig 확장을 초기화합니다.

민트 초기화

동일한 트랜잭션에서 *rsInitializeMint*로 민트를 초기화합니다.

토큰 계정 생성 및 토큰 발행

소스, 수신자 및 수수료 수령자를 위한 token account를 생성한 다음 소스 token account에 토큰을 발행합니다.

TransferCheckedWithFee로 전송

*rsTransferCheckedWithFee*로 토큰을 전송하고 예상 수수료를 확인합니다.

TransferChecked로 전송

*rsTransferChecked*로 토큰을 전송합니다. 설정된 수수료는 여전히 대상 token account에서 원천징수됩니다.

token account에서 원천징수된 수수료 인출

*rsWithdrawWithheldTokensFromAccounts*를 사용하여 token account에서 수수료 수령자 token account로 원천징수된 수수료를 직접 이동합니다.

mint로 원천징수된 수수료 수집

*rsHarvestWithheldTokensToMint*를 사용하여 token account에서 mint account의 원천징수 누적기로 원천징수된 수수료를 이동합니다.

mint에서 원천징수된 수수료 인출

*rsWithdrawWithheldTokensFromMint*를 사용하여 mint account에서 수수료 수령자 token account로 수집된 수수료를 이동합니다.

다음 전송 수수료 업데이트

*rsSetTransferFee*를 사용하여 다음 전송 수수료 구성을 업데이트합니다.

Example
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));

명령어 순서

_rsInitializeTransferFeeConfig_는 _rsInitializeMint_보다 먼저 실행되어야 합니다. CreateAccount, InitializeTransferFeeConfig, 그리고 _rsInitializeMint_는 동일한 트랜잭션에 포함되어야 합니다.

소스 참조

항목설명소스
TransferFeeConfig수수료 권한, 누적된 보류 금액, 이전 및 최신 전송 수수료 설정을 저장하는 민트 확장입니다.소스
TransferFee*rsTransferFeeConfig*에 저장되는 수수료 설정 구조체로, 수수료가 적용되는 에포크, 최대 수수료, 베이시스 포인트 단위 수수료를 정의합니다.소스
TransferFeeAmount토큰 계정의 전송 중 보류된 금액을 저장하는 토큰 계정 확장입니다.소스
TransferFeeInstruction::InitializeTransferFeeConfigInitializeMint 이전에 전송 수수료 설정을 초기화하는 명령어입니다.소스
TransferFeeInstruction::TransferCheckedWithFee호출자가 제공한 수수료가 민트의 현재 전송 수수료 설정과 일치하는지 확인하는 전송 명령어입니다.소스
TransferFeeInstruction::WithdrawWithheldTokensFromAccounts민트의 withdraw_withheld_authority가 서명하여 토큰 계정에서 수수료 수령자 토큰 계정으로 보류된 수수료를 직접 이동하는 명령어입니다.소스
TransferFeeInstruction::HarvestWithheldTokensToMint토큰 계정에서 민트 계정의 보류 누적기로 보류된 수수료를 이동하는 권한 없이 실행 가능한 명령어입니다.소스
TransferFeeInstruction::WithdrawWithheldTokensFromMint민트의 withdraw_withheld_authority가 서명하여 민트 계정의 보류된 수수료를 수수료 수령자 토큰 계정으로 인출하는 명령어입니다.소스
TransferFeeInstruction::SetTransferFee두 에포크 후부터 적용될 최신 전송 수수료 설정을 업데이트하는 명령어입니다.소스
process_initialize_transfer_fee_config초기화되지 않은 민트에서 TransferFeeConfig 확장을 초기화하고 이전 및 최신 전송 수수료 설정을 모두 초기 수수료로 설정하는 프로세서 로직입니다.소스
process_withdraw_withheld_tokens_from_accountswithdraw_withheld_authority를 검증하고 토큰 계정에서 대상 토큰 계정으로 보류된 수수료를 이동하는 프로세서 로직입니다.소스
process_harvest_withheld_tokens_to_mint토큰 계정에서 민트 계정의 보류 누적기로 보류된 수수료를 수집하는 프로세서 로직입니다.소스
process_withdraw_withheld_tokens_from_mintwithdraw_withheld_authority를 검증하고 민트 계정의 보류 금액을 대상 토큰 계정으로 이동하는 프로세서 로직입니다.소스
process_set_transfer_fee전송 수수료 설정 권한을 검증하고 두 에포크 후부터 적용될 최신 전송 수수료 설정을 업데이트하는 프로세서 로직입니다.소스
get_required_init_account_extensions민트에 활성화된 확장을 기반으로 토큰 계정이 초기화될 때 토큰 계정 확장을 자동으로 추가하는 데 사용됩니다. TransferFeeConfig 민트의 경우 *rsTransferFeeAmount*를 추가합니다.소스

Typescript

아래 Kit 예제는 명시적인 Token-2022 명령어를 사용합니다. @solana/web3.js@solana/spl-token를 사용하는 레거시 예제는 참고용으로 포함되어 있습니다.

Kit

Instructions
import {
lamports,
createClient,
generateKeyPairSigner,
unwrapOption
} from "@solana/kit";
import { solanaRpc, rpcAirdrop } from "@solana/kit-plugin-rpc";
import { generatedPayer, airdropPayer } from "@solana/kit-plugin-signer";
import { getCreateAccountInstruction } from "@solana-program/system";
import {
extension,
fetchMint,
fetchToken,
findAssociatedTokenPda,
getCreateAssociatedTokenInstructionAsync,
getHarvestWithheldTokensToMintInstruction,
getInitializeMintInstruction,
getInitializeTransferFeeConfigInstruction,
getMintSize,
getMintToCheckedInstruction,
getSetTransferFeeInstruction,
getTransferCheckedWithFeeInstruction,
getWithdrawWithheldTokensFromAccountsInstruction,
getWithdrawWithheldTokensFromMintInstruction,
isExtension,
TOKEN_2022_PROGRAM_ADDRESS
} from "@solana-program/token-2022";
const client = await createClient()
.use(generatedPayer())
.use(
solanaRpc({
rpcUrl: "http://localhost:8899",
rpcSubscriptionsUrl: "ws://localhost:8900"
})
)
.use(rpcAirdrop())
.use(airdropPayer(lamports(1_000_000_000n)));
const mint = await generateKeyPairSigner();
const recipientA = await generateKeyPairSigner();
const recipientB = await generateKeyPairSigner();
const feeReceiver = await generateKeyPairSigner();
const transferFeeConfigExtension = extension("TransferFeeConfig", {
transferFeeConfigAuthority: client.payer.address,
withdrawWithheldAuthority: client.payer.address,
withheldAmount: 0n,
olderTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
},
newerTransferFee: {
epoch: 0n,
maximumFee: 10n,
transferFeeBasisPoints: 150
}
});
const mintSpace = BigInt(getMintSize([transferFeeConfigExtension]));
const mintRent = await client.rpc
.getMinimumBalanceForRentExemption(mintSpace)
.send();
await client.sendTransaction([
getCreateAccountInstruction({
payer: client.payer, // Account funding account creation.
newAccount: mint, // New mint account to create.
lamports: mintRent, // Lamports funding the mint account rent.
space: mintSpace, // Account size in bytes for the mint plus TransferFeeConfig.
programAddress: TOKEN_2022_PROGRAM_ADDRESS // Program that owns the mint account.
}),
getInitializeTransferFeeConfigInstruction({
mint: mint.address, // Mint account that stores the TransferFeeConfig extension.
transferFeeConfigAuthority: client.payer.address, // Authority allowed to update the transfer fee later.
withdrawWithheldAuthority: client.payer.address, // Value stored in the mint's `withdraw_withheld_authority` field.
transferFeeBasisPoints: 150, // Transfer fee in basis points.
maximumFee: 10n // Maximum fee charged on each transfer.
}),
getInitializeMintInstruction({
mint: mint.address, // Mint account to initialize.
decimals: 2, // Number of decimals for the token.
mintAuthority: client.payer.address, // Authority allowed to mint new tokens.
freezeAuthority: client.payer.address // Authority allowed to freeze token accounts.
})
]);
const [sourceToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: client.payer.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationAToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientA.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [destinationBToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: recipientB.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
const [feeReceiverToken] = await findAssociatedTokenPda({
mint: mint.address,
owner: feeReceiver.address,
tokenProgram: TOKEN_2022_PROGRAM_ADDRESS
});
await client.sendTransaction([
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: client.payer.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientA.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: recipientB.address
}),
await getCreateAssociatedTokenInstructionAsync({
payer: client.payer,
mint: mint.address,
owner: feeReceiver.address
}),
getMintToCheckedInstruction({
mint: mint.address,
token: sourceToken,
mintAuthority: client.payer,
amount: 1_000n,
decimals: 2
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken, // Token account sending the transfer.
mint: mint.address, // Mint with the transfer fee configuration.
destination: destinationAToken, // Token account receiving the transfer.
authority: client.payer, // Signer approving the transfer.
amount: 200n, // Token amount in base units.
decimals: 2, // Decimals defined on the mint.
fee: 3n // Expected transfer fee for this transfer.
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromAccountsInstruction({
mint: mint.address, // Mint with the transfer fee configuration.
feeReceiver: feeReceiverToken, // Token account receiving the withdrawn fees.
withdrawWithheldAuthority: client.payer, // Signer matching the mint's `withdraw_withheld_authority`.
numTokenAccounts: 1, // Number of token accounts listed in `sources`.
sources: [destinationAToken] // Token accounts to withdraw withheld fees from.
})
]);
await client.sendTransaction([
getTransferCheckedWithFeeInstruction({
source: sourceToken, // Token account sending the transfer.
mint: mint.address, // Mint with the transfer fee configuration.
destination: destinationBToken, // Token account receiving the transfer.
authority: client.payer, // Signer approving the transfer.
amount: 200n, // Token amount in base units.
decimals: 2, // Decimals defined on the mint.
fee: 3n // Expected transfer fee for this transfer.
})
]);
await client.sendTransaction([
getHarvestWithheldTokensToMintInstruction({
mint: mint.address, // Mint that collects harvested withheld fees.
sources: [destinationBToken] // Token accounts to harvest withheld fees from.
})
]);
await client.sendTransaction([
getWithdrawWithheldTokensFromMintInstruction({
mint: mint.address, // Mint storing harvested withheld fees.
feeReceiver: feeReceiverToken, // Token account receiving withdrawn fees.
withdrawWithheldAuthority: client.payer // Signer matching the mint's `withdraw_withheld_authority`.
}),
getSetTransferFeeInstruction({
mint: mint.address, // Mint whose next transfer fee configuration is updated.
transferFeeConfigAuthority: client.payer, // Signer authorized to update the transfer fee later.
transferFeeBasisPoints: 250, // New transfer fee in basis points.
maximumFee: 25n // New maximum fee for the next transfer fee configuration.
})
]);
const destinationAAccount = await fetchToken(client.rpc, destinationAToken);
const destinationBAccount = await fetchToken(client.rpc, destinationBToken);
const feeReceiverAccount = await fetchToken(client.rpc, feeReceiverToken);
const mintAccount = await fetchMint(client.rpc, mint.address);
const transferFeeConfig = (
unwrapOption(mintAccount.data.extensions) ?? []
).find((item) => isExtension("TransferFeeConfig", item));
console.log("Mint Address:", mint.address);
console.dir(
{
destinationA: {
amount: destinationAAccount.data.amount,
extensions: destinationAAccount.data.extensions
},
destinationB: {
amount: destinationBAccount.data.amount,
extensions: destinationBAccount.data.extensions
},
feeReceiver: {
amount: feeReceiverAccount.data.amount,
extensions: feeReceiverAccount.data.extensions
},
transferFeeConfig
},
{ depth: null }
);
Console
Click to execute the code.

Web3.js

Instructions
import {
Connection,
Keypair,
LAMPORTS_PER_SOL,
sendAndConfirmTransaction,
SystemProgram,
Transaction
} from "@solana/web3.js";
import {
ASSOCIATED_TOKEN_PROGRAM_ID,
ExtensionType,
createAssociatedTokenAccountInstruction,
createHarvestWithheldTokensToMintInstruction,
createInitializeMintInstruction,
createInitializeTransferFeeConfigInstruction,
createMintToCheckedInstruction,
createSetTransferFeeInstruction,
createTransferCheckedWithFeeInstruction,
createWithdrawWithheldTokensFromAccountsInstruction,
createWithdrawWithheldTokensFromMintInstruction,
getAccount,
getAssociatedTokenAddressSync,
getMint,
getMintLen,
getTransferFeeAmount,
getTransferFeeConfig,
TOKEN_2022_PROGRAM_ID
} from "@solana/spl-token";
const connection = new Connection("http://localhost:8899", "confirmed");
const latestBlockhash = await connection.getLatestBlockhash();
const feePayer = Keypair.generate();
const recipientA = Keypair.generate();
const recipientB = Keypair.generate();
const feeReceiver = Keypair.generate();
const airdropSignature = await connection.requestAirdrop(
feePayer.publicKey,
5 * LAMPORTS_PER_SOL
);
await connection.confirmTransaction({
blockhash: latestBlockhash.blockhash,
lastValidBlockHeight: latestBlockhash.lastValidBlockHeight,
signature: airdropSignature
});
const mint = Keypair.generate();
const transferFeeConfigExtensions = [ExtensionType.TransferFeeConfig];
const mintLen = getMintLen(transferFeeConfigExtensions);
const mintRent = await connection.getMinimumBalanceForRentExemption(mintLen);
const sourceToken = getAssociatedTokenAddressSync(
mint.publicKey,
feePayer.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const destinationAToken = getAssociatedTokenAddressSync(
mint.publicKey,
recipientA.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const destinationBToken = getAssociatedTokenAddressSync(
mint.publicKey,
recipientB.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
const feeReceiverToken = getAssociatedTokenAddressSync(
mint.publicKey,
feeReceiver.publicKey,
false,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
SystemProgram.createAccount({
fromPubkey: feePayer.publicKey, // Account funding account creation.
newAccountPubkey: mint.publicKey, // New mint account to create.
lamports: mintRent, // Lamports funding the mint account rent.
space: mintLen, // Account size in bytes for the mint plus TransferFeeConfig.
programId: TOKEN_2022_PROGRAM_ID // Program that owns the mint account.
}),
createInitializeTransferFeeConfigInstruction(
mint.publicKey, // Mint account that stores the TransferFeeConfig extension.
feePayer.publicKey, // Authority allowed to update the transfer fee later.
feePayer.publicKey, // Value stored in the mint's `withdraw_withheld_authority` field.
150, // Transfer fee in basis points.
10n, // Maximum fee charged on each transfer.
TOKEN_2022_PROGRAM_ID // Token program that owns the mint.
),
createInitializeMintInstruction(
mint.publicKey, // Mint account to initialize.
2, // Number of decimals for the token.
feePayer.publicKey, // Authority allowed to mint new tokens.
feePayer.publicKey, // Authority allowed to freeze token accounts.
TOKEN_2022_PROGRAM_ID // Program that owns the mint account.
)
),
[feePayer, mint],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
sourceToken,
feePayer.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
destinationAToken,
recipientA.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
destinationBToken,
recipientB.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createAssociatedTokenAccountInstruction(
feePayer.publicKey,
feeReceiverToken,
feeReceiver.publicKey,
mint.publicKey,
TOKEN_2022_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
),
createMintToCheckedInstruction(
mint.publicKey,
sourceToken,
feePayer.publicKey,
1_000,
2,
[],
TOKEN_2022_PROGRAM_ID
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createTransferCheckedWithFeeInstruction(
sourceToken, // Token account sending the transfer.
mint.publicKey, // Mint with the transfer fee configuration.
destinationAToken, // Token account receiving the transfer.
feePayer.publicKey, // Signer approving the transfer.
200n, // Token amount in base units.
2, // Decimals defined on the mint.
3n, // Expected transfer fee for this transfer.
[], // Additional multisig signers.
TOKEN_2022_PROGRAM_ID // Token program that processes the transfer.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createWithdrawWithheldTokensFromAccountsInstruction(
mint.publicKey, // Mint with the transfer fee configuration.
feeReceiverToken, // Token account receiving the withdrawn fees.
feePayer.publicKey, // Signer matching the mint's `withdraw_withheld_authority`.
[], // Additional multisig signers.
[destinationAToken], // Token accounts to withdraw withheld fees from.
TOKEN_2022_PROGRAM_ID // Token program that processes the withdraw.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createTransferCheckedWithFeeInstruction(
sourceToken, // Token account sending the transfer.
mint.publicKey, // Mint with the transfer fee configuration.
destinationBToken, // Token account receiving the transfer.
feePayer.publicKey, // Signer approving the transfer.
200n, // Token amount in base units.
2, // Decimals defined on the mint.
3n, // Expected transfer fee for this transfer.
[], // Additional multisig signers.
TOKEN_2022_PROGRAM_ID // Token program that processes the transfer.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createHarvestWithheldTokensToMintInstruction(
mint.publicKey, // Mint that collects harvested withheld fees.
[destinationBToken], // Token accounts to harvest withheld fees from.
TOKEN_2022_PROGRAM_ID // Token program that processes the harvest.
)
),
[feePayer],
{ commitment: "confirmed" }
);
await sendAndConfirmTransaction(
connection,
new Transaction().add(
createWithdrawWithheldTokensFromMintInstruction(
mint.publicKey, // Mint storing harvested withheld fees.
feeReceiverToken, // Token account receiving withdrawn fees.
feePayer.publicKey, // Signer matching the mint's `withdraw_withheld_authority`.
[], // Additional multisig signers.
TOKEN_2022_PROGRAM_ID // Token program that processes the withdraw.
),
createSetTransferFeeInstruction(
mint.publicKey, // Mint whose next transfer fee configuration is updated.
feePayer.publicKey, // Authority allowed to update the transfer fee later.
[], // Additional multisig signers.
250, // New transfer fee in basis points.
25n, // New maximum fee for the next transfer fee configuration.
TOKEN_2022_PROGRAM_ID // Token program that owns the mint.
)
),
[feePayer],
{ commitment: "confirmed" }
);
const destinationAAccount = await getAccount(
connection,
destinationAToken,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
const destinationBAccount = await getAccount(
connection,
destinationBToken,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
const feeReceiverAccount = await getAccount(
connection,
feeReceiverToken,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
const mintAccount = await getMint(
connection,
mint.publicKey,
"confirmed",
TOKEN_2022_PROGRAM_ID
);
console.log("Mint Address:", mint.publicKey.toBase58());
console.log("Destination A Amount:", destinationAAccount.amount.toString());
console.log(
"Destination A Transfer Fee Amount:",
getTransferFeeAmount(destinationAAccount)
);
console.log("Destination B Amount:", destinationBAccount.amount.toString());
console.log(
"Destination B Transfer Fee Amount:",
getTransferFeeAmount(destinationBAccount)
);
console.log("Fee Receiver Amount:", feeReceiverAccount.amount.toString());
console.log(
"Fee Receiver Transfer Fee Amount:",
getTransferFeeAmount(feeReceiverAccount)
);
console.log("Transfer Fee Config:", getTransferFeeConfig(mintAccount));
Console
Click to execute the code.

Rust

Rust
use anyhow::Result;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_commitment_config::CommitmentConfig;
use solana_sdk::{
signature::{Keypair, Signer},
transaction::Transaction,
};
use solana_system_interface::instruction::create_account;
use spl_associated_token_account_interface::{
address::get_associated_token_address_with_program_id,
instruction::create_associated_token_account,
};
use spl_token_2022_interface::{
extension::{
transfer_fee::{
instruction::{
harvest_withheld_tokens_to_mint, initialize_transfer_fee_config,
set_transfer_fee, transfer_checked_with_fee,
withdraw_withheld_tokens_from_accounts, withdraw_withheld_tokens_from_mint,
},
TransferFeeAmount, TransferFeeConfig,
},
BaseStateWithExtensions, ExtensionType, StateWithExtensions,
},
instruction::{initialize_mint, mint_to_checked},
state::{Account, Mint},
ID as TOKEN_2022_PROGRAM_ID,
};
#[tokio::main]
async fn main() -> Result<()> {
let client = RpcClient::new_with_commitment(
String::from("http://localhost:8899"),
CommitmentConfig::confirmed(),
);
let fee_payer = Keypair::new();
let recipient_a = Keypair::new();
let recipient_b = Keypair::new();
let fee_receiver = Keypair::new();
let decimals = 2;
let airdrop_signature = client
.request_airdrop(&fee_payer.pubkey(), 5_000_000_000)
.await?;
loop {
let confirmed = client.confirm_transaction(&airdrop_signature).await?;
if confirmed {
break;
}
}
let mint = Keypair::new();
let mint_space =
ExtensionType::try_calculate_account_len::<Mint>(&[ExtensionType::TransferFeeConfig])?;
let mint_rent = client
.get_minimum_balance_for_rent_exemption(mint_space)
.await?;
let mint_blockhash = client.get_latest_blockhash().await?;
let mint_transaction = Transaction::new_signed_with_payer(
&[
create_account(
&fee_payer.pubkey(), // Account funding account creation.
&mint.pubkey(), // New mint account to create.
mint_rent, // Lamports funding the mint account rent.
mint_space as u64, // Account size in bytes for the mint plus TransferFeeConfig.
&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.
),
initialize_transfer_fee_config(
&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.
&mint.pubkey(), // Mint account that stores the TransferFeeConfig extension.
Some(&fee_payer.pubkey()), // Authority allowed to update the transfer fee later.
Some(&fee_payer.pubkey()), // Value stored in the mint's `withdraw_withheld_authority` field.
150, // Transfer fee in basis points.
10, // Maximum fee charged on each transfer.
)?,
initialize_mint(
&TOKEN_2022_PROGRAM_ID, // Program that owns the mint account.
&mint.pubkey(), // Mint account to initialize.
&fee_payer.pubkey(), // Authority allowed to mint new tokens.
Some(&fee_payer.pubkey()), // Authority allowed to freeze token accounts.
decimals, // Number of decimals for the token.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer, &mint],
mint_blockhash,
);
client.send_and_confirm_transaction(&mint_transaction).await?;
let source_token_address = get_associated_token_address_with_program_id(
&fee_payer.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let destination_a_token_address = get_associated_token_address_with_program_id(
&recipient_a.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let destination_b_token_address = get_associated_token_address_with_program_id(
&recipient_b.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let fee_receiver_token_address = get_associated_token_address_with_program_id(
&fee_receiver.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
);
let setup_blockhash = client.get_latest_blockhash().await?;
let setup_transaction = Transaction::new_signed_with_payer(
&[
create_associated_token_account(
&fee_payer.pubkey(),
&fee_payer.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
create_associated_token_account(
&fee_payer.pubkey(),
&recipient_a.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
create_associated_token_account(
&fee_payer.pubkey(),
&recipient_b.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
create_associated_token_account(
&fee_payer.pubkey(),
&fee_receiver.pubkey(),
&mint.pubkey(),
&TOKEN_2022_PROGRAM_ID,
),
mint_to_checked(
&TOKEN_2022_PROGRAM_ID,
&mint.pubkey(),
&source_token_address,
&fee_payer.pubkey(),
&[],
1_000,
decimals,
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
setup_blockhash,
);
client.send_and_confirm_transaction(&setup_transaction).await?;
let first_transfer_blockhash = client.get_latest_blockhash().await?;
let first_transfer_transaction = Transaction::new_signed_with_payer(
&[
transfer_checked_with_fee(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the transfer.
&source_token_address, // Token account sending the transfer.
&mint.pubkey(), // Mint with the transfer fee configuration.
&destination_a_token_address, // Token account receiving the transfer.
&fee_payer.pubkey(), // Signer approving the transfer.
&[], // Additional multisig signers.
200, // Token amount in base units.
decimals, // Decimals defined on the mint.
3, // Expected transfer fee for this transfer.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
first_transfer_blockhash,
);
client
.send_and_confirm_transaction(&first_transfer_transaction)
.await?;
let withdraw_accounts_blockhash = client.get_latest_blockhash().await?;
let withdraw_accounts_transaction = Transaction::new_signed_with_payer(
&[
withdraw_withheld_tokens_from_accounts(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the withdraw.
&mint.pubkey(), // Mint with the transfer fee configuration.
&fee_receiver_token_address, // Token account receiving withdrawn fees.
&fee_payer.pubkey(), // Signer matching the mint's `withdraw_withheld_authority`.
&[], // Additional multisig signers.
&[&destination_a_token_address], // Token accounts to withdraw withheld fees from.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
withdraw_accounts_blockhash,
);
client
.send_and_confirm_transaction(&withdraw_accounts_transaction)
.await?;
let second_transfer_blockhash = client.get_latest_blockhash().await?;
let second_transfer_transaction = Transaction::new_signed_with_payer(
&[
transfer_checked_with_fee(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the transfer.
&source_token_address, // Token account sending the transfer.
&mint.pubkey(), // Mint with the transfer fee configuration.
&destination_b_token_address, // Token account receiving the transfer.
&fee_payer.pubkey(), // Signer approving the transfer.
&[], // Additional multisig signers.
200, // Token amount in base units.
decimals, // Decimals defined on the mint.
3, // Expected transfer fee for this transfer.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
second_transfer_blockhash,
);
client
.send_and_confirm_transaction(&second_transfer_transaction)
.await?;
let harvest_blockhash = client.get_latest_blockhash().await?;
let harvest_transaction = Transaction::new_signed_with_payer(
&[
harvest_withheld_tokens_to_mint(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the harvest.
&mint.pubkey(), // Mint that collects harvested withheld fees.
&[&destination_b_token_address], // Token accounts to harvest withheld fees from.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
harvest_blockhash,
);
client.send_and_confirm_transaction(&harvest_transaction).await?;
let finalize_blockhash = client.get_latest_blockhash().await?;
let finalize_transaction = Transaction::new_signed_with_payer(
&[
withdraw_withheld_tokens_from_mint(
&TOKEN_2022_PROGRAM_ID, // Token program that processes the withdraw.
&mint.pubkey(), // Mint storing harvested withheld fees.
&fee_receiver_token_address, // Token account receiving withdrawn fees.
&fee_payer.pubkey(), // Signer matching the mint's `withdraw_withheld_authority`.
&[], // Additional multisig signers.
)?,
set_transfer_fee(
&TOKEN_2022_PROGRAM_ID, // Token program that owns the mint.
&mint.pubkey(), // Mint whose next transfer fee configuration is updated.
&fee_payer.pubkey(), // Authority allowed to update the transfer fee later.
&[], // Additional multisig signers.
250, // New transfer fee in basis points.
25, // New maximum fee for the next transfer fee configuration.
)?,
],
Some(&fee_payer.pubkey()),
&[&fee_payer],
finalize_blockhash,
);
client.send_and_confirm_transaction(&finalize_transaction).await?;
let destination_a_account = client.get_account(&destination_a_token_address).await?;
let destination_a_state = StateWithExtensions::<Account>::unpack(&destination_a_account.data)?;
let destination_b_account = client.get_account(&destination_b_token_address).await?;
let destination_b_state = StateWithExtensions::<Account>::unpack(&destination_b_account.data)?;
let fee_receiver_account = client.get_account(&fee_receiver_token_address).await?;
let fee_receiver_state = StateWithExtensions::<Account>::unpack(&fee_receiver_account.data)?;
let mint_account = client.get_account(&mint.pubkey()).await?;
let mint_state = StateWithExtensions::<Mint>::unpack(&mint_account.data)?;
println!("Mint: {}", mint.pubkey());
println!("Destination A Balance: {}", destination_a_state.base.amount);
println!(
"Destination A Transfer Fee Amount: {:#?}",
destination_a_state.get_extension::<TransferFeeAmount>()?
);
println!("Destination B Balance: {}", destination_b_state.base.amount);
println!(
"Destination B Transfer Fee Amount: {:#?}",
destination_b_state.get_extension::<TransferFeeAmount>()?
);
println!("Fee Receiver Balance: {}", fee_receiver_state.base.amount);
println!(
"Fee Receiver Transfer Fee Amount: {:#?}",
fee_receiver_state.get_extension::<TransferFeeAmount>()?
);
println!(
"Transfer Fee Config: {:#?}",
mint_state.get_extension::<TransferFeeConfig>()?
);
Ok(())
}
Console
Click to execute the code.

Is this page helpful?

목차

페이지 편집

관리자

© 2026 솔라나 재단.
모든 권리 보유.
연결하기