本文介绍了Amman,一个由Metaplex构建和维护的Solana本地测试验证器包装器,旨在简化Solana开发者的体验。
Amman 不再维护
Amman 不再由 Metaplex 积极维护。本指南仅用于历史教育目的。
对于现代替代方案,请查看如何使用 LiteSVM 测试 Solana 程序指南以了解如何测试 Solana 程序。
Amman 是一个 Solana 本地测试验证器包装器,可简化开发人员体验。Amman 由 Metaplex 构建和维护,Metaplex 是 Solana 上著名的 NFT 标准。Amman 可以减少测试 Solana 程序所需的时间。让我们开始吧!
Amman 是 “A modern mandatory toolbelt to help test solana SDK libraries and apps on a locally running validator.” 的缩写。
值得注意的功能:
让我们创建一个简单的脚本来测试 Amman 的功能。该脚本将:
注意:这将以我们的 Fungible Tokens 指南 为模型,使用 Umi。
首先,让我们设置我们的项目:
mkdir amman-test
cd amman-test
npm init -y
npm install @metaplex-foundation/amman @solana/web3.js@1 @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @metaplex-foundation/mpl-token-metadata @metaplex-foundation/mpl-toolbox @metaplex-foundation/umi-web3js-adapters
我们将使用 npx 而不是全局安装进行测试,但如果愿意,你也可以全局安装 Amman。将以下脚本添加到你的 package.json 中:
"scripts": {
"amman:start": "npx amman start"
}
太好了!我们准备好开始编写我们的脚本了。
在你的项目根目录中创建一个新的 app.ts 文件。
echo > app.ts
在你的代码编辑器中打开该文件,让我们开始吧!
首先,让我们导入必要的依赖项:
import { Amman, LOCALHOST, AMMAN_EXPLORER } from "@metaplex-foundation/amman-client";
import { Connection } from "@solana/web3.js";
import { percentAmount, generateSigner, signerIdentity, TransactionBuilderSendAndConfirmOptions, transactionBuilder, createSignerFromKeypair, Umi, Keypair, Signer, KeypairSigner } from '@metaplex-foundation/umi';
import { TokenStandard, createAndMint, mplTokenMetadata, CreateV1InstructionDataArgs } from '@metaplex-foundation/mpl-token-metadata';
import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import { base58 } from "@metaplex-foundation/umi/serializers";
import { fromWeb3JsKeypair, toWeb3JsPublicKey } from "@metaplex-foundation/umi-web3js-adapters";
import { findAssociatedTokenPda, transferTokens as transferTokensUmi, createAssociatedToken } from '@metaplex-foundation/mpl-toolbox';
让我们定义一些类型和接口来帮助我们管理元数据。在你的导入下方添加以下内容:
type TokenMetadata = Pick<CreateV1InstructionDataArgs, 'name' | 'symbol' | 'uri'>;
type MetaplexFile = Readonly<{
buffer: Buffer;
fileName: string;
displayName: string;
uniqueName: string;
contentType: string | null;
extension: string | null;
tags: [];
}>;
type OffchainMetadata = {
name: string;
symbol: string;
image: string;
description: string;
creator: {
name: string;
site: string;
}
}
我们正在定义一个 TokenMetadata 类型,它将用于创建我们的 token。它是 Umi 库中 CreateV1InstructionDataArgs 类型的一个子集。我们还将定义一个 MetaplexFile 类型,它将表示上传到 Amman 的模拟存储的文件。最后,我们定义一个 OffchainMetadata 类型,它将存储我们 token 的元数据。
下面,让我们定义一些常量,以便在我们的脚本中使用。在你的导入下方添加以下内容:
const AIRDROP_AMOUNT = 100; // 100 SOL
const TOKEN_DECIMALS = 5;
const INITIAL_MINT_AMOUNT = 1_000_000 * Math.pow(10, TOKEN_DECIMALS); // 1 million tokens
const TRANSFER_AMOUNT = 100 * Math.pow(10, TOKEN_DECIMALS); // 100 tokens
const STORAGE_ID = "mock-storage";
const DEFAULT_OPTIONS: TransactionBuilderSendAndConfirmOptions = {
send: { skipPreflight: true },
confirm: { commitment: 'processed' },
};
const OFF_CHAIN_METADATA: OffchainMetadata = {
"name": "Fake Bonk",
"symbol": "xBONK",
"image": "https://arweave.net/hQiPZOsRZXGXBJd_82PhVdlM_hACsT_q6wqwf5cSY7I",
"description": "The FAKE Bonk Inu token",
"creator": {
"name": "Quicknode Guides",
"site": "https://www.quicknode.com/guides/"
}
}
以下是常量的一个简要说明:
AIRDROP_AMOUNT:要空投到授权/发送者帐户的 SOL 金额。TOKEN_DECIMALS:token 的小数位数(我们将创建一个具有 5 位小数的伪 BONK token)。INITIAL_MINT_AMOUNT:要铸造的 token 的初始数量。TRANSFER_AMOUNT:要转移的 token 数量。STORAGE_ID:用于 Amman 的模拟存储的存储 ID(这将需要与我们的 Amman 配置匹配——我们将在本指南的后面部分介绍)。DEFAULT_OPTIONS:用于交易的默认选项——我们将对我们的交易使用 skipPreflight 和 processed commitment 以加快我们的测试。OFF_CHAIN_METADATA:我们将上传到 Amman 的模拟存储的 token 的示例链下元数据。现在,让我们定义一些函数来帮助我们设置我们的项目。在你的导入下方添加以下内容:
async function setupUmi(): Promise<Umi> {
const connection = new Connection(LOCALHOST);
const umi = createUmi(connection);
umi.use(mplTokenMetadata());
return umi;
}
async function setupAuthority(umi: Umi, amman: Amman, connection: Connection): Promise<[Umi, Keypair]> {
const [_authorityPublicKey, authorityKeypair] = await amman.genLabeledKeypair("Authority");
const authority = fromWeb3JsKeypair(authorityKeypair);
const authoritySigner = createSignerFromKeypair(umi, authority);
umi.use(signerIdentity(authoritySigner));
await airdropSol(umi, authority, amman, connection);
return [umi, authority];
}
async function airdropSol(umi: Umi, authority: Keypair, amman: Amman, connection: Connection): Promise<void> {
try {
await amman.airdrop(connection, toWeb3JsPublicKey(authority.publicKey), AIRDROP_AMOUNT);
console.log(`✅ - Airdropped ${AIRDROP_AMOUNT} SOL to ${authority.publicKey}`);
} catch (err) {
console.error("❌ - Error airdropping SOL:", err);
}
}
在这里,我们定义了三个用于设置我们项目的辅助函数:
setupUmi:此函数初始化 Umi 库并返回它的一个新实例。请注意,Amman 库有一个方便的 LOCALHOST 常量,我们可以使用它来连接到本地验证器。由于我们正在使用 token 元数据,因此我们还必须使用 mplTokenMetadata 函数来启用 token 元数据程序。setupAuthority:此函数设置授权帐户,返回一个具有授权签名者的新 Umi 实例,并返回授权的密钥对。请注意我们使用了 Amman 客户端中的 genLabeledKeypair 函数。此函数将创建一个新的 Solana 密钥对,并向其添加一个标签,该标签将在 Amman Explorer 中访问。airdropSol:此函数将 SOL 空投到授权帐户。让我们创建一个函数,利用 Amman 的模拟存储来上传我们的 token 元数据。在你的 airdropSol 函数下方添加以下内容:
async function uploadTokenMetadata(amman: Amman, tokenMetadata: OffchainMetadata): Promise<string> {
const storage = amman.createMockStorageDriver(STORAGE_ID, 1);
const file: MetaplexFile = {
buffer: Buffer.from(JSON.stringify(tokenMetadata)),
fileName: "xBONK.json",
displayName: "xBONK.json",
uniqueName: "xBONK.json",
contentType: "application/json",
extension: "json",
tags: [],
}
try {
const uploadResponse = await storage.upload(file);
console.log(`✅ - Successfully uploaded metadata`);
return uploadResponse;
} catch (err) {
console.error("❌ - Error uploading metadata:", err);
throw err;
}
}
该文件将使用 Amman 实例创建一个新的模拟存储驱动程序,其中包含 STORAGE_ID 和任意成本 1。然后,我们使用 token 元数据创建一个 MetaplexFile 对象(upload 函数需要该对象)并将其上传到存储。最后,我们记录一条成功消息并返回上传响应。
接下来,定义一个函数来铸造我们的 token。在你的 uploadTokenMetadata 函数下方添加以下内容:
async function mintTokens(umi: Umi, mint: Signer, authority: Keypair, metadata: TokenMetadata): Promise<void> {
try {
const response = await createAndMint(umi, {
mint,
authority: umi.identity,
name: metadata.name,
symbol: metadata.symbol,
uri: metadata.uri,
sellerFeeBasisPoints: percentAmount(0),
decimals: TOKEN_DECIMALS,
amount: INITIAL_MINT_AMOUNT,
tokenOwner: authority.publicKey,
tokenStandard: TokenStandard.Fungible,
}).useLegacyVersion().sendAndConfirm(umi, DEFAULT_OPTIONS);
console.log(`✅ - Successfully minted ${INITIAL_MINT_AMOUNT / Math.pow(10, TOKEN_DECIMALS)} tokens (${mint.publicKey})`);
const [signature] = base58.deserialize(response.signature);
console.log(` ${AMMAN_EXPLORER}/#/tx/${signature}`);
} catch (err) {
console.error("❌ - Error minting tokens:", err);
}
}
这将仅使用 Umi 库中的 createAndMint 函数创建一个具有指定元数据的新 token。然后,我们记录一条成功消息和指向 Amman Explorer 中交易的链接(请注意 amman-client 库中的 AMMAN_EXPLORER URL 常量)。
接下来,让我们定义一个函数来转移我们的 token。在你的 mintTokens 函数下方添加以下内容:
async function transferTokens(umi: Umi, mint: Signer, authority: Keypair, receiver: KeypairSigner): Promise<void> {
const [senderAta] = findAssociatedTokenPda(umi, { mint: mint.publicKey, owner: authority.publicKey });
const [receiverAta] = findAssociatedTokenPda(umi, { mint: mint.publicKey, owner: receiver.publicKey });
const createAtaInstruction = createAssociatedToken(umi, {
payer: umi.identity,
owner: receiver.publicKey,
mint: mint.publicKey,
ata: receiverAta,
});
const transferInstruction = transferTokensUmi(umi, {
source: senderAta,
destination: receiverAta,
amount: TRANSFER_AMOUNT,
});
const transferTransaction = transactionBuilder()
.add(createAtaInstruction)
.add(transferInstruction);
try {
const response = await transferTransaction.useLegacyVersion().sendAndConfirm(umi, DEFAULT_OPTIONS);
if (response.result.value.err) {
throw new Error(JSON.stringify(response.result.value.err));
}
console.log(`✅ - Successfully transferred ${TRANSFER_AMOUNT / Math.pow(10, TOKEN_DECIMALS)} tokens`);
const [signature] = base58.deserialize(response.signature);
console.log(` ${AMMAN_EXPLORER}/#/tx/${signature}`);
} catch (err) {
console.error("❌ - Error sending tokens:", err);
}
}
在这里,我们需要做两件事:
createAssociatedToken 函数。transferTokens 函数转移 token(我们将其强制转换为 transferTokensUmi 以避免类型错误)。
然后,我们记录一条成功消息和指向 Amman Explorer 中交易的链接。太好了!现在我们已经定义了我们的函数,让我们定义我们的 main 函数,它将把我们的所有部分组合在一起。在你的 transferTokens 函数下方添加以下内容:
async function main() {
const amman = Amman.instance();
const connection = new Connection(LOCALHOST, 'processed');
let umi = await setupUmi();
const [updatedUmi, authority] = await setupAuthority(umi, amman, connection);
umi = updatedUmi;
const mint = generateSigner(umi);
const receiver = generateSigner(umi);
const [senderAta] = findAssociatedTokenPda(umi, { mint: mint.publicKey, owner: authority.publicKey });
const [receiverAta] = findAssociatedTokenPda(umi, { mint: mint.publicKey, owner: receiver.publicKey });
amman.addr.addLabel("xBONK", toWeb3JsPublicKey(mint.publicKey));
amman.addr.addLabel("Receiver Wallet", toWeb3JsPublicKey(receiver.publicKey));
amman.addr.addLabel("Sender xBONK Account", toWeb3JsPublicKey(senderAta));
amman.addr.addLabel("Receiver xBONK Account", toWeb3JsPublicKey(receiverAta));
const uri = await uploadTokenMetadata(amman, OFF_CHAIN_METADATA);
const metadata: TokenMetadata = {
name: "FakeBONK",
symbol: "xBONK",
uri
};
await mintTokens(umi, mint, authority, metadata);
await transferTokens(umi, mint, authority, receiver);
}
main().then(() => console.log('Done.'));
让我们分解一下:
Amman 实例和一个 Connection 实例到我们的本地验证器。setupUmi 函数来设置我们的 Umi 实例。setupAuthority 函数来设置我们的授权帐户并向其空投一些 SOL。我们使用授权签名者更新我们的 Umi 实例。generateSigner 函数创建一个 mint 和 receiver 签名者。有了这些,我们可以找到授权和接收者帐户的 ATA。amman.addr.addLabel 函数向我们的帐户添加标签。该函数的功能类似于 Amman 库中的 genLabeledKeypair 函数,但会向现有帐户添加标签。通常,我们可能会使用相同的方法来定义此处的密钥对,但我们使用此方法只是为了展示使用 Amman 库的不同方式。uploadTokenMetadata 函数来将我们的 token 元数据上传到 Amman 的模拟存储,然后我们使用它的响应来创建我们的 token 元数据对象。mintTokens 函数来创建一个新 token。transferTokens 函数来将我们的 token 转移到接收者帐户。在我们可以运行我们的脚本之前,我们需要配置 Amman。Amman 有许多配置选项——我们将介绍其中的一些,但请查看 Amman 文档 或 Amman 源代码 以获取更多信息。
在你的项目根目录中创建一个新的 .ammanrc.js 文件,其中包含以下内容:
const { LOCALHOST } = require("@metaplex-foundation/amman");
const path = require('path');
function localDeployPath(programName) {
return path.join(__dirname, `programs`, `${programName}.so`);
}
module.exports = {
validator: {
killRunningValidators: true,
accountsCluster: "https://example.solana-mainnet.quiknode.pro/123456/", // 👈 替换为你自己的 Quicknode 端点
programs: [\
{\
label: 'Metaplex Metadata',\
programId: "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s",\
deployPath: localDeployPath('metadata')\
}\
],
jsonRpcUrl: LOCALHOST,
websocketUrl: "",
commitment: "confirmed",
resetLedger: true,
verifyFees: false,
detached: process.env.CI != null,
},
relay: {
enabled: process.env.CI == null,
killRunningRelay: true,
},
storage: {
enabled: process.env.CI == null,
storageId: "mock-storage",
clearOnStart: true,
},
};
validator 配置是我们指定希望 solana-test-validator 如何启动和运行的地方:
killRunningValidators:这将在启动新的验证器之前终止任何正在运行的验证器。accountsCluster:这是我们用于从 Quicknode 端点获取帐户数据并启动我们的验证器的 URL。例如,如果我们希望程序或 token 模拟 mainnet 部署,我们可以使用 mainnet 端点。如果你没有 Quicknode 端点,你可以在此处免费获得一个。或者,你可以使用来自 Solana.com 的公共端点。programs:这是我们希望加载到我们的验证器的程序数组。在这种情况下,我们正在部署 Metaplex Metadata 程序。你会注意到我们正在使用 localDeployPath 函数来指定程序所在的位置的路径。我们需要创建此目录并将程序 dump 到其中——我们接下来将这样做。jsonRpcUrl 和 websocketUrl:这些是我们的本地验证器的 URL。我们将使用这些来连接到我们的本地验证器并与之交互。commitment:这是我们希望用于我们的验证器的 commitment 级别。我们将使用它来确保我们的验证器与网络同步。resetLedger:这将在启动验证器之前重置账本。我们将使用它来每次启动验证器时都从创世块开始。verifyFees:如果为 true,则在验证器收取交易费用之前,不会将其视为已完全启动。detached:这将在后台运行验证器,允许 amman 在继续运行时退出。relay 配置指定了我们的数据如何在验证器和 Amman Explorer 之间共享。我们将使用此示例的默认配置。有关 relay 配置的更多信息,请查看 Amman 源代码。
最后,storage 配置指定了我们希望如何存储我们的模拟存储。我们将创建一个名为 mock-storage 的存储标识符,以匹配我们脚本中的 STORAGE_ID 常量。我们还将 clearOnStart 标志设置为 true,这将在启动验证器之前清除存储。
如上所述,我们必须从 Mainnet 克隆 Metaplex Metadata 程序。为此,我们可以使用 Solana CLI 中的 dump 命令。dump 命令会将程序帐户的可执行字节代码导出到一个文件,然后我们可以将其部署到我们的 localnet。我们将需要程序 ID。对于 Metaplex Token Metadata,程序 ID 为 metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s。
在你的终端中,创建一个名为 programs 的新目录并输入以下命令以将程序转储到文件中:
mkdir programs
然后,输入以下命令以将程序转储到文件中:
solana program dump -u m metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s programs/metadata.so
这应该会在 programs 目录中创建一个 metadata.so 文件,该文件与我们在配置文件中指定的文件匹配。我们应该一切顺利!
通过运行以下命令启动 Amman 验证器:
npm run amman:start
打开一个新的终端并导航到你的项目目录。 如果你愿意,可以通过运行以下命令浏览 Amman CLI 工具:
npx amman help
现在,运行你的脚本:
ts-node app.ts
你应该会看到类似于以下内容的输出:
✅ - Airdropped 100 SOL to 5L8siRBhjiAE4GJKZmZSSkfCYBSRZXMv44SVuoD888Yt
✅ - Successfully uploaded metadata
✅ - Successfully minted 1000000 tokens (7nE1GmnMmDKiycFZxTUdf2Eis9KoYZWN6xNapYzcVCu2)
https://amman-explorer.metaplex.com/#/tx/2AQhGq2BxmGFfvVEJ6uKkMYKGmAswWWZ9vg4YHw2iqr1Nd9wGChL9rLJWZTqKRBxZepTMPQutJ6Lqj2ZbRuNKkX
✅ - Successfully transferred 100 tokens
https://amman-explorer.metaplex.com/#/tx/3X6CAX6YUWZQ4X8AyHK2EuFHZHZ3iS8GrJnCwNvQKsyF8UjWZ6ysRgXCH7Y2q7q3zJNkiNKn1CwS6xEQ5R1YuZfv
Done.
你还会注意到,运行你的本地验证器的终端窗口将显示你的交易的实时日志。你还可以通过单击终端中的链接在 Amman Explorer 上查看你的交易和帐户。继续并单击转移交易的链接以在浏览器上查看交易。
你应该会注意到一些新事物:


很酷,对吧?
Amman 是一套强大的 Solana 开发人员工具,可使本地开发和测试更加高效。随着你继续在 Solana 上进行开发,请考虑将 Amman 集成到你的工作流程中,以简化你的开发流程。查看以下其他资源以了解更多信息:
你是否已经在使用 Amman?我们很乐意听到你的使用情况。通过 Twitter 或 Discord 向我们发送你的经验、问题或反馈。
如果你有任何反馈或关于新主题的请求,请告诉我们。我们很乐意收到你的来信。
- 原文链接: quicknode.com/guides/sol...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!