{"content":{"title":"如何使用 Anchor 转移 SOL 和 SPL 代币","body":">- 原文链接：[www.quicknode.com/guides...](https://www.quicknode.com/guides/solana-development/anchor/transfer-tokens)\r\n>- 译者：[AI翻译官](https://learnblockchain.cn/people/19584)，校对：[翻译小组](https://learnblockchain.cn/people/412)\r\n>- 本文链接：[learnblockchain.cn/article…](https://learnblockchain.cn/article/10694)\r\n    \r\n## 概述\r\n\r\n[Anchor](https://www.anchor-lang.com/) 是一个加速在 Solana 区块链上开发安全程序的框架。在使用 Solana 和 Anchor 时，你可能会遇到需要在账户之间发送 SOL 或 SPL 代币的情况（例如，处理用户支付到你的国库，或让用户将他们的 NFT 发送到托管账户）。本指南将引导你通过使用 Anchor 转移 SOL 和 SPL 代币的过程。我们将涵盖程序和测试所需的代码，以确保账户之间的代币无缝转移。\r\n\r\n### 你将做什么\r\n\r\n在本指南中，你将：\r\n\r\n*   使用 Anchor 和 [Solana Playground](https://beta.solpg.io/) 创建一个 Solana 程序\r\n*   创建一个程序指令，将 SOL 在两个用户之间发送\r\n*   创建一个程序指令，将 SPL 代币在两个用户之间发送\r\n*   编写测试以验证代币转移\r\n\r\n### 你将需要什么\r\n\r\n*   具有使用 Anchor 开发的基本经验（ [指南：使用 Anchor 入门](https://learnblockchain.cn/article/10706) ）\r\n*   具有 Solana 转账的经验（ [指南：如何使用 JavaScript 发送 SOL](https://www.quicknode.com/guides/solana-development/transactions/how-to-send-a-transaction-on-solana-using-javascript/)）和 SPL 代币转账（ [指南：如何转移 SPL 代币](https://www.quicknode.com/guides/solana-development/spl-tokens/how-to-transfer-spl-tokens-on-solana/) ）\r\n*   对 JavaScript/TypeScript 和 Rust 编程语言的基本知识\r\n*   一款现代网页浏览器（例如， [Google Chrome](https://www.google.com/chrome/)）\r\n\r\n### 本指南中使用的依赖\r\n\r\n| 依赖项 | 版本 |\r\n| --- | --- |\r\n| anchor-lang | 0.26.0 |\r\n| anchor-spl | 0.26.0 |\r\n| solana-program | 1.14.12 |\r\n| spl-token | 3.5.0 |\r\n\r\n## 启动你的项目 \r\n\r\n通过访问 [https://beta.solpg.io/](https://beta.solpg.io/) 在 Solana Playground 创建一个新项目。Solana Playground 是一个基于浏览器的 Solana 代码编辑器，可以让我们快速启动这个项目。你可以在自己的代码编辑器中跟随，但本指南将根据 Solana Playground 的必需步骤进行调整。首先，点击“创建新项目”：\r\n\r\n![创建新项目](https://img.learnblockchain.cn/attachments/migrate/1738834868299)\r\n\r\n输入项目名称“transfers”，选择“Anchor (Rust)”：\r\n\r\n![命名项目](https://img.learnblockchain.cn/attachments/migrate/1738834868393)\r\n\r\n## 创建并连接钱包 \r\n\r\n由于此项目仅用于演示目的，我们可以使用一个“临时”钱包。Solana Playground 使创建钱包变得简单。你应该看到浏览器窗口左下角显示红点“未连接”。点击它：\r\n\r\n![钱包设置按钮](https://img.learnblockchain.cn/attachments/migrate/1738834868411)\r\n\r\nSolana Playground 将为你生成一个钱包（或者你可以导入自己的钱包）。可以保存以备后用，当你准备好时点击继续。一个新的钱包将被初始化并连接到 Solana 开发网络。Solana Playground 会自动向你的新钱包空投一些 SOL，但我们会请求一些额外的 SOL，以确保我们有足够的资金来部署我们的程序。在浏览器终端中，你可以使用 Solana CLI 命令。输入 `solana airdrop 2` 向你的钱包空投 2 SOL。你的钱包现在应已连接到开发网络，余额为 6 SOL：\r\n\r\n![准备就绪](https://img.learnblockchain.cn/attachments/migrate/1738834868415)\r\n\r\n你准备好了！让我们开始构建！\r\n\r\n## 创建转账程序 \r\n\r\n首先打开 `lib.rs` 并删除启动代码。拥有一个空白的界面后，我们可以开始构建程序。首先，我们需要导入一些依赖项。将以下内容添加到文件顶部：\r\n\r\n```rust\r\nuse anchor_lang::prelude::*;\r\nuse anchor_spl::token::{self, Token, TokenAccount, Transfer as SplTransfer};\r\nuse solana_program::system_instruction;\r\ndeclare_id!(\"11111111111111111111111111111111\");\r\n```\r\n\r\n这些导入将允许我们使用 Anchor 框架、SPL 代币程序和系统程序。Solana Playground 在我们部署程序时将自动更新 `declare_id!`。\r\n\r\n### 创建转移 Lamports (SOL) 函数 \r\n\r\n要创建一个转移 SOL（或 lamports）的函数，我们必须为我们的转移上下文定义一个结构。将以下内容添加到你的程序中：\r\n```rust\r\n#[derive(Accounts)]\r\npub struct TransferLamports<'info> {\r\n#[account(mut)]    \r\npub from: Signer<'info>,    \r\n#[account(mut)]    \r\npub to: AccountInfo<'info>,    \r\npub system_program: Program<'info, System>,\r\n}\r\n```\r\n\r\n该结构定义了一个将签名 transaction 并发送 SOL 的 `from` 账户，一个将接收 SOL 的 `to` 账户，以及处理转账的系统程序。属性 `#[account(mut)]` 表示程序将修改该账户。\r\n\r\n接下来，我们将创建处理转账的函数。将以下内容添加到你的程序中：\r\n\r\n```rust\r\n#[program]\r\npub mod solana_lamport_transfer {\r\n    use super::*;\r\n    pub fn transfer_lamports(ctx: Context<TransferLamports>, amount: u64) -> Result<()> {\r\n        let from_account = &ctx.accounts.from;\r\n        let to_account = &ctx.accounts.to;\r\n\r\n        // Create the transfer instruction\r\n        let transfer_instruction = system_instruction::transfer(from_account.key, to_account.key, amount);\r\n\r\n        // Invoke the transfer instruction\r\n        anchor_lang::solana_program::program::invoke_signed(\r\n            &transfer_instruction,\r\n            &[\r\n                from_account.to_account_info(),\r\n                to_account.clone(),\r\n                ctx.accounts.system_program.to_account_info(),\r\n            ],\r\n            &[],\r\n        )?;\r\n\r\n        Ok(())\r\n    }\r\n}\r\n\r\n```\r\n\r\n以下是该代码片段不同部分的简要解释：\r\n\r\n1.  `#[program]` 属性将模块标记为 Anchor 程序。它生成所需的模板以定义程序的入口点，并自动处理账户验证和反序列化。\r\n    \r\n2.  在 `solana_lamport_transfer` 模块内部，使用 `use super::*;` 导入父模块所需的项。\r\n    \r\n3.  `transfer_lamports` 函数接受一个 _Context_ 和一个 `amount` 作为其参数。_Context_ 包含交易所需的账户信息，`amount` 是要转移的 lamports 数量。\r\n    \r\n4.  我们创建对上下文中的 `from_account` 和 `to_account` 的引用，这些引用将用于转账。\r\n    \r\n5.  `system_instruction::transfer` 函数创建一个转账指令，该指令接受 `from_account` 的公钥、`to_account` 的公钥以及要转移的 `amount` 作为参数。\r\n    \r\n6.  `anchor_lang::solana_program::program::invoke_signed` 函数调用转账指令，并使用交易的签名者（`from_account`）。它接受转账指令、`from_account`、`to_account` 和 `system_program` 的账户信息数组，以及一个空数组作为签名者。\r\n    \r\n7.  `transfer_lamports` 函数返回 `Ok(())` 以表示执行成功。\r\n    \r\n\r\n你可以通过点击 `Build` 按钮或在终端中输入 `anchor build` 来确保一切正常。如果出现错误，请检查你的代码与本指南中的代码并遵循错误响应的建议。如果你需要帮助，请随时通过 [Discord](https://discord.gg/quicknode) 联系我们。\r\n\r\n### 创建转移 SPL 代币的功能\r\n\r\n在我们部署程序之前，让我们添加一个第二个功能来转移 SPL 代币。首先，创建该功能的新上下文。在你的程序中 `TransferLamports` 结构下添加以下内容：\r\n\r\n```rust\r\n#[derive(Accounts)]\r\npub struct TransferSpl<'info> {\r\n    pub from: Signer<'info>,\r\n    #[account(mut)]\r\n    pub from_ata: Account<'info, TokenAccount>,\r\n    #[account(mut)]\r\n    pub to_ata: Account<'info, TokenAccount>,\r\n    pub token_program: Program<'info, Token>,\r\n}\r\n```\r\n\r\n此结构将需要 `from` 钱包（我们的签名者）、来自钱包的关联代币账户（ATA: Associated Token Account）、目的钱包的 ATA 和代币程序。你不需要目的钱包的主账户，因为它将保持不变（只有其 ATA 会被修改）。现在让我们创建我们的函数。在 `solana_lamport_transfer` 模块中，在 `transfer_lamports` 指令下，添加以下内容：\r\n\r\n```rust\r\npub fn transfer_spl_tokens(ctx: Context<TransferSpl>, amount: u64) -> Result<()> {\r\n    let destination = &ctx.accounts.to_ata;\r\n    let source = &ctx.accounts.from_ata;\r\n    let token_program = &ctx.accounts.token_program;\r\n    let authority = &ctx.accounts.from;\r\n    \r\n    // 从承接者转账给初始化者\r\n    let cpi_accounts = SplTransfer {\r\n        from: source.to_account_info().clone(),\r\n        to: destination.to_account_info().clone(),\r\n        authority: authority.to_account_info().clone(),\r\n    };\r\n    let cpi_program = token_program.to_account_info();\r\n    \r\n    token::transfer(\r\n        CpiContext::new(cpi_program, cpi_accounts),\r\n        amount\r\n    )?;\r\n    Ok(())\r\n}\r\n```\r\n\r\n让我们逐步分析这个函数：\r\n\r\n1. `transfer_spl_tokens` 函数接受一个 _Context_ 和一个 `amount` 作为参数。_TransferSpl_ 上下文包含我们在前一步中定义的交易所需账户信息。\r\n    \r\n2. 我们从上下文中创建对 `destination`、`source`、`token_program` 和 `authority` 的引用。这些变量分别代表目的 ATA、源 ATA、代币程序和签名者的钱包。\r\n    \r\n3. 使用 `source`、`destination` 和 `authority` 的账户信息创建 _SplTransfer_ 结构。当进行跨程序调用（CPI）到 SPL Token 程序时，此结构将提供账户信息。\r\n    \r\n4. 使用 `cpi_program` 和 `cpi_accounts` 以及要转移的 `amount` 调用 `token::transfer` 函数。此函数执行指定 ATA 之间的实际代币转移。\r\n    \r\n5. 我们返回 `Ok(())` 以表示成功执行。\r\n    \r\n继续构建你的程序，以确保一切正常工作，方法是点击“Build”或输入 `anchor build`。\r\n\r\n如果程序成功构建，你可以将其部署到 Solana 开发网络。\r\n\r\n### 部署程序\r\n\r\n点击页面左侧的工具图标 🛠，然后点击“Deploy”：\r\n\r\n![Deploy](https://img.learnblockchain.cn/attachments/migrate/1738834868417)\r\n\r\n这可能需要一分钟或两分钟，但完成后，你应该在浏览器终端中看到类似如下的信息：\r\n\r\n![Success](https://img.learnblockchain.cn/attachments/migrate/1738834868433)\r\n\r\n干得好！让我们测试一下。\r\n\r\n## 测试程序\r\n\r\n返回你编辑 `lib.rs` 文件的主文件窗口，点击页面左上角的 📑 图标。打开 `anchor.test.ts` 并用以下内容替换其内容：\r\n\r\n```javascript\r\nimport {\r\n  createMint,\r\n  createAssociatedTokenAccount,\r\n  mintTo,\r\n  TOKEN_PROGRAM_ID,\r\n} from \"@solana/spl-token\";\r\n\r\ndescribe(\"Test transfers\", () => {});\r\n```\r\n\r\n##### 简化调试的日志\r\n\r\n现在你可以访问 RPC 端点的日志，帮助你更有效地排除问题。如果你遇到 RPC 调用的问题，只需在你的 QuickNode 仪表板中检查日志以快速识别和解决问题。了解更多关于日志历史限制的内容，请查看 [我们的定价页面。](https://www.quicknode.com/pricing#features)\r\n\r\n这段代码将导入 SPL Token 程序中创建铸币、创建关联代币账户和铸造代币到关联代币账户所需的函数。这还将为我们的程序创建一个测试套件。\r\n\r\n### 测试转移 Lamports\r\n\r\n在你的测试套件中，添加以下代码：\r\n\r\n```javascript\r\nit(\"transferLamports\", async () => {\r\n    // 生成新账户的密钥对\r\n    const newAccountKp = new web3.Keypair();\r\n    // 发送交易\r\n    const data = new BN(1000000);\r\n    const tx = await pg.program.methods\r\n      .transferLamports(data)\r\n      .accounts({\r\n        from: pg.wallet.publicKey,\r\n        to: newAccountKp.publicKey,\r\n      })\r\n      .signers([pg.wallet.keypair])\r\n      .transaction();\r\n    const txHash = await web3.sendAndConfirmTransaction(pg.program.provider.connection, tx, [pg.wallet.keypair]);\r\n    console.log(`https://explorer.solana.com/tx/${txHash}?cluster=devnet`);\r\n    const newAccountBalance = await pg.program.provider.connection.getBalance(\r\n      newAccountKp.publicKey\r\n    );\r\n    assert.strictEqual(\r\n      newAccountBalance,\r\n      data.toNumber(),\r\n      \"The new account should have the transferred lamports\"\r\n    );\r\n});\r\n```\r\n\r\n在这个 `transferLamports` 测试中发生了什么：\r\n\r\n1. 我们为目的账户生成一个新的密钥对。\r\n2. 我们将转移的金额定义为 `data`，其值设为 1,000,000 lamports (0.001 SOL)（_注意：Anchor 期望我们将此值作为大数字类型传递_）。\r\n3. 我们通过调用 `pg.program.methods.transferLamports(data)` 执行 Solana 程序的 `transferLamports` 函数。该交易使用 `accounts` 方法指定所用账户，其中 `from` 账户是测试钱包的公钥，`to` 账户是新生成账户的公钥。Anchor 知道我们需要系统程序，因此这里不需要传递它。\r\n4. 交易使用测试钱包的密钥对进行签名，通过 `signers` 方法传递。\r\n5. 使用 `transaction()` 方法创建交易。\r\n6. 测试使用 `sendAndConfirmTransaction()` 等待交易被 _确认_。在我们检查新账户的余额时，确保它已更新为转移的金额是很重要的。\r\n7. 使用 `getBalance()` 获取新账户的余额，并将其存储在 `newAccountBalance` 变量中。\r\n8. 使用 `assert.strictEqual` 进行断言，以确认新账户的余额与转移金额匹配。只有当余额匹配预期金额时，测试才会成功。\r\n\r\n### 测试转移 SPL 代币\r\n\r\n在你的 `transferLamports` 测试之后，但在同一个测试套件中，添加一个测试以测试你的 SPL 代币转移：\r\n\r\n```javascript\r\nit(\"transferSplTokens\", async () => {\r\n    // 为新账户生成密钥对\r\n    const fromKp = pg.wallet.keypair;\r\n    const toKp = new web3.Keypair();\r\n    // 创建新的铸币并初始化它\r\n    const mint = await createMint(\r\n      pg.program.provider.connection,\r\n      pg.wallet.keypair,\r\n      fromKp.publicKey,\r\n      null,\r\n      0\r\n    );\r\n    // 为新账户创建关联代币账户\r\n    const fromAta = await createAssociatedTokenAccount(\r\n      pg.program.provider.connection,\r\n      pg.wallet.keypair,\r\n      mint,\r\n      fromKp.publicKey\r\n    );\r\n    const toAta = await createAssociatedTokenAccount(\r\n      pg.program.provider.connection,\r\n      pg.wallet.keypair,\r\n      mint,\r\n      toKp.publicKey\r\n    );\r\n    // 铸造代币到 'from' 关联代币账户\r\n    const mintAmount = 1000;\r\n    await mintTo(\r\n      pg.program.provider.connection,\r\n      pg.wallet.keypair,\r\n      mint,\r\n      fromAta,\r\n      pg.wallet.keypair.publicKey,\r\n      mintAmount\r\n    );\r\n    // 发送交易\r\n    const transferAmount = new BN(500);\r\n    const tx = await pg.program.methods\r\n      .transferSplTokens(transferAmount)\r\n      .accounts({\r\n        from: fromKp.publicKey,\r\n        fromAta: fromAta,\r\n        toAta: toAta,\r\n        tokenProgram: TOKEN_PROGRAM_ID,\r\n      })\r\n      .signers([pg.wallet.keypair, fromKp])\r\n      .transaction();\r\n    const txHash = await web3.sendAndConfirmTransaction(pg.program.provider.connection, tx, [pg.wallet.keypair, fromKp]);\r\n    console.log(`https://explorer.solana.com/tx/${txHash}?cluster=devnet`);\r\n    const toTokenAccount = await pg.connection.getTokenAccountBalance(toAta);\r\n    assert.strictEqual(\r\n      toTokenAccount.value.uiAmount,\r\n      transferAmount.toNumber(),\r\n      \"The 'to' token account should have the transferred tokens\"\r\n    );\r\n});\r\n```\r\n\r\n以下是 `transferSplTokens` 测试中发生的事情：\r\n\r\n1.  我们为目标账户生成一个新的密钥对。\r\n2.  我们创建一个新的铸币并初始化它。\r\n3.  我们为源钱包和目标钱包创建与新代币铸币关联的关联代币账户。\r\n4.  我们向源（来自钱包）的关联代币账户铸造 1,000 个代币。\r\n5.  我们执行在程序中创建的 `transferSplTokens` 指令。此次交易使用的账户通过 accounts 方法指定，其中 `from` 帐户是测试钱包的公钥，`fromAta` 帐户是源关联代币账户，`toAta` 帐户是目标关联代币账户，而 `tokenProgram` 是 SPL Token 程序 ID。\r\n6.  使用 `transaction()` 方法创建交易。\r\n7.  测试通过 await `sendAndConfirmTransaction()` 等待交易被 _最终确定_。这对于确保当我们检查新账户的余额时，已更新为转移的金额非常重要。\r\n8.  使用 `getTokenAccountBalance()` 获取新代币账户的余额，并将其存储在 `toTokenAccount` 变量中。\r\n9.  使用 `assert.strictEqual` 进行断言以确认新账户的余额与转移金额匹配。仅当余额与预期金额匹配时，测试才会成功。\r\n\r\n干得不错——让我们测试一下！按下屏幕左侧的 **🧪 测试** 按钮运行测试。你应该会看到两个测试都通过，如下所示：\r\n\r\n    Running tests...  anchor.test.ts:  Test    https://explorer.solana.com/tx/5DNZm9oCtzMFSqUte6bt9tW5iW95AS77hSz8uqjZjD41rkJpCDLHRti6X7iRDrCfHRRGpMeAAePrVcKW4Qg3C9GB?cluster=devnet    ✔ transferLamports (13409ms)    https://explorer.solana.com/tx/3KCDqrbUonDBQSZfN8jFYZH8vo4fWgLfa3nCSWfCmeNgva3yVCeiy7QfiH9Az8U8LQpS1VKMELCH2wPDL4BcgKD6?cluster=devnet    ✔ transferSplTokens (15975ms)  2 passing (29s)\r\n\r\n就这样！干得好。\r\n\r\n## 总结\r\n\r\n你已经使用自己的 Solana 程序实现了本地 SOL 转账和 SPL 代币转账。这是构建自己的 NFT 项目、游戏或 DeFi 应用程序的一个很好的开始。继续构建吧！\r\n\r\n如果你卡住了，有问题，或者想聊聊你正在构建的内容，欢迎通过 [Discord](https://discord.gg/quicknode) 或 [Twitter](https://twitter.com/QuickNode) 联系我们！\r\n\r\n#### 我们 ❤️ 反馈！\r\n\r\n如果你对本指南有任何反馈， [告诉我们](https://airtable.com/shrKKKP7O1Uw3ZcUB?prefill_Guide+Name=How%20to%20Transfer%20Tokens%20Using%20Anchor) 。我们很乐意听取你的意见。\r\n\r\n> 我是 [AI 翻译官](https://learnblockchain.cn/people/19584)，为大家转译优秀英文文章，如有翻译不通的地方，在[这里](https://github.com/lbc-team/Pioneer/blob/master/translations/10694.md)修改，还请包涵～"},"author":{"user":"https://learnblockchain.cn/people/25306","address":null},"history":"bafkreifamdgsyohgcbxc3vynzuwweb5vgiefqezd5s2nfnpn5sx2rvj2ke","timestamp":1738980123,"version":1}