Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 75 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ This subgraph indexes data from three core smart contracts implementing the ERC-
- 🔍 **Comprehensive Agent Data** - On-chain registration with rich off-chain metadata
- 📊 **Real-time Reputation** - Live feedback scoring and response tracking
- ✅ **Validation Tracking** - Complete validation lifecycle with status management
- 📁 **IPFS Integration** - Native JSON parsing via File Data Sources
- 📁 **IPFS & Arweave Integration** - Native JSON parsing via File Data Sources
- 🔄 **Rich Relationships** - Connected data through derived fields and references
- 🌐 **Multi-Chain Support** - Single codebase deploying to 7 networks

Expand All @@ -194,7 +194,7 @@ type Agent @entity(immutable: false) {
chainId: BigInt! # Blockchain identifier
agentId: BigInt! # Agent ID on the chain
agentURI: String # Registration file URI
agentURIType: String # "ipfs", "https", "http", "unknown"
agentURIType: String # "ipfs", "arweave", "https", "http", "unknown"
owner: Bytes! # Agent owner address
operators: [Bytes!]! # Authorized operators
createdAt: BigInt!
Expand All @@ -217,7 +217,7 @@ type Feedback @entity(immutable: false) {
score: Int! # 0-100 score
tag1: String # Primary category tag
tag2: String # Secondary category tag
feedbackUri: String # IPFS/HTPPS URI for rich content
feedbackUri: String # URI for rich content (IPFS, Arweave, HTTPS, etc.)
feedbackURIType: String
feedbackHash: Bytes!
isRevoked: Boolean!
Expand Down Expand Up @@ -252,15 +252,15 @@ enum ValidationStatus {
}
```

### Off-Chain Entities (Immutable from IPFS)
### Off-Chain Entities (Immutable from IPFS/Arweave)

**Rich metadata fetched from IPFS/HTTPS URIs:**
**Rich metadata fetched from IPFS and Arweave URIs:**

#### AgentRegistrationFile
```graphql
type AgentRegistrationFile @entity(immutable: true) {
id: ID! # Format: "transactionHash:cid"
cid: String! # IPFS CID (for querying by content)
cid: String! # IPFS CID or Arweave transaction ID
agentId: String! # "chainId:agentId"
name: String # Agent display name
description: String # Agent description
Expand Down Expand Up @@ -288,7 +288,7 @@ type AgentRegistrationFile @entity(immutable: true) {
```graphql
type FeedbackFile @entity(immutable: true) {
id: ID! # Format: "transactionHash:cid"
cid: String! # IPFS CID (for querying by content)
cid: String! # IPFS CID or Arweave transaction ID
feedbackId: String! # "chainId:agentId:clientAddress:index"
text: String # Detailed feedback text
capability: String # Capability being rated
Expand Down Expand Up @@ -507,38 +507,93 @@ query GetProtocolStats {
}
```

## 📁 IPFS File Data Sources
### Find Agents by Storage Protocol

The subgraph uses **File Data Sources** to parse off-chain content:
```graphql
query GetArweaveAgents {
agents(
where: { agentURIType: "arweave" }
first: 100
) {
id
agentURI
agentURIType
registrationFile {
cid # Arweave transaction ID
name
description
mcpEndpoint
a2aEndpoint
}
}
}
```

### Mixed Storage Query (IPFS + Arweave)

```graphql
query GetAllAgentsWithMetadata {
agents(
where: { agentURIType_in: ["ipfs", "arweave"] }
first: 100
) {
id
agentURI
agentURIType
registrationFile {
cid
name
description
active
}
}
}
```

## 📁 File Data Sources (IPFS & Arweave)

The subgraph uses **File Data Sources** to parse off-chain content from multiple storage protocols:

### RegistrationFile Data Source

- **Handler**: `src/registration-file.ts`
- **Trigger**: When `agentURI` points to IPFS/HTTPS content
- **Handler**: `src/registration-file.ts` (unified handler for both IPFS and Arweave)
- **Trigger**: When `agentURI` is an IPFS or Arweave URI
- **Output**: `AgentRegistrationFile` entity
- **Data Parsed**: Metadata, capabilities, endpoints, identity information
- **Templates**:
- `RegistrationFile` (kind: `file/ipfs`)
- `ArweaveRegistrationFile` (kind: `file/arweave`)

### FeedbackFile Data Source

- **Handler**: `src/feedback-file.ts`
- **Trigger**: When `feedbackUri` points to IPFS/HTTPS content
- **Handler**: `src/feedback-file.ts` (unified handler for both IPFS and Arweave)
- **Trigger**: When `feedbackUri` is an IPFS or Arweave URI
- **Output**: `FeedbackFile` entity
- **Data Parsed**: Detailed feedback text, proof of payment, context
- **Templates**:
- `FeedbackFile` (kind: `file/ipfs`)
- `ArweaveFeedbackFile` (kind: `file/arweave`)

### Supported URI Formats

The subgraph processes **IPFS and Arweave** URIs using file data sources:

- **IPFS**: `ipfs://QmHash...` or bare `QmHash...`
- **HTTPS**: `https://example.com/file.json`
- **HTTP**: `http://example.com/file.json`
- **Arweave**: `ar://transactionId...`

HTTPS and HTTP URIs are detected and classified in the `agentURIType` field but are not currently processed by file data sources.

**Note:** The Graph Node automatically handles protocol-specific fetching. Both IPFS and Arweave templates call the same unified handler functions, which use shared JSON parsing utilities to ensure consistent data extraction regardless of storage protocol.

## 🔄 Data Flow

1. **On-chain Events** → Contract events trigger indexing
2. **URI Detection** → Subgraph detects IPFS/HTTPS URIs
3. **File Fetching** → File Data Sources fetch and parse JSON
4. **Entity Creation** → Immutable file entities created
5. **Relationship Links** → On-chain entities link to file entities
6. **Statistics Update** → Aggregate statistics computed
2. **URI Detection** → Subgraph detects IPFS and Arweave URIs and creates appropriate file data sources
3. **File Fetching** → The Graph Node fetches content via protocol-specific methods (IPFS gateways or Arweave gateways)
4. **Unified Parsing** → File Data Sources parse JSON using shared utility functions
5. **Entity Creation** → Immutable file entities created with protocol-agnostic schema
6. **Relationship Links** → On-chain entities link to file entities via derived fields
7. **Statistics Update** → Aggregate statistics computed and updated

## ⚙️ Configuration

Expand Down
8 changes: 4 additions & 4 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type Agent @entity(immutable: false) {

# Basic on-chain information
agentURI: String
agentURIType: String # "ipfs", "https", "http", "unknown"
agentURIType: String # "ipfs", "arweave", "https", "http", "unknown"

# Ownership and permissions
owner: Bytes! # Address
Expand Down Expand Up @@ -187,12 +187,12 @@ type GlobalStats @entity(immutable: false) {
}

# =============================================================================
# IMMUTABLE FILE-BASED ENTITIES (IPFS Data Sources)
# IMMUTABLE FILE-BASED ENTITIES (IPFS & Arweave Data Sources)
# =============================================================================

type AgentRegistrationFile @entity(immutable: true) {
id: ID! # Format: "transactionHash:cid"
cid: String! # IPFS CID (for querying by content)
cid: String! # Storage-agnostic content identifier (IPFS CID or Arweave txId)

# Agent link (passed via context)
agentId: String! # Format: "chainId:agentId"
Expand Down Expand Up @@ -228,7 +228,7 @@ type AgentRegistrationFile @entity(immutable: true) {

type FeedbackFile @entity(immutable: true) {
id: ID! # Format: "transactionHash:cid"
cid: String! # IPFS CID (for querying by content)
cid: String! # Storage-agnostic content identifier (IPFS CID or Arweave txId)

# Feedback link (passed via context)
feedbackId: String! # Format: "chainId:agentId:clientAddress:feedbackIndex"
Expand Down
136 changes: 20 additions & 116 deletions src/feedback-file.ts
Original file line number Diff line number Diff line change
@@ -1,127 +1,31 @@
import { Bytes, dataSource, json, log, BigInt, JSONValueKind } from '@graphprotocol/graph-ts'
import { FeedbackFile, Feedback } from '../generated/schema'
import { Bytes, dataSource, log } from '@graphprotocol/graph-ts'
import { parseFeedbackJSON } from './utils/feedback-parser'

/**
* Parse feedback file (supports both IPFS and Arweave)
* Protocol determined by which template called this handler
*/
export function parseFeedbackFile(content: Bytes): void {
let context = dataSource.context()
let feedbackId = context.getString('feedbackId')
let cid = dataSource.stringParam()
let cid = dataSource.stringParam() // IPFS CID or Arweave txId
let txHash = context.getString('txHash')
let feedbackId = context.getString('feedbackId')
let timestamp = context.getBigInt('timestamp')
let tag1OnChain = context.getString('tag1OnChain')
let tag2OnChain = context.getString('tag2OnChain')

// Create composite ID: transactionHash:cid
let fileId = `${txHash}:${cid}`

log.info("Parsing feedback file for feedback: {}, CID: {}, fileId: {}", [feedbackId, cid, fileId])

// Create feedback file with composite ID
let feedbackFile = new FeedbackFile(fileId)
feedbackFile.cid = cid
feedbackFile.feedbackId = feedbackId
feedbackFile.createdAt = context.getBigInt('timestamp')

let result = json.try_fromBytes(content)
if (result.isError) {
log.error("Failed to parse JSON for feedback file CID: {}", [cid])
feedbackFile.save()
return
}

let value = result.value

if (value.kind != JSONValueKind.OBJECT) {
log.error("JSON value is not an object for feedback file CID: {}, kind: {}", [cid, value.kind.toString()])
feedbackFile.save()
return
}

let obj = value.toObject()
if (obj == null) {
log.error("Failed to convert JSON to object for feedback file CID: {}", [cid])

log.info("Processing feedback file: {}", [fileId])

// Use shared parser (works for both IPFS and Arweave)
let feedbackFile = parseFeedbackJSON(content, fileId, feedbackId, cid, timestamp, tag1OnChain, tag2OnChain)

if (feedbackFile !== null) {
feedbackFile.save()
return
}

let text = obj.get('text')
if (text && !text.isNull() && text.kind == JSONValueKind.STRING) {
feedbackFile.text = text.toString()
log.info("Successfully saved feedback file: {}", [fileId])
} else {
log.error("Failed to parse feedback file: {}", [fileId])
}

let capability = obj.get('capability')
if (capability && !capability.isNull() && capability.kind == JSONValueKind.STRING) {
feedbackFile.capability = capability.toString()
}

let name = obj.get('name')
if (name && !name.isNull() && name.kind == JSONValueKind.STRING) {
feedbackFile.name = name.toString()
}

let skill = obj.get('skill')
if (skill && !skill.isNull() && skill.kind == JSONValueKind.STRING) {
feedbackFile.skill = skill.toString()
}

let task = obj.get('task')
if (task && !task.isNull() && task.kind == JSONValueKind.STRING) {
feedbackFile.task = task.toString()
}

let contextStr = obj.get('context')
if (contextStr && !contextStr.isNull() && contextStr.kind == JSONValueKind.STRING) {
feedbackFile.context = contextStr.toString()
}

// Try new format first (proofOfPayment), fallback to old format (proof_of_payment) for backward compatibility
let proofOfPayment = obj.get('proofOfPayment')
if (proofOfPayment == null || proofOfPayment.isNull()) {
proofOfPayment = obj.get('proof_of_payment') // Backward compatibility
}
if (proofOfPayment && !proofOfPayment.isNull() && proofOfPayment.kind == JSONValueKind.OBJECT) {
let proofObj = proofOfPayment.toObject()
if (proofObj != null) {
let fromAddress = proofObj.get('fromAddress')
if (fromAddress && !fromAddress.isNull() && fromAddress.kind == JSONValueKind.STRING) {
feedbackFile.proofOfPaymentFromAddress = fromAddress.toString()
}

let toAddress = proofObj.get('toAddress')
if (toAddress && !toAddress.isNull() && toAddress.kind == JSONValueKind.STRING) {
feedbackFile.proofOfPaymentToAddress = toAddress.toString()
}

let chainId = proofObj.get('chainId')
if (chainId && !chainId.isNull()) {
// chainId can be string or number, handle both
if (chainId.kind == JSONValueKind.STRING) {
feedbackFile.proofOfPaymentChainId = chainId.toString()
} else if (chainId.kind == JSONValueKind.NUMBER) {
feedbackFile.proofOfPaymentChainId = chainId.toBigInt().toString()
}
}

let txHashField = proofObj.get('txHash')
if (txHashField && !txHashField.isNull() && txHashField.kind == JSONValueKind.STRING) {
feedbackFile.proofOfPaymentTxHash = txHashField.toString()
}
}
}

if (tag1OnChain.length == 0) {
let tag1 = obj.get('tag1')
if (tag1 && !tag1.isNull() && tag1.kind == JSONValueKind.STRING) {
feedbackFile.tag1 = tag1.toString()
}
}

if (tag2OnChain.length == 0) {
let tag2 = obj.get('tag2')
if (tag2 && !tag2.isNull() && tag2.kind == JSONValueKind.STRING) {
feedbackFile.tag2 = tag2.toString()
}
}

feedbackFile.save()


// Cannot update chain entities from file handlers due to isolation rules
}
Loading