The Graph 子图索引器指南 — MSG Chain 区块链
数据来源:MSG Chain 代码库核实
主网状态: No-Go — 当前 MSGChain 主网裁决为 No-Go,以下内容反映代码实际状态,不代表生产可用。
目录
- 概述
- 架构
- 开发环境
- 子图清单 (subgraph.yaml)
- GraphQL 架构 (schema.graphql)
- 事件处理器 (AssemblyScript)
- 数据源模板
- 编译与部署
- 前端查询示例
- 托管计划
- 性能优化
- 当前边界与限制
- 故障排查
- 附录
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 子图的主要理由:
- GraphQL 查询灵活性:前端可以精确指定需要的数据字段,减少过度获取和欠获取
- 关联数据导航:通过
@derivedFrom自动建立实体间关系,无需手动 JOIN - 实时索引:事件驱动架构确保数据在区块确认后立即可用
- 生态兼容:与 Ethereum/Cosmos 生态共享相同的工具链
- 去中心化选项:可以选择加入 The Graph Network 获得去中心化索引服务
1.3 与类似解决方案的对比
- 类似 The Graph 的 Ethereum 子图:MSG Chain 子图遵循相同的 manifest + schema + mappings 架构,但使用
cosmwasm数据源类型而非ethereum/contract - 类似 SubQuery 的 Cosmos 索引:SubQuery 是另一种索引方案,但 The Graph 支持更广泛的生态和 GraphQL 标准
- 类似 CosmWasm 的 cosmwasm-storage 索引:合约级索引,能力有限;子图提供跨合约、跨链的索引能力
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 数据流
- 区块轮询:Graph Node 通过 PollingBlockStream 从 MSG Chain RPC 获取新区块
- 事件提取:从区块中提取 CosmWasm 事件(类型为 wasm 的事件)
- 事件过滤:根据 subgraph.yaml 中定义的 eventHandlers 匹配事件类型
- 映射执行:将匹配的事件传递给 AssemblyScript 处理器
- 数据写入:处理器通过 store.set() 将实体写入 PostgreSQL
- GraphQL 响应:所有写入数据通过 GraphQL 端点暴露
3. 开发环境
3.1 前置条件
- Node.js >= 16.x
- Yarn 或 npm
- Docker & Docker Compose(用于本地 Graph Node)
- Git
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),有两种数据源类型可选:
- kind: cosmwasm:特定于 CosmWasm 合约,可以监听合约发出的事件。这是 MSG Chain 智能合约的主要选择
- kind: cosmos:通用的 Cosmos 数据源,用于监听原生 Cosmos 模块事件(如 bank、staking、IBC)。适用于区块和交易级处理
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 优化最佳实践
- 优先使用事件处理器,避免区块处理器中的复杂逻辑
- 批量写入:在处理器末尾统一调用 save()
- 减少 store.load() 调用:尽量从事件属性获取数据
- 启用 Cosmos 支持:设置 GRAPH_ENABLE_COSMOS=true
- 匹配轮询间隔: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 已知限制
-
CosmWasm 事件解析:CosmWasm 事件通过 Tendermint 的 wasm 类型事件传递,事件属性是 key-value 对数组。Graph Node 的 Cosmos 适配器需要正确解析这些事件
-
Graph Node Cosmos 支持的实验性:GRAPH_ENABLE_COSMOS 标记为实验性特性,生产环境使用需谨慎
-
合约地址验证:合约地址需要使用 MSG Chain 的 bech32 前缀 msg
-
The Graph Network 集成:MSG Chain 当前未在 The Graph Network 上注册,无法使用去中心化索引服务
-
ABI 文件:CosmWasm 合约的 ABI 文件格式与 Ethereum 不同,可能需要手动适配
-
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 相关资源
- The Graph 文档: https://thegraph.com/docs
- graph-cli 参考: https://github.com/graphprotocol/graph-cli
- graph-ts 参考: https://github.com/graphprotocol/graph-ts
- CosmWasm 文档: https://docs.cosmwasm.com
- MSG Chain 文档: https://docs.msgchain.org
- Graph Node Docker: https://github.com/graphprotocol/graph-node/tree/master/docker
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 子图索引指南。
