以做網(wǎng)站為畢設(shè)挖掘關(guān)鍵詞的工具
從零到英雄的 Solana 代幣 2022 — Transfer Hook?
?
Token 2022 計(jì)劃引入了幾項(xiàng)令人興奮的擴(kuò)展,增強(qiáng)了鑄造和代幣賬戶的功能。在這些功能中,我個(gè)人最喜歡的是Transfer Hook (轉(zhuǎn)賬鉤子) 。
想象時(shí)間
讓我們戴上想象的帽子,想象一下這個(gè)美好的場景:你是一個(gè) NFT 項(xiàng)目的所有者。你向持有者發(fā)放代幣,以獎(jiǎng)勵(lì)他們將 NFT 抵押給你。你將你的項(xiàng)目視為一個(gè)“封閉社區(qū)”,這意味著只有你的 NFT 持有者才能持有你的代幣。在沒有轉(zhuǎn)賬鉤子之前,你必須構(gòu)建自己的程序來處理轉(zhuǎn)賬,以強(qiáng)制執(zhí)行這種行為。但有了轉(zhuǎn)賬鉤子,這就非常簡單了!你只需要構(gòu)建一個(gè)轉(zhuǎn)賬鉤子程序,驗(yàn)證轉(zhuǎn)賬的目標(biāo)地址是否確實(shí)是白名單地址之一,如果不是,則停止交易。另一個(gè)例子?假設(shè)你希望用戶在每次進(jìn)行代幣轉(zhuǎn)賬時(shí)支付額外費(fèi)用……沒問題!這也可以通過構(gòu)建一個(gè)處理轉(zhuǎn)賬的鉤子來實(shí)現(xiàn)!
現(xiàn)在,在 NFT 項(xiàng)目的背景下,這些例子可能看起來有些愚蠢,但想象一下更大的圖景,這樣的鉤子可以幫助遵守監(jiān)管要求,例如強(qiáng)制執(zhí)行持有期或設(shè)置代幣的最大持有量。
Transfer Hook — 它到底是什么?
顧名思義,Transfer Hook 擴(kuò)展與代幣轉(zhuǎn)賬密切相關(guān)。每當(dāng)一個(gè)鑄造配置了 Transfer Hook 時(shí),每次在該鑄造上執(zhí)行轉(zhuǎn)賬指令時(shí),都會自動觸發(fā)一個(gè)指令。如果這個(gè)概念看起來很熟悉,那是因?yàn)樗_實(shí)如此——這個(gè)流程與 web2 中的 webhooks 工作方式非常相似。
轉(zhuǎn)賬鉤子指令可以訪問一個(gè)變量,即轉(zhuǎn)賬金額,但我們可以提供一個(gè)額外的賬戶(extra-account-meta-list
),其中包含指令可以使用的其他自定義賬戶。我們很快會詳細(xì)討論這個(gè)賬戶。
雖然轉(zhuǎn)賬鉤子確實(shí)可以訪問初始轉(zhuǎn)賬賬戶,但需要注意的是,它們作為只讀賬戶傳遞,這意味著發(fā)送者的簽名權(quán)限不會擴(kuò)展到Transfer Hook程序。
最后,當(dāng)使用 Anchor 編寫Transfer Hook程序時(shí),需要一個(gè)回退指令來手動匹配原生程序指令鑒別器并調(diào)用Transfer Hook指令。這是因?yàn)?Token 2022 計(jì)劃是一個(gè)原生程序,因此我們需要“橋接”其原生接口與 Anchor。
是時(shí)候動手了
好了,話不多說——讓我們構(gòu)建一些東西!我們將構(gòu)建一個(gè)“鯨魚警報(bào)”轉(zhuǎn)賬鉤子 🐋!對于每次代幣轉(zhuǎn)賬,轉(zhuǎn)賬鉤子指令將比較轉(zhuǎn)賬金額與預(yù)定義的值(例如,10,000 代幣)。如果等于或大于該值,我們將更新一個(gè)賬戶,記錄最新的鯨魚詳情,并發(fā)出一個(gè)事件供客戶端處理。
?? 你將需要安裝 Solana CLI 工具和 Anchor。如果你還沒有安裝,請查看?Anchor 文檔?以獲取安裝說明。
第一步:轉(zhuǎn)賬鉤子程序
讓我們設(shè)計(jì)我們的轉(zhuǎn)賬鉤子程序。我們的轉(zhuǎn)賬鉤子程序首先需要一個(gè)指令,可以在轉(zhuǎn)賬完成后調(diào)用。我們將使用 Anchor 構(gòu)建我們的程序,然而,Token 2022 計(jì)劃是一個(gè)原生 Solana 程序——因此我們需要另一個(gè)指令:一個(gè)指令來匹配指令鑒別器到轉(zhuǎn)賬鉤子的?execute
?接口,并在匹配時(shí)調(diào)用?transfer_hook
?指令。所以目前有兩個(gè)指令。
當(dāng)我們的鉤子被調(diào)用時(shí),它會將轉(zhuǎn)賬金額與一個(gè)值進(jìn)行比較——但這個(gè)值來自哪里?當(dāng)然,我們可以在程序中硬編碼這個(gè)值——但如果明天我們決定要增加/減少這個(gè)值呢?我們需要重新部署程序。更好的處理方式是將比較值保存在一個(gè)賬戶中。要將額外的賬戶傳遞給轉(zhuǎn)賬鉤子,我們使用?extra_account_meta_list
?賬戶。這是一個(gè) PDA 賬戶,存儲轉(zhuǎn)賬鉤子在調(diào)用時(shí)可以使用的賬戶。extra_account_meta_list
?賬戶將始終使用硬編碼字符串?extra-account-metas?和代幣的鑄造地址作為種子。
現(xiàn)在你可能會問自己,我們?nèi)绾蝿?chuàng)建和初始化這個(gè)?extra_account_meta_list
?賬戶?這就是我們程序的第三個(gè)也是最后一個(gè)指令——initialize_extra_account_meta_list
?指令的作用。
在完成我們程序的高層設(shè)計(jì)后,讓我們開始實(shí)際編寫轉(zhuǎn)賬鉤子程序 🎉。
- 創(chuàng)建一個(gè)新的 Anchor 項(xiàng)目并在你喜歡的 IDE 中打開它:
anchor init transfer-hook-whale
- 安裝?anchor-spl, spl-transfer-hook-interface?和?spl_tlv_account_resolution?crates。這些 crates 包含幫助函數(shù)和接口,使我們在處理 SPL 代幣和擴(kuò)展指令時(shí)更加輕松。
cd programs/transfer-hook-whale
cargo add anchor-spl spl-transfer-hook-interface spl_tlv_account_resolution
- 從 Anchor 0.30 開始,我們需要告訴 Anchor 為我們使用的 crates 生成類型定義,因此我們需要告訴它為?anchor-spl?crate 生成類型定義。打開?Cargo.toml?文件,位于?programs/transfer-hook-whale?文件夾內(nèi),并按如下方式更新?
idl-build
:
[features]
...
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
- 打開?lib.rs?并添加以下?
use
?語句:
use anchor_lang::system_program::{create_account, CreateAccount};
use anchor_spl::{associated_token::AssociatedToken,token_interface::{Mint, TokenInterface},
};
use spl_transfer_hook_interface::instruction::TransferHookInstruction;use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed, state::ExtraAccountMetaList,
};
use spl_transfer_hook_interface::instruction::ExecuteInstruction;
一個(gè)轉(zhuǎn)賬鉤子程序通常由三個(gè)指令組成:
- initialize_extra_account_meta?— 一個(gè)創(chuàng)建賬戶(
extra_account_meta_list
)的指令,該賬戶保存轉(zhuǎn)賬鉤子可以使用的額外賬戶列表。
在我們的例子中,這個(gè)賬戶將保存最新“鯨魚”地址和金額的詳細(xì)信息。 - transfer_hook?— 鉤子本身。這是每次發(fā)生代幣轉(zhuǎn)賬時(shí)實(shí)際調(diào)用的指令。
- fallback?— Token 2022 計(jì)劃是一個(gè)原生程序,但我們使用 Anchor 構(gòu)建我們的程序,因此我們需要添加一個(gè)回退指令,該指令將指令鑒別器匹配到轉(zhuǎn)賬鉤子的?
execute
?接口,并在匹配時(shí)調(diào)用我們的?transfer_hook
?指令。
實(shí)現(xiàn) initialize_extra_account_meta 指令
- 在你的?lib.rs?底部,添加以下賬戶。此賬戶將保存最新“鯨魚”轉(zhuǎn)賬的詳細(xì)信息:
#[account]
pub struct WhaleAccount {pub whale_address: Pubkey,pub transfer_amount: u64
}
- 接下來,我們定義?
initialize_extra_account_meta
?指令執(zhí)行所需的所有賬戶。在?LatestWhaleAccount
?上方添加以下代碼片段:
#[derive(Accounts)]
pub struct InitializeExtraAccountMeta<'info> {#[account(mut)]pub payer: Signer<'info>,/// CHECK: ExtraAccountMetaList Account, must use these exact seeds#[account(mut, seeds=[b"extra-account-metas", mint.key().as_ref()], bump)]pub extra_account_meta_list: AccountInfo<'info>,pub mint: InterfaceAccount<'info, Mint>,#[account(init, seeds=[b"whale_account"], bump, payer=payer, space=8+32+8)]pub latest_whale_account: Account<'info, WhaleAccount>,pub token_program: Interface<'info, TokenInterface>,pub associated_token_program: Program<'info, AssociatedToken>,pub system_program: Program<'info, System>,
}
🧑?🏫?提示:
- 將保存額外賬戶的賬戶 (
extra_account_meta_list
)?必須有一個(gè)非常特定的種子用于其 PDA:字節(jié)串?extra-account-metas?和代幣的?mint
?賬戶的公鑰。 - 將保存鯨魚詳細(xì)信息的賬戶 (
latest_whale_account
) 將有一個(gè)簡單的種子用于其 PDA:字符串?whale_account?的字節(jié)。這意味著我們鑄造的所有代幣將共享同一個(gè)賬戶。 - 我們必須為指令提供代幣程序、關(guān)聯(lián)代幣程序和系統(tǒng)程序,因?yàn)樗谶\(yùn)行時(shí)需要使用它們。
- 接下來,讓我們添加?
initialize_extra_account_meta
?指令。用以下內(nèi)容替換默認(rèn)的?initialize
?指令:
pub fn initialize_extra_account(ctx: Context<InitializeExtraAccountMeta>) -> Result<()> {// 這是我們需要的額外賬戶的向量。在我們的例子中// 只有一個(gè)賬戶 - 鯨魚詳細(xì)信息賬戶。let account_metas = vec![ExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "whale_account".as_bytes().to_vec(),}],false,true,)?];// 計(jì)算賬戶大小和租金let account_size = ExtraAccountMetaList::size_of(account_metas.len())? as u64;let lamports = Rent::get()?.minimum_balance(account_size as usize);// 從上下文中獲取鑄造賬戶公鑰。let mint = ctx.accounts.mint.key();// ExtraAccountMetaList PDA 的種子。let signer_seeds: &[&[&[u8]]] = &[&[b"extra-account-metas",&mint.as_ref(),&[ctx.bumps.extra_account_meta_list],]];// 創(chuàng)建 ExtraAccountMetaList 賬戶create_account(CpiContext::new(ctx.accounts.system_program.to_account_info(),CreateAccount {from: ctx.accounts.payer.to_account_info(),to: ctx.accounts.extra_account_meta_list.to_account_info(),},).with_signer(signer_seeds),lamports,account_size,ctx.program_id,)?;// 使用額外賬戶初始化 ExtraAccountMetaList 賬戶ExtraAccountMetaList::init::<ExecuteInstruction>(&mut ctx.accounts.extra_account_meta_list.try_borrow_mut_data()?,&account_metas,)?;Ok(())
}
這里有很多內(nèi)容,讓我們逐步理解:
- 首先我們聲明一個(gè)?
ExtraAccountMeta
?賬戶的向量 (account_metas
),指定我們將使用的不同額外賬戶。這些可以從種子、公鑰等指定。在我們的例子中,我們只需要一個(gè)額外賬戶——鯨魚詳細(xì)信息賬戶。 - 我們計(jì)算賬戶大小,并基于此計(jì)算租金所需的 lamports 數(shù)量。
- 我們指定需要簽名的種子為?
extra_account_meta_list
?PDA。 - 我們調(diào)用?
create_account
?CPI 實(shí)際創(chuàng)建?extra_account_meta_list
?賬戶,提供所有需要的數(shù)據(jù)(付款人、簽名者種子、賬戶大小等)。 - 最后,我們用在步驟 1 中聲明的額外賬戶向量初始化新創(chuàng)建的賬戶。
實(shí)現(xiàn) transfer_hook 指令
我們達(dá)到了程序的核心——transfer_hook
?指令!這是實(shí)際操作發(fā)生的地方,因?yàn)槊慨?dāng)進(jìn)行交易時(shí)都會調(diào)用此指令。
- 我們首先為指令添加傳入賬戶。在?lib.rs?中?
InitializeExtraAccountMeta
?下方添加以下內(nèi)容:
#[derive(Accounts)]
pub struct TransferHook<'info> {#[account(token::mint = mint, token::authority = owner)]pub source_token: InterfaceAccount<'info, TokenAccount>,pub mint: InterfaceAccount<'info, Mint>,#[account(token::mint = mint)]pub destination_token: InterfaceAccount<'info, TokenAccount>,/// CHECK: source token account owner, /// can be SystemAccount or PDA owned by another program pub owner: UncheckedAccount<'info>,/// CHECK: ExtraAccountMetaList Account,#[account(seeds = [b"extra-account-metas", mint.key().as_ref()],bump)]pub extra_account_meta_list: UncheckedAccount<'info>,#[account(mut, seeds=[b"whale_account"], bump)]pub latest_whale_account: Account<'info, WhaleAccount>,
}
🧑?🏫?提示:
- 這里指定的賬戶順序很重要:
- 前四個(gè)賬戶是代幣轉(zhuǎn)賬所需的賬戶(源、鑄造、目標(biāo)和所有者——按此順序),
- 第五個(gè)賬戶是?
ExtraAccountMetaList
?賬戶的地址。 - 剩余的賬戶是?
ExtraAccountMetaList
?賬戶所需的額外賬戶。
- 注意我們在這里傳遞的約束——它們是為了幫助我們確保傳遞的賬戶確實(shí)是我們期望的賬戶(即正確的鑄造賬戶,正確的所有者等)。
- 現(xiàn)在我們可以繼續(xù)進(jìn)行實(shí)際指令。在?
initialize_extra_account
?指令下方添加以下代碼片段:
pub fn transfer_hook(ctx: Context<TransferHook>, amount: u64) -> Result<()> {msg!(&format!("Transfer hook fired for an amount of {}", amount));if amount >= 1000 * (u64::pow(10, ctx.accounts.mint.decimals as u32)) {// 我們有一個(gè)鯨魚!ctx.accounts.latest_whale_account.whale_address = ctx.accounts.owner.key();ctx.accounts.latest_whale_account.transfer_amount = amount;emit!(WhaleTransferEvent {whale_address: ctx.accounts.owner.key(),transfer_amount: amount});}Ok(())
}
🦁 這里沒有太多瘋狂的事情:
- 我們檢查轉(zhuǎn)賬金額是否大于或等于 1000 代幣(這是我選擇的任意金額,你可以選擇其他金額)。
- 如果金額確實(shí)是 1000 或更多,我們更新鯨魚詳細(xì)信息賬戶的新地址和金額。
- 最后,我們發(fā)出一個(gè)名為?
WhaleTransferEvent
?的事件,供客戶端(例如 Discord 機(jī)器人、Web 應(yīng)用程序等)監(jiān)聽。
- 在 lib.rs 底部添加實(shí)際的事件聲明:
#[event]
pub struct WhaleTransferEvent {pub whale_address: Pubkey,pub transfer_amount: u64,
}
實(shí)現(xiàn) fallback 指令
最后但同樣重要的是,fallback
?指令。在?transfer_hook
?指令之后添加以下代碼片段:
pub fn fallback<'info>(program_id: &Pubkey,accounts: &'info [AccountInfo<'info>],data: &[u8],
) -> Result<()> {let instruction = TransferHookInstruction::unpack(data)?;// 匹配指令識別符以執(zhí)行 transfer hook 接口指令// token2022 程序在代幣轉(zhuǎn)移時(shí)調(diào)用此指令match instruction {TransferHookInstruction::Execute { amount } => {let amount_bytes = amount.to_le_bytes();// 在我們的程序中調(diào)用自定義 transfer hook 指令__private::__global::transfer_hook(program_id, accounts, &amount_bytes)}_ => return Err(ProgramError::InvalidInstructionData.into()),}
}
fallback 指令取自 Solana 的hello-world transfer hook 示例 。
雖然看起來很復(fù)雜,但這個(gè)指令其實(shí)相當(dāng)簡單:
- 解包指令數(shù)據(jù)并將其轉(zhuǎn)換為?
TransferHookInstruction
。 - 匹配?
TransferHookInstruction
?枚舉。 - 如果是?
Execute
?指令,獲取轉(zhuǎn)移的金額并調(diào)用?transfer_hook
?指令。 - 如果不是?
Execute
?指令,則返回?zé)o效指令數(shù)據(jù)的錯(cuò)誤。
繼續(xù)將程序部署到 Devnet 🎉
第 2 步:鑄造
好的,我們的程序已經(jīng)部署,現(xiàn)在我們可以將注意力轉(zhuǎn)向創(chuàng)建一個(gè)實(shí)際使用它的代幣鑄造。
- 使用以下命令創(chuàng)建一個(gè)鑄造:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token --transfer-hook <your transfer hook program id>
🧑?🏫?提示:
- 注意命令中的程序 id?那是 Token 2022 程序的地址。我們必須指定它以便鑄造實(shí)際使用新標(biāo)準(zhǔn)。
- 為鑄造注冊 transfer hook 就像在?
spl-token
?命令中添加?--transfer-hook <address>
?子命令一樣簡單。
- 為你配置的錢包創(chuàng)建代幣賬戶:
spl-token create-account <the token address from previous step>
- 向你的錢包鑄造一些代幣:
spl-token mint <the token address from previous step> 3000
第 3 步:初始化 ExtraAccountMeta 賬戶
如果你嘗試轉(zhuǎn)移代幣(例如使用?spl-token transfer
?命令),你將遇到?AccountNotFound
?錯(cuò)誤。這是因?yàn)槲覀儚奈闯跏蓟覀兊睦吓笥?ExtraAccountMetaList
我們在程序中創(chuàng)建了一個(gè)指令來初始化賬戶,initialize_extra_account
,所以現(xiàn)在是調(diào)用它的最佳時(shí)機(jī)。
我創(chuàng)建了這個(gè)小的 TypeScript 代碼來調(diào)用它,但你可以隨意使用任何你喜歡的方法:
import { readFile } from "fs/promises";
import * as anchor from "@coral-xyz/anchor";
// 這兩個(gè)文件是從構(gòu)建的 Anchor 程序 Target/idl 和 Target/types 文件夾中復(fù)制的。
import { TransferHookWhale } from "./program/transfer_hook_whale";
import idl from './program/transfer_hook_whale.json';import { TOKEN_2022_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
import "dotenv/config";const kpFile = ".<your keypair file>";
const mint = new anchor.web3.PublicKey("<your mint public address>")const main = async () => {if (!process.env.SOLANA_RPC) {console.log("Missing required env variables");return;}console.log("💰 Reading wallet...");const keyFile = await readFile(kpFile);const keypair: anchor.web3.Keypair = anchor.web3.Keypair.fromSecretKey(new Uint8Array(JSON.parse(keyFile.toString())));const wallet = new anchor.Wallet(keypair);console.log("?? Setting provider and program...");const connection = new anchor.web3.Connection(process.env.SOLANA_RPC);const provider = new anchor.AnchorProvider(connection, wallet, {});anchor.setProvider(provider);const program = new anchor.Program<TransferHookWhale>(idl as TransferHookWhale, provider);console.log("🪝 Initializing transfer hook accounts");const [extraAccountMetaListPDA] = anchor.web3.PublicKey.findProgramAddressSync([Buffer.from("extra-account-metas"), mint.toBuffer()],program.programId);const [whalePDA] = anchor.web3.PublicKey.findProgramAddressSync([Buffer.from("whale_account")], program.programId);const initializeExtraAccountMetaListInstruction = await program.methods.initializeExtraAccount().accounts({mint,extraAccountMetaList: extraAccountMetaListPDA,latestWhaleAccount: whalePDA,systemProgram: anchor.web3.SystemProgram.programId,tokenProgram: TOKEN_2022_PROGRAM_ID,associatedTokenProgram: ASSOCIATED_TOKEN_PROGRAM_ID,}).instruction();const transaction = new anchor.web3.Transaction().add(initializeExtraAccountMetaListInstruction);const tx = await anchor.web3.sendAndConfirmTransaction(connection, transaction, [wallet.payer], {commitment: "confirmed",});console.log("Transaction Signature:", tx);
}main().then(() => {console.log("done!");process.exit(0);
}).catch((e) => {console.log("Error: ", e);process.exit(1);
});
💈?完整項(xiàng)目在GitHub
第 4 步:讓轉(zhuǎn)移開始!
就是這樣,朋友們!隨著 transfer hook 程序的部署,配置使用它的鑄造,以及?ExtraAccountMetaList
?賬戶的初始化,我們終于可以開始使用我們的 hook 了:
transfer hook 正在運(yùn)行,更多相關(guān)信息,,https://t.me/gtokentool?
?