什么是Amman,以及如何使用它来增强Solana本地开发?

本文介绍了Amman,一个由Metaplex构建和维护的Solana本地测试验证器包装器,旨在简化Solana开发者的体验。

Amman 不再维护

Amman 不再由 Metaplex 积极维护。本指南仅用于历史教育目的。

对于现代替代方案,请查看如何使用 LiteSVM 测试 Solana 程序指南以了解如何测试 Solana 程序。

概述

Amman 是一个 Solana 本地测试验证器包装器,可简化开发人员体验。Amman 由 Metaplex 构建和维护,Metaplex 是 Solana 上著名的 NFT 标准。Amman 可以减少测试 Solana 程序所需的时间。让我们开始吧!

你将做什么

  • 了解 Amman 是什么及其功能
  • 编写一个简单的脚本来测试 Amman 的一些功能
  • 创建一个 Amman 配置文件
  • 在 Amman 浏览器中测试你的脚本并浏览你的交易

你需要的材料

什么是 Amman?

Amman 是 “A modern mandatory toolbelt to help test solana SDK libraries and apps on a locally running validator.” 的缩写。

值得注意的功能:

  • Amman Explorer - 一种基于 Web 的浏览器,可连接到你的本地 Solana 验证器、跟踪最近的交易、为帐户提供自定义标签以及反序列化帐户数据。
  • 模拟存储服务器 - Amman 可用于模拟 Solana 程序的存储服务器,这对于测试与 NFT 相关的程序非常方便。
  • 程序和帐户加载 - Amman 可用于从本地文件系统或指定的 Solana 集群加载程序和帐户。
  • 可共享的配置 - Amman 可以配置为在多个项目中共享相同的验证器和存储服务器,这可以节省时间和精力。

如何使用 Amman?

让我们创建一个简单的脚本来测试 Amman 的功能。该脚本将:

  • 创建一个 fungible token,利用 Amman 的模拟存储服务器来模拟上传 token 元数据
  • 将 token 铸造到钱包
  • 将 token 从发送者转移到接收者
  • 使用 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:用于交易的默认选项——我们将对我们的交易使用 skipPreflightprocessed 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 函数

接下来,定义一个函数来铸造我们的 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 函数

接下来,让我们定义一个函数来转移我们的 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);
    }
}

在这里,我们需要做两件事:

  1. 为接收者创建关联 Token 帐户 (ATA)。我们使用 Umi 库中的 createAssociatedToken 函数。
  2. 使用 Umi 库中的 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.'));

让我们分解一下:

  1. 我们首先创建一个新的 Amman 实例和一个 Connection 实例到我们的本地验证器。
  2. 然后,我们调用我们的 setupUmi 函数来设置我们的 Umi 实例。
  3. 然后,我们调用我们的 setupAuthority 函数来设置我们的授权帐户并向其空投一些 SOL。我们使用授权签名者更新我们的 Umi 实例。
  4. 我们使用 Umi 库中的 generateSigner 函数创建一个 mintreceiver 签名者。有了这些,我们可以找到授权和接收者帐户的 ATA。
  5. 我们使用 amman.addr.addLabel 函数向我们的帐户添加标签。该函数的功能类似于 Amman 库中的 genLabeledKeypair 函数,但会向现有帐户添加标签。通常,我们可能会使用相同的方法来定义此处的密钥对,但我们使用此方法只是为了展示使用 Amman 库的不同方式。
  6. 然后,我们调用我们的 uploadTokenMetadata 函数来将我们的 token 元数据上传到 Amman 的模拟存储,然后我们使用它的响应来创建我们的 token 元数据对象。
  7. 我们通过调用 mintTokens 函数来创建一个新 token。
  8. 然后,我们调用我们的 transferTokens 函数来将我们的 token 转移到接收者帐户。

配置 Amman

在我们可以运行我们的脚本之前,我们需要配置 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 到其中——我们接下来将这样做。
  • jsonRpcUrlwebsocketUrl:这些是我们的本地验证器的 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 上查看你的交易和帐户。继续并单击转移交易的链接以在浏览器上查看交易。

你应该会注意到一些新事物:

  1. “Account Input(s)”部分现在是一个标记帐户的列表(而不是我们在 Solana Explorer 上看到的原始公钥)。这些应该与我们在脚本中定义的帐户标签匹配:

帐户输入

  1. 在屏幕顶部,你应该会看到一个“加载历史记录”按钮。单击此按钮将加载我们在脚本中使用的帐户的交易历史记录。你应该会看到我们刚刚运行的交易:

转移历史

  1. 快照可用于查看和存储你的帐户的状态。这对于调试和测试非常有用。你可以通过单击屏幕右上角的 💾 图标来查看快照。你可以存储这些快照并在以后加载它们以返回到以前的状态。🤯

很酷,对吧?

总结

Amman 是一套强大的 Solana 开发人员工具,可使本地开发和测试更加高效。随着你继续在 Solana 上进行开发,请考虑将 Amman 集成到你的工作流程中,以简化你的开发流程。查看以下其他资源以了解更多信息:

资源

让我们连接!

你是否已经在使用 Amman?我们很乐意听到你的使用情况。通过 TwitterDiscord 向我们发送你的经验、问题或反馈。

We ❤️ Feedback!

如果你有任何反馈或关于新主题的请求,请告诉我们。我们很乐意收到你的来信。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。