目前官方釋出的代幣擴充功能總共 14 項,其中又以 Transfer fee 這個擴充為最多人使用。從 Dune 的數據可看出Transfer fee 幾乎成了整個token extension 的主要應用,Transfer fee 嚴格規範了每筆轉帳交易的手續費,讓代幣的發行者擁有更好手續費來源,同時也保障 NFT 創作者的版稅收益。
為了更深入了解 Transfer fee,以下將介紹 Transfer fee 的具體創建及應用:
今天的教程裡,我們將會用到 Bun 作為我們套件管理工具和執行環境,不只處理速度上非常快,我們也能省去不少轉譯的時間,最後一行則是安裝 Solana 常見的兩個 package 。
打開你習慣的編輯器,輸入以下指令
mkdir token-extension-transfer-fees
cd token-extension-transfer-fees/
bun init -y
bun add @solana/web3.js @solana/spl-token
安裝完成 package 之後,我們接著將
1. 建立一個與Devnet 連結的 Connection(我們將會都在 Devnet 進行交易)
secret.txt
文件將如下的程式碼複製到你的 Bun 替你建立好的 index.ts 文件裡,複製成功後
可在終端機輸入 bun run index.ts
檢查是否成功建立 Solana 帳號,並成功獲得的空投作為進行交易的手續費
import {
clusterApiUrl,
Connection,
Keypair,
LAMPORTS_PER_SOL,
} from "@solana/web3.js";
// 1. create Connection with Devnet
const connection = new Connection(clusterApiUrl("devnet"), "confirmed");
const FILE_PATH = "./secret.txt";
// 2. create payer account
const secretFileIsExist = await Bun.file(FILE_PATH).exists();
if (!secretFileIsExist) {
const payer = Keypair.generate();
await Bun.write(FILE_PATH, payer.secretKey);
}
const secret = await Bun.file(FILE_PATH)
.arrayBuffer()
.then((buffer) => new Uint8Array(buffer));
const payer = Keypair.fromSecretKey(secret);
console.log("🚀 Payer: ", payer.publicKey.toString());
// 3. request Airdrop 1 SOL
const accountBalance = await connection.getBalance(payer.publicKey);
console.log("🚀 account balance:", accountBalance);
if (accountBalance == 0) {
const airdropSig = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL,
);
await connection.confirmTransaction({
signature: airdropSig,
...(await connection.getLatestBlockhash()),
});
console.log("aridrop to: ", payer.publicKey);
}
// 完成後將會看類似如下的資訊
// 🚀 Payer: Du9vC64s1ZJRY1i7qfA1CWvt6hWMwEeguebifqsjztcC
// 🚀 account balance: 978490640
成功請求 SOL 後,我們就可以開始進行打包交易了。這邊我們會先試著建立一個全新的 token,並設定一些 transfer fee 所需要的config,例如手續費的收取%數、手續費的最高酌收數量以及一些正常 token account 會需要的一些 metadata, ex: decimals.
1. 建立所需要的帳戶(在 Solana 上如果要儲存任何資訊,需要建立 Account 作為儲存資料的最小單位)
initializeMintInstruction
這筆指令擺在創造帳戶的指令 createAccountInstruction
前面,會造成交易失敗,因為要設定 mint Account 時,這個 Account 尚未被建立)...
// 1. create required accounts
const mintAuthority = Keypair.generate();
const mintKeypair = Keypair.generate();
const mint = mintKeypair.publicKey; // mint account responsible for token mint/burn & token metadata
const transferFeeConfigAuthority = Keypair.generate(); // the authority is assigned cofig to
const withdrawWithheldAuthority = Keypair.generate(); // account that can withdraw the withheld token
// 2. setup token info
const TOKEN_DECIMALS = 9;
const FEE_BASIS_POINTS = 50; // 0.5%
const MAX_FEE = BigInt(5000); // Capped at 5K
const SCALING = BigInt(10_000); // per BPS
// 3. calculate storage lamports
const mintLen = getMintLen([ExtensionType.TransferFeeConfig]);
const lamports = await connection.getMinimumBalanceForRentExemption(mintLen);
// 4. create required instructions
const createAccountInstruction = SystemProgram.createAccount({
fromPubkey: payer.publicKey, // fee payer
newAccountPubkey: mint, // created account
space: mintLen, // allocated storage
lamports, // required rent lamports
programId: TOKEN_2022_PROGRAM_ID, // assigned programId
});
const initializeTransferFeeConfigInstruction =
createInitializeTransferFeeConfigInstruction(
mint,
transferFeeConfigAuthority.publicKey,
withdrawWithheldAuthority.publicKey,
FEE_BASIS_POINTS,
MAX_FEE,
TOKEN_2022_PROGRAM_ID,
);
const initializeMintInstruction = createInitializeMintInstruction(
mint,
TOKEN_DECIMALS,
mintAuthority.publicKey,
mintAuthority.publicKey,
TOKEN_2022_PROGRAM_ID,
);
// 5. batch and sent Tx
const tx = new Transaction().add(
createAccountInstruction,
initializeTransferFeeConfigInstruction,
initializeMintInstruction,
);
const txSig = await sendAndConfirmTransaction(
connection,
tx,
[payer, mintKeypair],
undefined,
);
console.log("🚀 MintAccointSig:", txSig);
// 完成後將會看類似如下的資訊
// 🚀 Payer: Du9vC64s1ZJRY1i7qfA1CWvt6hWMwEeguebifqsjztcC
// 🚀 account balance: 978490640
// 🚀 MintAccointSig: 3P2Vs3TQ5nNG2CDjGKkFg7c5i1JoiBUYTk4ouvP5UoxNEMuQALQUuEhNervVBXSxsmPKuuv8QddCMCf85rFKF6TX
成功後可以,可以前往 solana explorer 輸入交易的簽名,查看交易的細節與情況。
可以看到交易按照順序顯示了我們剛打包好的三筆指令,同時第二筆設定 transfer fee Config 這邊顯示了我們剛剛設定的 5% 手續費以及最高手續費5000
成功創立 Mint Account 之後,我們將可以透過這個 Mint account 鑄造代幣,進行相關的代幣轉帳。
...
// 1. create account to own 2 different token accounts
const owner = Keypair.generate();
const sourceAccount = await createAccount(
connection,
payer,
mint,
owner.publicKey,
undefined,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// 2. mint tokens to source token account
const mintAmount = BigInt(10 ** 9);
await mintTo( // topup source Account
connection,
payer,
mint,
sourceAccount,
mintAuthority,
mintAmount,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
// 3. create recipient token account
const recipient = Keypair.generate();
const recipientAccount = await createAccount(
connection,
payer,
mint,
owner.publicKey,
recipient,
undefined,
TOKEN_2022_PROGRAM_ID,
);
// 4. transfer and calculate the required fee amt
const transferAmount = BigInt(10 ** 6);
const feeAmount = (transferAmount * BigInt(FEE_BASIS_POINTS)) / SCALING;// calculate the charged fee when we execture transfer tx; otherwise the tx would fail
const transferSig = await transferCheckedWithFee(
connection,
payer,
sourceAccount,
mint,
recipientAccount,
owner,
transferAmount,
TOKEN_DECIMALS,
feeAmount,
[],
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log("🚀 TransferSig:", transferSig);
// 完成後將會看類似如下的資訊
// 🚀 Payer: Du9vC64s1ZJRY1i7qfA1CWvt6hWMwEeguebifqsjztcC
// 🚀 account balance: 978490640
// 🚀 MintAccointSig: Jtc2hMbqymcZySmpvuqCtgNyZ5cdBKnffMAXScJ64AJQ2KjMydA7tyZQ1hty9rr5zUNPdDPykaLmSn2F1xBNnZg
// 🚀 TransferSig: NkQpF5KMjNpN7yQPbbGAaXTXxvcPRrbdf4hCmjae7tC65LET7nrTNkocv2AdqyXqpcqqP9HRKJCqy6cwDCqDqM2
同樣前往 solana explorer 查看交易的細節與情況。
我們可以看到轉帳成功的交易以及被酌收的手續費。
轉帳金額為 1,000,000 以及徵收的 5% 手續費 1,000,000 * 5% = 5000。
在每一筆轉帳交易後都會存有額外的手續費 Withheld Amount 存在獨立的 token account 裡。假使要提領出來的話,我們會需要把每個token account 的資訊撈出來,確認是否有閒置的手續費尚未領取。
...
// 1. find all token accounts under Mint account
const allAccounts = await connection.getProgramAccounts(TOKEN_2022_PROGRAM_ID, {
commitment: "confirmed",
filters: [
{
memcmp: {
offset: 0,
bytes: mint.toString(),
},
},
],
});
// 2. filter out the accounts with withheld tokens
const accounsWithWithheldToken = allAccounts
.filter((acc) => {
const data = unpackAccount(acc.pubkey, acc.account, TOKEN_2022_PROGRAM_ID);
const feeAmt = getTransferFeeAmount(data);
return feeAmt && feeAmt.withheldAmount > 0;
})
.map((acc) => acc.pubkey);
// 3. sedn withdraw tx
const withdrawSig = await withdrawWithheldTokensFromAccounts(
connection,
payer,
mint,
recipientAccount,
withdrawWithheldAuthority,
[],
accounsWithWithheldToken,
undefined,
TOKEN_2022_PROGRAM_ID,
);
console.log("🚀 WithdrawSig:", withdrawSig);
// 完成後將會看類似如下的資訊
// 🚀 Payer: Du9vC64s1ZJRY1i7qfA1CWvt6hWMwEeguebifqsjztcC
// 🚀 account balance: 978490640
// 🚀 MintAccointSig: rZUTtGkwRKieJB3bX8Br6MpgVs1GR8W6jqed1qbGeJcPJYaBtkwXCUzKAKE9nE8UE158FBr89gdyDe7tGRajkTw
// 🚀 TransferSig: 3Bv5s6yzmmfcgLggd6Cjo2myX9ZM4CpLUHfS9VkcumkfPVMvHjxJsL932c9LXeyG1cMy2Z9PrancqHyuhZwxfirA
// 🚀 TransferSig: 3Kwu5TEURuCRiKH4bDjVyHVwhyML9VKWdJ5biG3EaBh7srUuZEE36Tww6SJowz8PUDJkttNsDQNdkseoTbRVKDwJ
從 solana explorer 確認交易是否成功,可以看見我們確實提領出了 5000 token
以上便是這次的 Transfer Fee 手把手教學,可以發現 Transfer Fee 這個擴充功能並沒有讓前端額外增加許多負擔,很多的功能以及函式呼叫基本上都已經打包好,只需要計算手續費以及額外輸入 Account 進行交易即可,十分方便 🚀
Token Extension的詳細介紹請參考上篇