diff --git a/schema.graphql b/schema.graphql index 15338313..969c2eea 100644 --- a/schema.graphql +++ b/schema.graphql @@ -522,6 +522,9 @@ type SubgraphDeployment @entity { activeSubgraphCount: Int! "Amount of Subgraph entities that were currently using this deployment when they got deprecated" deprecatedSubgraphCount: Int! + + "The contract sources in this subgraph deployment's manifest" + contracts: [SubgraphDeploymentContract!]! @derivedFrom(field:"subgraphDeployment") } # TODO - add when we have the ability to parse data sources @@ -1219,6 +1222,41 @@ enum Revocability { Disabled } +""" +Events from contract sources in subgraph manifests +""" +type ContractEvent @entity { + "Address-Event" + id: ID! + "Event" + event: String + "The contract this event is associated with" + contract: Contract! +} + +""" +Contracts in subgraph manifests +""" +type Contract @entity { + "Contract Address" + id: ID! + "Placeholder for contract name, if it can be determined" + name: String + "Subgraph deployments which include this contract in their manifests" + subgraphDeployments: [SubgraphDeploymentContract!]! @derivedFrom(field:"contract") + "Events associated with this contract" + contractEvents: [ContractEvent!]! @derivedFrom(field: "contract") +} + +""" +Deployment-to-Contract relational entity +""" +type SubgraphDeploymentContract @entity { + id: ID! # Concat: Deployment ID, Contract Address + subgraphDeployment: SubgraphDeployment! + contract: Contract! +} + """ Full test search for displayName and description on the Subgraph Entity """ @@ -1241,3 +1279,15 @@ type _Schema_ algorithm: rank include: [{ entity: "Delegator", fields: [{ name: "defaultDisplayName" }, { name: "id" }] }] ) + @fulltext( + name: "contractEventSearch" + language: en + algorithm: rank + include: [{ entity: "ContractEvent", fields: [{ name: "event" }] }] + ) + @fulltext( + name: "contractSearch" + language: en + algorithm: rank + include: [{ entity: "Contract", fields: [{ name: "id" }] }] + ) diff --git a/src/mappings/gns.ts b/src/mappings/gns.ts index 303c0c59..bd6b6f63 100644 --- a/src/mappings/gns.ts +++ b/src/mappings/gns.ts @@ -54,7 +54,7 @@ import { convertBigIntSubgraphIDToBase58, duplicateOrUpdateSubgraphWithNewID, duplicateOrUpdateSubgraphVersionWithNewID, - duplicateOrUpdateNameSignalWithNewID, + duplicateOrUpdateNameSignalWithNewID } from './helpers' import { fetchSubgraphMetadata, fetchSubgraphVersionMetadata } from './metadataHelpers' @@ -250,7 +250,7 @@ export function handleSubgraphPublished(event: SubgraphPublished): void { // Create subgraph deployment, if needed. Can happen if the deployment has never been staked on let subgraphDeploymentID = event.params.subgraphDeploymentID.toHexString() let deployment = createOrLoadSubgraphDeployment(subgraphDeploymentID, event.block.timestamp) - + // Create subgraph version let subgraphVersion = new SubgraphVersion(versionIDNew) subgraphVersion.entityVersion = 2 @@ -765,7 +765,7 @@ export function handleSubgraphPublishedV2(event: SubgraphPublished1): void { // Create subgraph deployment, if needed. Can happen if the deployment has never been staked on let subgraphDeploymentID = event.params.subgraphDeploymentID.toHexString() let deployment = createOrLoadSubgraphDeployment(subgraphDeploymentID, event.block.timestamp) - + // Create subgraph version let subgraphVersion = new SubgraphVersion(versionID) subgraphVersion.entityVersion = 2 @@ -774,6 +774,7 @@ export function handleSubgraphPublishedV2(event: SubgraphPublished1): void { subgraphVersion.version = versionNumber.toI32() subgraphVersion.createdAt = event.block.timestamp.toI32() subgraphVersion.save() + let oldDeployment: SubgraphDeployment | null = null if (oldVersionID != null) { diff --git a/src/mappings/helpers.ts b/src/mappings/helpers.ts index f7bfcc9b..a158aba7 100644 --- a/src/mappings/helpers.ts +++ b/src/mappings/helpers.ts @@ -19,10 +19,13 @@ import { SubgraphCategoryRelation, NameSignalSubgraphRelation, CurrentSubgraphDeploymentRelation, + Contract, + ContractEvent, + SubgraphDeploymentContract } from '../types/schema' import { ENS } from '../types/GNS/ENS' import { Controller } from '../types/Controller/Controller' -import { fetchSubgraphDeploymentManifest } from './metadataHelpers' +import { fetchSubgraphDeploymentManifest, processManifestForContracts } from './metadataHelpers' import { addresses } from '../../config/addresses' export function createOrLoadSubgraph( @@ -80,6 +83,10 @@ export function createOrLoadSubgraphDeployment( deployment as SubgraphDeployment, deployment.ipfsHash, ) + + //Associate Contracts and Events with Deployment + processManifestForContracts(deployment) + deployment.createdAt = timestamp.toI32() deployment.stakedTokens = BigInt.fromI32(0) deployment.indexingRewardAmount = BigInt.fromI32(0) @@ -538,6 +545,50 @@ export function createOrLoadGraphNetwork( return graphNetwork as GraphNetwork } +export function createOrLoadSubgraphDeploymentContract( + deployment: SubgraphDeployment, + contract: Contract +): SubgraphDeploymentContract { + let assocID = joinID([deployment.id,contract.id]) + let assoc = SubgraphDeploymentContract.load(assocID) + if (assoc == null) { + assoc = new SubgraphDeploymentContract(assocID) + assoc.subgraphDeployment = deployment.id + assoc.contract = contract.id + assoc.save() + } + return assoc as SubgraphDeploymentContract +} + +export function standardizeAddress(address:String): String { + if(address.length == 40) { + address = '0x' + address + } + return address.toLowerCase() as String +} + +export function createOrLoadContract(contractID: String): Contract { + let contract = Contract.load(contractID) + if(contract == null) { + contract = new Contract(contractID) + contract.save() + } + return contract as Contract +} + +export function createOrLoadContractEvent(contractID: String,event: String): ContractEvent { +// TODO This could really benefit from the use of name mangling, if possible. There might be contract event redundancies without it. + let contractEvent = ContractEvent.load(joinID([contractID,event])) + if(contractEvent == null) { + contractEvent = new ContractEvent(joinID([contractID,event])) + } + contractEvent.contract = contractID + contractEvent.event = event + contractEvent.save() + return contractEvent as ContractEvent +} + + export function addQm(a: ByteArray): ByteArray { let out = new Uint8Array(34) out[0] = 0x12 diff --git a/src/mappings/metadataHelpers.template.ts b/src/mappings/metadataHelpers.template.ts index fee76388..925a0779 100644 --- a/src/mappings/metadataHelpers.template.ts +++ b/src/mappings/metadataHelpers.template.ts @@ -1,7 +1,7 @@ import { json, ipfs, Bytes, JSONValueKind, log } from '@graphprotocol/graph-ts' -import { GraphAccount, Subgraph, SubgraphVersion, SubgraphDeployment } from '../types/schema' +import { GraphAccount, Subgraph, SubgraphVersion, SubgraphDeployment, Contract, ContractEvent, SubgraphDeploymentContract } from '../types/schema' import { jsonToString } from './utils' -import { createOrLoadSubgraphCategory, createOrLoadSubgraphCategoryRelation, createOrLoadNetwork } from './helpers' +import { createOrLoadSubgraphCategory, createOrLoadSubgraphCategoryRelation, createOrLoadNetwork, createOrLoadContract, createOrLoadContractEvent, createOrLoadSubgraphDeploymentContract, standardizeAddress } from './helpers' export function fetchGraphAccountMetadata(graphAccount: GraphAccount, ipfsHash: string): void { {{#ipfs}} @@ -126,3 +126,126 @@ export function fetchSubgraphDeploymentManifest(deployment: SubgraphDeployment, {{/ipfs}} return deployment as SubgraphDeployment } + +/* Subgraph Contract Metadata Extraction & Helpers */ + +export function stripQuotes(str: String): String { + let res = '' + let remove = ['\'','"',' '] + for(let i = 0; i < str.length; i++) { + if(!remove.includes(str[i])) res = res.concat(str[i]) + } + return res as String +} + +export function formatEvent(str: String): String { + let res = '' + let pass = '' + // Strip Quotes - TODO breakout into function common to stripQuotes() + let remove = ['\'','"'] + for(let i = 0; i < str.length; i++) { + if(!remove.includes(str[i])) pass = pass.concat(str[i]) + } + // Newline handling + pass = pass.replaceAll('\r',' ') + pass = pass.replaceAll('\n',' ') + pass = pass.replaceAll('>-',' ') + // Space handling + let last = ' ' + for(let i = 0; i < pass.length; i++) { + if(pass[i] == ' ' && last == ' ') { + continue + } else { + res = res.concat(pass[i]) + } + last = pass[i] + } + res = res.trim() + return res as String +} + +export function extractContractEvents(kind: String, contract: Contract): void { + let eventHandlersSplit = kind.split("eventHandlers:",2) + let eventHandlersStr = '' + if(eventHandlersSplit.length >= 2) { + eventHandlersStr = eventHandlersSplit[1] + } + let eventSplit = eventHandlersStr.split("- event:") + for(let i = 1; i < eventSplit.length; i++) { + let sanitizeSplit = eventSplit[i].split("handler:",2) + let eventIso = formatEvent(sanitizeSplit[0]) + log.debug("Contract event extracted: '{}'",[eventIso]) + let contractEvent = createOrLoadContractEvent(contract.id,eventIso) + } +} + +export function extractContractAddresses(ipfsData: String): Array { + let res = new Array(0) + // Use split() until a suitable YAML parser is found. Approach was used in graph-network-subgraph. + let dataSourcesSplit = ipfsData.split('dataSources:\n',2) + let dataSourcesStr = '' + if(dataSourcesSplit.length >= 2) { + dataSourcesStr = dataSourcesSplit[1]; + } else { + // Problem + return res as Array + } + // Determine where 'dataSources:' ends, exclude everything thereafter. + let sanitizeSplit = dataSourcesStr.split('\n') + let shouldDelete = false + // Assumes 32 for space. + dataSourcesStr = '' + for(let i = 0; i < sanitizeSplit.length; i++) { + if(sanitizeSplit[i].charAt(0) != ' ' || shouldDelete) { + shouldDelete = true + } else { + dataSourcesStr = dataSourcesStr.concat(sanitizeSplit[i]) + if(i < sanitizeSplit.length - 1) { + dataSourcesStr = dataSourcesStr.concat('\n') + } + } + } + // Extract + let kindSplit = dataSourcesStr.split('- kind:') + let sourceStr = '' + let addressStr = '' + let addressIso = '' + for(let i = 1; i < kindSplit.length; i++) { + addressIso = '' + // Source Address + let sourceSplit = kindSplit[i].split(' source:',2) + if(sourceSplit.length < 2) continue + else sourceStr = sourceSplit[1] + + let addressSplit = sourceStr.split(' address:',2) + if(addressSplit.length < 2) continue + else addressStr = addressSplit[1] + + let addressStrSplit = addressStr.split('\n',2) + if(addressStrSplit.length < 2) continue + else addressIso = addressStrSplit[0] + + log.debug("Contract address '{}' extracted",[addressIso]) + res.push(standardizeAddress(stripQuotes(addressIso))) + + // Isolate contract events + let contract = createOrLoadContract(standardizeAddress(stripQuotes(addressIso))) + extractContractEvents(kindSplit[i],contract) + } + + return res as Array +} + +export function processManifestForContracts(deployment: SubgraphDeployment): void { + let manifest = deployment.manifest + if(manifest !== null) { + let contractAddresses = extractContractAddresses(manifest) + let address = '' + for(let i = 0; i < contractAddresses.length; i++) { + address = contractAddresses[i] + log.debug("Associating address '{}'",[address]) + let contract = createOrLoadContract(address) + let assoc = createOrLoadSubgraphDeploymentContract(deployment,contract) + } + } +} diff --git a/subgraph.template.yaml b/subgraph.template.yaml index c8364fc8..632385fe 100644 --- a/subgraph.template.yaml +++ b/subgraph.template.yaml @@ -48,6 +48,9 @@ dataSources: - Subgraph - SubgraphVersion - SubgraphDeployment + - SubgraphDeploymentContract + - Contract + - ContractEvent - GraphAccount - NameSignal abis: diff --git a/tests/metadata.test.ts b/tests/metadata.test.ts new file mode 100644 index 00000000..6af08e47 --- /dev/null +++ b/tests/metadata.test.ts @@ -0,0 +1,18 @@ +import { assert, test } from "matchstick-as/assembly/index" +import { Address, BigInt, DataSourceContext, store, Value, Bytes, log, ethereum } from "@graphprotocol/graph-ts" +import { standardizeAddress } from '../src/mappings/helpers' + +test("testStandardizeAddresses", () => { + let addresses = [ + "f55041e37e12cd407ad00ce2910b8269b01263b9", + "F55041E37E12cD407ad00CE2910B8269B01263b9", + "0xf55041e37e12cd407ad00ce2910b8269b01263b9", + "0xF55041E37E12cD407ad00CE2910B8269B01263b9", + ] + + addresses.forEach(function(x) { + let stdAddr = standardizeAddress(x); + let refAddr = "0xf55041e37e12cd407ad00ce2910b8269b01263b9"; + assert.equals(ethereum.Value.fromString(stdAddr),ethereum.Value.fromString(refAddr)); + }); +})