以太坊 - 如何在Eclipse上使用Nifty资产标准铸造NFT - Quicknode

  • QuickNode
  • 发布于 2025-01-30 14:54
  • 阅读 12

本文介绍了如何在Eclipse区块链上使用Nifty资产标准创建NFT,包括准备工作、桥接ETH到Eclipse、NFT铸造和数据验证等步骤。文章详细阐述了所需依赖、代码实例和每一步的执行过程,适合想要在Eclipse上进行NFT开发的开发者。

概述

Eclipse 是一个新区块链,声称是“以太坊最快的第二层”,这是通过在以太坊上运行 Solana 虚拟机(SVM)作为 Rollup 实现的。Eclipse 最近向开发者推出了主网,其发布为社区带来了很多兴奋。一个令人激动的发布是Nifty Asset Standard 程序在 Eclipse 上的部署,这是一个轻量级工具和高效的标准,用于管理 Solana 上的数字资产。

在本指南中,我们将学习如何使用 Eclipse 上的 Nifty Asset Standard 创建 NFT。

先决条件

在开始之前,请确保你已安装以下内容:

  • Node.js(版本 16.15 或更高)
  • Typescript 经验和ts-node已安装(推荐 TypeScript 版本为 4.0 或更高)
  • Solana CLI 最新版本
  • MetaMask(或类似的 EVM 兼容钱包)

虽然不是必需的,我们建议你了解以下内容:

本指南中使用的依赖项

依赖项 版本
@metaplex-foundation/umi ^0.9.2
@metaplex-foundation/umi-bundle-defaults ^0.9.2
@nifty-oss/asset ^0.6.1
solana cli 1.18.8

你将要做的事情

本指南将引导你如何将以太坊桥接到 Eclipse 并使用 Nifty Asset Standard 在 Eclipse 上铸造 NFT。具体来说,我们将涵盖以下功能:

  • 桥接:将 ETH 从以太坊转移到 Eclipse。
  • 上传:使用 QuickNode 的 IPFS 网关 上传图像和元数据到 IPFS。
  • 铸造:使用 Nifty Asset Standard 创建新的数字资产。
  • 验证:验证 NFT 的链上数据。

让我们开始吧!

Nifty Asset 基础

本项目将利用 Eclipse 上的Nifty Asset Standard。Nifty Asset 是一种新的 NFT 方法,在 Solana 上提高了效率和灵活性。与基于 SPL Token 程序的传统 NFT 标准不同,Nifty Asset 使用单个账户来表示资产,从而优化存储和计算使用。它提供了诸如特征、元数据、版税执行和通过扩展锁定资产等功能。资产账户结构包括基本元数据和扩展数据,所有者在资产账户内定义,以便更有效的管理。有关 Nifty Asset 的更详细信息,包括其功能和实现,请参考我们的Nifty Asset 指南

未经审计的程序

Nifty Asset 标准仍在开发中,尚未经过审计。部署到主网时请谨慎,并确保你在 devnet 或 testnet 上充分测试你的应用后再部署到主网。请查看他们的GitHub 以获取最新的更新和文档。

在 Eclipse 上获取 ETH 代币

要开始,你需要在 Eclipse 上拥有一些 ETH 代币。要桥接到 Eclipse,你需要在以太坊主网或 Sepolia 测试网(在 Eclipse 测试网上使用)上拥有一些 ETH。如果你已经拥有 ETH,可以跳到桥接;如果没有,你需要获取一些 Sepolia 测试 ETH。

在测试网工作?获取 Sepolia ETH 代币

如果你尚未准备好主网,可以获取可以桥接到 Eclipse 测试网的 Sepolia ETH 代币。

前往 QuickNode 的多链水龙头并选择 Sepolia ETH 网络。输入你的钱包地址并单击“发送我 ETH”:

QuickNode Faucet

确保在 Metamask 的“测试网络”下拉列表中选择“Sepolia”(或者使用你的 QuickNode 端点添加自己的网络 - 你可以在这里免费创建一个)。你应该在几秒钟后在钱包中看到 Sepolia ETH 代币:

Sepolia ETH Tokens

干得不错!你现在在钱包中拥有 Sepolia ETH 代币。接下来,让我们把这些代币桥接到 Eclipse。

桥接 ETH 到 Eclipse

Eclipse 基金会创建了一个桥接合约和 脚本 将主网或 Sepolia ETH 代币转移到 Eclipse 的主网和测试网。让我们创建一个目标 Solana 钱包并用一些 ETH 进行资助。

创建目标 Solana 钱包

如果你还没有用于 Solana CLI 的 Solana 纸钱包,你需要创建一个。我们可以使用 Solana CLI 与 Eclipse 网络进行交互,因为它是 SVM 的一个实例! 注意:目前有一些小瑕疵(主要是围绕 UI);例如,使用 solana balance 将返回正确的余额,但它将显示 X SOL 而不是 X ETH(尽管正在表示的底层代币实际上是 ETH)。

你可以通过在终端中运行以下命令创建一个新钱包:

solana-keygen new --outfile /path-to-wallet/my-wallet.json

然后,更新你的 Solana CLI 配置以使用新钱包和适当的 Eclipse 集群。根据你希望的网络,将以下命令输入你的终端:

  • Eclipse 主网
  • Eclipse 测试网
solana config set --url https://mainnetbeta-rpc.eclipse.xyz
solana config set --url https://testnet.dev2.eclipsenetwork.xyz/

并且

solana config set --keypair /path-to-wallet/my-wallet.json

通过运行以下命令获取你的地址:

solana address

保留好这个--我们稍后需要它!

克隆 Eclipse 桥接脚本

克隆 Eclipse Bridge 仓库。在你希望克隆该仓库的目录中打开一个终端窗口并运行:

git clone https://github.com/Eclipse-Laboratories-Inc/eclipse-deposit

并导航到 eclipse-deposit 目录:

cd eclipse-deposit

安装依赖项:

yarn install

获取你的以太坊私钥

在你的以太坊钱包中复制你的私钥。在 MetaMask 中,你可以通过以下步骤找到这一点:

“账户详情” -> “查看账户” -> “显示私钥” -> “复制私钥”

这应该是一个 64 字符的十六进制字符串。将其保存到名为 private-key.txt 的文件中。留着这个--我们稍后需要用它来运行我们的脚本。

运行桥接脚本

你应该能够按照克隆的 repos 的 README 中的说明或者 Eclipse 的文档这里进行操作。你需要在终端中运行以下命令(不带括号):

  • Eclipse 主网
  • Eclipse 测试网
node bin/cli.js -k [path_to_private_key] -d [solana_destination_address] -a [amount_in_ether] --mainnet
node bin/cli.js -k [path_to_private_key] -d [solana_destination_address] -a [amount_in_ether] --sepolia

这里是参数说明:

  • [path_to_private_key] 是你刚刚从 MetaMask 复制的 64 字符字符串的路径,例如 private-key.txt
  • [solana_destination_address] 是你使用 Solana CLI 生成的,在 my-wallet.json 中保存的地址。
  • [amount_in_ether] 是你想要转移到 Eclipse 的 ETH 数量,例如 0.01
  • --mainnet 标志用于转移到 Eclipse 主网,--sepolia 标志用于转移到 Eclipse 测试网。

你应该看到类似以下内容:

Transaction successful: 0xb763990f73f1801197d...

你可以在 Etherscan 这里(或 Sepolia Etherscan)查看交易。在过了几秒钟后,你应该能够在你创建的 Solana 钱包中看到 ETH 余额。由于你已经将 Solana CLI 配置为 Eclipse 测试网,你只需运行以下命令即可检查余额:

solana balance

你将看到类似 0.001 SOL 的内容,具体取决于你存入了多少。请注意,这里 SOL 代表的是 ETH。你可以通过在 Eclipse 区块浏览器这里 检查你的钱包来进行验证。 确保你的浏览器设置为正确的集群(请查看浏览器窗口右上角)。如果你在错误的集群上,你将看不到你的余额。 粘贴你的钱包地址,你应该会看到你的账户余额。

非常不错!你已成功将 ETH 代币桥接到 Eclipse 主网。现在,让我们在 Eclipse 上铸造一个 NFT!

设置项目

初始化新的 Node.js 项目

首先,为你的项目创建一个新目录并初始化一个 Node.js 项目。

mkdir eclipse-nft

然后更改到新目录:

cd eclipse-nft

然后,初始化一个新的 Node.js 项目:

npm init -y

安装依赖项

接下来,安装必要的依赖项:

npm install @metaplex-foundation/umi @metaplex-foundation/umi-bundle-defaults @nifty-oss/asset

如果你使用的是低于 18 的 Node.js 版本,你可能需要将 @types/node 包作为开发依赖项安装:

npm install @types/node --save-dev

创建项目文件

在你的项目目录中创建一个名为 index.ts 的新文件。

echo > index.ts

导入依赖项

首先,导入所需的模块并设置 UMI 实例和签名者。在 index.ts 文件中添加以下代码:

import { createUmi } from '@metaplex-foundation/umi-bundle-defaults';
import {
    TransactionBuilderSendAndConfirmOptions,
    createGenericFile,
    createGenericFileFromJson,
    createSignerFromKeypair,
    generateSigner,
    keypairIdentity,
} from '@metaplex-foundation/umi';
import {
    metadata,
    mint,
    niftyAsset,
    fetchAsset,
    Metadata,
    royalties,
    creators,
    Royalties,
    Creators,
} from '@nifty-oss/asset';
import { readFile } from "fs/promises";
import { uploadToIpfs } from './upload';
import fs from 'fs';

createUmi 函数使用默认选项初始化 Umi 客户端。Umi 是一个 Solana 客户端库,提供与 Solana 区块链交互的高级 API。Nifty Asset 库包括用于铸造和管理数字资产的函数,我们将稍后详细讨论这些函数。

设置常量

一旦客户端就绪,我们还需要设置一些常量。我们需要以下内容:

  • 集群:Eclipse 主网或测试网
  • 选项:用于发送和确认交易的选项
  • NFT 详细信息:你要铸造的 NFT 的详细信息
  • IPFS API:IPFS API 端点

index.ts 文件中添加以下代码:

const CLUSTERS = {
    'mainnet': 'https://mainnetbeta-rpc.eclipse.xyz',
    'testnet': 'https://testnet.dev2.eclipsenetwork.xyz',
    'devnet': 'https://staging-rpc.dev2.eclipsenetwork.xyz',
    'localnet': 'http://127.0.0.1:8899',
};

const OPTIONS: TransactionBuilderSendAndConfirmOptions = {
    confirm: { commitment: 'processed' }
};

const NFT_DETAILS = {
    name: "QuickNode Pixel",
    symbol: "QP",
    royalties: 500, // 基点(5%)
    description: '为每个人提供的像素基础设施!',
    imgType: 'image/png',
    attributes: [\
        { trait_type: 'Speed', value: 'Quick' },\
    ]
};

const IPFS_API = 'REPLACE_WITH_YOUR_KEY'; // 👈 用你的 IPFS API 端点替换此内容

可以根据你希望的 NFT 更新 NFT_DETAILS 字段。我们将在下面使用每个字段来定义 NFT 的元数据。

在继续之前,请确保将 REPLACE_WITH_YOUR_KEY 占位符替换为你的 IPFS API 密钥。你可以从 QuickNode Dashboard 获取 API 密钥。如果你尚未拥有 QuickNode 帐户,你可以在这里免费创建一个。要了解有关 QuickNode 上 IPFS 的更多信息,请查看我们的IPFS 指南

设置 Umi 客户端

接下来,初始化 Umi 客户端并为 creatorownerasset 账户创建签名者。我们还将设置用于发送和确认交易的默认选项。

index.ts 文件中添加以下代码:

const umi = createUmi(CLUSTERS.mainnet, OPTIONS.confirm).use(niftyAsset()); // 👈 用你的集群替换此内容

const wallet = './my-wallet.json'; // 👈 用你的钱包路径替换此内容
const secretKey = JSON.parse(fs.readFileSync(wallet, 'utf-8'));
const keypair = umi.eddsa.createKeypairFromSecretKey(new Uint8Array(secretKey));
umi.use(keypairIdentity(keypair));

const creator = createSignerFromKeypair(umi, keypair);
const owner = creator; // 铸造给创建者
const asset = generateSigner(umi);

确保在 createUmi 函数中根据你的集群相应更新 CLUSTERS 选择(例如,CLUSTERS.mainnetCLUSTERS.testnet)。

确保用你钱包文件的路径替换 wallet 占位符。如果你不确定钱包文件在何处,可以通过在终端中运行以下命令找到 Solana CLI 配置:

solana config get

你应该会看到类似如下内容:

Keypair Path: ./my-wallet.json

将图像和元数据上传到 IPFS

现在我们已经配置了钱包和环境,我们可以编写一些助手函数,将图像和元数据上传到 IPFS。

index.ts 文件中添加以下代码:

async function uploadImage(path: string, contentType = 'image/png'): Promise<string> {
    try {
        const image = await readFile(path);
        const fileName = path.split('/').pop() ?? 'unknown.png';
        const genericImage = createGenericFile(image, fileName, { contentType });
        const cid = await uploadToIpfs(genericImage, IPFS_API);
        console.log(`1. ✅ - 上传图像到 IPFS`);
        return cid;
    } catch (error) {
        console.error('1. ❌ - 上传图像时出错:', error);
        throw error;
    }
}

async function uploadMetadata(imageUri: string): Promise<string> {
    try {
        const metadata = {
            name: NFT_DETAILS.name,
            description: NFT_DETAILS.description,
            image: imageUri,
            attributes: NFT_DETAILS.attributes,
            properties: {
                files: [\
                    {\
                        type: NFT_DETAILS.imgType,\
                        uri: imageUri,\
                    },\
                ]
            }
        };

        const file = createGenericFileFromJson(metadata, 'metadata.json');
        const cid = await uploadToIpfs(file, IPFS_API);
        console.log(`2. ✅ - 上传元数据到 IPFS`);
        return cid;
    } catch (error) {
        console.error('2. ❌ - 上传元数据时出错:', error);
        throw error;
    }
}

让我们来分解这段代码。

  • 第一个函数 uploadImage 接受图像的路径和可选的内容类型。它从文件系统读取图像,创建一个通用文件对象(这是 Umi 上传器配置所必要的),并将其上传到 IPFS。它返回上传图像的 CID。
  • 第二个函数 uploadMetadata 接受上传图像的 URI 并将元数据上传到 IPFS。它返回上传元数据的 CID。 这两个函数都使用 uploadToIpfs 函数将文件上传到 IPFS。所用的这个函数是从一个名为 upload.ts 的文件中导入的。现在让我们创建这个文件。

在终端中运行以下命令创建文件:

echo > upload.ts

然后,将以下代码复制到文件中:

import {
    GenericFile,
    request,
    HttpInterface,
    HttpRequest,
    HttpResponse,
} from '@metaplex-foundation/umi';

interface QuickNodeUploadResponse {
    requestid: string;
    status: string;
    created: string;
    pin: {
        cid: string;
        name: string;
        origins: string[];
        meta: Record<string, unknown>;
    };
    info: {
        size: string;
    };
    delegates: string[];
}

const createQuickNodeFetch = (): HttpInterface => ({
    send: async <ResponseData, RequestData = unknown>(
        request: HttpRequest<RequestData>
    ): Promise<HttpResponse<ResponseData>> => {
        let headers = new Headers(
            Object.entries(request.headers).map(([name, value]) => [name, value] as [string, string])
        );

        if (!headers.has('x-api-key')) {
            throw new Error('缺少 x-api-key 头');
        }

        const isJsonRequest = headers.get('content-type')?.includes('application/json') ?? false;
        const body = isJsonRequest && request.data ? JSON.stringify(request.data) : request.data as string | undefined;

        try {
            const response = await fetch(request.url, {
                method: request.method,
                headers,
                body,
                redirect: 'follow',
                signal: request.signal as AbortSignal,
            });

            const bodyText = await response.text();
            const isJsonResponse = response.headers.get('content-type')?.includes('application/json');
            const data = isJsonResponse ? JSON.parse(bodyText) : bodyText;

            return {
                data,
                body: bodyText,
                ok: response.ok,
                status: response.status,
                statusText: response.statusText,
                headers: Object.fromEntries(response.headers.entries()),
            };
        } catch (error) {
            console.error('获取请求失败:', error);
            throw error;
        }
    },
});

const getUrl = (cid: string, gatewayUrl = 'https://qn-shared.quicknode-ipfs.com/ipfs/'): string => {
    if (!cid) throw new Error('无效 CID:CID 不能为空。');
    const baseUrl = gatewayUrl.endsWith('/') ? gatewayUrl : `${gatewayUrl}/`;
    return `${baseUrl}${encodeURIComponent(cid)}`;
};

export const uploadToIpfs = async <T>(
    file: GenericFile,
    apiKey: string
): Promise<string> => {
    const http = createQuickNodeFetch();
    const endpoint = 'https://api.quicknode.com/ipfs/rest/v1/s3/put-object';
    const formData = new FormData();

    const fileBlob = new Blob([file.buffer], { type: 'application/json' });

    formData.append('Body', fileBlob);
    formData.append("Key", file.fileName);
    formData.append("ContentType", file.contentType || '');

    const qnRequest = request()
        .withEndpoint('POST', endpoint)
        .withHeader("x-api-key", apiKey)
        .withData(formData);

    try {
        const response = await http.send<QuickNodeUploadResponse, FormData>(qnRequest);
        if (!response.ok) throw new Error(`${response.status} - 请求发送失败: ${response.statusText}`);
        return getUrl(response.data.pin.cid, /* OPTIONAL_GATEWAY_URL  */); // 👈 在此处添加你的网关 URL
    } catch (error) {
        console.error('请求发送失败:', error);
        throw error;
    }
};

这看起来代码量很多,但其实这只是一个简单的 HTTP 请求到 QuickNode IPFS API。我们使用 Umi SDK 的 request 函数创建请求对象,再使用 HttpInterface 将请求发送到 IPFS API。如果请求成功,我们返回上传文件的 URL。如果出现错误,我们记录错误并抛出它。如果你想了解更多关于 QuickNode 的 IPFS API,请查看我们的文档,或者如果你想了解更多关于 Umi SDK 处理 HTTP 请求的信息,请查阅Metaplex 文档

_如果需要,你可以将自己的网关 URL 传入 getUrl 函数。如果这样做,请确保在 uploadToIpfs 函数中更新 OPTIONAL_GATEWAY_URL。_

干得不错!让我们返回 index.ts 文件并完成剩下的代码。请注意导入 uploadToIpfs 函数的任何错误现在应该都已解决。

铸造 NFT

现在我们已经有了用于上传 NFT 的图像和元数据的函数,我们可以编写铸造 NFT 的函数。在 index.ts 中添加以下功能以铸造新的数字资产:

async function mintAsset(metadataUri: string): Promise<void> {
    try {
        await mint(umi, {
            asset,
            owner: owner.publicKey,
            authority: creator.publicKey,
            payer: umi.identity,
            mutable: false,
            standard: 0,
            name: NFT_DETAILS.name,
            extensions: [\
                metadata({\
                    uri: metadataUri,\
                    symbol: NFT_DETAILS.symbol,\
                    description: NFT_DETAILS.description,\
                }),\
                royalties(NFT_DETAILS.royalties),\
                creators([{ address: creator.publicKey, share: 100 }]),\
            ]
        }).sendAndConfirm(umi, OPTIONS);
        const nftAddress = asset.publicKey.toString();
        console.log(`3. ✅ - 铸造了新资产: ${nftAddress}`);
    } catch (error) {
        console.error('3. ❌ - 铸造新 NFT 时出错。', error);
    }
}

我们的函数将接受上传的元数据的 URI,并铸造新的 NFT,然后使用 Nifty mint 函数铸造 NFT。

  • 我们使用 metadata 扩展定义上传元数据的 URI
  • 我们使用 royalties 扩展定义 NFT 的版税
  • 我们使用 creators 扩展定义 NFT 的创造者

可以自由探索其他 Nifty 扩展,并根据你的具体需求修改代码。

验证链上数据

一旦我们铸造 NFT,让我们验证链上的数据是否与预期匹配。我们来创建一个新的函数 verifyOnChainData 来实现此目的。此步骤不是必需的,但我们包括它是为了让你了解如何通过 Nifty SDK 访问数据。在 index.ts 中添加以下函数以验证链上数据:

async function verifyOnChainData(metadataUri: string): Promise<void> {
    try {
        const assetData = await fetchAsset(umi, asset.publicKey, OPTIONS.confirm);

        const onChainCreators = assetData.extensions.find(ext => ext.type === 3) as Creators;
        const onChainMetadata = assetData.extensions.find(ext => ext.type === 5) as Metadata;
        const onChainRoyalties = assetData.extensions.find(ext => ext.type === 7) as Royalties;

        const checks = [\
            // 资产检查\
            { condition: assetData.owner.toString() === owner.publicKey.toString(), message: '所有者匹配' },\
            { condition: assetData.publicKey.toString() === asset.publicKey.toString(), message: '公钥匹配' },\
            { condition: assetData.name === NFT_DETAILS.name, message: '资产名称匹配' },\
\
            // 创建者扩展检查\
            { condition: !!onChainCreators, message: '未找到创建者扩展' },\
            { condition: onChainCreators.values.length === 1, message: '创建者数量匹配' },\
            { condition: onChainCreators.values[0].address.toString() === creator.publicKey.toString(), message: '创建者地址匹配' },\
            { condition: onChainCreators.values[0].share === 100, message: '创建者分享匹配' },\
            { condition: onChainCreators.values[0].verified === true, message: '创建者未验证' },\
\
            // 元数据扩展检查\
            { condition: !!onChainMetadata, message: '未找到元数据扩展' },\
            { condition: onChainMetadata.symbol === NFT_DETAILS.symbol, message: '符号匹配' },\
            { condition: onChainMetadata.description === NFT_DETAILS.description, message: '描述匹配' },\
            { condition: onChainMetadata.uri === metadataUri, message: '元数据 URI 匹配' },\
\
            // 版税扩展检查\
            { condition: !!onChainRoyalties, message: '未找到版税扩展' },\
            { condition: onChainRoyalties.basisPoints.toString() === NFT_DETAILS.royalties.toString(), message: '版税基点匹配' },\
        ];

        checks.forEach(({ condition, message }) => {
            if (!condition) throw new Error(`验证失败: ${message}`);
        });

        console.log(`4. ✅ - 验证资产数据`);
    } catch (error) {
        console.error('4. ❌ - 验证资产数据时出错:', error);
    }
}

这里的内容比较多,我们来逐步分析。

  • 首先,我们使用 Nifty SDK 的 fetchAsset 函数获取资产数据
  • 接下来,我们从响应的 assetData 中找到预期的扩展数据。我们通过 Nifty SDK 中 ExtensionType 的枚举值位置找到正确的扩展数据(来源:这里)。
  • 然后,我们创建一个对象数组,用于检查数据是否与我们预期相符。每个对象将包含一个 condition(布尔值)和一个 message(字符串),如果条件不满足则记录。我们的 checks 数组包含多个对比 NFT_DETAILSassetData 对象的要求。
  • 最后,我们遍历 checks 数组,并在任何条件未满足时记录错误。

主函数

创建一个 main() 函数,将所有单个函数串联在一起,以顺序执行整个过程。在 index.ts 文件的末尾添加以下内容:

async function main() {
    const imageCid = await uploadImage('./pixel.png'); // 👈 用你的图像路径替换此内容
    const metadataCid = await uploadMetadata(imageCid);
    await mintAsset(metadataCid);
    await verifyOnChainData(metadataCid);
}

main();

确保向你的项目目录根目录添加一个 ./pixel.png 文件,或在 uploadImage 函数中更新图像的路径。

运行代码

要运行代码,请在终端中执行以下命令:

ts-node index.ts

如果一切设置正确,你应该看到指示每个步骤成功执行的控制台日志,或者在遇到问题时显示详细的错误信息。

ts-node index.ts
1. ✅ - 上传图像到 IPFS
2. ✅ - 上传元数据到 IPFS
3. ✅ - 铸造了新资产: F66dGYgKhRKzqkGAJEnJSHEL5qjLHG9vZSw6jMYcaDTM
4. ✅ - 验证资产数据

太棒了!相当不错吧?让我们在 Eclipse Explorer 上查看我们的 NFT:

只需搜索控制台输出中的账户地址,你就可以看到你的 NFT:

Eclipse Explorer

以及 NFT 元数据:

Eclipse Explorer

干得不错!你现在在 Eclipse 上拥有一个 NFT!

持续构建!

在本指南中,我们介绍了在 Eclipse 上铸造 NFTs 的基础知识。你现在具备了构建可以在 Eclipse 上处理各种数字资产的客户端应用程序的工具。那你还在等什么呢?

如果你有任何问题或想分享的想法,请在 DiscordTwitter 上与我们联系!

我们❤️反馈!

告诉我们 如果你有任何反馈或请求新主题。我们很乐意听到你的声音。

资源

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

0 条评论

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