Solana 代币 2022 — Transfer Hook

devtools/2024/11/9 0:02:46/

从零到英雄的 Solana 代币 2022 — Transfer Hook 

 

Token 2022 计划引入了几项令人兴奋的扩展,增强了铸造和代币账户的功能。在这些功能中,我个人最喜欢的是Transfer Hook (转账钩子) 。

想象时间

让我们戴上想象的帽子,想象一下这个美好的场景:你是一个 NFT 项目的所有者。你向持有者发放代币,以奖励他们将 NFT 抵押给你。你将你的项目视为一个“封闭社区”,这意味着只有你的 NFT 持有者才能持有你的代币。在没有转账钩子之前,你必须构建自己的程序来处理转账,以强制执行这种行为。但有了转账钩子,这就非常简单了!你只需要构建一个转账钩子程序,验证转账的目标地址是否确实是白名单地址之一,如果不是,则停止交易。另一个例子?假设你希望用户在每次进行代币转账时支付额外费用……没问题!这也可以通过构建一个处理转账的钩子来实现!

现在,在 NFT 项目的背景下,这些例子可能看起来有些愚蠢,但想象一下更大的图景,这样的钩子可以帮助遵守监管要求,例如强制执行持有期或设置代币的最大持有量。

Transfer Hook — 它到底是什么?

顾名思义,Transfer Hook 扩展与代币转账密切相关。每当一个铸造配置了 Transfer Hook 时,每次在该铸造上执行转账指令时,都会自动触发一个指令。如果这个概念看起来很熟悉,那是因为它确实如此——这个流程与 web2 中的 webhooks 工作方式非常相似。

转账钩子指令可以访问一个变量,即转账金额,但我们可以提供一个额外的账户(extra-account-meta-list),其中包含指令可以使用的其他自定义账户。我们很快会详细讨论这个账户。

虽然转账钩子确实可以访问初始转账账户,但需要注意的是,它们作为只读账户传递,这意味着发送者的签名权限不会扩展到Transfer Hook程序。

最后,当使用 Anchor 编写Transfer Hook程序时,需要一个回退指令来手动匹配原生程序指令鉴别器并调用Transfer Hook指令。这是因为 Token 2022 计划是一个原生程序,因此我们需要“桥接”其原生接口与 Anchor。

是时候动手了

好了,话不多说——让我们构建一些东西!我们将构建一个“鲸鱼警报”转账钩子 🐋!对于每次代币转账,转账钩子指令将比较转账金额与预定义的值(例如,10,000 代币)。如果等于或大于该值,我们将更新一个账户,记录最新的鲸鱼详情,并发出一个事件供客户端处理。

⚒️ 你将需要安装 Solana CLI 工具和 Anchor。如果你还没有安装,请查看 Anchor 文档 以获取安装说明。

第一步:转账钩子程序

让我们设计我们的转账钩子程序。我们的转账钩子程序首先需要一个指令,可以在转账完成后调用。我们将使用 Anchor 构建我们的程序,然而,Token 2022 计划是一个原生 Solana 程序——因此我们需要另一个指令:一个指令来匹配指令鉴别器到转账钩子的 execute 接口,并在匹配时调用 transfer_hook 指令。所以目前有两个指令。

当我们的钩子被调用时,它会将转账金额与一个值进行比较——但这个值来自哪里?当然,我们可以在程序中硬编码这个值——但如果明天我们决定要增加/减少这个值呢?我们需要重新部署程序。更好的处理方式是将比较值保存在一个账户中。要将额外的账户传递给转账钩子,我们使用 extra_account_meta_list 账户。这是一个 PDA 账户,存储转账钩子在调用时可以使用的账户。extra_account_meta_list 账户将始终使用硬编码字符串 extra-account-metas 和代币的铸造地址作为种子。

现在你可能会问自己,我们如何创建和初始化这个 extra_account_meta_list 账户?这就是我们程序的第三个也是最后一个指令——initialize_extra_account_meta_list 指令的作用。

在完成我们程序的高层设计后,让我们开始实际编写转账钩子程序 🎉。

  1. 创建一个新的 Anchor 项目并在你喜欢的 IDE 中打开它:
anchor init transfer-hook-whale
  1. 安装 anchor-spl, spl-transfer-hook-interface 和 spl_tlv_account_resolution crates。这些 crates 包含帮助函数和接口,使我们在处理 SPL 代币和扩展指令时更加轻松。
cd programs/transfer-hook-whale
cargo add anchor-spl spl-transfer-hook-interface spl_tlv_account_resolution
  1. 从 Anchor 0.30 开始,我们需要告诉 Anchor 为我们使用的 crates 生成类型定义,因此我们需要告诉它为 anchor-spl crate 生成类型定义。打开 Cargo.toml 文件,位于 programs/transfer-hook-whale 文件夹内,并按如下方式更新 idl-build
[features]
...
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]
  1. 打开 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;

一个转账钩子程序通常由三个指令组成:

  • initialize_extra_account_meta — 一个创建账户(extra_account_meta_list)的指令,该账户保存转账钩子可以使用的额外账户列表。
    在我们的例子中,这个账户将保存最新“鲸鱼”地址和金额的详细信息。
  • transfer_hook — 钩子本身。这是每次发生代币转账时实际调用的指令。
  • fallback — Token 2022 计划是一个原生程序,但我们使用 Anchor 构建我们的程序,因此我们需要添加一个回退指令,该指令将指令鉴别器匹配到转账钩子的 execute 接口,并在匹配时调用我们的 transfer_hook 指令。

实现 initialize_extra_account_meta 指令

  1. 在你的 lib.rs 底部,添加以下账户。此账户将保存最新“鲸鱼”转账的详细信息:
#[account]
pub struct WhaleAccount {pub whale_address: Pubkey,pub transfer_amount: u64
}
  1. 接下来,我们定义 initialize_extra_account_meta 指令执行所需的所有账户。在 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必须有一个非常特定的种子用于其 PDA:字节串 extra-account-metas 和代币的 mint 账户的公钥。
  • 将保存鲸鱼详细信息的账户 (latest_whale_account) 将有一个简单的种子用于其 PDA:字符串 whale_account 的字节。这意味着我们铸造的所有代币将共享同一个账户。
  • 我们必须为指令提供代币程序、关联代币程序和系统程序,因为它在运行时需要使用它们。
  1. 接下来,让我们添加 initialize_extra_account_meta 指令。用以下内容替换默认的 initialize 指令:
pub fn initialize_extra_account(ctx: Context<InitializeExtraAccountMeta>) -> Result<()> {// 这是我们需要的额外账户的向量。在我们的例子中// 只有一个账户 - 鲸鱼详细信息账户。let account_metas = vec![ExtraAccountMeta::new_with_seeds(&[Seed::Literal {bytes: "whale_account".as_bytes().to_vec(),}],false,true,)?];// 计算账户大小和租金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],]];// 创建 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(())
}

这里有很多内容,让我们逐步理解:

  1. 首先我们声明一个 ExtraAccountMeta 账户的向量 (account_metas),指定我们将使用的不同额外账户。这些可以从种子、公钥等指定。在我们的例子中,我们只需要一个额外账户——鲸鱼详细信息账户。
  2. 我们计算账户大小,并基于此计算租金所需的 lamports 数量。
  3. 我们指定需要签名的种子为 extra_account_meta_list PDA。
  4. 我们调用 create_account CPI 实际创建 extra_account_meta_list 账户,提供所有需要的数据(付款人、签名者种子、账户大小等)。
  5. 最后,我们用在步骤 1 中声明的额外账户向量初始化新创建的账户。

实现 transfer_hook 指令

我们达到了程序的核心——transfer_hook 指令!这是实际操作发生的地方,因为每当进行交易时都会调用此指令。

  1. 我们首先为指令添加传入账户。在 lib.rs 中 InitializeExtraAccountMeta 下方添加以下内容:
#[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>,
}

🧑‍🏫 提示

  • 这里指定的账户顺序很重要:
    • 前四个账户是代币转账所需的账户(源、铸造、目标和所有者——按此顺序),
    • 第五个账户是 ExtraAccountMetaList 账户的地址。
    • 剩余的账户是 ExtraAccountMetaList 账户所需的额外账户。
  • 注意我们在这里传递的约束——它们是为了帮助我们确保传递的账户确实是我们期望的账户(即正确的铸造账户,正确的所有者等)。
  1. 现在我们可以继续进行实际指令。在 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)) {// 我们有一个鲸鱼!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(())
}

🦁 这里没有太多疯狂的事情:

  • 我们检查转账金额是否大于或等于 1000 代币(这是我选择的任意金额,你可以选择其他金额)。
  • 如果金额确实是 1000 或更多,我们更新鲸鱼详细信息账户的新地址和金额。
  • 最后,我们发出一个名为 WhaleTransferEvent 的事件,供客户端(例如 Discord 机器人、Web 应用程序等)监听。
  1. 在 lib.rs 底部添加实际的事件声明:
#[event]
pub struct WhaleTransferEvent {pub whale_address: Pubkey,pub transfer_amount: u64,
}

实现 fallback 指令

最后但同样重要的是,fallback 指令。在 transfer_hook 指令之后添加以下代码片段:

pub fn fallback<'info>(program_id: &Pubkey,accounts: &'info [AccountInfo<'info>],data: &[u8],
) -> Result<()> {let instruction = TransferHookInstruction::unpack(data)?;// 匹配指令识别符以执行 transfer hook 接口指令// token2022 程序在代币转移时调用此指令match instruction {TransferHookInstruction::Execute { amount } => {let amount_bytes = amount.to_le_bytes();// 在我们的程序中调用自定义 transfer hook 指令__private::__global::transfer_hook(program_id, accounts, &amount_bytes)}_ => return Err(ProgramError::InvalidInstructionData.into()),}
}

fallback 指令取自 Solana 的hello-world transfer hook 示例 。

虽然看起来很复杂,但这个指令其实相当简单:

  1. 解包指令数据并将其转换为 TransferHookInstruction
  2. 匹配 TransferHookInstruction 枚举。
  3. 如果是 Execute 指令,获取转移的金额并调用 transfer_hook 指令。
  4. 如果不是 Execute 指令,则返回无效指令数据的错误。

继续将程序部署到 Devnet 🎉

第 2 步:铸造

好的,我们的程序已经部署,现在我们可以将注意力转向创建一个实际使用它的代币铸造。

  1. 使用以下命令创建一个铸造:
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token --transfer-hook <your transfer hook program id>

🧑‍🏫 提示

  • 注意命令中的程序 id?那是 Token 2022 程序的地址。我们必须指定它以便铸造实际使用新标准。
  • 为铸造注册 transfer hook 就像在 spl-token 命令中添加 --transfer-hook <address> 子命令一样简单。
  1. 为你配置的钱包创建代币账户:
spl-token create-account <the token address from previous step>
  1. 向你的钱包铸造一些代币:
spl-token mint <the token address from previous step> 3000

第 3 步:初始化 ExtraAccountMeta 账户

如果你尝试转移代币(例如使用 spl-token transfer 命令),你将遇到 AccountNotFound 错误。这是因为我们从未初始化我们的老朋友 ExtraAccountMetaList

我们在程序中创建了一个指令来初始化账户,initialize_extra_account,所以现在是调用它的最佳时机。

我创建了这个小的 TypeScript 代码来调用它,但你可以随意使用任何你喜欢的方法:

import { readFile } from "fs/promises";
import * as anchor from "@coral-xyz/anchor";
// 这两个文件是从构建的 Anchor 程序 Target/idl 和 Target/types 文件夹中复制的。
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);
});

💈 完整项目在GitHub

第 4 步:让转移开始!

就是这样,朋友们!随着 transfer hook 程序的部署,配置使用它的铸造,以及 ExtraAccountMetaList 账户的初始化,我们终于可以开始使用我们的 hook 了:

transfer hook 正在运行,更多相关信息,,https://t.me/gtokentool 

 


http://www.ppmy.cn/devtools/132428.html

相关文章

【系统架构设计师】高分论文:论软件的可用性设计

更多内容请见: 备考系统架构设计师-专栏介绍和目录 文章目录 摘要正文摘要 2021年5月,我参加了某市人才集团信息化集中项目的建设。在该项目中,我担任系统架构师。该项目合同金额为 523.5 万元,建设工期为8个月,项目建设内容包含新建一个门户网站、新建4个子系统以及集成…

腾讯为什么支持开源?

今天看到一条新闻&#xff0c;感觉腾讯在 AI 大模型方面确实挺厉害的&#xff0c;符合它低调务实的风格&#xff0c;在不知不觉中一天竟然开源了两个核心的&#xff0c;重要的 AI 大模型。 据新闻报道&#xff0c;11月 5 日&#xff0c;腾讯混元宣布最新的 MoE 模型“混元 Larg…

window10解决 docker is starting 问题

win10 需要开启 Hyper-V。 在程序和功能中开启服务Server (不开启的话&#xff0c;安装完会报错) 安装toolbox 最新版 Toolbox 下载地址&#xff1a; 访问 https://www.docker.com/get-started&#xff0c;注册一个账号&#xff0c;然后登录。 点击 Get started with Docke…

Python并发编程库:Asyncio的异步编程实战

Python并发编程库&#xff1a;Asyncio的异步编程实战 在现代应用中&#xff0c;并发和高效的I/O处理是影响系统性能的关键因素之一。Python的asyncio库是专为异步编程设计的模块&#xff0c;提供了一种更加高效、易读的并发编程方式&#xff0c;适用于处理大量的I/O密集型任务…

Flutter 鸿蒙next版本:自定义对话框与表单验证的动态反馈与错误处理

在现代移动应用开发中&#xff0c;用户体验是至关重要的一环。Flutter和鸿蒙操作系统&#xff08;HarmonyOS&#xff09;的结合&#xff0c;为开发者提供了一个强大的平台&#xff0c;以创建跨平台、高性能的应用程序。本文将探讨如何在Flutter与鸿蒙next版本中创建自定义对话框…

Spring Boot 项目启动时打印端口号、项目名及访问地址

背景 在开发过程中&#xff0c;我们经常需要在项目启动时知道应用使用的端口号和访问地址。为了提高开发效率&#xff0c;我们可以通过简单的配置&#xff0c;在项目启动后直接把这些信息打印在控制台上。 解决方案 通过使用 Environment 类&#xff0c;可以在项目启动时获取…

[docker]拉取镜像失败

List item vim /etc/docker/daemon.json在insecure-registries中添加你的socket&#xff08;IP端口号&#xff09; {"bip": "33.33.33.1/24","registry-mirrors": ["https://docker.mirrors.ustc.edu.cn/"],"insecure-registri…

Git Bash 常用命令

关于版本控制&#xff0c;前几年&#xff0c;我认为只使用 TortoiseGit 图形界面就足够了&#xff0c;而现在&#xff0c;我改变了这个看法。 Git Bash 在某些情况下要方便的多&#xff0c;比如“创建分支并切换到新分支”、“切换分支”、“删除分支”等。 另一个方面&#xf…