> ## Documentation Index
> Fetch the complete documentation index at: https://docs.monad.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# 如何使用 Envio HyperSync 查询代币数据

export const CopyToClipboard = ({value, children}) => {
  const [copied, setCopied] = useState(false);
  const handleCopy = async () => {
    try {
      await navigator.clipboard.writeText(value);
      setCopied(true);
      setTimeout(() => setCopied(false), 1000);
    } catch {
      const textarea = document.createElement("textarea");
      textarea.value = value;
      textarea.style.position = "fixed";
      textarea.style.opacity = "0";
      document.body.appendChild(textarea);
      textarea.select();
      document.execCommand("copy");
      document.body.removeChild(textarea);
      setCopied(true);
      setTimeout(() => setCopied(false), 1000);
    }
  };
  return <span style={{
    display: "inline",
    whiteSpace: "nowrap"
  }}>
      {children}
      <button onClick={handleCopy} title={copied ? "Copied!" : "Copy to clipboard"} style={{
    background: "none",
    border: "none",
    cursor: "pointer",
    padding: "2px",
    display: "inline-flex",
    alignItems: "center",
    verticalAlign: "middle",
    marginLeft: "4px",
    opacity: copied ? 1 : 0.4,
    transition: "opacity 0.15s"
  }} onMouseEnter={e => {
    if (!copied) e.currentTarget.style.opacity = "0.8";
  }} onMouseLeave={e => {
    if (!copied) e.currentTarget.style.opacity = "0.4";
  }}>
        {copied ? <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
            <polyline points="20 6 9 17 4 12" />
          </svg> : <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <rect x="9" y="9" width="13" height="13" rx="2" ry="2" />
            <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
          </svg>}
      </button>
    </span>;
};

在本指南中，您将学习如何使用 Envio HyperSync 加速这一过程，为 Monad 上的 ERC-721、ERC-20 和 ERC-1155 合约高效计算代币归属情况。

## 背景

EVM 开发者经常遇到的问题是"历史余额问题" —— 恢复某个 ERC-20、ERC-721 或 ERC-1155 代币账户到余额的完整映射。Solidity 不会追踪 mapping 中的键；值在对键进行哈希后存储到相应的存储槽中。因此，要恢复余额，常见策略是重放该代币的所有转账事件并计算滚动累加。

Envio HyperSync 是一个索引器，允许开发者在几秒钟内查询数百万个区块链事件。在本指南中，我们将使用来自 HyperSync 的数据重建代币的历史余额。

## 前置条件

* Node.js 18+
* 从 [envio.dev/app/api-tokens](https://envio.dev/app/api-tokens) 获取的免费 HyperSync API 密钥

## HyperSync 端点

<div class="mintlify-table-wrapper">
  <table class="mintlify-table">
    <thead>
      <tr>
        <th>网络</th>
        <th>URL</th>
      </tr>
    </thead>

    <tbody>
      <tr>
        <td>测试网</td>

        <td>
          <CopyToClipboard value="https://monad-testnet.hypersync.xyz">`https://monad-testnet.hypersync.xyz`</CopyToClipboard>
        </td>
      </tr>

      <tr>
        <td>主网</td>

        <td>
          <CopyToClipboard value="https://monad.hypersync.xyz">`https://monad.hypersync.xyz`</CopyToClipboard>
        </td>
      </tr>
    </tbody>
  </table>
</div>

## 组件

本指南概述如何重建三种最流行代币标准（ERC-20、ERC-721 和 ERC-1155）的余额。每种都使用一些通用组件，但需要不同的逻辑来组合出最终结果。

### 查询 Transfer 事件

首先，让我们编写代码来查询合约的**所有** Transfer 事件，并按适当的签名过滤。

ERC-20 和 ERC-721 使用下面的 `Transfer` 事件，而 ERC-1155 使用 `TransferSingle` 和 `TransferBatch` 事件：

| Event                                                        | Signature                                                            |
| ------------------------------------------------------------ | -------------------------------------------------------------------- |
| `Transfer(address,address,uint256)`                          | `0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef` |
| `TransferSingle(address,address,address,uint256,uint256)`    | `0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62` |
| `TransferBatch(address,address,address,uint256[],uint256[])` | `0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb` |

```typescript lines title="lib/signatures.ts" theme={null}
// ERC-20 and ERC-721 share the same Transfer signature
const TRANSFER_EVENT = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

// ERC-1155 events
const TRANSFER_SINGLE = "0xc3d58168c5ae7397731d063d5bbf3d657854427343f4c083240f7aacaa2d0f62";
const TRANSFER_BATCH = "0x4a39dc06d4c0dbc64b70af90fd698a233a518aa5d07e595d983b8c0526c8f7fb";
```

以下是查询 ERC-20 合约事件的代码：

```typescript lines title="lib/hypersync.ts" theme={null}
const HYPERSYNC_URL = "https://monad-testnet.hypersync.xyz";

// keccak256("Transfer(address,address,uint256)") - same for ERC-20 and ERC-721
const TRANSFER_SIGNATURE = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

async function queryTransfers(contractAddress: string, apiKey: string) {
  const response = await fetch(`${HYPERSYNC_URL}/query`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      from_block: 0,
      logs: [
        {
          address: [contractAddress],
          topics: [[TRANSFER_SIGNATURE]],
        },
      ],
      field_selection: {
        log: ["topic0", "topic1", "topic2", "topic3", "data"],
      },
    }),
  });

  return response.json();
}
```

### 字段选择

为了优化查询，我们只请求实际需要的字段。这可以减少响应大小并显著加快查询速度。不同代币标准将数据编码在不同的 topic 中：

**ERC-721**（tokenId 位于 topic3）：

```json theme={null}
{ "log": ["topic0", "topic2", "topic3"] }
```

**ERC-20**（value 位于 data）：

```json theme={null}
{ "log": ["topic0", "topic1", "topic2", "data"] }
```

**ERC-1155**（id 和 value 位于 data）：

```json theme={null}
{ "log": ["topic0", "topic1", "topic2", "topic3", "data"] }
```

### 解析日志数据

现在我们有了原始事件数据，需要将其解析为可用的值。Topics 是 32 字节的十六进制字符串，其中地址左侧填充零，占据最后 20 字节：

```typescript lines title="lib/parse.ts" theme={null}
function parseAddress(topic: string): string {
  return "0x" + topic.slice(-40).toLowerCase();
}

function parseTokenId(topic: string): string {
  return BigInt(topic).toString();
}

function parseValue(data: string): bigint | null {
  if (!data || data === "0x") return null;
  return BigInt(data);
}
```

> **注意：** `parseValue` 返回原始代币值作为 `bigint`。ERC-20 代币具有 `decimals` 属性（通常为 18）—— 除以 `10n ** BigInt(decimals)` 以转换为人类可读的数量。

### 分页

HyperSync 返回分页结果以高效处理大型数据集。我们需要持续查询，直到 `next_block` 为 undefined，以获取所有转账：

```typescript lines title="lib/paginate.ts" theme={null}
async function fetchAllTransfers(contractAddress: string, apiKey: string) {
  const ownership = new Map<string, string>();
  let fromBlock = 0;
  let hasMore = true;

  while (hasMore) {
    const response = await queryHypersync(contractAddress, fromBlock, apiKey);

    // Process logs
    for (const block of response.data) {
      for (const log of block.logs) {
        const to = parseAddress(log.topic2);
        const tokenId = parseTokenId(log.topic3);
        ownership.set(tokenId, to);
      }
    }

    // Check for more pages
    if (response.next_block && response.next_block > fromBlock) {
      fromBlock = response.next_block;
    } else {
      hasMore = false;
    }
  }

  return ownership;
}
```

现在我们可以把所有内容整合起来。

## ERC-721 余额快照

对于 ERC-721，我们通过重放所有 Transfer 事件重建当前的持有状态。对于 NFT，每个 token ID 的最后一次转账决定了当前的所有者：

```typescript lines title="app/api/snapshot/route.ts" theme={null}
import { NextRequest, NextResponse } from "next/server";

const HYPERSYNC_URL = "https://monad-testnet.hypersync.xyz";

// keccak256("Transfer(address,address,uint256)") - same for ERC-20 and ERC-721
const TRANSFER_SIGNATURE = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";

const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

function parseAddress(topic: string): string {
  return "0x" + topic.slice(-40).toLowerCase();
}

function parseTokenId(topic: string): string {
  return BigInt(topic).toString();
}

interface HypersyncLog {
  topic0: string;
  topic2: string;
  topic3: string;
}

interface HypersyncResponse {
  data: { logs: HypersyncLog[] }[];
  next_block?: number;
}

async function queryHypersync(
  contractAddress: string,
  fromBlock: number,
  apiKey: string
): Promise<HypersyncResponse> {
  const response = await fetch(`${HYPERSYNC_URL}/query`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      from_block: fromBlock,
      logs: [{ address: [contractAddress], topics: [[TRANSFER_SIGNATURE]] }],
      field_selection: { log: ["topic0", "topic2", "topic3"] },
    }),
  });

  if (!response.ok) {
    throw new Error(`HyperSync error: ${response.status}`);
  }

  return response.json();
}

export async function GET(request: NextRequest) {
  const contract = request.nextUrl.searchParams.get("contract");
  const apiKey = process.env.HYPERSYNC_BEARER_TOKEN;

  if (!contract || !apiKey) {
    return NextResponse.json({ error: "Missing parameters" }, { status: 400 });
  }

  // Track current owner for each tokenId
  const ownership = new Map<string, string>();
  let fromBlock = 0;
  let hasMore = true;

  while (hasMore) {
    const response = await queryHypersync(contract, fromBlock, apiKey);

    for (const block of response.data) {
      for (const log of block.logs) {
        const to = parseAddress(log.topic2);
        const tokenId = parseTokenId(log.topic3);
        ownership.set(tokenId, to); // Last transfer wins
      }
    }

    if (response.next_block && response.next_block > fromBlock) {
      fromBlock = response.next_block;
    } else {
      hasMore = false;
    }
  }

  // Filter out burned tokens
  const snapshot = Array.from(ownership.entries())
    .filter(([, owner]) => owner !== ZERO_ADDRESS)
    .map(([tokenId, owner]) => ({ tokenId, owner }));

  return NextResponse.json({ snapshot });
}
```

## ERC-20 余额快照

对于 ERC-20 代币，我们需要累加所有转账以计算当前余额。与 NFT 追踪所有权不同，ERC-20 余额需要为每个地址累加所有转入和转出的转账。

首先，用正确的字段选择查询 ERC-20 转账（topic1 为 `from`，topic2 为 `to`，`data` 为值）：

```typescript lines title="lib/erc20.ts" theme={null}
async function queryERC20Transfers(
  contractAddress: string,
  fromBlock: number,
  apiKey: string
): Promise<HypersyncResponse> {
  const response = await fetch(`${HYPERSYNC_URL}/query`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "Authorization": `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      from_block: fromBlock,
      logs: [{ address: [contractAddress], topics: [[TRANSFER_SIGNATURE]] }],
      field_selection: { log: ["topic0", "topic1", "topic2", "data"] },
    }),
  });

  if (!response.ok) {
    throw new Error(`HyperSync error: ${response.status}`);
  }

  return response.json();
}
```

然后通过累加所有转账重建余额：

```typescript lines title="lib/erc20.ts" theme={null}
async function getERC20Balances(contractAddress: string, apiKey: string) {
  const balances = new Map<string, bigint>();
  let fromBlock = 0;
  let hasMore = true;

  while (hasMore) {
    const response = await queryERC20Transfers(contractAddress, fromBlock, apiKey);

    for (const block of response.data) {
      for (const log of block.logs) {
        const from = parseAddress(log.topic1);
        const to = parseAddress(log.topic2);
        const value = parseValue(log.data);

        if (value === null) continue;

        // Subtract from sender
        if (from !== ZERO_ADDRESS) {
          const current = balances.get(from) || 0n;
          balances.set(from, current - value);
        }

        // Add to receiver
        if (to !== ZERO_ADDRESS) {
          const current = balances.get(to) || 0n;
          balances.set(to, current + value);
        }
      }
    }

    if (response.next_block && response.next_block > fromBlock) {
      fromBlock = response.next_block;
    } else {
      hasMore = false;
    }
  }

  // Filter positive balances, sort descending
  return Array.from(balances.entries())
    .filter(([, balance]) => balance > 0n)
    .sort((a, b) => (b[1] > a[1] ? 1 : -1))
    .map(([address, balance]) => ({ address, balance: balance.toString() }));
}
```

## ERC-1155 余额快照

ERC-1155 代币与 ERC-20 和 ERC-721 的工作方式不同。它们将 `id` 和 `value` 存储在 data 字段而不是 topic 中，需要更复杂的解析：

```typescript lines title="lib/erc1155.ts" theme={null}
// TransferSingle: data = id (32 bytes) + value (32 bytes)
function parseTransferSingle(data: string): { tokenId: string; value: bigint } | null {
  if (!data || data.length < 130) return null;
  const tokenId = BigInt("0x" + data.slice(2, 66)).toString();
  const value = BigInt("0x" + data.slice(66, 130));
  return { tokenId, value };
}

// TransferBatch: ABI-encoded arrays
function parseTransferBatch(data: string): { tokenId: string; value: bigint }[] | null {
  if (!data || data.length < 258) return null;

  // Decode offsets
  const idsOffset = Number(BigInt("0x" + data.slice(2, 66)));
  const valuesOffset = Number(BigInt("0x" + data.slice(66, 130)));

  // Read array lengths
  const idsLengthStart = 2 + idsOffset * 2;
  const idsLength = Number(BigInt("0x" + data.slice(idsLengthStart, idsLengthStart + 64)));

  const valuesLengthStart = 2 + valuesOffset * 2;
  const valuesLength = Number(BigInt("0x" + data.slice(valuesLengthStart, valuesLengthStart + 64)));

  if (idsLength !== valuesLength || idsLength === 0) return null;

  // Parse each id/value pair
  const results: { tokenId: string; value: bigint }[] = [];
  for (let i = 0; i < idsLength; i++) {
    const idStart = idsLengthStart + 64 + i * 64;
    const valueStart = valuesLengthStart + 64 + i * 64;
    const tokenId = BigInt("0x" + data.slice(idStart, idStart + 64)).toString();
    const value = BigInt("0x" + data.slice(valueStart, valueStart + 64));
    results.push({ tokenId, value });
  }

  return results;
}
```

### 查询多种事件类型

在处理 ERC-1155 代币时，我们可以通过在单个请求中同时查询 TransferSingle 和 TransferBatch 事件来优化：

```typescript lines title="lib/multi-event.ts" theme={null}
const query = {
  from_block: 0,
  logs: [
    {
      address: [contractAddress],
      topics: [[TRANSFER_SINGLE, TRANSFER_BATCH]], // Both ERC-1155 events
    },
  ],
  field_selection: {
    log: ["topic0", "topic1", "topic2", "topic3", "data"],
  },
};
```

然后根据 `topic0` 路由：

```typescript lines theme={null}
for (const log of block.logs) {
  if (log.topic0 === TRANSFER_SINGLE) {
    const parsed = parseTransferSingle(log.data);
    // handle single transfer...
  } else if (log.topic0 === TRANSFER_BATCH) {
    const parsed = parseTransferBatch(log.data);
    // handle batch transfer...
  }
}
```

## 获取最新区块

要验证您已同步所有可用数据，您可以检查当前链的高度：

```typescript lines title="lib/height.ts" theme={null}
async function getLatestBlock(apiKey: string): Promise<number> {
  const response = await fetch(`${HYPERSYNC_URL}/height`, {
    headers: { "Authorization": `Bearer ${apiKey}` },
  });
  const data = await response.json();
  return data.height;
}
```

## 常见错误

### 1. 忘记分页

HyperSync 返回的是部分结果。请始终检查 `next_block`：

```typescript theme={null}
// Wrong - only gets first page
const response = await queryHypersync(contract, 0);

// Right - loop until done
while (response.next_block) {
  // continue querying...
}
```

### 2. 请求未使用的字段

每个字段都会增加响应大小。请显式指定：

```typescript theme={null}
// Wrong - fetches everything
field_selection: { log: ["*"] }

// Right - only what you need
field_selection: { log: ["topic0", "topic2", "topic3"] }
```

### 3. 使用库进行简单解析

HyperSync 返回原始十六进制。原生 BigInt 可以处理它：

```typescript theme={null}
// Unnecessary - adds dependency
import { decodeAbiParameters } from "viem";

// Sufficient - native JS
const value = BigInt("0x" + data.slice(2, 66));
```

### 4. 未过滤已销毁的代币

地址 `0x000...000` 表示已销毁：

```typescript theme={null}
const ZERO_ADDRESS = "0x0000000000000000000000000000000000000000";

// Filter them out
const active = ownership.filter(([, owner]) => owner !== ZERO_ADDRESS);
```

## API 参考

### POST /query

查询事件日志。

**请求体：**

| 字段                    | 类型           | 说明                          |
| --------------------- | ------------ | --------------------------- |
| `from_block`          | number       | 起始区块（包含）                    |
| `to_block`            | number       | 结束区块（可选）                    |
| `logs`                | array        | 日志过滤器                       |
| `logs[].address`      | string\[]    | 要过滤的合约地址                    |
| `logs[].topics`       | string\[]\[] | Topic 过滤器（数组内为 OR，跨数组为 AND） |
| `field_selection.log` | string\[]    | 要返回的字段                      |

**响应：**

| 字段            | 类型     | 说明            |
| ------------- | ------ | ------------- |
| `data`        | array  | 包含匹配日志的区块     |
| `data[].logs` | array  | 区块中的匹配日志      |
| `next_block`  | number | 下一个要查询的区块（分页） |

### GET /height

获取当前链的高度。

**响应：**

| 字段       | 类型     | 说明    |
| -------- | ------ | ----- |
| `height` | number | 最新区块号 |

## 总结

Envio HyperSync 让您可以通过单个分页 API 查询 Monad 上任何合约的事件历史 —— 无需运行节点，也无需维护索引器。

`/query` 端点接受 topic 和地址过滤器，`/height` 会给您当前链的高度。请查看[完整 HyperSync 文档](https://docs.envio.dev/docs/HyperSync/overview)以了解更多查询类型和选项。

在此基础上，您可以扩展此模式以构建空投资格检查器、治理投票权快照或多代币组合追踪器。

## 后续步骤

* [Envio 文档](https://docs.envio.dev/docs/HyperSync/overview) - 完整的 HyperSync 文档
* [API tokens](https://envio.dev/app/api-tokens) - 获取您的免费 API 密钥
