您正在查看: 2025年2月

如何使用 Anchor 转移 SOL 和 SPL 代币

概述

Anchor 是一个加速在 Solana 区块链上开发安全程序的框架。在使用 Solana 和 Anchor 时,你可能会遇到需要在账户之间发送 SOL 或 SPL 代币的情况(例如,处理用户支付到你的国库,或让用户将他们的 NFT 发送到托管账户)。本指南将引导你通过使用 Anchor 转移 SOL 和 SPL 代币的过程。我们将涵盖程序和测试所需的代码,以确保账户之间的代币无缝转移。

你将做什么

在本指南中,你将:

  • 使用 Anchor 和 Solana Playground 创建一个 Solana 程序
  • 创建一个程序指令,将 SOL 在两个用户之间发送
  • 创建一个程序指令,将 SPL 代币在两个用户之间发送
  • 编写测试以验证代币转移

你将需要什么

本指南中使用的依赖

依赖项 版本
anchor-lang 0.26.0
anchor-spl 0.26.0
solana-program 1.14.12
spl-token 3.5.0

启动你的项目

通过访问 https://beta.solpg.io/ 在 Solana Playground 创建一个新项目。Solana Playground 是一个基于浏览器的 Solana 代码编辑器,可以让我们快速启动这个项目。你可以在自己的代码编辑器中跟随,但本指南将根据 Solana Playground 的必需步骤进行调整。首先,点击“创建新项目”:

输入项目名称“transfers”,选择“Anchor (Rust)”:

创建并连接钱包

由于此项目仅用于演示目的,我们可以使用一个“临时”钱包。Solana Playground 使创建钱包变得简单。你应该看到浏览器窗口左下角显示红点“未连接”。点击它:

Solana Playground 将为你生成一个钱包(或者你可以导入自己的钱包)。可以保存以备后用,当你准备好时点击继续。一个新的钱包将被初始化并连接到 Solana 开发网络。Solana Playground 会自动向你的新钱包空投一些 SOL,但我们会请求一些额外的 SOL,以确保我们有足够的资金来部署我们的程序。在浏览器终端中,你可以使用 Solana CLI 命令。输入 solana airdrop 2 向你的钱包空投 2 SOL。你的钱包现在应已连接到开发网络,余额为 2 SOL:
https://explorer.solana.com/address/DGbfDHnXsQSyswLjkEkadxjiJmK3SUfrPueokNHYXEpi?cluster=devnet
你准备好了!让我们开始构建!

创建转账程序

首先打开 lib.rs 并删除启动代码。拥有一个空白的界面后,我们可以开始构建程序。首先,我们需要导入一些依赖项。将以下内容添加到文件顶部:

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};
use solana_program::system_instruction;
declare_id!("11111111111111111111111111111111");

这些导入将允许我们使用 Anchor 框架、SPL 代币程序和系统程序。Solana Playground 在我们部署程序时将自动更新 declare_id!。

创建转移 Lamports (SOL) 函数

要创建一个转移 SOL(或 lamports)的函数,我们必须为我们的转移上下文定义一个结构。将以下内容添加到你的程序中:

#[derive(Accounts)]
pub struct TransferLamports<'info> {
    #[account(mut)]
    pub from: Signer<'info>,
    #[account(mut)]
    pub to: AccountInfo<'info>,
    pub system_program: Program<'info, System>,
}

该结构定义了一个将签名 transaction 并发送 SOL 的 from 账户,一个将接收 SOL 的 to 账户,以及处理转账的系统程序。属性 #[account(mut)] 表示程序将修改该账户。
接下来,我们将创建处理转账的函数。将以下内容添加到你的程序中:

#[program]
pub mod solana_lamport_transfer {
    use super::*;
    pub fn transfer_lamports(ctx: Context<TransferLamports>, amount: u64) -> Result<()> {
        let from_account = &ctx.accounts.from;
        let to_account = &ctx.accounts.to;

        // Create the transfer instruction
        let transfer_instruction =
            system_instruction::transfer(from_account.key, to_account.key, amount);

        // Invoke the transfer instruction
        anchor_lang::solana_program::program::invoke_signed(
            &transfer_instruction,
            &[
                from_account.to_account_info(),
                to_account.clone(),
                ctx.accounts.system_program.to_account_info(),
            ],
            &[],
        )?;

        Ok(())
    }
}

以下是该代码片段不同部分的简要解释:

  1. #[program] 属性将模块标记为 Anchor 程序。它生成所需的模板以定义程序的入口点,并自动处理账户验证和反序列化。
  2. solana_lamport_transfer 模块内部,使用 use super::*; 导入父模块所需的项。
  3. transfer_lamports 函数接受一个 Context 和一个 amount 作为其参数。Context 包含交易所需的账户信息,amount 是要转移的 lamports 数量。
  4. 我们创建对上下文中的 from_accountto_account 的引用,这些引用将用于转账。
  5. system_instruction::transfer 函数创建一个转账指令,该指令接受 from_account 的公钥、to_account 的公钥以及要转移的 amount 作为参数。
  6. anchor_lang::solana_program::program::invoke_signed 函数调用转账指令,并使用交易的签名者(from_account)。它接受转账指令、from_accountto_accountsystem_program 的账户信息数组,以及一个空数组作为签名者。
  7. transfer_lamports 函数返回 Ok(()) 以表示执行成功。

你可以通过点击 Build 按钮或在终端中输入 anchor build 来确保一切正常。如果出现错误,请检查你的代码与本指南中的代码并遵循错误响应的建议。

创建转移 SPL 代币的功能

在我们部署程序之前,让我们添加一个第二个功能来转移 SPL 代币。首先,创建该功能的新上下文。在你的程序中 TransferLamports 结构下添加以下内容

#[derive(Accounts)]
pub struct TransferSpl<'info> {
    pub from: Signer<'info>,
    #[account(mut)]
    pub from_ata: Account<'info, TokenAccount>,
    #[account(mut)]
    pub to_ata: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
}

此结构将需要 from 钱包(我们的签名者)、来自钱包的关联代币账户(ATA: Associated Token Account)、目的钱包的 ATA 和代币程序。你不需要目的钱包的主账户,因为它将保持不变(只有其 ATA 会被修改)。现在让我们创建我们的函数。在 solana_lamport_transfer 模块中,在 transfer_lamports 指令下,添加以下内容:

pub fn transfer_spl_tokens(ctx: Context<TransferSpl>, amount: u64) -> Result<()> {
    let destination = &ctx.accounts.to_ata;
    let source = &ctx.accounts.from_ata;
    let token_program = &ctx.accounts.token_program;
    let authority = &ctx.accounts.from;

    // 从承接者转账给初始化者
    let cpi_accounts = SplTransfer {
        from: source.to_account_info().clone(),
        to: destination.to_account_info().clone(),
        authority: authority.to_account_info().clone(),
    };
    let cpi_program = token_program.to_account_info();

    token::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)?;
    Ok(())
}

让我们逐步分析这个函数:

  1. transfer_spl_tokens 函数接受一个 Context 和一个 amount 作为参数。TransferSpl 上下文包含我们在前一步中定义的交易所需账户信息。
  2. 我们从上下文中创建对 destinationsourcetoken_programauthority 的引用。这些变量分别代表目的 ATA、源 ATA、代币程序和签名者的钱包。
  3. 使用 sourcedestinationauthority 的账户信息创建 SplTransfer 结构。当进行跨程序调用(CPI)到 SPL Token 程序时,此结构将提供账户信息。
  4. 使用 cpi_programcpi_accounts 以及要转移的 amount 调用 token::transfer 函数。此函数执行指定 ATA 之间的实际代币转移。
  5. 我们返回 Ok(()) 以表示成功执行。

继续构建你的程序,以确保一切正常工作,方法是点击“Build”或输入 anchor build

如果程序成功构建,你可以将其部署到 Solana 开发网络。

部署程序

点击页面左侧的工具图标,然后点击“Deploy”:

当前测试时至少需要5 SOL, 如果不足,使用https://faucet.solana.com/ 进行领水

这可能需要一分钟或两分钟,但完成后,你应该在浏览器终端中看到类似如下的信息:

Program ID:4d39PLXUXdtmWN571CUETGpvnXgf7iAQ35WYSX5J5kGX

干得好!让我们测试一下。

测试程序

返回你编辑 lib.rs 文件的主文件窗口,点击页面左上角的图标。打开 anchor.test.ts 并用以下内容替换其内容:

import {
  createMint,
  createAssociatedTokenAccount,
  mintTo,
  TOKEN_PROGRAM_ID,
} from "@solana/spl-token";

describe("Test transfers", () => {});

测试转移 Lamports

在你的测试套件中,添加以下代码:

describe("Test transfers", () => {
  it("transferLamports", async () => {
    // 生成新账户的密钥对
    const newAccountKp = new web3.Keypair();
    // 发送交易
    const data = new BN(1000000);
    const tx = await pg.program.methods
      .transferLamports(data)
      .accounts({
        from: pg.wallet.publicKey,
        to: newAccountKp.publicKey,
      })
      .signers([pg.wallet.keypair])
      .transaction();
    const txHash = await web3.sendAndConfirmTransaction(
      pg.program.provider.connection,
      tx,
      [pg.wallet.keypair]
    );
    console.log(`https://explorer.solana.com/tx/${txHash}?cluster=devnet`);
    const newAccountBalance = await pg.program.provider.connection.getBalance(
      newAccountKp.publicKey
    );
    assert.strictEqual(
      newAccountBalance,
      data.toNumber(),
      "The new account should have the transferred lamports"
    );
  });
});


https://explorer.solana.com/tx/126HvQaMXvPYoJQbwZLk68rmPf98uMvmV7ucqd6KtypD3xNqUYh5XjD6VVsVsXHsysKvgFQrTC1ER1SGND1LHaQ4?cluster=devnet

在这个 transferLamports 测试中发生了什么:

  1. 我们为目的账户生成一个新的密钥对。
  2. 我们将转移的金额定义为 data,其值设为 1,000,000 lamports (0.001 SOL)(注意:Anchor 期望我们将此值作为大数字类型传递)。
  3. 我们通过调用 pg.program.methods.transferLamports(data) 执行 Solana 程序的 transferLamports 函数。该交易使用 accounts 方法指定所用账户,其中 from 账户是测试钱包的公钥,to 账户是新生成账户的公钥。Anchor 知道我们需要系统程序,因此这里不需要传递它。
  4. 交易使用测试钱包的密钥对进行签名,通过 signers 方法传递。
  5. 使用 transaction() 方法创建交易。
  6. 测试使用 sendAndConfirmTransaction() 等待交易被 确认。在我们检查新账户的余额时,确保它已更新为转移的金额是很重要的。
  7. 使用 getBalance() 获取新账户的余额,并将其存储在 newAccountBalance 变量中。
  8. 使用 assert.strictEqual 进行断言,以确认新账户的余额与转移金额匹配。只有当余额匹配预期金额时,测试才会成功。

测试转移 SPL 代币

在你的 transferLamports 测试之后,但在同一个测试套件中,添加一个测试以测试你的 SPL 代币转移:

describe("Test transfers", () => {
  it("transferSplTokens", async () => {
    // 为新账户生成密钥对
    const fromKp = pg.wallet.keypair;
    const toKp = new web3.Keypair();
    // 创建新的铸币并初始化它
    const mint = await createMint(
      pg.program.provider.connection,
      pg.wallet.keypair,
      fromKp.publicKey,
      null,
      0
    );
    // 为新账户创建关联代币账户
    const fromAta = await createAssociatedTokenAccount(
      pg.program.provider.connection,
      pg.wallet.keypair,
      mint,
      fromKp.publicKey
    );
    const toAta = await createAssociatedTokenAccount(
      pg.program.provider.connection,
      pg.wallet.keypair,
      mint,
      toKp.publicKey
    );
    // 铸造代币到 'from' 关联代币账户
    const mintAmount = 1000;
    await mintTo(
      pg.program.provider.connection,
      pg.wallet.keypair,
      mint,
      fromAta,
      pg.wallet.keypair.publicKey,
      mintAmount
    );
    // 发送交易
    const transferAmount = new BN(500);
    const tx = await pg.program.methods
      .transferSplTokens(transferAmount)
      .accounts({
        from: fromKp.publicKey,
        fromAta: fromAta,
        toAta: toAta,
        tokenProgram: TOKEN_PROGRAM_ID,
      })
      .signers([pg.wallet.keypair, fromKp])
      .transaction();
    const txHash = await web3.sendAndConfirmTransaction(
      pg.program.provider.connection,
      tx,
      [pg.wallet.keypair, fromKp]
    );
    console.log(`https://explorer.solana.com/tx/${txHash}?cluster=devnet`);
    const toTokenAccount = await pg.connection.getTokenAccountBalance(toAta);
    assert.strictEqual(
      toTokenAccount.value.uiAmount,
      transferAmount.toNumber(),
      "The 'to' token account should have the transferred tokens"
    );
  });
});

以下是 transferSplTokens 测试中发生的事情:

  1. 我们为目标账户生成一个新的密钥对。
  2. 我们创建一个新的铸币并初始化它。
  3. 我们为源钱包和目标钱包创建与新代币铸币关联的关联代币账户。
  4. 我们向源(来自钱包)的关联代币账户铸造 1,000 个代币。
  5. 我们执行在程序中创建的 transferSplTokens 指令。此次交易使用的账户通过 accounts 方法指定,其中 from 帐户是测试钱包的公钥,fromAta 帐户是源关联代币账户,toAta 帐户是目标关联代币账户,而 tokenProgram 是 SPL Token 程序 ID。
  6. 使用 transaction() 方法创建交易。
  7. 测试通过 await sendAndConfirmTransaction() 等待交易被 最终确定。这对于确保当我们检查新账户的余额时,已更新为转移的金额非常重要。
  8. 使用 getTokenAccountBalance() 获取新代币账户的余额,并将其存储在 toTokenAccount 变量中。
  9. 使用 assert.strictEqual 进行断言以确认新账户的余额与转移金额匹配。仅当余额与预期金额匹配时,测试才会成功。

干得不错——让我们测试一下!按下屏幕左侧的 测试 按钮运行测试,如下所示:
https://explorer.solana.com/tx/2eAQANiLddDh3fxyqkc28LmVNpWQHNeQUi3waxnu3qoNev9h1juX5npkbprFen3u8Xm8JzpdrstMbBevxcUWcEuB?cluster=devnet

总结

你已经使用自己的 Solana 程序实现了本地 SOL 转账和 SPL 代币转账。这是构建自己的 NFT 项目、游戏或 DeFi 应用程序的一个很好的开始。继续构建吧!

完整代码

use anchor_lang::prelude::*;
use anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};
use solana_program::system_instruction;
declare_id!("4d39PLXUXdtmWN571CUETGpvnXgf7iAQ35WYSX5J5kGX");

#[derive(Accounts)]
pub struct TransferLamports<'info> {
    #[account(mut)]
    pub from: Signer<'info>,
    #[account(mut)]
    pub to: AccountInfo<'info>,
    pub system_program: Program<'info, System>,
}

#[derive(Accounts)]
pub struct TransferSpl<'info> {
    pub from: Signer<'info>,
    #[account(mut)]
    pub from_ata: Account<'info, TokenAccount>,
    #[account(mut)]
    pub to_ata: Account<'info, TokenAccount>,
    pub token_program: Program<'info, Token>,
}

#[program]
pub mod solana_lamport_transfer {
    use super::*;
    pub fn transfer_lamports(ctx: Context<TransferLamports>, amount: u64) -> Result<()> {
        let from_account = &ctx.accounts.from;
        let to_account = &ctx.accounts.to;

        // Create the transfer instruction
        let transfer_instruction =
            system_instruction::transfer(from_account.key, to_account.key, amount);

        // Invoke the transfer instruction
        anchor_lang::solana_program::program::invoke_signed(
            &transfer_instruction,
            &[
                from_account.to_account_info(),
                to_account.clone(),
                ctx.accounts.system_program.to_account_info(),
            ],
            &[],
        )?;

        Ok(())
    }

    pub fn transfer_spl_tokens(ctx: Context<TransferSpl>, amount: u64) -> Result<()> {
        let destination = &ctx.accounts.to_ata;
        let source = &ctx.accounts.from_ata;
        let token_program = &ctx.accounts.token_program;
        let authority = &ctx.accounts.from;

        // 从承接者转账给初始化者
        let cpi_accounts = SplTransfer {
            from: source.to_account_info().clone(),
            to: destination.to_account_info().clone(),
            authority: authority.to_account_info().clone(),
        };
        let cpi_program = token_program.to_account_info();

        token::transfer(CpiContext::new(cpi_program, cpi_accounts), amount)?;
        Ok(())
    }
}

原文:https://www.quicknode.com/guides/solana-development/anchor/transfer-tokens

Gulf Stream:Solana 的无内存池交易转发协议

Solana是全球性能最强的免许可区块链。在 Solana 测试网的当前迭代中,由 200 个物理上不同的节点组成的网络在使用 GPU 运行时支持每秒超过 50,000 笔交易的持续吞吐量。

有 8 项关键创新使得 Solana 网络成为可能:

在这篇博文中,我们将探讨Gulf Stream,这是 Solana 为高性能对抗网络提供的内存池管理解决方案。在后续的博文中,我们将介绍这 7 项关键创新。

内存池详解

内存池是一组已提交但尚未被网络处理的交易。您现在可以查看比特币以太坊内存池。


以字节为单位测量 30 天的比特币内存池。

以交易量衡量的 30 天以太坊内存池

对于比特币和以太坊,未确认交易的数量通常约为 20K-100K,如上所示。内存池的大小(通常以未确认交易的数量来衡量)取决于区块空间的供需。即使在区块链时代的早期,当内存池增加时,也会对整个网络造成严重的瓶颈效应。

那么,Solana 做得更好吗?在不增加网络吞吐量的情况下,Solana 验证器可以管理 100,000 大小的内存池。这意味着,在网络吞吐量为 50,000 TPS 的情况下,100,000 个交易的内存池只需几秒钟即可执行。这就是 Solana 成为世界上性能最高的无许可区块链的原因。

令人印象深刻,对吧?但这种简单的分析忽略了很多重要因素……

以太坊和比特币中的内存池使用八卦协议以对等方式在随机节点之间传播。网络中的节点定期构建代表本地内存池的布隆过滤器,并从网络上的其他节点请求任何与该过滤器不匹配的交易(以及其他一些条件,例如最低费用)。将单个交易传播到网络的其余部分将至少需要 log(N) 步,消耗过滤它所需的带宽、内存和计算资源。

当基准客户端开始每秒生成 100,000 笔交易时,八卦协议就会不堪重负。计算过滤器并在机器之间应用过滤器,同时将所有交易保留在内存中的成本变得高得令人望而却步。领导者(区块生产者)还必须在一个区块中重新传输相同的交易,这意味着每笔交易至少要通过网络传播两次。这既不高效也不实用。

Gulf Stream 简介

我们在 Solana 网络上解决此问题的方法就是将交易缓存和转发推到网络边缘。我们称之为Gulf Stream。由于每个验证者都知道即将上任的领导者的顺序,因此客户端和验证者会提前将交易转发给预期的领导者。这使得验证者可以提前执行交易,减少确认时间,更快地切换领导者,并减少未确认交易池对验证者的内存压力。这种解决方案在具有非确定性领导者的网络中是不可能的

那么它是如何工作的呢?客户端(例如钱包)签署引用特定区块哈希的交易。客户端选择一个已被网络完全确认的较新的区块哈希。大约每 800 毫秒提出一个区块,并且需要指数增加的超时来展开每个附加区块。使用我们的默认超时曲线,在最坏情况下,完全确认的区块哈希是 32 个区块。交易仅在引用区块的子区块中有效,并且仅对 X 个区块有效。虽然 X 尚未最终确定,但我们预计区块哈希的 TTL(生存时间)约为 32 个区块。假设区块时间为 800 毫秒,则相当于 24 秒。

一旦交易被转发给任何验证者,验证者就会将其转发给即将上任的领导者之一。客户端可以订阅来自验证者的交易确认。客户端知道区块哈希会在有限的时间内过期,或者交易已得到网络确认。这允许客户端签署保证执行或失败的交易。一旦网络越过回滚点,交易引用的区块哈希已过期,客户端就可以保证交易现在无效,并且永远不会在链上执行。

这种架构本身具有许多积极的副作用。首先,在负载下,验证者可以提前执行交易并丢弃任何失败的交易。其次,领导者可以根据转发交易的验证者的权益权重对交易进行优先处理。这允许网络在发生大规模拒绝服务时优雅地降级。

到目前为止,区块链网络的功能性取决于其内存池的最小程度,这一点已经变得非常明显。虽然交易吞吐量有限的网络承担着尝试改造全新扩展技术以应对不断增长的内存池的崇高使命,但 Solana 自构思以来就采用了历史证明Gulf StreamSealevel等优化技术,以解决第一代区块链网络的问题并实现巨大的交易吞吐量。从一开始,这就是全球范围内的惊人速度,也是为世界各地的企业、经济体和人民创建高度实用的去中心化基础设施的根本性发展。

原文:https://medium.com/solana-labs/gulf-stream-solanas-mempool-less-transaction-forwarding-protocol-d342e72186ad