如何构建一个代币持有者索引器和钱包API

  • QuickNode
  • 发布于 2025-01-30 13:53
  • 阅读 21

本文详细介绍了如何使用QuickNode的Streams、Functions和Key-Value Store构建一个ERC721合约的Token持有者索引器和钱包API。步骤清晰地指导用户设置流、过滤转账数据、及实现API读取Token元数据,适合希望监控和检索区块链上Token持有情况的开发者。

数据流

数据流对所有拥有 QuickNode 计划的用户可用。对于有独特需求的团队,我们提供定制数据集、专属支持和自定义集成。 联系我们 以获取更多信息。

概述

Token Holder Indexer 和 Wallet API 提供了一种全面的解决方案,用于跟踪与特定 ERC721 合约相关的代币所有权。本指南将指导你设置一个索引系统,该系统监控代币转移,并通过 QuickNode 的 StreamsFunctions、Key-Value Store 和 RPC 按钱包地址返回持有的代币和代币元数据。

你将做什么

  • 在 QuickNode 上设置一个 Stream 以监控代币转移。
  • 过滤传入的 Stream 数据以更新 Key-Value Store 中的代币持有量。
  • 实现一个函数通过 RPC 检索代币元数据。
  • 调用函数的内置 API 端点以返回钱包持有的代币和元数据。

你将需要什么

  • 一个 QuickNode 账号。
  • 对以太坊交易和事件的基本了解。查看我们的指南 以太坊交易和事件 以了解更多。
  • 一个位于 Ethereum Mainnet 的 QuickNode RPC 端点。
  • 当检测到转移时接收通知的 webhook URL。你可以在 webhook.site 获取一个免费的 webhook URL。

为什么选择 QuickNode Functions

Functions 是一个服务器无关的计算平台,用于部署轻量级、可扩展的代码,针对 web3 进行了优化,并内置对常见 web3 包(如 ethers.js、web3.js 和 QuickNode SDK)的访问。

  • 具有成本效益的无服务器计算:你只需为使用的资源付费,使 Functions 成为处理按需任务(如检索和处理区块链数据)的理想解决方案。使用费用在包含的用量超出后按每 GB-秒 $0.0000159 的固定费率计费,对于免费用户,包含的用量为 500 GB-秒。
  • 内置对实用包和 Web3 库的支持:QuickNode Functions 内置对各种实用包和 web3 库的支持,从而简化开发过程。这意味着你可以快速集成 web3 功能,而无需进行大量设置。
  • 内置 API 端点:你创建的每个函数都包括一个内置 API 端点。这样可以轻松将函数暴露为 web 服务,使你能够创建强大和可扩展的基于区块链的应用程序。
  • 可扩展性能:QuickNode 的全球负载均衡、自动扩展基础设施确保即使在高峰负载下也能顺利运作。这使得无论需求如何,你的 Functions 都能可靠运行。
  • 针对区块链进行了优化:将你的函数用作 Streams 的目标,新的数据输入时将自动激活。你还可以通过 API 激活你的函数,并在激活期间选择一个特定的区块链数据集以在函数内部访问。
  • 存储访问:在你的函数内无缝访问和管理 Key-Value Store 数据,提供了统一且高效的工作流程。

构建区块链索引器

在 QuickNode 上创建一个 Stream

首先,导航至 Dashboard 上的 QuickNode Streams 页面,然后单击“创建 Stream”。

接下来,使用以下设置创建一个 Stream:

QuickNode Stream settings

  • :以太坊
  • 网络:主网
  • 数据集:Receipts
  • Stream 开始:16985981(设置为指定合约部署区块前的区块)
  • Stream 负载:修改 Stream 负载以进行流式传输前的数据处理
  • 最新块延迟:12(这将帮助我们避免重组)

QuickNode Stream block delay

选择在流式传输前修改负载的选项。这允许你过滤流式数据并与 Key-Value Store 进行交互。

QuickNode Stream modify payload

接下来,复制并粘贴以下代码,以过滤(PIRATE)代币的 ERC721 合约的转移事件。该过滤器监听我们正在监控的代币合约上的转移事件,然后创建一个负载将代币持有量更新到 Key-Value Store 中。该过滤器将在 Key-Value Store 中创建以钱包地址为标识的 lists,并在将转移发送到钱包时以 contractAddress-tokenId 格式向列表中添加 items,并从发送者的持有地址列表中删除该项。

const test = false; // 如果你正在测试流,请将此值更改为 true

function stripPadding(logTopic) {
    return '0x' + logTopic.slice(-40).toLowerCase();
}

function main(stream) {
    var results = {
        fromActions: [],
        toActions: []
    };
    var actionsTaken = false;

    try {

        var stream = stream.data[0];
        var erc721TransferEvent = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef';
        var contractAddress = '0x1B41d54B3F8de13d58102c50D7431Fd6Aa1a2c48'.toLowerCase(); // 代币合约

        stream.forEach(receipt => {
            receipt.logs.forEach(log => {
                if (log.topics[0] === erc721TransferEvent && log.address.toLowerCase() === contractAddress) {
                    var from = stripPadding(log.topics[1]);
                    var to = stripPadding(log.topics[2]);
                    var tokenId = BigInt(log.topics[3]).toString();
                    var item = `${contractAddress}-${tokenId}`;

                    if (from !== '0x0000000000000000000000000000000000000000') {
                        results.fromActions.push({
                            wallet: from,
                            removed: [item],
                            action: test ? "模拟删除操作" : qnUpsertList(from, {
                                remove_items: [item]
                            })
                        });
                        actionsTaken = true;
                    }

                    if (to !== '0x0000000000000000000000000000000000000000') {
                        results.toActions.push({
                            wallet: to,
                            added: [item],
                            action: test ? "模拟添加操作" : qnUpsertList(to, {
                                add_items: [item]
                            })
                        });
                        actionsTaken = true;
                    }
                }
            });
        });

        if (!actionsTaken) {
            return null;
        } else {
            return {
                timestamp: new Date().toISOString(),
                actions: results
            };
        }
    } catch (e) {
        return { error: e.message };
    }
}

测试 Stream 过滤器

Key-Value Store

从此 Stream 中创建的 Key-Value Store 列表可通过 Streams、Functions 和 Key-Value Store REST API 全球访问。当我们在此步骤中测试 Stream 时,请确保将 const test = true; 设置为避免实际编辑 Key-Value Store 中的列表。完成测试后,请确保将 const test = false; 设回。

你可以通过在过滤器中设置 const test = true; 并在测试块字段中使用区块编号 20290986 来测试你的 Stream。然后单击运行测试按钮以测试你的过滤器。测试完成后,记得在单击下一步之前更新过滤器代码 const test = false;

QuickNode Stream filter test blockQuickNode Stream filter test results

单击“下一步”按钮,然后选择 "webhook" 作为你的 Stream 目的地。输入你的 webhook URL,以接收操作总结。你可以在 webhook.site 生成一个免费的 webhook URL。注意:保持任何未提及的设置不变。

实现函数

使用以下代码 实现函数,该函数通过 RPC 检索代币元数据。确保将 your-quicknode-rpc-endpoint 替换为实际的端点 URL。

const { Web3 } = require('web3');
const axios = require('axios');
const https = require('https');

const QUICKNODE_URL = 'your-quicknode-rpc-endpoint';

const web3 = new Web3(QUICKNODE_URL);

const httpsAgent = new https.Agent({
  rejectUnauthorized: false, // 警告:这会绕过 SSL 证书验证。请谨慎使用。
  secureProtocol: 'TLSv1_2_method'
});

/**
  *
  * main() 将在你调用此操作时运行。
  *
  * @param params 传递给函数的参数。
  *
  * @return 此操作的输出,必须是 JSON 对象。
  *
  */

async function main(params) {
    let wallet = params.user_data.wallet;
    let tokens = await qnLib.qnGetList(wallet); // 假设这返回一个代币字符串数组

    if (!tokens || tokens.length === 0) {
        return {
            message: '未找到代币'
        };
    } else {
        const tokenMetadata = await getTokenMetadata(tokens);
        return {
            message: wallet,
            tokens: tokenMetadata
        };
    }
}

async function getTokenMetadata(tokens) {
    const tokenMetadata = [];

    for (const token of tokens) {
        const [contractAddress, tokenId] = token.split('-');
        const metadata = await fetchTokenMetadata(contractAddress, tokenId);
        tokenMetadata.push({ contractAddress, tokenId, metadata });
    }

    return tokenMetadata;
}

async function fetchTokenMetadata(contractAddress, tokenId) {
    const contractABI = [\
        // 获取代币元数据的函数的 ABI 片段\
        {\
            "constant": true,\
            "inputs": [\
                {\
                    "name": "tokenId",\
                    "type": "uint256"\
                }\
            ],\
            "name": "tokenURI",\
            "outputs": [\
                {\
                    "name": "",\
                    "type": "string"\
                }\
            ],\
            "payable": false,\
            "stateMutability": "view",\
            "type": "function"\
        }\
    ];

    const contract = new web3.eth.Contract(contractABI, contractAddress);

    try {
        const tokenURI = await contract.methods.tokenURI(tokenId).call();
        // 使用 axios 从 tokenURI 获取元数据
        const response = await axios.get(tokenURI, { httpsAgent });
        return response.data;
    } catch (error) {
        console.error(`获取代币 ${tokenId} 在 ${contractAddress} 的元数据时出错:`, error);
        return null;
    }
}

module.exports = { main };

接下来,在 Functions 代码编辑器的右列中找到“用户数据”框,在该文本框中添加一个测试钱包,然后单击 Functions 代码编辑器中的“保存并测试”按钮。

{"Wallet":"0xde27d2e6b5009ead76ebc07452b54364fb54fdcd"}

QuickNode Function test

测试 API

一旦你的 Stream 已赶上当前区块,你可以检查一个钱包中持有的(PIRATE)代币。你将通过发送 wallet 地址到 user_data 中调用你的函数 API。如果钱包包含来自指定合约的代币,并且基本 URL 可访问,API 将返回钱包的持有代币和元数据。

示例 API 调用

curl -X POST "https://api.quicknode.com/functions/rest/v1/namespaces/{your-namespace-id}/functions/{your-function-name}/call?result_only=true" \
  -H "accept: application/json" \
  -H "Content-Type: application/json" \
  -H "x-api-key: QN_your-api-key" \
  -d '{
    "user_data": {
      "Wallet":"0xde27d2e6b5009ead76ebc07452b54364fb54fdcd"
    }
  }'

示例响应

{
  "wallet": "0xde27d2e6b5009ead76ebc07452b54364fb54fdcd",
  "tokens": [\
    {\
      "contractAddress": "0x1b41d54b3f8de13d58102c50d7431fd6aa1a2c48",\
      "metadata": {\
        "animation_url": "ipfs://QmQYSdcxKKkWcSb2jMiii6nY5x67gsifRxZihMuSH3EEem/1005",\
        "attributes": [\
          {"trait_type": "背景", "value": "弯刀"},\
          {"trait_type": "角色类型", "value": "人类男性"},\
          {"trait_type": "外套", "value": "紫色"},\
          {"trait_type": "掷骰子结果 1", "value": 5},\
          {"trait_type": "掷骰子结果 2", "value": 4},\
          {"trait_type": "眼睛", "value": "深邃"},\
          {"trait_type": "面部毛发", "value": "复古现代"},\
          {"trait_type": "头饰", "value": "刀具"},\
          {"trait_type": "肤色", "value": "浅棕"},\
          {"trait_type": "星座", "value": "狮子"},\
          {"trait_type": "世代", "value": 0},\
          {"trait_type": "经验值", "value": 28100},\
          {"trait_type": "等级", "value": 30},\
          {"trait_type": "已领取的宝箱", "value": 5},\
          {"trait_type": "元素亲和力", "value": "土"},\
          {"trait_type": "专长", "value": "健康"},\
          {"trait_type": "等级", "value": 30},\
          {"trait_type": "经验值", "value": 28100}\
        ],\
        "description": "与你的海盗团队一起出海!探索世界,收集经验值、战利品和无尽的财富,争当世界上最伟大的海盗船长!请访问 https://piratenation.game 游戏",\
        "external_url": "https://piratenation.game/pirate/0x1E52c21b9DfCd947d03E9546448f513F1EE8706c/1005",\
        "image": "ipfs://QmRjgEp89ovHdr1M8rkjoC6iEgbNna5kBDjbfubm4zeVDd/1005",\
        "image_png_1024": "ipfs://QmRjgEp89ovHdr1M8rkjoC6iEgbNna5kBDjbfubm4zeVDd/1005",\
        "image_png_128": "ipfs://QmdqWrDHCH2VoXpwkGskBjooVj99Sqw5zqTBKvwEAYuF3J/1005",\
        "image_png_2048": "ipfs://QmRjgEp89ovHdr1M8rkjoC6iEgbNna5kBDjbfubm4zeVDd/1005",\
        "image_png_256": "ipfs://Qmdfts291hNpr8SmqkN6sQgKLzpfabHQxXme7oBekHWDo3/1005",\
        "image_png_32": "ipfs://Qma2rcLonwgUR7dmz6zUVRHeTTfT8FwnNsrC7bmKnQrScU/1005",\
        "image_png_512": "ipfs://QmUZ4JXK5VhoAMt3UTMyGML9jE4TaTx5EakMHdU4o5V5Cz/1005",\
        "image_png_64": "ipfs://QmfHwFYQY5rV3pQ2J5UbNyeWstx384UWV16K1PvCoDmV7q/1005",\
        "image_svg": "ipfs://QmdnsH8mf1rN8C8w9Km3Et84VEqfTgDb2yJP5CWtKVwFxw/1005",\
        "model_gltf_url": "ipfs://QmeBen9iF2RQQP7h5haQjWKKxBUmmVMVzc3wGmveENuEZ5/1005",\
        "name": "创始人海盗 #1005"\
      },\
      "tokenId": "1005"\
    }\
  ]
}

其他资源

值得探索的 Functions 想法

  • 创建一个具有实时更新的自定义 NFT 市场
  • 创建自动交易机器人,根据特定的链上事件作出反应
  • 开发区块链数据的高级分析平台

结论

在本指南中,我们详细介绍了针对特定 ERC721 合约的代币所有权跟踪解决方案的设置。通过利用 QuickNode Streams、Functions 和 Key-Value Store,你可以有效地监控和检索按钱包地址提供的代币持有量和元数据,为各种基于区块链的应用程序提供一个强大的工具。

我们❤️反馈!

让我们知道 如果你有任何反馈或新的主题请求。我们期待你的声音。

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

0 条评论

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