跳转至主要内容

如何使用Solana 计划

更新于
2026年8月7日

阅读时间 8 分钟

概述

借助Solana 等工具,查询交易非常方便,但单纯的价值转移往往无法完整反映story。有时,您需要为交易添加背景信息,例如易于理解的备注、发票编号,或是您自身系统中订单的引用。

Solana Memo 程序通过将任意 UTF-8 文本作为交易的一部分写入链上,解决了这一问题。在本指南中,您将使用基于 Pinocchio 的新 Memo 程序,向交易附加备注并将其读取回来(p-memo) 和 Solana 用于客户端代码。


简而言之
  • Memo 程序会将任意的 UTF-8 文本作为交易中的指令写入区块链。
  • p-memo 这是一个基于Pinocchio的Memo程序重构版本,其计算单元消耗仅为原程序的一小部分(约513个CU,而单签名Memo程序则需13,525个CU)。
  • 您将使用Solana 和 solana 请客户发送一份备忘录,内容为 p-memo, 然后从已确认的交易中将其解码回来。

什么是 p-memo?

p-memo 这是基于以下内容重新构建的 SPL Memo 程序: 木偶奇遇记,这是一个用于编写高性能Solana 、无依赖项的 Rust 框架。通过使用低级别的 Pinocchio 原语而非高级抽象, p-memo 这极大地减少了每次备忘操作所需的计算单元(CU):

签署人p-memo (CU)简体中文(大陆)SPL 备忘录(CU)p-memo 成本与 SPL Memo 的对比
02872,022~14%
151313,525~4%
262825,111~3%
374336,406~2%

一份典型的备忘录只有一名签署者(费用支付方),因此实际上你花费的CU约为513,而非13,525,成本大约降低了26倍。当你在计算预算固定的交易中将备忘录与其他指令打包在一起时,这种CU优化就显得尤为重要。

信息

本指南中使用的 Memo 程序版本部署在 Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH. 请注意,该地址与旧版 Memo 程序的地址不同(MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr).

您将负责的工作内容


  • 使用Solana 和 Memo 程序客户端创建一个 TypeScript 项目
  • 使用Solana 生成并为 Devnet 密钥对充值
  • 使用以下方法构建并发送一条附有备注的交易: p-memo
  • 阅读已确认交易的备忘录

您需要准备的物品

本指南假设读者对Solana TypeScript 有基本的了解。如果您是首次使用Solana 发送交易,建议先阅读《如何使用Solana 发送交易》指南作为入门指南。

开始之前,请确保您已准备好以下内容:


  • 已安装Node.js(v24 及以上版本,可直接运行 TypeScript 文件)
  • 已安装Solana ,用于生成密钥对并为其充值
  • 一个用于访问Solana endpoint Quicknode 。如果您还没有,可以在此免费注册一个。
依赖关系版本
节点24.8.0
solana3.0.6
solana6.9.0
solana0.11.1
solana0.10.0
solana0.11.1

设置项目

创建一个新的项目目录,并将其初始化为 Node.js 项目:

mkdir solana-memo && cd solana-memo
npm init -y

安装Solana 、其 RPC 和签名者插件、Memo 程序客户端,以及 Node 类型定义:

npm install @solana/kit@6.9.0 @solana/kit-plugin-rpc@0.11.1 @solana/kit-plugin-signer@0.10.0 @solana-program/memo@0.11.1
npm install --save-dev @types/node@25

设置 "type": "module" 字段在 package.json 因此,Node.js 会将你的代码视为一个 ES 模块:

npm pkg set type=module

创建一个空的 app.ts 文件位于项目根目录下。您将在后续章节中添加事务代码:

touch app.ts

接下来,生成并为密钥对充值——该密钥对将用于签署并支付您的备注交易。

生成并为密钥对充值

您需要一个已充值账户来支付交易费并签署备注。使用Solana 将新密钥对生成到项目本地文件中:

solana-keygen new --no-bip39-passphrase --outfile ./my-keypair.json

向其空投 1 个 Devnet SOL,以便支付手续费:

solana airdrop 1 --keypair ./my-keypair.json --url devnet

注意:Devnet SOL 没有实际价值,仅用于测试。endpoint ,因此如果命令执行失败,请稍等片刻后再试,或者使用网页水龙头。如需了解更多信息,请参阅《Solana测试网 SOL 空投完整指南》

由于这组密钥属于机密信息,请勿将其纳入版本控制。请创建一个 .gitignore 文件,以免您不小心将密钥或环境变量提交到代码库:

.gitignore
node_modules
my-keypair.json
.env

设置Quicknode Endpoint

您可以使用公共Solana ,但公共节点在负载较重时会实施速率限制并出现延迟。若需更快、更可靠的响应,请创建一个Quicknode endpoint:

  1. 在此注册一个免费账户。
  2. 创建一个endpoint,选择 SolanaDevnet网络,然后复制 HTTP 提供商 URL。

Quicknode

在下一步中,您将把此 URL 添加到环境变量中。Solana 会从该 URL 推导出确认交易endpoint WebSocketendpoint ,因此您只需保留 HTTP 提供程序的链接即可。

配置环境变量

与其将endpoint 密钥路径硬编码,不如将它们存储在 .env 项目根目录下的文件:

.env
KEYPAIR_PATH=./my-keypair.json
SOLANA_RPC_URL=https://example.solana-devnet.quiknode.pro/0123456/
MEMO_PROGRAM_ADDRESS=Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH
# Cluster used only for the Explorer link (e.g. devnet, mainnet-beta, testnet).
EXPLORER_CLUSTER=devnet

替换 SOLANA 并填入您上面复制的 HTTP 提供程序 URL。Node.js 可以在运行时通过 --env-file 标志,因此无需额外依赖项。

编写应用程序

打开 app.ts 并分步构建:最上方是一个共享的初始化代码块,然后是两个函数及其执行代码,按顺序依次添加在下方。按照每个步骤操作,最终你将得到完整的文件。

首先进行导入和初始化。该代码会读取环境变量,然后构建一个集成了 RPC 连接、WebSocket 订阅和签名器的 Kit 客户端,并定义一些将在各个函数中重复使用的常量:

app.ts
import {
address,
createClient,
getBase58Encoder,
type Signature,
} from "@solana/kit";
import {
getAddMemoInstruction,
getAddMemoInstructionDataDecoder,
} from "@solana-program/memo";
import { signerFromFile } from "@solana/kit-plugin-signer";
import { solanaRpc } from "@solana/kit-plugin-rpc";

const { KEYPAIR_PATH, SOLANA_RPC_URL, MEMO_PROGRAM_ADDRESS } = process.env;
if (!KEYPAIR_PATH || !SOLANA_RPC_URL || !MEMO_PROGRAM_ADDRESS) {
throw new Error(
"Set KEYPAIR_PATH, SOLANA_RPC_URL, and MEMO_PROGRAM_ADDRESS in your environment"
);
}

// A Kit client bundles the RPC connection, WebSocket subscriptions, and signer.
// signerFromFile loads the keypair JSON as the client's payer/identity, and
// solanaRpc wires up the RPC URL (the subscriptions URL is derived by swapping
// http -> ws). The client also gains a high-level sendTransaction helper.
const client = await createClient()
.use(signerFromFile(KEYPAIR_PATH))
.use(solanaRpc({ rpcUrl: SOLANA_RPC_URL }));

// The memo text to attach to the transaction.
const MEMO_TEXT = "Learning about the Memo program with Quicknode";

// Resolve the program address once, here where the env var is known to be set.
const MEMO_PROGRAM = address(MEMO_PROGRAM_ADDRESS);

// Cluster used only for the Explorer link below. Set EXPLORER_CLUSTER in your
// environment if SOLANA_RPC_URL points at a non-devnet network.
const explorerCluster = process.env.EXPLORER_CLUSTER ?? "devnet";

添加 sendMemo 功能

sendMemo 负责在链上编写备忘录。客户端的 sendTransaction 该辅助函数会为您处理所有底层操作——获取最新的区块哈希、设置手续费支付方、签名、发送以及确认。

将其添加到设置下方:

app.ts
// Send a single Memo instruction and confirm it. The client handles fetching a
// blockhash, setting the fee payer, signing, sending, and confirming. Returns
// the confirmed transaction's signature.
async function sendMemo(memo: string): Promise<Signature> {
const result = await client.sendTransaction(
getAddMemoInstruction(
{ memo, signers: [client.payer] },
{ programAddress: MEMO_PROGRAM }
)
);

return result.context.signature;
}

有几点值得特别指出:


  • client.sendTransaction 封装了整个“构建-签名-发送-确认flow 否则您需要手动使用Solana 底层交易函数来构建flow 。
  • getAddMemoInstruction 来自生成的 solana 客户端。通过 签署人:[客户.付款人] 将缴费人记录为备注说明中的签字人(即上表中的“单一签字人”情况),并且 程序地址:MEMO_PROGRAM 目标 p-memo 而不是旧版程序。
  • 该已确认交易的签名可在以下位置查阅: result.context.signature.

添加 fetchMemo 功能

fetchMemo 通过从链上读取备注内容来验证整个往返过程。它根据签名检索已确认的交易,找到属于 Memo 程序的指令,并将该指令的数据解码回原始文本。

请在下面添加 sendMemo:

app.ts
// Fetch a transaction by signature and decode the plain-text memo it contains.
async function fetchMemo(signature: Signature): Promise<string> {
const transaction = await client.rpc
.getTransaction(signature, {
commitment: "confirmed",
maxSupportedTransactionVersion: 0,
encoding: "json",
})
.send();
if (!transaction) {
throw new Error(`Transaction not found: ${signature}`);
}

const { accountKeys, instructions } = transaction.transaction.message;
const memoInstruction = instructions.find(
(ix) => accountKeys[ix.programIdIndex] === MEMO_PROGRAM
);
if (!memoInstruction) {
throw new Error("No memo instruction found in transaction");
}

// The RPC returns instruction data as a base58 string. A base58 "encoder"
// turns that string into raw bytes (its .encode() = string -> bytes), and the
// memo decoder then reads those bytes back into the original memo text.
const data = getBase58Encoder().encode(memoInstruction.data);
return getAddMemoInstructionDataDecoder().decode(data).memo;
}

未识别的程序 ID

如果您之前使用过原版的“备忘录”程序,您可能曾从 备忘录 由...返回的字段 getSignaturesForAddress. 该字段仅由 RPC 节点在以下情况下填充: 被认可的 记录程序 ID,并且因为 p-memo 如果该程序部署在新的地址上,该字段将不会包含您的文本。相反,您需要自行获取交易并解码指令数据,这种方法适用于任何版本的备注程序。

发送并阅读备忘录

最后,按顺序执行以下两个步骤:发送备忘录,记录 Explorer 链接,然后获取并记录解码后的备忘录。因为 app.ts 是一个 ES 模块,你可以 等待 在顶层——无需包装函数。

将此内容添加到 app.ts:

app.ts
// Write a memo onchain, then read it back to prove the round-trip.
const signature = await sendMemo(MEMO_TEXT);
console.log(
`https://explorer.solana.com/tx/${signature}?cluster=${explorerCluster}`
);

const memo = await fetchMemo(signature);
console.log(`Memo: ${memo}`);

运行您的代码

运行该脚本,加载您的 .env 使用 Node 的 --env-file flag:

node --env-file=.env app.ts

您应该能在Solana 上看到一条指向您交易的链接,后面跟着解码后的备注:

https://explorer.solana.com/tx/5Pp...Xy?cluster=devnet
Memo: Learning about the Memo program with Quicknode

打开“资源管理器”链接,您将在“备忘录”程序的说明和程序日志中找到您的消息。

带备注文本的Solana

已收到备忘录。干得不错!

总结

现在,您已经使用基于 Pinocchio 的 Memo 程序和Solana 将一条备注附加到Solana 并成功读取了该备注。您编写的备注只是交易中的一条指令,这意味着您可以将其与任何其他指令(例如 SOL 转账或调用您自己的程序)结合使用,从而在您正在发送的交易中记录上下文信息。通过 p-memo 由于其计算开销仅为原始程序的一小部分,因此您可以将更多交易中的计算单元(CU)预算留给其他指令。

常见问题解答

什么是Solana 计划?

Memo 程序是一个原生的Solana ,它将任意的 UTF-8 文本作为交易中的指令记录在链上。该程序通常用于将人类可读的备注、发票 ID 或外部引用附加到交易中,并会验证传递给该指令的任何账户是否为签名人。

什么是 p-memo?它与原版 Memo 程序有什么不同?

p-memo 是基于 Pinocchio(一个零依赖的 Rust 框架)对 SPL Memo 程序的重新实现。其指令接口与原版完全一致(指令数据即备忘录文本),但消耗的计算单元更少。 使用 p-memo 时,单签名备忘录大约消耗 513 CU,而原始程序则消耗约 13,525 CU。它部署在另一个地址上:Memo4c2pN8afCj432Lb7RMVKi9PbQnnW7ewFFaV3oAH。

为什么我无法读取 getSignaturesForAddress 方法的备忘录?

getSignaturesForAddress 返回的备注字段仅在识别出备注程序 ID 时才会由 RPC 节点填充。由于 p-memo 部署在新的地址上,该字段将不会包含您的文本。因此,请使用 getTransaction 获取交易,并直接解码备注指令的数据,这种方法适用于任何版本的备注程序。

我可以在已经执行了其他操作的交易中添加备注吗?

是的。备注只是交易中的一条指令,因此你可以将其与其他指令(例如 SOL 转账或调用你自己的程序)一起附加,以便将上下文信息记录到你正在发送的交易中。

我可以在Mainnet使用这个吗?

是的。各clusters代码完全相同;仅您的endpoint 用于充值的密钥对会有所不同。请将SOLANA 指向Mainnet endpoint 使用已充值Mainnet 。请注意,在Mainnet,SOL 具有实际价值,您需要支付实际的交易手续费。

资源