Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Integrate parsing and entity logic for contracts and events #182

Open
wants to merge 5 commits into
base: mainnet-staging
Choose a base branch
from
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
50 changes: 50 additions & 0 deletions schema.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
"""
Expand All @@ -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" }] }]
)
7 changes: 4 additions & 3 deletions src/mappings/gns.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
convertBigIntSubgraphIDToBase58,
duplicateOrUpdateSubgraphWithNewID,
duplicateOrUpdateSubgraphVersionWithNewID,
duplicateOrUpdateNameSignalWithNewID,
duplicateOrUpdateNameSignalWithNewID
} from './helpers'
import { fetchSubgraphMetadata, fetchSubgraphVersionMetadata } from './metadataHelpers'

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down
53 changes: 52 additions & 1 deletion src/mappings/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
127 changes: 125 additions & 2 deletions src/mappings/metadataHelpers.template.ts
Original file line number Diff line number Diff line change
@@ -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}}
Expand Down Expand Up @@ -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<String> {
let res = new Array<String>(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<String>
}
// 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<String>
}

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)
}
}
}
3 changes: 3 additions & 0 deletions subgraph.template.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ dataSources:
- Subgraph
- SubgraphVersion
- SubgraphDeployment
- SubgraphDeploymentContract
- Contract
- ContractEvent
- GraphAccount
- NameSignal
abis:
Expand Down
18 changes: 18 additions & 0 deletions tests/metadata.test.ts
Original file line number Diff line number Diff line change
@@ -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));
});
})