From 0d10d27a32f417bcd607a6300f2932f9743cdf4c Mon Sep 17 00:00:00 2001 From: William Kempster Date: Wed, 5 Nov 2025 21:58:16 +0000 Subject: [PATCH 01/10] refactor: extract registration JSON parsing into shared utility --- src/registration-file.ts | 238 +++---------------------------- src/utils/registration-parser.ts | 221 ++++++++++++++++++++++++++++ 2 files changed, 241 insertions(+), 218 deletions(-) create mode 100644 src/utils/registration-parser.ts diff --git a/src/registration-file.ts b/src/registration-file.ts index 2782982..44d4eaa 100644 --- a/src/registration-file.ts +++ b/src/registration-file.ts @@ -1,228 +1,30 @@ -import { Bytes, dataSource, json, log, BigInt, JSONValueKind } from '@graphprotocol/graph-ts' -import { AgentRegistrationFile, Agent } from '../generated/schema' +import { Bytes, dataSource, log } from '@graphprotocol/graph-ts' +import { parseRegistrationJSON } from './utils/registration-parser' +/** + * Parse registration file (supports both IPFS and Arweave) + * Protocol determined by which template called this handler + */ export function parseRegistrationFile(content: Bytes): void { let context = dataSource.context() - let agentId = context.getString('agentId') - let cid = dataSource.stringParam() + let cid = dataSource.stringParam() // IPFS CID or Arweave txId let txHash = context.getString('txHash') - - // Create composite ID: transactionHash:cid + let agentId = context.getString('agentId') + let timestamp = context.getBigInt('timestamp') let fileId = `${txHash}:${cid}` - - log.info("Parsing registration file for agent: {}, CID: {}, fileId: {}", [agentId, cid, fileId]) - - // Create registration file with composite ID - let metadata = new AgentRegistrationFile(fileId) - metadata.cid = cid - metadata.agentId = agentId - metadata.createdAt = context.getBigInt('timestamp') - metadata.supportedTrusts = [] - metadata.mcpTools = [] - metadata.mcpPrompts = [] - metadata.mcpResources = [] - metadata.a2aSkills = [] - - let result = json.try_fromBytes(content) - if (result.isError) { - log.error("Failed to parse JSON for registration file CID: {}", [cid]) - metadata.save() - return - } - - let value = result.value - - if (value.kind != JSONValueKind.OBJECT) { - log.error("JSON value is not an object for registration file CID: {}, kind: {}", [cid, value.kind.toString()]) - metadata.save() - return - } - - let obj = value.toObject() - if (obj == null) { - log.error("Failed to convert JSON to object for registration file CID: {}", [cid]) + + log.info("Processing registration file: {}", [fileId]) + + // Use shared parser (works for both IPFS and Arweave) + let metadata = parseRegistrationJSON(content, fileId, agentId, cid, timestamp) + + if (metadata !== null) { metadata.save() - return + log.info("Successfully saved registration file: {}", [fileId]) + } else { + log.error("Failed to parse registration file: {}", [fileId]) } - - let name = obj.get('name') - if (name && !name.isNull() && name.kind == JSONValueKind.STRING) { - metadata.name = name.toString() - } - - let description = obj.get('description') - if (description && !description.isNull() && description.kind == JSONValueKind.STRING) { - metadata.description = description.toString() - } - - let image = obj.get('image') - if (image && !image.isNull() && image.kind == JSONValueKind.STRING) { - metadata.image = image.toString() - } - - let active = obj.get('active') - if (active && !active.isNull()) { - metadata.active = active.toBool() - } - - let x402support = obj.get('x402support') - if (x402support && !x402support.isNull()) { - metadata.x402support = x402support.toBool() - } - - let supportedTrusts = obj.get('supportedTrusts') - if (!supportedTrusts || supportedTrusts.isNull()) { - supportedTrusts = obj.get('supportedTrust') - } - if (supportedTrusts && !supportedTrusts.isNull() && supportedTrusts.kind == JSONValueKind.ARRAY) { - let trustsArray = supportedTrusts.toArray() - let trusts: string[] = [] - for (let i = 0; i < trustsArray.length; i++) { - let trust = trustsArray[i] - if (trust.kind == JSONValueKind.STRING) { - trusts.push(trust.toString()) - } - } - metadata.supportedTrusts = trusts - } - - // Parse endpoints array - let endpoints = obj.get('endpoints') - if (endpoints && !endpoints.isNull() && endpoints.kind == JSONValueKind.ARRAY) { - let endpointsArray = endpoints.toArray() - - for (let i = 0; i < endpointsArray.length; i++) { - let endpointValue = endpointsArray[i] - if (endpointValue.kind != JSONValueKind.OBJECT) { - continue - } - let endpoint = endpointValue.toObject() - if (endpoint == null) { - continue - } - - let endpointName = endpoint.get('name') - if (endpointName && !endpointName.isNull() && endpointName.kind == JSONValueKind.STRING) { - let nameStr = endpointName.toString() - - if (nameStr == 'MCP') { - let mcpEndpoint = endpoint.get('endpoint') - if (mcpEndpoint && !mcpEndpoint.isNull() && mcpEndpoint.kind == JSONValueKind.STRING) { - metadata.mcpEndpoint = mcpEndpoint.toString() - } - - let mcpVersion = endpoint.get('version') - if (mcpVersion && !mcpVersion.isNull() && mcpVersion.kind == JSONValueKind.STRING) { - metadata.mcpVersion = mcpVersion.toString() - } - - let mcpTools = endpoint.get('mcpTools') - if (mcpTools && !mcpTools.isNull() && mcpTools.kind == JSONValueKind.ARRAY) { - let toolsArray = mcpTools.toArray() - let tools: string[] = [] - for (let j = 0; j < toolsArray.length; j++) { - let tool = toolsArray[j] - if (tool.kind == JSONValueKind.STRING) { - tools.push(tool.toString()) - } - } - metadata.mcpTools = tools - } - - let mcpPrompts = endpoint.get('mcpPrompts') - if (mcpPrompts && !mcpPrompts.isNull() && mcpPrompts.kind == JSONValueKind.ARRAY) { - let promptsArray = mcpPrompts.toArray() - let prompts: string[] = [] - for (let j = 0; j < promptsArray.length; j++) { - let prompt = promptsArray[j] - if (prompt.kind == JSONValueKind.STRING) { - prompts.push(prompt.toString()) - } - } - metadata.mcpPrompts = prompts - } - - let mcpResources = endpoint.get('mcpResources') - if (mcpResources && !mcpResources.isNull() && mcpResources.kind == JSONValueKind.ARRAY) { - let resourcesArray = mcpResources.toArray() - let resources: string[] = [] - for (let j = 0; j < resourcesArray.length; j++) { - let resource = resourcesArray[j] - if (resource.kind == JSONValueKind.STRING) { - resources.push(resource.toString()) - } - } - metadata.mcpResources = resources - } - } else if (nameStr == 'A2A') { - let a2aEndpoint = endpoint.get('endpoint') - if (a2aEndpoint && !a2aEndpoint.isNull() && a2aEndpoint.kind == JSONValueKind.STRING) { - metadata.a2aEndpoint = a2aEndpoint.toString() - } - - let a2aVersion = endpoint.get('version') - if (a2aVersion && !a2aVersion.isNull() && a2aVersion.kind == JSONValueKind.STRING) { - metadata.a2aVersion = a2aVersion.toString() - } - - let a2aSkills = endpoint.get('a2aSkills') - if (a2aSkills && !a2aSkills.isNull() && a2aSkills.kind == JSONValueKind.ARRAY) { - let skillsArray = a2aSkills.toArray() - let skills: string[] = [] - for (let j = 0; j < skillsArray.length; j++) { - let skill = skillsArray[j] - if (skill.kind == JSONValueKind.STRING) { - skills.push(skill.toString()) - } - } - metadata.a2aSkills = skills - } - } else if (nameStr == 'agentWallet') { - let agentWallet = endpoint.get('endpoint') - if (agentWallet && !agentWallet.isNull() && agentWallet.kind == JSONValueKind.STRING) { - let walletStr = agentWallet.toString() - - if (walletStr.startsWith("eip155:")) { - let parts = walletStr.split(":") - let hasAddress = parts.length > 2 - if (hasAddress) { - let addressPart = parts[2] - if (addressPart.startsWith("0x") && addressPart.length == 42) { - metadata.agentWallet = Bytes.fromHexString(addressPart) - let chainIdPart = parts[1] - if (chainIdPart.length > 0) { - metadata.agentWalletChainId = BigInt.fromString(chainIdPart) - } - } - } - } else if (walletStr.startsWith("0x") && walletStr.length == 42) { - metadata.agentWallet = Bytes.fromHexString(walletStr) - } - } - } else if (nameStr == 'ENS') { - let ensEndpoint = endpoint.get('endpoint') - if (ensEndpoint && !ensEndpoint.isNull() && ensEndpoint.kind == JSONValueKind.STRING) { - metadata.ens = ensEndpoint.toString() - } - } else if (nameStr == 'DID') { - let didEndpoint = endpoint.get('endpoint') - if (didEndpoint && !didEndpoint.isNull() && didEndpoint.kind == JSONValueKind.STRING) { - metadata.did = didEndpoint.toString() - } - } - } - } - } - - metadata.save() - - log.info("Successfully parsed registration file for fileId: {}, CID: {}, name: {}, description: {}", [ - fileId, - cid, - metadata.name ? metadata.name! : "null", - metadata.description ? metadata.description! : "null" - ]) - + // Note: We cannot update chain entities (Agent) from file data source handlers due to isolation rules. // The registrationFile connection is set from the chain handler in identity-registry.ts } diff --git a/src/utils/registration-parser.ts b/src/utils/registration-parser.ts new file mode 100644 index 0000000..82b877e --- /dev/null +++ b/src/utils/registration-parser.ts @@ -0,0 +1,221 @@ +import { Bytes, json, log, BigInt, JSONValueKind } from '@graphprotocol/graph-ts' +import { AgentRegistrationFile } from '../../generated/schema' + +/** + * Parse registration file JSON content into AgentRegistrationFile entity + * Shared by both IPFS and Arweave file handlers + */ +export function parseRegistrationJSON( + content: Bytes, + fileId: string, + agentId: string, + cid: string, + timestamp: BigInt +): AgentRegistrationFile | null { + + log.info("Parsing registration file: fileId={}, agentId={}, cid={}", [fileId, agentId, cid]) + + // Create entity + let metadata = new AgentRegistrationFile(fileId) + metadata.cid = cid // Works for both IPFS CID and Arweave txId + metadata.agentId = agentId + metadata.createdAt = timestamp + metadata.supportedTrusts = [] + metadata.mcpTools = [] + metadata.mcpPrompts = [] + metadata.mcpResources = [] + metadata.a2aSkills = [] + + // Parse JSON content + let result = json.try_fromBytes(content) + if (result.isError) { + log.error("Failed to parse JSON for fileId: {}", [fileId]) + return null + } + + let value = result.value + + if (value.kind != JSONValueKind.OBJECT) { + log.error("JSON value is not an object for fileId: {}, kind: {}", [fileId, value.kind.toString()]) + return null + } + + let obj = value.toObject() + if (obj == null) { + log.error("Failed to convert JSON to object for fileId: {}", [fileId]) + return null + } + + // Extract basic metadata fields + let name = obj.get('name') + if (name && !name.isNull() && name.kind == JSONValueKind.STRING) { + metadata.name = name.toString() + } + + let description = obj.get('description') + if (description && !description.isNull() && description.kind == JSONValueKind.STRING) { + metadata.description = description.toString() + } + + let image = obj.get('image') + if (image && !image.isNull() && image.kind == JSONValueKind.STRING) { + metadata.image = image.toString() + } + + let active = obj.get('active') + if (active && !active.isNull()) { + metadata.active = active.toBool() + } + + let x402support = obj.get('x402support') + if (x402support && !x402support.isNull()) { + metadata.x402support = x402support.toBool() + } + + // Extract supportedTrusts array + let supportedTrusts = obj.get('supportedTrusts') + if (!supportedTrusts || supportedTrusts.isNull()) { + supportedTrusts = obj.get('supportedTrust') + } + if (supportedTrusts && !supportedTrusts.isNull() && supportedTrusts.kind == JSONValueKind.ARRAY) { + let trustsArray = supportedTrusts.toArray() + let trusts: string[] = [] + for (let i = 0; i < trustsArray.length; i++) { + let trust = trustsArray[i] + if (trust.kind == JSONValueKind.STRING) { + trusts.push(trust.toString()) + } + } + metadata.supportedTrusts = trusts + } + + // Parse endpoints array + let endpoints = obj.get('endpoints') + if (endpoints && !endpoints.isNull() && endpoints.kind == JSONValueKind.ARRAY) { + let endpointsArray = endpoints.toArray() + + for (let i = 0; i < endpointsArray.length; i++) { + let endpointValue = endpointsArray[i] + if (endpointValue.kind != JSONValueKind.OBJECT) { + continue + } + let endpoint = endpointValue.toObject() + if (endpoint == null) { + continue + } + + let endpointName = endpoint.get('name') + if (endpointName && !endpointName.isNull() && endpointName.kind == JSONValueKind.STRING) { + let nameStr = endpointName.toString() + + if (nameStr == 'MCP') { + let mcpEndpoint = endpoint.get('endpoint') + if (mcpEndpoint && !mcpEndpoint.isNull() && mcpEndpoint.kind == JSONValueKind.STRING) { + metadata.mcpEndpoint = mcpEndpoint.toString() + } + + let mcpVersion = endpoint.get('version') + if (mcpVersion && !mcpVersion.isNull() && mcpVersion.kind == JSONValueKind.STRING) { + metadata.mcpVersion = mcpVersion.toString() + } + + let mcpTools = endpoint.get('mcpTools') + if (mcpTools && !mcpTools.isNull() && mcpTools.kind == JSONValueKind.ARRAY) { + let toolsArray = mcpTools.toArray() + let tools: string[] = [] + for (let j = 0; j < toolsArray.length; j++) { + let tool = toolsArray[j] + if (tool.kind == JSONValueKind.STRING) { + tools.push(tool.toString()) + } + } + metadata.mcpTools = tools + } + + let mcpPrompts = endpoint.get('mcpPrompts') + if (mcpPrompts && !mcpPrompts.isNull() && mcpPrompts.kind == JSONValueKind.ARRAY) { + let promptsArray = mcpPrompts.toArray() + let prompts: string[] = [] + for (let j = 0; j < promptsArray.length; j++) { + let prompt = promptsArray[j] + if (prompt.kind == JSONValueKind.STRING) { + prompts.push(prompt.toString()) + } + } + metadata.mcpPrompts = prompts + } + + let mcpResources = endpoint.get('mcpResources') + if (mcpResources && !mcpResources.isNull() && mcpResources.kind == JSONValueKind.ARRAY) { + let resourcesArray = mcpResources.toArray() + let resources: string[] = [] + for (let j = 0; j < resourcesArray.length; j++) { + let resource = resourcesArray[j] + if (resource.kind == JSONValueKind.STRING) { + resources.push(resource.toString()) + } + } + metadata.mcpResources = resources + } + } else if (nameStr == 'A2A') { + let a2aEndpoint = endpoint.get('endpoint') + if (a2aEndpoint && !a2aEndpoint.isNull() && a2aEndpoint.kind == JSONValueKind.STRING) { + metadata.a2aEndpoint = a2aEndpoint.toString() + } + + let a2aVersion = endpoint.get('version') + if (a2aVersion && !a2aVersion.isNull() && a2aVersion.kind == JSONValueKind.STRING) { + metadata.a2aVersion = a2aVersion.toString() + } + + let a2aSkills = endpoint.get('a2aSkills') + if (a2aSkills && !a2aSkills.isNull() && a2aSkills.kind == JSONValueKind.ARRAY) { + let skillsArray = a2aSkills.toArray() + let skills: string[] = [] + for (let j = 0; j < skillsArray.length; j++) { + let skill = skillsArray[j] + if (skill.kind == JSONValueKind.STRING) { + skills.push(skill.toString()) + } + } + metadata.a2aSkills = skills + } + } else if (nameStr == 'agentWallet') { + let agentWallet = endpoint.get('endpoint') + if (agentWallet && !agentWallet.isNull() && agentWallet.kind == JSONValueKind.STRING) { + let walletStr = agentWallet.toString() + + if (walletStr.startsWith("eip155:")) { + let parts = walletStr.split(":") + let hasAddress = parts.length > 2 + if (hasAddress) { + let addressPart = parts[2] + if (addressPart.startsWith("0x") && addressPart.length == 42) { + metadata.agentWallet = Bytes.fromHexString(addressPart) + let chainIdPart = parts[1] + if (chainIdPart.length > 0) { + metadata.agentWalletChainId = BigInt.fromString(chainIdPart) + } + } + } + } else if (walletStr.startsWith("0x") && walletStr.length == 42) { + metadata.agentWallet = Bytes.fromHexString(walletStr) + } + } + } else if (nameStr == 'ENS') { + let ensEndpoint = endpoint.get('endpoint') + if (ensEndpoint && !ensEndpoint.isNull() && ensEndpoint.kind == JSONValueKind.STRING) { + metadata.ens = ensEndpoint.toString() + } + } else if (nameStr == 'DID') { + let didEndpoint = endpoint.get('endpoint') + if (didEndpoint && !didEndpoint.isNull() && didEndpoint.kind == JSONValueKind.STRING) { + metadata.did = didEndpoint.toString() + } + } + } + } + } + + return metadata +} From 136ecc602894e679f686bc96549cb4dfc2b8c44e Mon Sep 17 00:00:00 2001 From: William Kempster Date: Wed, 5 Nov 2025 21:58:47 +0000 Subject: [PATCH 02/10] refactor: extract feedback JSON parsing into shared utility --- src/feedback-file.ts | 136 ++++++----------------------------- src/utils/feedback-parser.ts | 128 +++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+), 116 deletions(-) create mode 100644 src/utils/feedback-parser.ts diff --git a/src/feedback-file.ts b/src/feedback-file.ts index 39d9aa2..533ed00 100644 --- a/src/feedback-file.ts +++ b/src/feedback-file.ts @@ -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 } diff --git a/src/utils/feedback-parser.ts b/src/utils/feedback-parser.ts new file mode 100644 index 0000000..56f89fc --- /dev/null +++ b/src/utils/feedback-parser.ts @@ -0,0 +1,128 @@ +import { Bytes, json, log, BigInt, JSONValueKind } from '@graphprotocol/graph-ts' +import { FeedbackFile } from '../../generated/schema' + +/** + * Parse feedback file JSON content into FeedbackFile entity + * Shared by both IPFS and Arweave feedback file handlers + */ +export function parseFeedbackJSON( + content: Bytes, + fileId: string, + feedbackId: string, + cid: string, + timestamp: BigInt, + tag1OnChain: string, + tag2OnChain: string +): FeedbackFile | null { + + log.info("Parsing feedback file: fileId={}, feedbackId={}, cid={}", [fileId, feedbackId, cid]) + + // Create entity + let feedbackFile = new FeedbackFile(fileId) + feedbackFile.cid = cid + feedbackFile.feedbackId = feedbackId + feedbackFile.createdAt = timestamp + + // Parse JSON content + let result = json.try_fromBytes(content) + if (result.isError) { + log.error("Failed to parse JSON for feedback file fileId: {}", [fileId]) + return null + } + + let value = result.value + + if (value.kind != JSONValueKind.OBJECT) { + log.error("JSON value is not an object for feedback file fileId: {}, kind: {}", [fileId, value.kind.toString()]) + return null + } + + let obj = value.toObject() + if (obj == null) { + log.error("Failed to convert JSON to object for feedback file fileId: {}", [fileId]) + return null + } + + // Extract feedback fields + let text = obj.get('text') + if (text && !text.isNull() && text.kind == JSONValueKind.STRING) { + feedbackFile.text = text.toString() + } + + 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() + } + } + } + + // Extract tags from file if not provided on-chain + 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() + } + } + + return feedbackFile +} From 87bf78541a82ec93cd49d160036aa32da2854e8a Mon Sep 17 00:00:00 2001 From: William Kempster Date: Wed, 5 Nov 2025 21:59:07 +0000 Subject: [PATCH 03/10] feat: add Arweave URI utilities and detection --- src/utils/arweave.ts | 29 +++++++++++++++++++++++++++++ src/utils/ipfs.ts | 5 ++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/utils/arweave.ts diff --git a/src/utils/arweave.ts b/src/utils/arweave.ts new file mode 100644 index 0000000..660ddde --- /dev/null +++ b/src/utils/arweave.ts @@ -0,0 +1,29 @@ +import { log } from "@graphprotocol/graph-ts" + +/** + * Check if a URI is an Arweave URI + */ +export function isArweaveUri(uri: string): boolean { + return uri.startsWith("ar://") +} + +/** + * Extract Arweave transaction ID from ar:// URI + */ +export function extractArweaveTxId(uri: string): string { + if (uri.startsWith("ar://")) { + return uri.substring(5) // Remove "ar://" + } + return "" +} + +/** + * Log Arweave transaction ID extraction + */ +export function logArweaveExtraction(context: string, uri: string, txId: string): void { + if (txId.length > 0) { + log.info("Arweave txId extracted for {}: {} -> {}", [context, uri, txId]) + } else { + log.warning("Failed to extract Arweave txId for {}: {}", [context, uri]) + } +} diff --git a/src/utils/ipfs.ts b/src/utils/ipfs.ts index d75274d..24f67be 100644 --- a/src/utils/ipfs.ts +++ b/src/utils/ipfs.ts @@ -165,9 +165,12 @@ export function extractIpfsHash(uri: string): string { /** * Determine the URI type for an agent or feedback URI + * Returns: "ipfs", "arweave", "https", "http", or "unknown" */ export function determineUriType(uri: string): string { - if (uri.startsWith("ipfs://")) { + if (uri.startsWith("ar://")) { + return "arweave" + } else if (uri.startsWith("ipfs://")) { return "ipfs" } else if (isIpfsGatewayUrl(uri)) { return "ipfs" // Gateway URLs are still IPFS From 981348e2cc1608b4992ed1431f025aa442dfea3f Mon Sep 17 00:00:00 2001 From: William Kempster Date: Wed, 5 Nov 2025 21:59:27 +0000 Subject: [PATCH 04/10] feat: add Arweave support to agent registration handlers --- src/identity-registry.ts | 61 +++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/identity-registry.ts b/src/identity-registry.ts index 25f0573..af71b48 100644 --- a/src/identity-registry.ts +++ b/src/identity-registry.ts @@ -1,6 +1,7 @@ import { Address, BigInt, Bytes, ethereum, log, BigDecimal, DataSourceContext } from "@graphprotocol/graph-ts" import { getChainId } from "./utils/chain" import { isIpfsUri, extractIpfsHash, determineUriType, logIpfsExtraction } from "./utils/ipfs" +import { isArweaveUri, extractArweaveTxId, logArweaveExtraction } from "./utils/arweave" import { Registered, MetadataSet, @@ -9,7 +10,7 @@ import { Approval, ApprovalForAll } from "../generated/IdentityRegistry/IdentityRegistry" -import { RegistrationFile } from "../generated/templates" +import { RegistrationFile, ArweaveRegistrationFile } from "../generated/templates" import { Agent, AgentMetadata, @@ -80,21 +81,43 @@ export function handleAgentRegistered(event: Registered): void { if (ipfsHash.length > 0) { let txHash = event.transaction.hash.toHexString() let fileId = `${txHash}:${ipfsHash}` - + let context = new DataSourceContext() context.setString('agentId', agentEntityId) context.setString('cid', ipfsHash) context.setString('txHash', txHash) context.setBigInt('timestamp', event.block.timestamp) RegistrationFile.createWithContext(ipfsHash, context) - + // Set the connection to the composite ID agent.registrationFile = fileId agent.save() log.info("Set registrationFile connection for agent {} to ID: {}", [agentEntityId, fileId]) } } - + + // Arweave handling + if (event.params.tokenURI.length > 0 && isArweaveUri(event.params.tokenURI)) { + let arweaveTxId = extractArweaveTxId(event.params.tokenURI) + logArweaveExtraction("agent registration", event.params.tokenURI, arweaveTxId) + + if (arweaveTxId.length > 0) { + let txHash = event.transaction.hash.toHexString() + let fileId = `${txHash}:${arweaveTxId}` + + let context = new DataSourceContext() + context.setString('agentId', agentEntityId) + context.setString('cid', arweaveTxId) // Storage-agnostic: use 'cid' for both IPFS and Arweave + context.setString('txHash', txHash) + context.setBigInt('timestamp', event.block.timestamp) + ArweaveRegistrationFile.createWithContext(arweaveTxId, context) + + agent.registrationFile = fileId + agent.save() + log.info("Set registrationFile connection for agent {} to Arweave ID: {}", [agentEntityId, fileId]) + } + } + log.info("Agent registered: {} on chain {}", [agentId.toString(), chainId.toString()]) } @@ -150,14 +173,14 @@ export function handleUriUpdated(event: UriUpdated): void { if (ipfsHash.length > 0) { let txHash = event.transaction.hash.toHexString() let fileId = `${txHash}:${ipfsHash}` - + let context = new DataSourceContext() context.setString('agentId', agentEntityId) context.setString('cid', ipfsHash) context.setString('txHash', txHash) context.setBigInt('timestamp', event.block.timestamp) RegistrationFile.createWithContext(ipfsHash, context) - + // Set the connection to the composite ID agent.registrationFile = fileId agent.save() @@ -165,7 +188,31 @@ export function handleUriUpdated(event: UriUpdated): void { } // updateProtocolActiveCounts removed - active/inactive stats removed } - + + // Arweave handling + if (isArweaveUri(event.params.newUri)) { + agent.agentURIType = "arweave" + agent.save() + let arweaveTxId = extractArweaveTxId(event.params.newUri) + logArweaveExtraction("agent URI update", event.params.newUri, arweaveTxId) + + if (arweaveTxId.length > 0) { + let txHash = event.transaction.hash.toHexString() + let fileId = `${txHash}:${arweaveTxId}` + + let context = new DataSourceContext() + context.setString('agentId', agentEntityId) + context.setString('cid', arweaveTxId) // Storage-agnostic: use 'cid' for both IPFS and Arweave + context.setString('txHash', txHash) + context.setBigInt('timestamp', event.block.timestamp) + ArweaveRegistrationFile.createWithContext(arweaveTxId, context) + + agent.registrationFile = fileId + agent.save() + log.info("Set registrationFile connection for agent {} to Arweave ID: {}", [agentEntityId, fileId]) + } + } + log.info("Agent URI updated for agent {}: {}", [agentEntityId, event.params.newUri]) } From 99d61d7cda53d94c012e62af5e06f2370d135293 Mon Sep 17 00:00:00 2001 From: William Kempster Date: Wed, 5 Nov 2025 21:59:41 +0000 Subject: [PATCH 05/10] feat: add Arweave support to feedback handlers --- src/reputation-registry.ts | 36 ++++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/src/reputation-registry.ts b/src/reputation-registry.ts index 665350e..cccfdad 100644 --- a/src/reputation-registry.ts +++ b/src/reputation-registry.ts @@ -1,12 +1,16 @@ import { BigInt, Bytes, ethereum, log, BigDecimal, DataSourceContext } from "@graphprotocol/graph-ts" import { getChainId } from "./utils/chain" import { isIpfsUri, extractIpfsHash, determineUriType, logIpfsExtraction, bytes32ToString } from "./utils/ipfs" +import { isArweaveUri, extractArweaveTxId, logArweaveExtraction } from "./utils/arweave" import { NewFeedback, FeedbackRevoked, ResponseAppended } from "../generated/ReputationRegistry/ReputationRegistry" -import { FeedbackFile as FeedbackFileTemplate } from "../generated/templates" +import { + FeedbackFile as FeedbackFileTemplate, + ArweaveFeedbackFile as ArweaveFeedbackFileTemplate +} from "../generated/templates" import { Agent, Feedback, @@ -67,7 +71,7 @@ export function handleNewFeedback(event: NewFeedback): void { if (ipfsHash.length > 0) { let txHash = event.transaction.hash.toHexString() let fileId = `${txHash}:${ipfsHash}` - + let context = new DataSourceContext() context.setString('feedbackId', feedbackId) context.setString('cid', ipfsHash) @@ -76,14 +80,38 @@ export function handleNewFeedback(event: NewFeedback): void { context.setString('tag1OnChain', feedback.tag1 ? feedback.tag1! : "") context.setString('tag2OnChain', feedback.tag2 ? feedback.tag2! : "") FeedbackFileTemplate.createWithContext(ipfsHash, context) - + // Set the connection to the composite ID feedback.feedbackFile = fileId feedback.save() log.info("Set feedbackFile connection for feedback {} to ID: {}", [feedbackId, fileId]) } } - + + // Arweave handling + if (event.params.feedbackUri.length > 0 && isArweaveUri(event.params.feedbackUri)) { + let arweaveTxId = extractArweaveTxId(event.params.feedbackUri) + logArweaveExtraction("feedback", event.params.feedbackUri, arweaveTxId) + + if (arweaveTxId.length > 0) { + let txHash = event.transaction.hash.toHexString() + let fileId = `${txHash}:${arweaveTxId}` + + let context = new DataSourceContext() + context.setString('feedbackId', feedbackId) + context.setString('cid', arweaveTxId) // Storage-agnostic: use 'cid' for both IPFS and Arweave + context.setString('txHash', txHash) + context.setBigInt('timestamp', event.block.timestamp) + context.setString('tag1OnChain', feedback.tag1 ? feedback.tag1! : "") + context.setString('tag2OnChain', feedback.tag2 ? feedback.tag2! : "") + ArweaveFeedbackFileTemplate.createWithContext(arweaveTxId, context) + + feedback.feedbackFile = fileId + feedback.save() + log.info("Set feedbackFile connection for feedback {} to Arweave ID: {}", [feedbackId, fileId]) + } + } + // Update agent statistics updateAgentStats(agent, event.params.score, event.block.timestamp) From 8d125086bcec8feaef1faff2946e217b951a06f5 Mon Sep 17 00:00:00 2001 From: William Kempster Date: Wed, 5 Nov 2025 21:59:54 +0000 Subject: [PATCH 06/10] feat: add Arweave file data source templates --- subgraph.yaml | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/subgraph.yaml b/subgraph.yaml index 11b6bc6..d0b6006 100644 --- a/subgraph.yaml +++ b/subgraph.yaml @@ -95,10 +95,11 @@ dataSources: file: ./src/validation-registry.ts # ============================================================================= -# FILE DATA SOURCES (IPFS) +# FILE DATA SOURCES (IPFS & ARWEAVE) # ============================================================================= templates: + # IPFS Templates - kind: file/ipfs name: RegistrationFile network: sepolia @@ -128,3 +129,34 @@ templates: abis: - name: ReputationRegistry file: ./abis/ReputationRegistry.json + + # Arweave Templates (reuse same handlers as IPFS) + - kind: file/arweave + name: ArweaveRegistrationFile + network: sepolia + mapping: + apiVersion: 0.0.7 + language: wasm/assemblyscript + file: ./src/registration-file.ts + handler: parseRegistrationFile + entities: + - AgentRegistrationFile + - Agent + abis: + - name: IdentityRegistry + file: ./abis/IdentityRegistry.json + + - kind: file/arweave + name: ArweaveFeedbackFile + network: sepolia + mapping: + apiVersion: 0.0.7 + language: wasm/assemblyscript + file: ./src/feedback-file.ts + handler: parseFeedbackFile + entities: + - FeedbackFile + - Feedback + abis: + - name: ReputationRegistry + file: ./abis/ReputationRegistry.json From 2ddd6cdfb8e78c9e0850ca836e43b96594864b6b Mon Sep 17 00:00:00 2001 From: William Kempster Date: Wed, 5 Nov 2025 22:00:38 +0000 Subject: [PATCH 07/10] docs: update schema for storage-agnostic cid fields --- schema.graphql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/schema.graphql b/schema.graphql index 3c311cb..42a9f2b 100644 --- a/schema.graphql +++ b/schema.graphql @@ -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 @@ -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" @@ -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" From a037395fcdd860caed3abebec1fd7a32a7038cb4 Mon Sep 17 00:00:00 2001 From: William Kempster Date: Thu, 6 Nov 2025 01:54:14 +0000 Subject: [PATCH 08/10] add Arweave integration documentation to README --- README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 18bc430..1e119af 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,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 **Note:** Currently deployed for Ethereum Sepolia only. Additional networks coming soon. @@ -72,7 +72,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! @@ -95,7 +95,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 # IPFS/Arweave/HTTPS URI for rich content feedbackURIType: String feedbackHash: Bytes! isRevoked: Boolean! @@ -130,15 +130,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/Arweave/HTTPS 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 @@ -166,7 +166,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 @@ -385,38 +385,91 @@ 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 all protocols) +- **Trigger**: When `agentURI` points to IPFS/Arweave/HTTPS content - **Output**: `AgentRegistrationFile` entity - **Data Parsed**: Metadata, capabilities, endpoints, identity information +- **Templates**: + - `IPFSRegistrationFile` (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 all protocols) +- **Trigger**: When `feedbackUri` points to IPFS/Arweave/HTTPS content - **Output**: `FeedbackFile` entity - **Data Parsed**: Detailed feedback text, proof of payment, context +- **Templates**: + - `IPFSFeedbackFile` (kind: `file/ipfs`) + - `ArweaveFeedbackFile` (kind: `file/arweave`) ### Supported URI Formats - **IPFS**: `ipfs://QmHash...` or bare `QmHash...` +- **Arweave**: `ar://transactionId...` - **HTTPS**: `https://example.com/file.json` - **HTTP**: `http://example.com/file.json` +**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/Arweave/HTTPS URIs and creates appropriate file data sources +3. **File Fetching** → The Graph Node fetches content via protocol-specific methods (IPFS gateways, Arweave gateways, or HTTPS) +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 From 06bdf5ec9b32115c34351659aa1229249befe8d5 Mon Sep 17 00:00:00 2001 From: Philip Mataras Date: Thu, 13 Nov 2025 16:19:12 -0500 Subject: [PATCH 09/10] fix: make IPFS and Arweave event handlers mutually exclusive MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed event handler logic from separate if statements to if/else if pattern to ensure only one storage protocol is processed per event. **Changes:** - identity-registry.ts: Made IPFS/Arweave handlers mutually exclusive in handleAgentRegistered and handleTokenURISet - reputation-registry.ts: Made IPFS/Arweave handlers mutually exclusive in handleFeedbackGiven This prevents edge cases where malformed URIs could trigger both handlers and ensures cleaner event processing logic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/identity-registry.ts | 16 ++++++++-------- src/reputation-registry.ts | 9 ++++----- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/identity-registry.ts b/src/identity-registry.ts index af71b48..7afe466 100644 --- a/src/identity-registry.ts +++ b/src/identity-registry.ts @@ -74,7 +74,8 @@ export function handleAgentRegistered(event: Registered): void { globalStats.updatedAt = event.block.timestamp globalStats.save() - + + // Handle file data source creation (mutually exclusive: IPFS or Arweave) if (event.params.tokenURI.length > 0 && isIpfsUri(event.params.tokenURI)) { let ipfsHash = extractIpfsHash(event.params.tokenURI) logIpfsExtraction("agent registration", event.params.tokenURI, ipfsHash) @@ -95,9 +96,8 @@ export function handleAgentRegistered(event: Registered): void { log.info("Set registrationFile connection for agent {} to ID: {}", [agentEntityId, fileId]) } } - - // Arweave handling - if (event.params.tokenURI.length > 0 && isArweaveUri(event.params.tokenURI)) { + // Arweave handling (mutually exclusive with IPFS) + else if (event.params.tokenURI.length > 0 && isArweaveUri(event.params.tokenURI)) { let arweaveTxId = extractArweaveTxId(event.params.tokenURI) logArweaveExtraction("agent registration", event.params.tokenURI, arweaveTxId) @@ -164,7 +164,8 @@ export function handleUriUpdated(event: UriUpdated): void { agent.agentURI = event.params.newUri agent.updatedAt = event.block.timestamp agent.save() - + + // Handle file data source creation (mutually exclusive: IPFS or Arweave) if (isIpfsUri(event.params.newUri)) { agent.agentURIType = "ipfs" agent.save() @@ -188,9 +189,8 @@ export function handleUriUpdated(event: UriUpdated): void { } // updateProtocolActiveCounts removed - active/inactive stats removed } - - // Arweave handling - if (isArweaveUri(event.params.newUri)) { + // Arweave handling (mutually exclusive with IPFS) + else if (isArweaveUri(event.params.newUri)) { agent.agentURIType = "arweave" agent.save() let arweaveTxId = extractArweaveTxId(event.params.newUri) diff --git a/src/reputation-registry.ts b/src/reputation-registry.ts index cccfdad..1e2ade3 100644 --- a/src/reputation-registry.ts +++ b/src/reputation-registry.ts @@ -63,8 +63,8 @@ export function handleNewFeedback(event: NewFeedback): void { } feedback.save() - - // Trigger IPFS file data source if URI is IPFS + + // Handle file data source creation (mutually exclusive: IPFS or Arweave) if (event.params.feedbackUri.length > 0 && isIpfsUri(event.params.feedbackUri)) { let ipfsHash = extractIpfsHash(event.params.feedbackUri) logIpfsExtraction("feedback", event.params.feedbackUri, ipfsHash) @@ -87,9 +87,8 @@ export function handleNewFeedback(event: NewFeedback): void { log.info("Set feedbackFile connection for feedback {} to ID: {}", [feedbackId, fileId]) } } - - // Arweave handling - if (event.params.feedbackUri.length > 0 && isArweaveUri(event.params.feedbackUri)) { + // Arweave handling (mutually exclusive with IPFS) + else if (event.params.feedbackUri.length > 0 && isArweaveUri(event.params.feedbackUri)) { let arweaveTxId = extractArweaveTxId(event.params.feedbackUri) logArweaveExtraction("feedback", event.params.feedbackUri, arweaveTxId) From 1d31269e0d73468c026160bf82469cb7a1ef23f2 Mon Sep 17 00:00:00 2001 From: Philip Mataras Date: Mon, 17 Nov 2025 09:25:07 -0500 Subject: [PATCH 10/10] docs: correct Arweave integration documentation in README MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed inaccuracies in the README regarding file data source support: - Corrected IPFS template names (RegistrationFile, FeedbackFile) - Clarified that only IPFS and Arweave URIs are processed by file data sources - Noted that HTTPS/HTTP URIs are detected for classification only - Updated data flow and trigger descriptions for accuracy 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 13307fd..37bee75 100644 --- a/README.md +++ b/README.md @@ -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/Arweave/HTTPS URI for rich content + feedbackUri: String # URI for rich content (IPFS, Arweave, HTTPS, etc.) feedbackURIType: String feedbackHash: Bytes! isRevoked: Boolean! @@ -254,7 +254,7 @@ enum ValidationStatus { ### Off-Chain Entities (Immutable from IPFS/Arweave) -**Rich metadata fetched from IPFS/Arweave/HTTPS URIs:** +**Rich metadata fetched from IPFS and Arweave URIs:** #### AgentRegistrationFile ```graphql @@ -556,38 +556,40 @@ The subgraph uses **File Data Sources** to parse off-chain content from multiple ### RegistrationFile Data Source -- **Handler**: `src/registration-file.ts` (unified handler for all protocols) -- **Trigger**: When `agentURI` points to IPFS/Arweave/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**: - - `IPFSRegistrationFile` (kind: `file/ipfs`) + - `RegistrationFile` (kind: `file/ipfs`) - `ArweaveRegistrationFile` (kind: `file/arweave`) ### FeedbackFile Data Source -- **Handler**: `src/feedback-file.ts` (unified handler for all protocols) -- **Trigger**: When `feedbackUri` points to IPFS/Arweave/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**: - - `IPFSFeedbackFile` (kind: `file/ipfs`) + - `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...` - **Arweave**: `ar://transactionId...` -- **HTTPS**: `https://example.com/file.json` -- **HTTP**: `http://example.com/file.json` + +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/Arweave/HTTPS URIs and creates appropriate file data sources -3. **File Fetching** → The Graph Node fetches content via protocol-specific methods (IPFS gateways, Arweave gateways, or HTTPS) +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