阅读时间 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 的对比 |
|---|---|---|---|
| 0 | 287 | 2,022 | ~14% |
| 1 | 513 | 13,525 | ~4% |
| 2 | 628 | 25,111 | ~3% |
| 3 | 743 | 36,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 |
| solana | 3.0.6 |
| solana | 6.9.0 |
| solana | 0.11.1 |
| solana | 0.10.0 |
| solana | 0.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 文件,以免您不小心将密钥或环境变量提交到代码库:
node_modules
my-keypair.json
.env
设置Quicknode Endpoint
您可以使用公共Solana ,但公共节点在负载较重时会实施速率限制并出现延迟。若需更快、更可靠的响应,请创建一个Quicknode endpoint:
- 请在此注册一个免费账户。
- 创建一个endpoint,选择 Solana 和Devnet网络,然后复制 HTTP 提供商 URL。

在下一步中,您将把此 URL 添加到环境变量中。Solana 会从该 URL 推导出确认交易endpoint WebSocketendpoint ,因此您只需保留 HTTP 提供程序的链接即可。
配置环境变量
与其将endpoint 密钥路径硬编码,不如将它们存储在 .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 客户端,并定义一些将在各个函数中重复使用的常量:
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 该辅助函数会为您处理所有底层操作——获取最新的区块哈希、设置手续费支付方、签名、发送以及确认。
将其添加到设置下方:
// 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:
// 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;
}
如果您之前使用过原版的“备忘录”程序,您可能曾从 备忘录 由...返回的字段 getSignaturesForAddress. 该字段仅由 RPC 节点在以下情况下填充: 被认可的 记录程序 ID,并且因为 p-memo 如果该程序部署在新的地址上,该字段将不会包含您的文本。相反,您需要自行获取交易并解码指令数据,这种方法适用于任何版本的备注程序。
发送并阅读备忘录
最后,按顺序执行以下两个步骤:发送备忘录,记录 Explorer 链接,然后获取并记录解码后的备忘录。因为 app.ts 是一个 ES 模块,你可以 等待 在顶层——无需包装函数。
将此内容添加到 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
打开“资源管理器”链接,您将在“备忘录”程序的说明和程序日志中找到您的消息。

已收到备忘录。干得不错!
总结
现在,您已经使用基于 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 具有实际价值,您需要支付实际的交易手续费。
