更新於 2024/08/15閱讀時間約 21 分鐘

玩轉 Solana 上的 Defi 模組 - Token Extension(下)



raw-image


Token Extension的主角之一 Transfer Fee

目前官方釋出的代幣擴充功能總共 14 項,其中又以 Transfer fee 這個擴充為最多人使用。從 Dune 的數據可看出Transfer fee 幾乎成了整個token extension 的主要應用,Transfer fee 嚴格規範了每筆轉帳交易的手續費,讓代幣的發行者擁有更好手續費來源,同時也保障 NFT 創作者的版稅收益。

Token Extension stat - Dune


為了更深入了解 Transfer fee,以下將介紹 Transfer fee 的具體創建及應用:


(一)package 安裝與基本設定


今天的教程裡,我們將會用到 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 進行交易)

  1. 建立一個 Solana 帳戶,同時將私鑰儲存在地端的 secret.txt文件
  2. 成功建立帳戶後,空投 SOL 到上面的帳戶裡,作為接下來交易的手續費

將如下的程式碼複製到你的 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



(二)建立 Mint Account & 相關 Config


成功請求 SOL 後,我們就可以開始進行打包交易了。這邊我們會先試著建立一個全新的 token,並設定一些 transfer fee 所需要的config,例如手續費的收取%數、手續費的最高酌收數量以及一些正常 token account 會需要的一些 metadata, ex: decimals.

1. 建立所需要的帳戶(在 Solana 上如果要儲存任何資訊,需要建立 Account 作為儲存資料的最小單位)

  1. 設定 token 資訊以及 transfer fee hook 的 config(要注意到的是 transfer fee 所支援的 fee % 數是用 bps, 萬分之一下去算的。因此我們要酌收0.5% (50/10000) 的手續費的話,需要輸入 ”50“)
  2. 計算帳戶所需要的空間與租金。(在創立Account的同時,為了避免浪費空間的使用以及閒置數據的佔用,影響鏈上的儲存空間與效率,我們需要再建立 Account 的同時宣告佔用的大小以及維持儲存空間的租金)
  3. 建立所需的指令( Instructions ):在這裡我們建立了三個不同的指令:建立 Mint Account 、初始化和設定 Config 變數、初始化和設定 Mint Account(要注意,由於區塊鏈在執行交易時是按照順序進行執行,不同指令的前後關係會影響鏈上執行的結果。舉例來說,我們如果再打包交易時將 initializeMintInstruction 這筆指令擺在創造帳戶的指令 createAccountInstruction 前面,會造成交易失敗,因為要設定 mint Account 時,這個 Account 尚未被建立)
  4. 打包所有指令並送出交易
...

// 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

initializ mint - Sol explorer


(三)執行轉帳並收取手續費

成功創立 Mint Account 之後,我們將可以透過這個 Mint account 鑄造代幣,進行相關的代幣轉帳。

  1. 創立 owner 帳號(這個帳號將會持有兩個不同代幣帳號,作為轉帳用途)
  2. 鑄造代幣到一個 Token Account,作為轉帳帳戶
  3. 建立另一個 Token Account 作為收款帳戶
  4. 發起轉帳交易,同時額外計算被徵收的手續費(轉帳金額設定為 1,000,000,並酌收 5000 手續費)
...


// 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。

destination account


transfer tx - Sol explorer


(四)提領手續費

在每一筆轉帳交易後都會存有額外的手續費 Withheld Amount 存在獨立的 token account 裡。假使要提領出來的話,我們會需要把每個token account 的資訊撈出來,確認是否有閒置的手續費尚未領取。


  1. 找出所有在Mint account 底下的 token account
  2. 根據我們的邏輯去撈出指定的帳戶(我們這邊撈出尚未被領取的手續費的帳號 withheldAmount > 0 )
  3. 送出交易
...

// 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

withdraw tx


以上便是這次的 Transfer Fee 手把手教學,可以發現 Transfer Fee 這個擴充功能並沒有讓前端額外增加許多負擔,很多的功能以及函式呼叫基本上都已經打包好,只需要計算手續費以及額外輸入 Account 進行交易即可,十分方便 🚀


Token Extension的詳細介紹請參考上篇


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.