dApp Docs/The Graph子图索引器指南
Development reference. Not independently verified for production.

The Graph 子图索引器指南 — MSG Chain 区块链

数据来源:MSG Chain 代码库核实

主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。


目录

  1. 概述
  2. 架构
  3. 开发环境
  4. 子图清单 (subgraph.yaml)
  5. GraphQL 架构 (schema.graphql)
  6. 事件处理器 (AssemblyScript)
  7. 数据源模板
  8. 编译与部署
  9. 前端查询示例
  10. 托管计划
  11. 性能优化
  12. 当前边界与限制
  13. 故障排查
  14. 附录

1. 概述

1.1 什么是 The Graph

The Graph 是一个去中心化的索引协议,允许开发者通过称为"子图"(subgraph)的开放 API 来查询区块链数据。它最初为 Ethereum 设计,但后来扩展到支持其他区块链,包括基于 Cosmos SDK 的链。The Graph 的核心组件——Graph Node——持续监听链上事件,执行用户定义的映射逻辑,并将数据存入 PostgreSQL 数据库,最终通过 GraphQL 端点提供查询服务。

The Graph 的架构类似于 Ethereum 子图的工作方式:开发者编写清单文件定义数据源,编写 GraphQL Schema 定义实体类型,然后通过 AssemblyScript 映射事件到数据库操作。对于 Cosmos 生态,Graph Node 支持 cosmwasm 和 cosmos 数据源类型,使其能够处理 Tendermint 事件和 CosmWasm 合约事件。

1.2 为什么在 MSG Chain 上使用子图

MSG Chain 目前拥有一个内部索引器 API,提供 RESTful 端点用于查询索引数据。然而,随着生态发展,内部索引器存在以下局限性:

方面 内部索引器 The Graph 子图
查询语言 REST + 有限搜索 GraphQL(灵活、类型安全)
数据关联 手动关联 自动外键关联(@derivedFrom)
实时性 轮询机制 实时事件驱动
去中心化 中心化 可选去中心化(The Graph Network)
可组合性 封闭 开放、可组合
索引效率 通用 针对特定数据优化

选择 The Graph 子图的主要理由:

  1. GraphQL 查询灵活性:前端可以精确指定需要的数据字段,减少过度获取和欠获取
  2. 关联数据导航:通过 @derivedFrom 自动建立实体间关系,无需手动 JOIN
  3. 实时索引:事件驱动架构确保数据在区块确认后立即可用
  4. 生态兼容:与 Ethereum/Cosmos 生态共享相同的工具链
  5. 去中心化选项:可以选择加入 The Graph Network 获得去中心化索引服务

1.3 与类似解决方案的对比


2. 架构

2.1 整体架构

+---------------------------+        +---------------------------+
|     MSG Chain 区块链       |        |     IPFS(子图部署)       |
|  - Tendermint RPC (26657) |        |  - 存储子图元数据         |
|  - CosmWasm 合约          |        |  - 存储映射 WASM         |
|  - 事件流                  |        |                           |
+-----------+---------------+        +------------+--------------+
            |                                      |
            | 轮询新区块 & 事件                       | 获取子图文件
            v                                      v
+----------------------------------------------------------+
|                  Graph Node (graph-node)                   |
|                                                            |
|  +------------------+  +--------------+  +--------------+  |
|  | Cosmos 适配器     |  | 事件过滤     |  | IPFS 客户端   |  |
|  | (cosmwasm 数据源) |  |              |  |              |  |
|  +--------+---------+  +------+-------+  +------+-------+  |
|           |                    |                 |          |
|           v                    v                 v          |
|  +--------------------------------------------------+     |
|  |              映射执行引擎 (WASM Runtime)           |     |
|  |  - AssemblyScript -> WASM                         |     |
|  |  - 事件处理器                                    |     |
|  +--------------------------------------------------+     |
|                        |                                    |
|                        v                                    |
|  +--------------------------------------------------+     |
|  |              PostgreSQL 数据库                     |     |
|  |  实体表:agents, did_documents, payment_sessions,  |     |
|  |  constitutions, rules, micropayment_channels,     |     |
|  |  blocks, transactions                             |     |
|  +--------------------------------------------------+     |
|                        |                                    |
|                        v                                    |
|  +--------------------------------------------------+     |
|  |           GraphQL 端点 (HTTP)                    |     |
|  |  http://localhost:8000/subgraphs/name/子图名      |     |
|  +--------------------------------------------------+     |
+----------------------------------------------------------+
                        |
                        v
+----------------------------------------------------------+
|                    前端 / dApp                             |
|  - Apollo Client 查询                                     |
|  - urql 查询                                              |
|  - graphql-request                                        |
+----------------------------------------------------------+

2.2 核心组件

组件 角色 端口
graph-node 主索引引擎 8000 (GraphQL), 8020 (WebSocket), 8030 (admin)
PostgreSQL 持久化索引数据 5432
IPFS 存储子图部署清单 5001 (API), 8080 (Gateway)
MSG Chain RPC 提供区块/事件数据 26657

2.3 数据流

  1. 区块轮询:Graph Node 通过 PollingBlockStream 从 MSG Chain RPC 获取新区块
  2. 事件提取:从区块中提取 CosmWasm 事件(类型为 wasm 的事件)
  3. 事件过滤:根据 subgraph.yaml 中定义的 eventHandlers 匹配事件类型
  4. 映射执行:将匹配的事件传递给 AssemblyScript 处理器
  5. 数据写入:处理器通过 store.set() 将实体写入 PostgreSQL
  6. GraphQL 响应:所有写入数据通过 GraphQL 端点暴露

3. 开发环境

3.1 前置条件

3.2 安装 graph-cli

npm install -g @graphprotocol/graph-cli
graph --version

3.3 初始化子图项目

graph init \
  --product subgraph-studio \
  --from-contract <可选> \
  --network msg-chain-1 \
  --abi <可选> \
  --node http://localhost:8020 \
  --protocol cosmwasm \
  msg-chain-subgraph

3.4 graph-ts 类型定义

yarn add --dev @graphprotocol/graph-ts@0.30.0
yarn add --dev assemblyscript@0.19.0

graph-ts 提供以下关键类型:

类型 用途
cosmos.Event Cosmos 事件
cosmos.Block 区块信息
store 数据持久化 API
Bytes 字节数组
BigInt 大整数
log 日志工具

3.5 完整的 package.json

{
  "name": "msg-chain-subgraph",
  "version": "1.0.0",
  "description": "MSG Chain 子图 - 索引 AI 代理合约",
  "private": true,
  "scripts": {
    "auth": "graph auth --product hosted-service <ACCESS_TOKEN>",
    "build": "graph build",
    "build:watch": "graph build --watch",
    "deploy": "graph deploy --node https://api.thegraph.com/deploy/ msg-chain/subgraph",
    "deploy:local": "graph create --node http://localhost:8020 msg-chain/subgraph && graph deploy --node http://localhost:8020 --ipfs http://localhost:5001 msg-chain/subgraph",
    "codegen": "graph codegen",
    "create:local": "graph create --node http://localhost:8020 msg-chain/subgraph",
    "remove:local": "graph remove --node http://localhost:8020 msg-chain/subgraph",
    "deploy:studio": "graph deploy --studio msg-chain-subgraph",
    "lint": "eslint . --ext .ts",
    "test": "graph test"
  },
  "devDependencies": {
    "@graphprotocol/graph-cli": "^0.62.0",
    "@graphprotocol/graph-ts": "^0.30.0",
    "assemblyscript": "^0.19.0",
    "matchstick-as": "^0.5.0",
    "@typescript-eslint/eslint-plugin": "^6.0.0",
    "@typescript-eslint/parser": "^6.0.0",
    "eslint": "^8.0.0"
  }
}

3.6 tsconfig.json

{
  "extends": "@graphprotocol/graph-ts/types/tsconfig.base.json",
  "compilerOptions": {
    "target": "es2020",
    "module": "commonjs",
    "lib": ["es2020"],
    "types": ["@graphprotocol/graph-ts"],
    "outDir": "dist",
    "rootDir": "src",
    "strict": true,
    "esModuleInterop": true,
    "resolveJsonModule": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules", "dist", "build"]
}

3.7 Docker Compose(本地 Graph Node)

version: "3.8"

services:
  postgres:
    image: postgres:15-alpine
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: graph-node
      POSTGRES_PASSWORD: let-me-in
      POSTGRES_DB: graph-node
      POSTGRES_INITDB_ARGS: "-E UTF8 --locale=C"
    volumes:
      - ./data/postgres:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U graph-node"]
      interval: 10s
      timeout: 5s
      retries: 5

  ipfs:
    image: ipfs/kubo:v0.20.0
    restart: always
    ports:
      - "5001:5001"
      - "8080:8080"
    volumes:
      - ./data/ipfs:/data/ipfs
    healthcheck:
      test: ["CMD", "ipfs", "id"]
      interval: 30s
      timeout: 10s
      retries: 5
    command:
      - "daemon"
      - "--migrate=true"

  graph-node:
    image: graphprotocol/graph-node:latest
    restart: always
    ports:
      - "8000:8000"
      - "8020:8020"
      - "8030:8030"
      - "8040:8040"
    depends_on:
      postgres:
        condition: service_healthy
      ipfs:
        condition: service_healthy
    environment:
      postgres_host: postgres
      postgres_port: 5432
      postgres_user: graph-node
      postgres_pass: let-me-in
      postgres_db: graph-node
      ipfs: http://ipfs:5001
      ETHEREUM_RPC: msg-chain-1:http://host.docker.internal:26657
      GRAPH_LOG: info
      GRAPH_ALLOW_NON_DETERMINISTIC_FULLTEXT_SEARCH: "true"
      GRAPH_MAX_GAS_PER_HANDLER: "10000000"
      GRAPH_ENABLE_COSMOS: "true"
      GRAPH_EXPERIMENTAL_SUBGRAPH_ERRORS: "1"
      GRAPH_ETHEREUM_BLOCK_POLLING_INTERVAL: "5000"
      GRAPH_ETHEREUM_MAX_BLOCK_RANGE_SIZE: "100"
      GRAPH_STORE_WRITE_BATCH_SIZE: "500"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:8030/health"]
      interval: 30s
      timeout: 10s
      retries: 5

启动本地 Graph Node:

docker compose up -d
docker compose logs -f graph-node
docker compose down

4. 子图清单 (subgraph.yaml)

4.1 完整清单文件

specVersion: 0.0.5
description: "MSG Chain AI Agent 子图 - 索引 AI 代理注册、支付、DID、宪章和微支付通道"
repository: https://github.com/msgchain/subgraph
schema:
  file: ./schema.graphql

dataSources:
  # ==============================================================
  # 数据源 1: agent_registry_v1
  # ==============================================================
  - kind: cosmwasm
    name: agent_registry_v1
    network: msg-chain-1
    source:
      address: "msg14hj2tavq8fpeswxxa0w5xlf3v6n3a2m9v0p5k6"
      startBlock: 0
      abi: agent_registry_v1
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/agent-registry.ts
      entities:
        - Agent
        - AgentHistory
      eventHandlers:
        - event: register_agent
          handler: handleRegisterAgent
        - event: update_agent
          handler: handleUpdateAgent
        - event: deregister_agent
          handler: handleDeregisterAgent
        - event: set_agent_status
          handler: handleSetAgentStatus
      abis:
        - name: agent_registry_v1
          file: ./abis/agent_registry_v1.json

  # ==============================================================
  # 数据源 2: agent_payment_v1
  # ==============================================================
  - kind: cosmwasm
    name: agent_payment_v1
    network: msg-chain-1
    source:
      address: "msg14hj2tavq8fpeswxxa0w5xlf3v6n3a2m9v0p5k7"
      startBlock: 0
      abi: agent_payment_v1
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/agent-payment.ts
      entities:
        - PaymentSession
        - PaymentMilestone
        - PaymentActivity
      eventHandlers:
        - event: create_session
          handler: handleCreateSession
        - event: fund_session
          handler: handleFundSession
        - event: release_payment
          handler: handleReleasePayment
        - event: dispute_payment
          handler: handleDisputePayment
        - event: close_session
          handler: handleCloseSession
        - event: complete_milestone
          handler: handleCompleteMilestone
      abis:
        - name: agent_payment_v1
          file: ./abis/agent_payment_v1.json

  # ==============================================================
  # 数据源 3: aidid_did_registry_v1
  # ==============================================================
  - kind: cosmwasm
    name: aidid_did_registry_v1
    network: msg-chain-1
    source:
      address: "msg14hj2tavq8fpeswxxa0w5xlf3v6n3a2m9v0p5k8"
      startBlock: 0
      abi: aidid_did_registry_v1
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/did-registry.ts
      entities:
        - DIDDocument
        - DIDController
        - VerificationMethod
        - DIDActivity
      eventHandlers:
        - event: create_did
          handler: handleCreateDID
        - event: update_did
          handler: handleUpdateDID
        - event: deactivate_did
          handler: handleDeactivateDID
        - event: add_verification_method
          handler: handleAddVerificationMethod
        - event: remove_verification_method
          handler: handleRemoveVerificationMethod
      abis:
        - name: aidid_did_registry_v1
          file: ./abis/aidid_did_registry_v1.json

  # ==============================================================
  # 数据源 4: ai_agent_constitution_v1
  # ==============================================================
  - kind: cosmwasm
    name: ai_agent_constitution_v1
    network: msg-chain-1
    source:
      address: "msg14hj2tavq8fpeswxxa0w5xlf3v6n3a2m9v0p5k9"
      startBlock: 0
      abi: ai_agent_constitution_v1
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/agent-constitution.ts
      entities:
        - Constitution
        - Rule
        - ConstitutionActivity
      eventHandlers:
        - event: add_rule
          handler: handleAddRule
        - event: remove_rule
          handler: handleRemoveRule
        - event: update_rule
          handler: handleUpdateRule
        - event: set_active
          handler: handleSetActive
      abis:
        - name: ai_agent_constitution_v1
          file: ./abis/ai_agent_constitution_v1.json

  # ==============================================================
  # 数据源 5: micropayment_session_v1
  # ==============================================================
  - kind: cosmwasm
    name: micropayment_session_v1
    network: msg-chain-1
    source:
      address: "msg14hj2tavq8fpeswxxa0w5xlf3v6n3a2m9v0p6k0"
      startBlock: 0
      abi: micropayment_session_v1
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/micropayment.ts
      entities:
        - MicropaymentChannel
        - ChannelState
        - ChannelActivity
      eventHandlers:
        - event: open_channel
          handler: handleOpenChannel
        - event: deposit_channel
          handler: handleDeposit
        - event: claim_channel
          handler: handleClaim
        - event: close_channel
          handler: handleCloseChannel
        - event: update_channel_state
          handler: handleUpdateChannelState
      abis:
        - name: micropayment_session_v1
          file: ./abis/micropayment_session_v1.json

  # ==============================================================
  # 数据源 6: 区块事务数据源
  # ==============================================================
  - kind: cosmos
    name: block_data
    network: msg-chain-1
    source:
      startBlock: 0
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/block-data.ts
      entities:
        - Block
        - Transaction
      blockHandlers:
        - handler: handleBlock
      transactionHandlers:
        - handler: handleTransaction

4.2 清单配置详解

字段 说明 示例值
specVersion 子图规范版本 0.0.5
kind 数据源类型 cosmwasm 或 cosmos
network 目标网络 msg-chain-1
source.address 合约地址 msg14hj2...
source.startBlock 起始索引区块 0
mapping.apiVersion 映射 API 版本 0.0.7
mapping.language 映射语言 wasm/assemblyscript
eventHandlers.event 事件名称 register_agent

4.3 关于 kind 字段的说明

对于 MSG Chain(基于 Cosmos SDK + CosmWasm),有两种数据源类型可选:


5. GraphQL 架构 (schema.graphql)

5.1 完整 Schema

"""
============================================================
枚举类型
============================================================
"""
enum AgentStatus {
  Active
  Inactive
  Suspended
  Deregistered
}

enum SessionStatus {
  Pending
  Funded
  InProgress
  Completed
  Disputed
  Cancelled
}

enum DisputeStatus {
  None
  Raised
  Resolved
  Escalated
}

enum ChannelStatus {
  Open
  Closing
  Closed
}

enum DIDStatus {
  Active
  Deactivated
}

enum RuleStatus {
  Active
  Inactive
}

enum ConstitutionStatus {
  Active
  Inactive
}

enum ActivityType {
  Created
  Updated
  Deactivated
  Deleted
  Funded
  Released
  Disputed
  Resolved
  Completed
}

"""
============================================================
AI 代理相关实体 (agent_registry_v1)
============================================================
"""
type Agent @entity {
  id: ID!
  agentId: String!
  owner: String!
  operator: String
  name: String!
  description: String
  metadata: String
  agentType: String!
  capabilities: [String!]!
  status: AgentStatus!
  statusReason: String
  constitution: Constitution
  did: DIDDocument
  sessions: [PaymentSession!] @derivedFrom(field: "agent")
  paymentChannels: [MicropaymentChannel!] @derivedFrom(field: "agent")
  totalSessions: BigInt!
  totalEarned: BigInt!
  createdAt: BigInt!
  updatedAt: BigInt!
  createdBlock: BigInt!
  updatedBlock: BigInt!
  createdTx: Transaction
  history: [AgentHistory!] @derivedFrom(field: "agent")
}

type AgentHistory @entity {
  id: ID!
  agent: Agent!
  status: AgentStatus!
  previousStatus: AgentStatus!
  reason: String
  block: BigInt!
  timestamp: BigInt!
  txHash: String!
}

"""
============================================================
DID 相关实体 (aidid_did_registry_v1)
============================================================
"""
type DIDDocument @entity {
  id: ID!
  did: String!
  address: String!
  controller: [DIDController!] @derivedFrom(field: "controlledDID")
  verificationMethods: [VerificationMethod!] @derivedFrom(field: "didDocument")
  authentication: [String!]
  assertionMethod: [String!]
  keyAgreement: [String!]
  capabilityInvocation: [String!]
  capabilityDelegation: [String!]
  serviceEndpoints: String
  created: BigInt!
  updated: BigInt!
  deactivated: Boolean!
  status: DIDStatus!
  agent: Agent @derivedFrom(field: "did")
  activities: [DIDActivity!] @derivedFrom(field: "didDocument")
  createdBlock: BigInt!
  updatedBlock: BigInt!
}

type DIDController @entity {
  id: ID!
  address: String!
  controlledDID: DIDDocument!
  permissions: [String!]
  addedAt: BigInt!
}

type VerificationMethod @entity {
  id: ID!
  methodId: String!
  didDocument: DIDDocument!
  type: String!
  controller: String!
  publicKeyMultibase: String
  blockchainAccountId: String
  addedAt: BigInt!
}

type DIDActivity @entity {
  id: ID!
  didDocument: DIDDocument!
  activityType: ActivityType!
  block: BigInt!
  timestamp: BigInt!
  txHash: String!
  details: String
}

"""
============================================================
支付相关实体 (agent_payment_v1)
============================================================
"""
type PaymentSession @entity {
  id: ID!
  sessionId: String!
  agent: Agent!
  client: String!
  totalAmount: BigInt!
  fundedAmount: BigInt!
  releasedAmount: BigInt!
  disputedAmount: BigInt!
  token: String!
  status: SessionStatus!
  disputeStatus: DisputeStatus!
  milestones: [PaymentMilestone!] @derivedFrom(field: "session")
  activities: [PaymentActivity!] @derivedFrom(field: "session")
  description: String
  metadata: String
  expiresAt: BigInt
  startedAt: BigInt!
  completedAt: BigInt
  createdAt: BigInt!
  updatedAt: BigInt!
}

type PaymentMilestone @entity {
  id: ID!
  milestoneId: String!
  session: PaymentSession!
  amount: BigInt!
  description: String
  status: String!
  completedAt: BigInt
  createdAt: BigInt!
}

type PaymentActivity @entity {
  id: ID!
  session: PaymentSession!
  activityType: ActivityType!
  amount: BigInt
  block: BigInt!
  timestamp: BigInt!
  txHash: String!
  from: String
  to: String
  details: String
}

"""
============================================================
宪章相关实体 (ai_agent_constitution_v1)
============================================================
"""
type Constitution @entity {
  id: ID!
  constitutionId: String!
  name: String!
  version: String!
  description: String
  status: ConstitutionStatus!
  rules: [Rule!] @derivedFrom(field: "constitution")
  agent: Agent!
  ruleCount: Int!
  isActive: Boolean!
  createdAt: BigInt!
  updatedAt: BigInt!
}

type Rule @entity {
  id: ID!
  ruleId: String!
  constitution: Constitution!
  title: String!
  description: String!
  ruleType: String!
  priority: Int!
  parameters: String
  status: RuleStatus!
  isActive: Boolean!
  version: Int!
  effectiveFrom: BigInt!
  createdAt: BigInt!
  updatedAt: BigInt!
}

type ConstitutionActivity @entity {
  id: ID!
  constitution: Constitution!
  ruleId: String
  activityType: ActivityType!
  block: BigInt!
  timestamp: BigInt!
  txHash: String!
  details: String
}

"""
============================================================
微支付通道相关实体 (micropayment_session_v1)
============================================================
"""
type MicropaymentChannel @entity {
  id: ID!
  channelId: String!
  agent: Agent!
  sender: String!
  receiver: String!
  totalDeposit: BigInt!
  claimedAmount: BigInt!
  remainingBalance: BigInt!
  token: String!
  status: ChannelStatus!
  nonce: BigInt!
  expiresAt: BigInt
  stateHistory: [ChannelState!] @derivedFrom(field: "channel")
  activities: [ChannelActivity!] @derivedFrom(field: "channel")
  createdAt: BigInt!
  updatedAt: BigInt!
}

type ChannelState @entity {
  id: ID!
  channel: MicropaymentChannel!
  nonce: BigInt!
  transferredAmount: BigInt!
  pendingAmount: BigInt!
  signature: String
  block: BigInt!
  timestamp: BigInt!
}

type ChannelActivity @entity {
  id: ID!
  channel: MicropaymentChannel!
  activityType: ActivityType!
  amount: BigInt
  block: BigInt!
  timestamp: BigInt!
  txHash: String!
  from: String
  to: String
  details: String
}

"""
============================================================
区块和交易实体
============================================================
"""
type Block @entity {
  id: ID!
  height: BigInt!
  hash: String!
  parentHash: String
  proposer: String
  timestamp: BigInt!
  txCount: Int!
  transactions: [Transaction!] @derivedFrom(field: "block")
}

type Transaction @entity {
  id: ID!
  txHash: String!
  block: Block!
  index: Int!
  from: String!
  to: String
  gasUsed: BigInt!
  gasWanted: BigInt!
  fee: BigInt!
  memo: String
  success: Boolean!
  code: Int!
  rawLog: String
  events: [String!]
  timestamp: BigInt!
}

"""
============================================================
聚合统计
============================================================
"""
type AggregationStats @entity {
  id: ID!
  totalAgents: Int!
  activeAgents: Int!
  totalSessions: Int!
  completedSessions: Int!
  totalVolume: BigInt!
  totalDIDs: Int!
  totalConstitutions: Int!
  activeChannels: Int!
  lastUpdated: BigInt!
}

5.2 实体关系图

Agent (1) ---> (N) PaymentSession
Agent (1) ---> (N) MicropaymentChannel
Agent (1) ---> (1) DIDDocument
Agent (1) ---> (1) Constitution
DIDDocument (1) ---> (N) VerificationMethod
DIDDocument (1) ---> (N) DIDController
Constitution (1) ---> (N) Rule
PaymentSession (1) ---> (N) PaymentMilestone
MicropaymentChannel (1) ---> (N) ChannelState
Block (1) ---> (N) Transaction

6. 事件处理器 (AssemblyScript)

6.1 公共工具函数

// src/mappings/helpers.ts

import { BigInt, TypedMap, log, cosmos } from "@graphprotocol/graph-ts";
import { Block, Transaction } from "../types/schema";

export function getEventAttribute(attributes: TypedMap<string, string>, key: string): string {
  const value = attributes.get(key);
  if (value === null) {
    log.warning("事件属性 '{}' 不存在", [key]);
    return "";
  }
  return value;
}

export function parseBigInt(value: string, defaultValue: BigInt = BigInt.zero()): BigInt {
  if (value === "" || value === null) {
    return defaultValue;
  }
  return BigInt.fromString(value);
}

export function parseBool(value: string, defaultValue: boolean = false): boolean {
  if (value === "" || value === null) {
    return defaultValue;
  }
  return value === "true" || value === "1";
}

export function generateEventId(txHash: string, eventIndex: i32, logIndex: i32): string {
  return txHash + "-" + eventIndex.toString() + "-" + logIndex.toString();
}

export function nanosToSeconds(nanos: BigInt): BigInt {
  return nanos.div(BigInt.fromI32(1000000000));
}

export function saveBlock(blockHeight: BigInt, blockHash: string, parentHash: string,
                          proposer: string, timestamp: BigInt, txCount: i32): Block {
  let block = Block.load(blockHeight.toString());
  if (block === null) {
    block = new Block(blockHeight.toString());
  }
  block.height = blockHeight;
  block.hash = blockHash;
  block.parentHash = parentHash;
  block.proposer = proposer;
  block.timestamp = nanosToSeconds(timestamp);
  block.txCount = txCount;
  block.save();
  return block;
}

export function saveTransaction(txHash: string, blockHeight: string, txIndex: i32,
                                from: string, to: string, gasUsed: BigInt,
                                gasWanted: BigInt, fee: BigInt, memo: string,
                                success: boolean, code: i32, rawLog: string,
                                eventTypes: string[], timestamp: BigInt): Transaction {
  let tx = Transaction.load(txHash);
  if (tx === null) {
    tx = new Transaction(txHash);
  }
  tx.txHash = txHash;
  tx.block = blockHeight;
  tx.index = txIndex;
  tx.from = from;
  tx.to = to;
  tx.gasUsed = gasUsed;
  tx.gasWanted = gasWanted;
  tx.fee = fee;
  tx.memo = memo;
  tx.success = success;
  tx.code = code;
  tx.rawLog = rawLog;
  tx.events = eventTypes;
  tx.timestamp = nanosToSeconds(timestamp);
  tx.save();
  return tx;
}

6.2 Agent Registry 处理器

// src/mappings/agent-registry.ts

import { cosmos, BigInt, log } from "@graphprotocol/graph-ts";
import { Agent, AgentHistory } from "../types/schema";
import { getEventAttribute, parseBigInt, parseBool, generateEventId } from "./helpers";

export function handleRegisterAgent(event: cosmos.Event): void {
  const attrs = event.attributes;
  const agentId = getEventAttribute(attrs, "agent_id");
  const owner = getEventAttribute(attrs, "owner");
  const name = getEventAttribute(attrs, "name");
  const agentType = getEventAttribute(attrs, "agent_type");
  const capabilitiesStr = getEventAttribute(attrs, "capabilities");

  const agent = new Agent(agentId);
  agent.agentId = agentId;
  agent.owner = owner;
  agent.operator = "";
  agent.name = name;
  agent.description = getEventAttribute(attrs, "description");
  agent.metadata = getEventAttribute(attrs, "metadata");
  agent.agentType = agentType;

  const capabilities: string[] = [];
  if (capabilitiesStr !== "") {
    const parsed = capabilitiesStr.split(",");
    for (let i = 0; i < parsed.length; i++) {
      capabilities.push(parsed[i].trim());
    }
  }
  agent.capabilities = capabilities;
  agent.status = "Active";
  agent.totalSessions = BigInt.zero();
  agent.totalEarned = BigInt.zero();
  agent.createdAt = event.blockTimestamp;
  agent.updatedAt = event.blockTimestamp;
  agent.createdBlock = event.blockHeight;
  agent.updatedBlock = event.blockHeight;
  agent.createdTx = event.transactionHash;
  agent.save();

  const history = new AgentHistory(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  history.agent = agentId;
  history.status = "Active";
  history.previousStatus = "Inactive";
  history.reason = "新代理注册";
  history.block = event.blockHeight;
  history.timestamp = event.blockTimestamp;
  history.txHash = event.transactionHash;
  history.save();

  log.info("已注册代理: {} (所有者: {})", [agentId, owner]);
}

export function handleUpdateAgent(event: cosmos.Event): void {
  const attrs = event.attributes;
  const agentId = getEventAttribute(attrs, "agent_id");

  const agent = Agent.load(agentId);
  if (agent === null) {
    log.warning("更新代理时未找到代理: {}", [agentId]);
    return;
  }

  const name = getEventAttribute(attrs, "name");
  if (name !== "") agent.name = name;

  const description = getEventAttribute(attrs, "description");
  if (description !== "") agent.description = description;

  const metadata = getEventAttribute(attrs, "metadata");
  if (metadata !== "") agent.metadata = metadata;

  const capabilitiesStr = getEventAttribute(attrs, "capabilities");
  if (capabilitiesStr !== "") {
    const capabilities: string[] = [];
    const parsed = capabilitiesStr.split(",");
    for (let i = 0; i < parsed.length; i++) {
      capabilities.push(parsed[i].trim());
    }
    agent.capabilities = capabilities;
  }

  agent.updatedAt = event.blockTimestamp;
  agent.updatedBlock = event.blockHeight;
  agent.save();

  log.info("已更新代理: {}", [agentId]);
}

export function handleDeregisterAgent(event: cosmos.Event): void {
  const attrs = event.attributes;
  const agentId = getEventAttribute(attrs, "agent_id");
  const reason = getEventAttribute(attrs, "reason");

  const agent = Agent.load(agentId);
  if (agent === null) {
    log.warning("注销代理时未找到代理: {}", [agentId]);
    return;
  }

  const previousStatus = agent.status;
  agent.status = "Deregistered";
  agent.statusReason = reason !== "" ? reason : "代理已注销";
  agent.updatedAt = event.blockTimestamp;
  agent.updatedBlock = event.blockHeight;
  agent.save();

  const history = new AgentHistory(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  history.agent = agentId;
  history.status = "Deregistered";
  history.previousStatus = previousStatus;
  history.reason = reason;
  history.block = event.blockHeight;
  history.timestamp = event.blockTimestamp;
  history.txHash = event.transactionHash;
  history.save();

  log.info("已注销代理: {} (原因: {})", [agentId, reason]);
}

export function handleSetAgentStatus(event: cosmos.Event): void {
  const attrs = event.attributes;
  const agentId = getEventAttribute(attrs, "agent_id");
  const status = getEventAttribute(attrs, "status");
  const reason = getEventAttribute(attrs, "reason");

  const agent = Agent.load(agentId);
  if (agent === null) {
    log.warning("设置状态时未找到代理: {}", [agentId]);
    return;
  }

  const previousStatus = agent.status;
  agent.status = status as string;
  if (reason !== "") agent.statusReason = reason;
  agent.updatedAt = event.blockTimestamp;
  agent.updatedBlock = event.blockHeight;
  agent.save();

  const history = new AgentHistory(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  history.agent = agentId;
  history.status = status;
  history.previousStatus = previousStatus;
  history.reason = reason;
  history.block = event.blockHeight;
  history.timestamp = event.blockTimestamp;
  history.txHash = event.transactionHash;
  history.save();

  log.info("代理 {} 状态变更: {} -> {}", [agentId, previousStatus, status]);
}

6.3 Agent Payment 处理器

// src/mappings/agent-payment.ts

import { cosmos, BigInt, log } from "@graphprotocol/graph-ts";
import { PaymentSession, PaymentMilestone, PaymentActivity, Agent } from "../types/schema";
import { getEventAttribute, parseBigInt, generateEventId } from "./helpers";

export function handleCreateSession(event: cosmos.Event): void {
  const attrs = event.attributes;
  const sessionId = getEventAttribute(attrs, "session_id");
  const agentId = getEventAttribute(attrs, "agent_id");
  const client = getEventAttribute(attrs, "client");
  const totalAmount = getEventAttribute(attrs, "total_amount");
  const token = getEventAttribute(attrs, "token");

  const agent = Agent.load(agentId);
  if (agent === null) {
    log.warning("创建会话时未找到代理: {}", [agentId]);
    return;
  }

  const session = new PaymentSession(sessionId);
  session.sessionId = sessionId;
  session.agent = agentId;
  session.client = client;
  session.totalAmount = parseBigInt(totalAmount);
  session.fundedAmount = BigInt.zero();
  session.releasedAmount = BigInt.zero();
  session.disputedAmount = BigInt.zero();
  session.token = token !== "" ? token : "umsg";
  session.status = "Pending";
  session.disputeStatus = "None";
  session.description = getEventAttribute(attrs, "description");
  session.metadata = getEventAttribute(attrs, "metadata");
  session.startedAt = event.blockTimestamp;
  session.createdAt = event.blockTimestamp;
  session.updatedAt = event.blockTimestamp;
  session.save();

  agent.totalSessions = agent.totalSessions.plus(BigInt.fromI32(1));
  agent.save();

  const activity = new PaymentActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.session = sessionId;
  activity.activityType = "Created";
  activity.amount = parseBigInt(totalAmount);
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = client;
  activity.details = "会话创建";
  activity.save();

  log.info("创建支付会话: {} (代理: {}, 金额: {})", [sessionId, agentId, totalAmount]);
}

export function handleFundSession(event: cosmos.Event): void {
  const attrs = event.attributes;
  const sessionId = getEventAttribute(attrs, "session_id");
  const funder = getEventAttribute(attrs, "funder");
  const amount = getEventAttribute(attrs, "amount");

  const session = PaymentSession.load(sessionId);
  if (session === null) {
    log.warning("充值时未找到会话: {}", [sessionId]);
    return;
  }

  const amountBigInt = parseBigInt(amount);
  session.fundedAmount = session.fundedAmount.plus(amountBigInt);
  if (session.status === "Pending") {
    session.status = "Funded";
  }
  session.updatedAt = event.blockTimestamp;
  session.save();

  const activity = new PaymentActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.session = sessionId;
  activity.activityType = "Funded";
  activity.amount = amountBigInt;
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = funder;
  activity.to = session.client;
  activity.details = "会话充值";
  activity.save();

  log.info("会话 {} 充值: {} (总充值: {})", [sessionId, amount, session.fundedAmount.toString()]);
}

export function handleReleasePayment(event: cosmos.Event): void {
  const attrs = event.attributes;
  const sessionId = getEventAttribute(attrs, "session_id");
  const amount = getEventAttribute(attrs, "amount");
  const milestoneId = getEventAttribute(attrs, "milestone_id");

  const session = PaymentSession.load(sessionId);
  if (session === null) {
    log.warning("释放支付时未找到会话: {}", [sessionId]);
    return;
  }

  const amountBigInt = parseBigInt(amount);
  session.releasedAmount = session.releasedAmount.plus(amountBigInt);
  session.updatedAt = event.blockTimestamp;
  session.save();

  const agent = Agent.load(session.agent);
  if (agent !== null) {
    agent.totalEarned = agent.totalEarned.plus(amountBigInt);
    agent.save();
  }

  const activity = new PaymentActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.session = sessionId;
  activity.activityType = "Released";
  activity.amount = amountBigInt;
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = session.client;
  activity.to = agent !== null ? agent.owner : "";
  activity.details = milestoneId !== "" ? "里程碑支付: " + milestoneId : "支付释放";
  activity.save();

  log.info("会话 {} 释放支付: {}", [sessionId, amount]);
}

export function handleDisputePayment(event: cosmos.Event): void {
  const attrs = event.attributes;
  const sessionId = getEventAttribute(attrs, "session_id");
  const disputer = getEventAttribute(attrs, "disputer");
  const amount = getEventAttribute(attrs, "amount");
  const reason = getEventAttribute(attrs, "reason");

  const session = PaymentSession.load(sessionId);
  if (session === null) {
    log.warning("争议时未找到会话: {}", [sessionId]);
    return;
  }

  const amountBigInt = parseBigInt(amount);
  session.disputedAmount = session.disputedAmount.plus(amountBigInt);
  session.status = "Disputed";
  session.disputeStatus = "Raised";
  session.updatedAt = event.blockTimestamp;
  session.save();

  const activity = new PaymentActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.session = sessionId;
  activity.activityType = "Disputed";
  activity.amount = amountBigInt;
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = disputer;
  activity.details = "争议原因: " + reason;
  activity.save();

  log.info("会话 {} 争议: {} (原因: {})", [sessionId, amount, reason]);
}

export function handleCloseSession(event: cosmos.Event): void {
  const attrs = event.attributes;
  const sessionId = getEventAttribute(attrs, "session_id");
  const reason = getEventAttribute(attrs, "reason");

  const session = PaymentSession.load(sessionId);
  if (session === null) {
    log.warning("关闭时未找到会话: {}", [sessionId]);
    return;
  }

  session.status = session.disputeStatus === "Raised" ? "Disputed" : "Completed";
  session.completedAt = event.blockTimestamp;
  session.updatedAt = event.blockTimestamp;
  session.save();

  const activity = new PaymentActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.session = sessionId;
  activity.activityType = "Completed";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = reason !== "" ? "关闭原因: " + reason : "会话正常关闭";
  activity.save();

  log.info("关闭会话: {} (状态: {})", [sessionId, session.status]);
}

export function handleCompleteMilestone(event: cosmos.Event): void {
  const attrs = event.attributes;
  const sessionId = getEventAttribute(attrs, "session_id");
  const milestoneId = getEventAttribute(attrs, "milestone_id");
  const amount = getEventAttribute(attrs, "amount");
  const description = getEventAttribute(attrs, "description");

  const session = PaymentSession.load(sessionId);
  if (session === null) {
    log.warning("完成里程碑时未找到会话: {}", [sessionId]);
    return;
  }

  const milestone = new PaymentMilestone(sessionId + "-" + milestoneId);
  milestone.milestoneId = milestoneId;
  milestone.session = sessionId;
  milestone.amount = parseBigInt(amount);
  milestone.description = description;
  milestone.status = "completed";
  milestone.completedAt = event.blockTimestamp;
  milestone.createdAt = event.blockTimestamp;
  milestone.save();

  if (session.status === "Funded") {
    session.status = "InProgress";
  }
  session.updatedAt = event.blockTimestamp;
  session.save();

  log.info("完成里程碑: {} (会话: {}, 金额: {})", [milestoneId, sessionId, amount]);
}

6.4 DID Registry 处理器

// src/mappings/did-registry.ts

import { cosmos, BigInt, log } from "@graphprotocol/graph-ts";
import { DIDDocument, DIDController, VerificationMethod, DIDActivity } from "../types/schema";
import { getEventAttribute, generateEventId } from "./helpers";

export function handleCreateDID(event: cosmos.Event): void {
  const attrs = event.attributes;
  const did = getEventAttribute(attrs, "did");
  const address = getEventAttribute(attrs, "address");
  const controllerAddr = getEventAttribute(attrs, "controller");

  const doc = new DIDDocument(did);
  doc.did = did;
  doc.address = address;
  doc.authentication = [];
  doc.assertionMethod = [];
  doc.keyAgreement = [];
  doc.capabilityInvocation = [];
  doc.capabilityDelegation = [];
  doc.serviceEndpoints = "{}";
  doc.created = event.blockTimestamp;
  doc.updated = event.blockTimestamp;
  doc.deactivated = false;
  doc.status = "Active";
  doc.createdBlock = event.blockHeight;
  doc.updatedBlock = event.blockHeight;
  doc.save();

  const controllerAddress = controllerAddr !== "" ? controllerAddr : address;
  const controller = new DIDController(controllerAddress + "-" + did);
  controller.address = controllerAddress;
  controller.controlledDID = did;
  controller.permissions = ["owner", "update", "deactivate"];
  controller.addedAt = event.blockTimestamp;
  controller.save();

  const vmType = getEventAttribute(attrs, "verification_method_type");
  const publicKey = getEventAttribute(attrs, "public_key");
  if (vmType !== "" && publicKey !== "") {
    const vm = new VerificationMethod(did + "#key-1");
    vm.methodId = did + "#key-1";
    vm.didDocument = did;
    vm.type = vmType;
    vm.controller = controllerAddress;
    vm.publicKeyMultibase = publicKey;
    vm.blockchainAccountId = address;
    vm.addedAt = event.blockTimestamp;
    vm.save();
  }

  const activity = new DIDActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.didDocument = did;
  activity.activityType = "Created";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "创建 DID: " + did;
  activity.save();

  log.info("创建 DID: {} (地址: {})", [did, address]);
}

export function handleUpdateDID(event: cosmos.Event): void {
  const attrs = event.attributes;
  const did = getEventAttribute(attrs, "did");

  const doc = DIDDocument.load(did);
  if (doc === null) {
    log.warning("更新 DID 时未找到: {}", [did]);
    return;
  }

  const newController = getEventAttribute(attrs, "new_controller");
  if (newController !== "") {
    const controllerId = newController + "-" + did;
    const existing = DIDController.load(controllerId);
    if (existing === null) {
      const controller = new DIDController(controllerId);
      controller.address = newController;
      controller.controlledDID = did;
      controller.permissions = ["owner", "update", "deactivate"];
      controller.addedAt = event.blockTimestamp;
      controller.save();
    }
  }

  const serviceEndpoints = getEventAttribute(attrs, "service_endpoints");
  if (serviceEndpoints !== "") {
    doc.serviceEndpoints = serviceEndpoints;
  }

  doc.updated = event.blockTimestamp;
  doc.updatedBlock = event.blockHeight;
  doc.save();

  const activity = new DIDActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.didDocument = did;
  activity.activityType = "Updated";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "更新 DID 属性";
  activity.save();

  log.info("更新 DID: {}", [did]);
}

export function handleDeactivateDID(event: cosmos.Event): void {
  const attrs = event.attributes;
  const did = getEventAttribute(attrs, "did");
  const reason = getEventAttribute(attrs, "reason");

  const doc = DIDDocument.load(did);
  if (doc === null) {
    log.warning("停用 DID 时未找到: {}", [did]);
    return;
  }

  doc.deactivated = true;
  doc.status = "Deactivated";
  doc.updated = event.blockTimestamp;
  doc.updatedBlock = event.blockHeight;
  doc.save();

  const activity = new DIDActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.didDocument = did;
  activity.activityType = "Deactivated";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = reason !== "" ? "停用原因: " + reason : "DID 已停用";
  activity.save();

  log.info("停用 DID: {} (原因: {})", [did, reason]);
}

export function handleAddVerificationMethod(event: cosmos.Event): void {
  const attrs = event.attributes;
  const did = getEventAttribute(attrs, "did");
  const methodId = getEventAttribute(attrs, "method_id");
  const methodType = getEventAttribute(attrs, "method_type");
  const controller = getEventAttribute(attrs, "controller");
  const publicKey = getEventAttribute(attrs, "public_key");

  const doc = DIDDocument.load(did);
  if (doc === null) {
    log.warning("添加验证方法时未找到 DID: {}", [did]);
    return;
  }

  const vmId = did + "#" + methodId;
  const vm = new VerificationMethod(vmId);
  vm.methodId = methodId;
  vm.didDocument = did;
  vm.type = methodType;
  vm.controller = controller;
  vm.publicKeyMultibase = publicKey;
  vm.blockchainAccountId = getEventAttribute(attrs, "blockchain_account_id");
  vm.addedAt = event.blockTimestamp;
  vm.save();

  doc.updated = event.blockTimestamp;
  doc.save();

  const activity = new DIDActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.didDocument = did;
  activity.activityType = "Updated";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "添加验证方法: " + methodId + " (类型: " + methodType + ")";
  activity.save();

  log.info("DID {} 添加验证方法: {} ({})", [did, methodId, methodType]);
}

export function handleRemoveVerificationMethod(event: cosmos.Event): void {
  const attrs = event.attributes;
  const did = getEventAttribute(attrs, "did");
  const methodId = getEventAttribute(attrs, "method_id");

  const doc = DIDDocument.load(did);
  if (doc !== null) {
    doc.updated = event.blockTimestamp;
    doc.save();
  }

  const activity = new DIDActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.didDocument = did;
  activity.activityType = "Updated";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "移除验证方法: " + methodId;
  activity.save();

  log.info("DID {} 移除验证方法: {}", [did, methodId]);
}

6.5 Agent Constitution 处理器

// src/mappings/agent-constitution.ts

import { cosmos, BigInt, log } from "@graphprotocol/graph-ts";
import { Constitution, Rule, ConstitutionActivity, Agent } from "../types/schema";
import { getEventAttribute, generateEventId, parseBool } from "./helpers";

export function handleAddRule(event: cosmos.Event): void {
  const attrs = event.attributes;
  const constitutionId = getEventAttribute(attrs, "constitution_id");
  const ruleId = getEventAttribute(attrs, "rule_id");
  const title = getEventAttribute(attrs, "title");
  const description = getEventAttribute(attrs, "description");
  const ruleType = getEventAttribute(attrs, "rule_type");
  const priority = getEventAttribute(attrs, "priority");
  const parameters = getEventAttribute(attrs, "parameters");
  const agentId = getEventAttribute(attrs, "agent_id");

  let constitution = Constitution.load(constitutionId);
  if (constitution === null) {
    constitution = new Constitution(constitutionId);
    constitution.constitutionId = constitutionId;
    constitution.name = "宪章 " + constitutionId;
    constitution.version = "1.0";
    constitution.description = "";
    constitution.status = "Active";
    constitution.ruleCount = 0;
    constitution.isActive = true;
    constitution.createdAt = event.blockTimestamp;
    constitution.updatedAt = event.blockTimestamp;
    if (agentId !== "") constitution.agent = agentId;
    constitution.save();
  }

  const rule = new Rule(ruleId);
  rule.ruleId = ruleId;
  rule.constitution = constitutionId;
  rule.title = title;
  rule.description = description;
  rule.ruleType = ruleType;
  rule.priority = priority !== "" ? I32.parseInt(priority) : 0;
  rule.parameters = parameters;
  rule.status = "Active";
  rule.isActive = true;
  rule.version = 1;
  rule.effectiveFrom = event.blockTimestamp;
  rule.createdAt = event.blockTimestamp;
  rule.updatedAt = event.blockTimestamp;
  rule.save();

  constitution.ruleCount = constitution.ruleCount + 1;
  constitution.updatedAt = event.blockTimestamp;
  constitution.save();

  const activity = new ConstitutionActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.constitution = constitutionId;
  activity.ruleId = ruleId;
  activity.activityType = "Created";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "添加规则: " + title;
  activity.save();

  log.info("宪章 {} 添加规则: {} ({})", [constitutionId, ruleId, title]);
}

export function handleRemoveRule(event: cosmos.Event): void {
  const attrs = event.attributes;
  const constitutionId = getEventAttribute(attrs, "constitution_id");
  const ruleId = getEventAttribute(attrs, "rule_id");

  const rule = Rule.load(ruleId);
  if (rule === null) {
    log.warning("移除规则时未找到: {}", [ruleId]);
    return;
  }

  rule.status = "Inactive";
  rule.isActive = false;
  rule.updatedAt = event.blockTimestamp;
  rule.save();

  const constitution = Constitution.load(constitutionId);
  if (constitution !== null) {
    constitution.ruleCount = constitution.ruleCount - 1;
    if (constitution.ruleCount < 0) constitution.ruleCount = 0;
    constitution.updatedAt = event.blockTimestamp;
    constitution.save();
  }

  const activity = new ConstitutionActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.constitution = constitutionId;
  activity.ruleId = ruleId;
  activity.activityType = "Deleted";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "移除规则: " + ruleId;
  activity.save();

  log.info("宪章 {} 移除规则: {}", [constitutionId, ruleId]);
}

export function handleUpdateRule(event: cosmos.Event): void {
  const attrs = event.attributes;
  const constitutionId = getEventAttribute(attrs, "constitution_id");
  const ruleId = getEventAttribute(attrs, "rule_id");

  const rule = Rule.load(ruleId);
  if (rule === null) {
    log.warning("更新规则时未找到: {}", [ruleId]);
    return;
  }

  const title = getEventAttribute(attrs, "title");
  if (title !== "") rule.title = title;

  const description = getEventAttribute(attrs, "description");
  if (description !== "") rule.description = description;

  const parameters = getEventAttribute(attrs, "parameters");
  if (parameters !== "") rule.parameters = parameters;

  const priority = getEventAttribute(attrs, "priority");
  if (priority !== "") rule.priority = I32.parseInt(priority);

  rule.version = rule.version + 1;
  rule.updatedAt = event.blockTimestamp;
  rule.save();

  const constitution = Constitution.load(constitutionId);
  if (constitution !== null) {
    constitution.updatedAt = event.blockTimestamp;
    constitution.save();
  }

  const activity = new ConstitutionActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.constitution = constitutionId;
  activity.ruleId = ruleId;
  activity.activityType = "Updated";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "更新规则: " + ruleId + " (版本 " + rule.version.toString() + ")";
  activity.save();

  log.info("宪章 {} 更新规则: {} (版本 {})", [constitutionId, ruleId, rule.version.toString()]);
}

export function handleSetActive(event: cosmos.Event): void {
  const attrs = event.attributes;
  const constitutionId = getEventAttribute(attrs, "constitution_id");
  const isActiveStr = getEventAttribute(attrs, "is_active");

  const constitution = Constitution.load(constitutionId);
  if (constitution === null) {
    log.warning("设置活跃状态时未找到宪章: {}", [constitutionId]);
    return;
  }

  const isActive = parseBool(isActiveStr, false);
  constitution.isActive = isActive;
  constitution.status = isActive ? "Active" : "Inactive";
  constitution.updatedAt = event.blockTimestamp;
  constitution.save();

  const activity = new ConstitutionActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.constitution = constitutionId;
  activity.activityType = "Updated";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.details = "宪章活跃状态: " + (isActive ? "Active" : "Inactive");
  activity.save();

  log.info("宪章 {} 活跃状态: {}", [constitutionId, isActive ? "Active" : "Inactive"]);
}

6.6 Micropayment 处理器

// src/mappings/micropayment.ts

import { cosmos, BigInt, log } from "@graphprotocol/graph-ts";
import { MicropaymentChannel, ChannelState, ChannelActivity, Agent } from "../types/schema";
import { getEventAttribute, parseBigInt, generateEventId } from "./helpers";

export function handleOpenChannel(event: cosmos.Event): void {
  const attrs = event.attributes;
  const channelId = getEventAttribute(attrs, "channel_id");
  const sender = getEventAttribute(attrs, "sender");
  const receiver = getEventAttribute(attrs, "receiver");
  const agentId = getEventAttribute(attrs, "agent_id");
  const totalDeposit = getEventAttribute(attrs, "total_deposit");
  const token = getEventAttribute(attrs, "token");
  const expiresAt = getEventAttribute(attrs, "expires_at");

  const channel = new MicropaymentChannel(channelId);
  channel.channelId = channelId;
  channel.agent = agentId;
  channel.sender = sender;
  channel.receiver = receiver;
  channel.totalDeposit = parseBigInt(totalDeposit);
  channel.claimedAmount = BigInt.zero();
  channel.remainingBalance = parseBigInt(totalDeposit);
  channel.token = token !== "" ? token : "umsg";
  channel.status = "Open";
  channel.nonce = BigInt.zero();
  channel.expiresAt = parseBigInt(expiresAt, BigInt.zero());
  channel.createdAt = event.blockTimestamp;
  channel.updatedAt = event.blockTimestamp;
  channel.save();

  const state = new ChannelState(channelId + "-0");
  state.channel = channelId;
  state.nonce = BigInt.zero();
  state.transferredAmount = BigInt.zero();
  state.pendingAmount = BigInt.zero();
  state.signature = "";
  state.block = event.blockHeight;
  state.timestamp = event.blockTimestamp;
  state.save();

  const activity = new ChannelActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.channel = channelId;
  activity.activityType = "Created";
  activity.amount = parseBigInt(totalDeposit);
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = sender;
  activity.to = receiver;
  activity.details = "开启微支付通道";
  activity.save();

  log.info("开启微支付通道: {} (发送方: {}, 接收方: {}, 金额: {})",
    [channelId, sender, receiver, totalDeposit]);
}

export function handleDeposit(event: cosmos.Event): void {
  const attrs = event.attributes;
  const channelId = getEventAttribute(attrs, "channel_id");
  const sender = getEventAttribute(attrs, "sender");
  const amount = getEventAttribute(attrs, "amount");

  const channel = MicropaymentChannel.load(channelId);
  if (channel === null) {
    log.warning("充值时未找到通道: {}", [channelId]);
    return;
  }

  const amountBigInt = parseBigInt(amount);
  channel.totalDeposit = channel.totalDeposit.plus(amountBigInt);
  channel.remainingBalance = channel.remainingBalance.plus(amountBigInt);
  channel.updatedAt = event.blockTimestamp;
  channel.save();

  const activity = new ChannelActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.channel = channelId;
  activity.activityType = "Funded";
  activity.amount = amountBigInt;
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = sender;
  activity.to = channel.receiver;
  activity.details = "通道充值";
  activity.save();

  log.info("通道 {} 充值: {} (总额: {})", [channelId, amount, channel.totalDeposit.toString()]);
}

export function handleClaim(event: cosmos.Event): void {
  const attrs = event.attributes;
  const channelId = getEventAttribute(attrs, "channel_id");
  const claimant = getEventAttribute(attrs, "claimant");
  const amount = getEventAttribute(attrs, "amount");
  const nonce = getEventAttribute(attrs, "nonce");

  const channel = MicropaymentChannel.load(channelId);
  if (channel === null) {
    log.warning("提取时未找到通道: {}", [channelId]);
    return;
  }

  const amountBigInt = parseBigInt(amount);
  const nonceBigInt = parseBigInt(nonce);

  channel.claimedAmount = channel.claimedAmount.plus(amountBigInt);
  channel.remainingBalance = channel.remainingBalance.minus(amountBigInt);
  channel.nonce = nonceBigInt;
  channel.updatedAt = event.blockTimestamp;
  channel.save();

  const state = new ChannelState(channelId + "-" + nonce);
  state.channel = channelId;
  state.nonce = nonceBigInt;
  state.transferredAmount = channel.claimedAmount;
  state.pendingAmount = BigInt.zero();
  state.signature = "";
  state.block = event.blockHeight;
  state.timestamp = event.blockTimestamp;
  state.save();

  const activity = new ChannelActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.channel = channelId;
  activity.activityType = "Released";
  activity.amount = amountBigInt;
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = channel.sender;
  activity.to = claimant;
  activity.details = "通道提取 (nonce: " + nonce + ")";
  activity.save();

  log.info("通道 {} 提取: {} (nonce: {}, 已提取总额: {})",
    [channelId, amount, nonce, channel.claimedAmount.toString()]);
}

export function handleCloseChannel(event: cosmos.Event): void {
  const attrs = event.attributes;
  const channelId = getEventAttribute(attrs, "channel_id");
  const closer = getEventAttribute(attrs, "closer");
  const reason = getEventAttribute(attrs, "reason");

  const channel = MicropaymentChannel.load(channelId);
  if (channel === null) {
    log.warning("关闭时未找到通道: {}", [channelId]);
    return;
  }

  const previousStatus = channel.status;
  channel.status = "Closed";
  channel.updatedAt = event.blockTimestamp;
  channel.save();

  const activity = new ChannelActivity(
    generateEventId(event.transactionHash, event.eventIndex, 0)
  );
  activity.channel = channelId;
  activity.activityType = "Completed";
  activity.block = event.blockHeight;
  activity.timestamp = event.blockTimestamp;
  activity.txHash = event.transactionHash;
  activity.from = closer;
  activity.details = reason !== "" ? "关闭原因: " + reason : "通道正常关闭";
  activity.save();

  log.info("关闭通道: {} (状态: {} -> Closed)", [channelId, previousStatus]);
}

export function handleUpdateChannelState(event: cosmos.Event): void {
  const attrs = event.attributes;
  const channelId = getEventAttribute(attrs, "channel_id");
  const transferredAmount = getEventAttribute(attrs, "transferred_amount");
  const pendingAmount = getEventAttribute(attrs, "pending_amount");
  const nonce = getEventAttribute(attrs, "nonce");
  const signature = getEventAttribute(attrs, "signature");

  const channel = MicropaymentChannel.load(channelId);
  if (channel === null) {
    log.warning("更新状态时未找到通道: {}", [channelId]);
    return;
  }

  const nonceBigInt = parseBigInt(nonce);

  const state = new ChannelState(channelId + "-state-" + nonce);
  state.channel = channelId;
  state.nonce = nonceBigInt;
  state.transferredAmount = parseBigInt(transferredAmount);
  state.pendingAmount = parseBigInt(pendingAmount);
  state.signature = signature;
  state.block = event.blockHeight;
  state.timestamp = event.blockTimestamp;
  state.save();

  channel.nonce = nonceBigInt;
  channel.updatedAt = event.blockTimestamp;
  channel.save();

  log.info("通道 {} 状态更新: nonce={}, transferred={}, pending={}",
    [channelId, nonce, transferredAmount, pendingAmount]);
}

6.7 区块和交易处理器

// src/mappings/block-data.ts

import { cosmos, BigInt, log } from "@graphprotocol/graph-ts";
import { Block, Transaction, AggregationStats } from "../types/schema";
import { saveBlock, saveTransaction } from "./helpers";

export function handleBlockHandler(block: cosmos.Block): void {
  const blockHeight = block.header.height;
  const blockHash = block.header.hash;
  const parentHash = block.header.parentHash;
  const proposer = block.header.proposerAddress;
  const timestamp = block.header.time;
  const txCount = block.transactions.length;

  saveBlock(blockHeight, blockHash, parentHash, proposer, timestamp, txCount);

  let stats = AggregationStats.load("global");
  if (stats === null) {
    stats = new AggregationStats("global");
    stats.totalAgents = 0;
    stats.activeAgents = 0;
    stats.totalSessions = 0;
    stats.completedSessions = 0;
    stats.totalVolume = BigInt.zero();
    stats.totalDIDs = 0;
    stats.totalConstitutions = 0;
    stats.activeChannels = 0;
  }
  stats.lastUpdated = timestamp;
  stats.save();

  log.debug("已处理区块: {} ({} 笔交易)", [blockHeight.toString(), txCount.toString()]);
}

export function handleTransactionHandler(transaction: cosmos.Transaction): void {
  const txHash = transaction.hash;
  const blockHeight = transaction.blockHeight;
  const txIndex = transaction.index;
  const from = transaction.from;
  const to = transaction.to;
  const gasUsed = transaction.gasUsed;
  const gasWanted = transaction.gasWanted;
  const fee = transaction.fee;
  const memo = transaction.memo;
  const success = transaction.success;
  const code = transaction.code;
  const rawLog = transaction.rawLog;
  const timestamp = transaction.blockTime;

  const eventTypes: string[] = [];
  const events = transaction.events;
  for (let i = 0; i < events.length; i++) {
    const event = events[i];
    if (event.type !== null) {
      eventTypes.push(event.type);
    }
  }

  saveTransaction(
    txHash, blockHeight.toString(), txIndex, from, to,
    gasUsed, gasWanted, fee, memo, success, code, rawLog,
    eventTypes, timestamp
  );

  log.debug("已处理交易: {} (区块: {}, 索引: {})", [txHash, blockHeight.toString(), txIndex.toString()]);
}

7. 数据源模板

7.1 模板定义

在 subgraph.yaml 末尾添加:

templates:
  - kind: cosmwasm
    name: AgentRegistry
    network: msg-chain-1
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/agent-registry-template.ts
      entities:
        - Agent
        - AgentHistory
      eventHandlers:
        - event: register_agent
          handler: handleRegisterAgent
        - event: update_agent
          handler: handleUpdateAgent
        - event: deregister_agent
          handler: handleDeregisterAgent
        - event: set_agent_status
          handler: handleSetAgentStatus
      abis:
        - name: agent_registry_v1
          file: ./abis/agent_registry_v1.json

  - kind: cosmwasm
    name: PaymentSession
    network: msg-chain-1
    mapping:
      apiVersion: 0.0.7
      language: wasm/assemblyscript
      file: ./src/mappings/payment-template.ts
      entities:
        - PaymentSession
        - PaymentMilestone
      eventHandlers:
        - event: fund_session
          handler: handleFundSession
        - event: release_payment
          handler: handleReleasePayment
        - event: dispute_payment
          handler: handleDisputePayment
        - event: close_session
          handler: handleCloseSession
      abis:
        - name: agent_payment_v1
          file: ./abis/agent_payment_v1.json

7.2 在映射中创建数据源

import { cosmos, dataSource, log } from "@graphprotocol/graph-ts";

export function createDynamicDataSource(contractAddress: string, templateName: string): void {
  dataSource.create(templateName, [contractAddress]);
  log.info("动态创建数据源: 模板={}, 地址={}", [templateName, contractAddress]);
}

8. 编译与部署

8.1 代码生成

graph codegen

8.2 编译子图

graph build

编译输出结构:

build/
  subgraph.yaml
  schema.graphql
  mappings/
    agent-registry.wasm
    agent-payment.wasm
    did-registry.wasm
    agent-constitution.wasm
    micropayment.wasm
    block-data.wasm

8.3 部署到本地 Graph Node

graph create --node http://localhost:8020 msg-chain/subgraph
graph deploy --node http://localhost:8020 --ipfs http://localhost:5001 msg-chain/subgraph

8.4 部署到 Subgraph Studio

graph auth --studio <DEPLOY_KEY>
graph deploy --studio msg-chain-subgraph

8.5 部署脚本

#!/bin/bash
# deploy.sh

set -euo pipefail

SUBGRAPH_NAME="msg-chain/subgraph"
LOCAL_NODE="http://localhost:8020"
LOCAL_IPFS="http://localhost:5001"
NETWORK=${1:-"local"}

echo ">>> 生成类型..."
graph codegen

echo ">>> 编译子图..."
graph build

case $NETWORK in
  local)
    echo ">>> 部署到本地 Graph Node..."
    graph create --node "$LOCAL_NODE" "$SUBGRAPH_NAME" || true
    graph deploy --node "$LOCAL_NODE" --ipfs "$LOCAL_IPFS" "$SUBGRAPH_NAME"
    ;;
  studio)
    echo ">>> 部署到 Subgraph Studio..."
    graph deploy --studio "$SUBGRAPH_NAME"
    ;;
  *)
    echo "未知网络: $NETWORK"
    exit 1
    ;;
esac

9. 前端查询示例

9.1 Apollo Client 配置

// src/lib/apollo-client.ts

import { ApolloClient, InMemoryCache, HttpLink, gql } from "@apollo/client/core";
import { setContext } from "@apollo/client/link/context";

const SUBGRAPH_URL = "http://localhost:8000/subgraphs/name/msg-chain/subgraph";

const httpLink = new HttpLink({ uri: SUBGRAPH_URL, fetch: fetch });

const authLink = setContext((_, { headers }) => {
  return { headers: { ...headers } };
});

export const apolloClient = new ApolloClient({
  link: authLink.concat(httpLink),
  cache: new InMemoryCache({
    typePolicies: {
      Agent: { keyFields: ["id"] },
      PaymentSession: { keyFields: ["id"] },
    },
  }),
  defaultOptions: {
    watchQuery: { fetchPolicy: "cache-and-network", errorPolicy: "all" },
    query: { fetchPolicy: "network-only", errorPolicy: "all" },
  },
});

9.2 获取代理列表

// src/hooks/useAgents.ts

import { gql, useQuery } from "@apollo/client";

const GET_AGENTS = gql`
  query GetAgents(
    $first: Int = 10
    $skip: Int = 0
    $orderBy: Agent_orderBy = createdAt
    $orderDirection: OrderDirection = desc
    $where: Agent_filter = {}
  ) {
    agents(
      first: $first
      skip: $skip
      orderBy: $orderBy
      orderDirection: $orderDirection
      where: $where
    ) {
      id
      agentId
      name
      owner
      agentType
      capabilities
      status
      totalSessions
      totalEarned
      createdAt
      constitution { id name isActive ruleCount }
      did { id did status }
      sessions(first: 5, orderBy: createdAt, orderDirection: desc) {
        id totalAmount fundedAmount status client
      }
      paymentChannels(first: 5, orderBy: createdAt, orderDirection: desc) {
        id totalDeposit claimedAmount status
      }
    }
  }
`;

export function useAgents(pageSize: number = 10, status?: string, agentType?: string) {
  const where: Record<string, unknown> = {};
  if (status) where.status = status;
  if (agentType) where.agentType = agentType;

  return useQuery(GET_AGENTS, {
    variables: { first: pageSize, skip: 0, where },
    notifyOnNetworkStatusChange: true,
    pollInterval: 30000,
  });
}

9.3 获取单个代理详情

// src/hooks/useAgentDetail.ts

import { gql, useQuery } from "@apollo/client";

const GET_AGENT_DETAIL = gql`
  query GetAgentDetail($id: ID!) {
    agent(id: $id) {
      id
      agentId
      owner
      operator
      name
      description
      metadata
      agentType
      capabilities
      status
      statusReason
      totalSessions
      totalEarned
      createdAt
      updatedAt
      constitution {
        id name description version isActive ruleCount
        rules(first: 50) {
          id ruleId title description ruleType priority isActive version
        }
      }
      did {
        id did status deactivated
        controller { address permissions }
        verificationMethods { id methodId type publicKeyMultibase }
      }
      sessions(first: 20, orderBy: createdAt, orderDirection: desc) {
        id sessionId client totalAmount fundedAmount
        releasedAmount disputedAmount status disputeStatus
        milestones { id milestoneId amount status completedAt }
        activities(orderBy: timestamp, orderDirection: desc) {
          id activityType amount timestamp txHash details
        }
      }
      paymentChannels(first: 20, orderBy: createdAt, orderDirection: desc) {
        id channelId sender receiver totalDeposit
        claimedAmount remainingBalance status nonce
        stateHistory(orderBy: timestamp, orderDirection: desc) {
          id nonce transferredAmount pendingAmount
        }
      }
      history(orderBy: timestamp, orderDirection: desc) {
        id status previousStatus reason block timestamp
      }
    }
  }
`;

export function useAgentDetail(agentId: string) {
  return useQuery(GET_AGENT_DETAIL, {
    variables: { id: agentId },
    skip: !agentId,
  });
}

9.4 代理仪表盘组件

// src/components/AgentDashboard.tsx

import React, { useState } from "react";
import { gql, useQuery } from "@apollo/client";
import { useAgents } from "../hooks/useAgents";

const GET_STATS = gql`
  query GetStats {
    aggregationStats(id: "global") {
      totalAgents
      activeAgents
      totalSessions
      completedSessions
      totalVolume
    }
  }
`;

export function AgentDashboard() {
  const [statusFilter, setStatusFilter] = useState<string>();

  const { data: agentsData, loading, error } = useAgents(10, statusFilter);

  return (
    <div className="dashboard">
      <div className="stats-grid">
        <div className="stat-card" style={{ border: '1px solid #ccc', padding: '16px', margin: '8px' }}>
          <h3>代理查询示例</h3>
          {loading && <p>加载中...</p>}
          {error && <p>查询出错: {error.message}</p>}
          {agentsData && agentsData.agents && (
            <ul>
              {agentsData.agents.map((agent: any) => (
                <li key={agent.id}>
                  {agent.name} - {agent.status} - {agent.agentType}
                </li>
              ))}
            </ul>
          )}
        </div>
      </div>
    </div>
  );
}

10. 托管计划

10.1 托管方案对比

方案 成本 去中心化 性能 维护 适用场景
本地 Docker 服务器费用 否 高 中等 开发/测试
The Graph Network 按查询付费 (GRT) 是 最高 无 生产/主网
自托管 服务器费用 否 最高 高 合规/隐私
Subgraph Studio 免费额度 是 高 低 中小型项目

10.2 生产环境 Docker Compose

version: "3.8"

services:
  postgres:
    image: postgres:15-alpine
    restart: always
    ports:
      - "5432:5432"
    environment:
      POSTGRES_USER: graph_node
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      POSTGRES_DB: graph_node
    volumes:
      - /data/postgres:/var/lib/postgresql/data
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: "8G"

  ipfs:
    image: ipfs/kubo:v0.20.0
    restart: always
    ports:
      - "5001:5001"
    volumes:
      - /data/ipfs:/data/ipfs

  graph-node:
    image: graphprotocol/graph-node:latest
    restart: always
    ports:
      - "8000:8000"
      - "8020:8020"
      - "8030:8030"
    depends_on:
      - postgres
      - ipfs
    environment:
      postgres_host: postgres
      postgres_port: 5432
      postgres_user: graph_node
      postgres_pass: ${DB_PASSWORD}
      postgres_db: graph_node
      ipfs: http://ipfs:5001
      ETHEREUM_RPC: msg-chain-1:${MSG_CHAIN_RPC_URL}
      GRAPH_LOG: info
      GRAPH_ENABLE_COSMOS: "true"
      GRAPH_ETHEREUM_BLOCK_POLLING_INTERVAL: "5000"
    deploy:
      resources:
        limits:
          cpus: "4"
          memory: "8G"

10.3 环境变量文件 (.env.prod)

DB_PASSWORD=your_secure_password_here
MSG_CHAIN_RPC_URL=http://rpc.msgchain.org:26657
GRAPH_LOG=info
GRAPH_ETHEREUM_BLOCK_POLLING_INTERVAL=5000
GRAPH_MAX_GAS_PER_HANDLER=10000000

10.4 监控 Prometheus 配置

global:
  scrape_interval: 15s

scrape_configs:
  - job_name: "graph-node"
    static_configs:
      - targets: ["graph-node:8040"]

  - job_name: "postgres"
    static_configs:
      - targets: ["postgres-exporter:9187"]

11. 性能优化

11.1 关键性能参数

参数 默认值 MSG Chain 推荐值 说明
GRAPH_ETHEREUM_BLOCK_POLLING_INTERVAL 5000ms 5000ms 匹配 5 秒出块时间
GRAPH_ETHEREUM_MAX_BLOCK_RANGE_SIZE 200 100 每次轮询区块范围
GRAPH_STORE_WRITE_BATCH_SIZE 500 1000 批量写入大小
GRAPH_MAX_GAS_PER_HANDLER 5000000 10000000 处理器 Gas 限制

11.2 处理器类型对比

处理器类型 触发条件 性能 适用场景
事件处理器 合约事件 高 大部分场景
区块处理器 每个区块 低 区块级统计
交易处理器 每笔交易 中 交易级数据

11.3 优化最佳实践

  1. 优先使用事件处理器,避免区块处理器中的复杂逻辑
  2. 批量写入:在处理器末尾统一调用 save()
  3. 减少 store.load() 调用:尽量从事件属性获取数据
  4. 启用 Cosmos 支持:设置 GRAPH_ENABLE_COSMOS=true
  5. 匹配轮询间隔:GRAPH_ETHEREUM_BLOCK_POLLING_INTERVAL 设为 5000ms

11.4 PostgreSQL 索引优化

CREATE INDEX IF NOT EXISTS idx_agents_status ON agents USING btree (status);
CREATE INDEX IF NOT EXISTS idx_agents_owner ON agents USING btree (owner);
CREATE INDEX IF NOT EXISTS idx_agents_created_at ON agents USING btree (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_sessions_agent ON payment_sessions USING btree (agent);
CREATE INDEX IF NOT EXISTS idx_sessions_status ON payment_sessions USING btree (status);
CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON payment_sessions USING btree (created_at DESC);
CREATE INDEX IF NOT EXISTS idx_did_address ON did_documents USING btree (address);
CREATE INDEX IF NOT EXISTS idx_channels_agent ON micropayment_channels USING btree (agent);
CREATE INDEX IF NOT EXISTS idx_channels_status ON micropayment_channels USING btree (status);
CREATE INDEX IF NOT EXISTS idx_transactions_block ON transactions USING btree (block);
CREATE INDEX IF NOT EXISTS idx_transactions_from ON transactions USING btree (from_addr);

11.5 graphman 管理工具

# 查看子图状态
graphman list
graphman info msg-chain/subgraph
graphman pause msg-chain/subgraph
graphman resume msg-chain/subgraph
graphman rewind msg-chain/subgraph 1000
graphman remove msg-chain/subgraph

12. 当前边界与限制

12.1 MSG Chain 子图就绪度

方面 状态 说明
CosmWasm 事件格式兼容 部分支持 Graph Node 支持 cosmwasm 数据源,Cosmos 支持为实验性
RPC 可用性 可用 MSG Chain 提供标准 Tendermint RPC (26657)
Graph Node Cosmos 支持 实验性 需要显式设置 GRAPH_ENABLE_COSMOS
AssemblyScript 映射 完全支持 graph-ts 0.30+ 提供完整 Cosmos 类型
动态数据源 需要验证 dataSource.create() 在 Cosmos 数据源上的行为
The Graph Network 不支持 需要等待官方添加 MSG Chain 支持
GraphQL 订阅 支持 WebSocket 订阅可用
全文本搜索 部分支持 需要 GRAPH_ALLOW_NON_DETERMINISTIC_FULLTEXT_SEARCH

12.2 已知限制

  1. CosmWasm 事件解析:CosmWasm 事件通过 Tendermint 的 wasm 类型事件传递,事件属性是 key-value 对数组。Graph Node 的 Cosmos 适配器需要正确解析这些事件

  2. Graph Node Cosmos 支持的实验性:GRAPH_ENABLE_COSMOS 标记为实验性特性,生产环境使用需谨慎

  3. 合约地址验证:合约地址需要使用 MSG Chain 的 bech32 前缀 msg

  4. The Graph Network 集成:MSG Chain 当前未在 The Graph Network 上注册,无法使用去中心化索引服务

  5. ABI 文件:CosmWasm 合约的 ABI 文件格式与 Ethereum 不同,可能需要手动适配

  6. AssemblyScript 限制:graph-ts 对某些 Cosmos 特有类型可能支持不完全

12.3 合约事件对照表

合约 事件 处理器 说明
agent_registry_v1 register_agent handleRegisterAgent 代理注册
agent_registry_v1 update_agent handleUpdateAgent 代理更新
agent_registry_v1 deregister_agent handleDeregisterAgent 代理注销
agent_registry_v1 set_agent_status handleSetAgentStatus 状态变更
agent_payment_v1 create_session handleCreateSession 创建支付会话
agent_payment_v1 fund_session handleFundSession 充值
agent_payment_v1 release_payment handleReleasePayment 支付释放
agent_payment_v1 dispute_payment handleDisputePayment 支付争议
agent_payment_v1 close_session handleCloseSession 关闭会话
agent_payment_v1 complete_milestone handleCompleteMilestone 完成里程碑
aidid_did_registry_v1 create_did handleCreateDID 创建 DID
aidid_did_registry_v1 update_did handleUpdateDID 更新 DID
aidid_did_registry_v1 deactivate_did handleDeactivateDID 停用 DID
aidid_did_registry_v1 add_verification_method handleAddVerificationMethod 添加验证方法
aidid_did_registry_v1 remove_verification_method handleRemoveVerificationMethod 移除验证方法
ai_agent_constitution_v1 add_rule handleAddRule 添加规则
ai_agent_constitution_v1 remove_rule handleRemoveRule 移除规则
ai_agent_constitution_v1 update_rule handleUpdateRule 更新规则
ai_agent_constitution_v1 set_active handleSetActive 设置活跃
micropayment_session_v1 open_channel handleOpenChannel 开启通道
micropayment_session_v1 deposit_channel handleDeposit 充值通道
micropayment_session_v1 claim_channel handleClaim 提取资金
micropayment_session_v1 close_channel handleCloseChannel 关闭通道
micropayment_session_v1 update_channel_state handleUpdateChannelState 更新状态

13. 故障排查

13.1 常见错误

错误: GRAPH_ENABLE_COSMOS not set
解决: 在环境变量中添加 GRAPH_ENABLE_COSMOS=true
错误: RPC endpoint not found
解决: 确认 MSG Chain RPC URL 正确且可达
错误: Handler not found for event type
解决: 检查 subgraph.yaml 中事件名称与合约发出的事件名称是否匹配
错误: Store set failed: entity not found
解决: 事件处理器中引用的关联实体需要先创建

13.2 调试技巧

// 在映射中添加调试日志
log.debug("调试信息: key={}, value={}", [key, value]);
log.info("信息日志: 已处理事件 type={}", [event.type]);
log.warning("警告: 未找到实体 id={}", [entityId]);
log.critical("严重错误: 处理器失败", []);

13.3 Graph Node 健康检查

# 检查 Graph Node 状态
curl http://localhost:8030/health

# 查询子图元数据
curl -X POST \
  -H "Content-Type: application/json" \
  -d '{"query": "{ _meta { deployment { block { number hash } } } }"}' \
  http://localhost:8000/subgraphs/name/msg-chain/subgraph

# 查看 Graph Node 日志
docker logs graph-node --tail 100

14. 附录

14.1 项目目录结构

msg-chain-subgraph/
├── abis/
│   ├── agent_registry_v1.json
│   ├── agent_payment_v1.json
│   ├── aidid_did_registry_v1.json
│   ├── ai_agent_constitution_v1.json
│   └── micropayment_session_v1.json
├── src/
│   ├── mappings/
│   │   ├── helpers.ts
│   │   ├── agent-registry.ts
│   │   ├── agent-payment.ts
│   │   ├── did-registry.ts
│   │   ├── agent-constitution.ts
│   │   ├── micropayment.ts
│   │   ├── block-data.ts
│   │   └── stats.ts
│   └── types/
│       └── schema.ts           # 由 graph codegen 生成
├── schema.graphql
├── subgraph.yaml
├── package.json
├── tsconfig.json
├── docker-compose.yml
└── deploy.sh

14.2 相关资源

14.3 版本兼容性

组件 版本 备注
graph-cli ^0.62.0 MSG Chain 推荐
graph-ts ^0.30.0 Cosmos 类型支持
assemblyscript ^0.19.0 WASM 编译
Graph Node latest Cosmos 实验性支持
PostgreSQL 15+ 持久化存储
IPFS v0.20.0 子图部署存储

14.4 术语对照

中文 English
子图 Subgraph
索引器 Indexer
映射 Mapping
清单 Manifest
数据源 Data Source
事件处理器 Event Handler
区块处理器 Block Handler
交易处理器 Transaction Handler
实体 Entity
架构 Schema
部署 Deploy
重新索引 Reindex

本文档为 MSG Chain 区块链的 The Graph 子图索引指南。