Skip to content

Commit

Permalink
More Generic Contract Reads
Browse files Browse the repository at this point in the history
  • Loading branch information
arietrouw committed Nov 2, 2023
1 parent f53d874 commit b8cf7dc
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 123 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,15 +30,19 @@ export type CryptoContractFunctionCall<
export const CryptoContractFunctionCallResultSchema = 'network.xyo.crypto.contract.function.call.result'
export type CryptoContractFunctionCallResultSchema = typeof CryptoContractFunctionCallResultSchema

export interface ContractFunctionResult<TResult = unknown> {
type?: 'BigNumber'
value: TResult
}

export type CryptoContractFunctionCallResult<TResult = unknown> = Payload<
{
address: string
call: string
chainId: number
result: {
type?: 'BigNumber'
value: TResult
}
functionName: string
params: unknown[]
result: ContractFunctionResult<TResult>
},
CryptoContractFunctionCallResultSchema
>
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { assertEx } from '@xylabs/assert'
import { Promisable } from '@xylabs/promise'
import { AbstractDiviner } from '@xyo-network/abstract-diviner'
import {
ContractFunctionResult,
CryptoContractFunctionCall,
CryptoContractFunctionCallResult,
CryptoContractFunctionCallResultSchema,
Expand All @@ -16,31 +17,24 @@ export type FindCallResult<TResult = string, TPayload = Payload> = [TResult, TPa
export const CryptoContractDivinerConfigSchema = 'network.xyo.crypto.contract.diviner.config'
export type CryptoContractDivinerConfigSchema = typeof CryptoContractDivinerConfigSchema

export type CryptoContractDivinerConfig = DivinerConfig
export type CryptoContractDivinerConfig = DivinerConfig<{
schema: CryptoContractDivinerConfigSchema
}>
export type CryptoContractDivinerParams = DivinerParams<CryptoContractDivinerConfig>

export const ContractInfoSchema = 'network.xyo.crypto.contract.info'
export type ContractInfoSchema = typeof ContractInfoSchema

export type OmittedContractInfo<TFields extends object | Payload | null = null, TSchema extends string | null = null> = Omit<
ContractInfo<TFields, TSchema extends null ? (TFields extends Payload ? TFields['schema'] : never) : TSchema>,
'address' | 'chainId'
export type ContractInfo = Payload<
{
address: string
chainId: string
results?: Record<string, ContractFunctionResult>
},
ContractInfoSchema
>

export type ContractInfo<TFields extends object | null = null, TSchema extends string = ContractInfoSchema> = Payload<
TFields extends null
? object
: TFields & {
address: string
chainId: string
},
TSchema
>

export abstract class CryptoContractDiviner<
TContractInfo extends Payload<Omit<ContractInfo, 'schema'>> = ContractInfo,
TParams extends CryptoContractDivinerParams = CryptoContractDivinerParams,
> extends AbstractDiviner<TParams> {
export class CryptoContractDiviner<TParams extends CryptoContractDivinerParams = CryptoContractDivinerParams> extends AbstractDiviner<TParams> {
static override configSchemas = [CryptoContractDivinerConfigSchema]

protected static async findCallResult<TResult = string>(
Expand All @@ -64,22 +58,23 @@ export abstract class CryptoContractDiviner<
return await PayloadHasher.hashAsync(callPayload)
}

protected static matchingExistingField<R = string, T extends Payload = Payload>(objs: T[], field: keyof T) {
protected static matchingExistingField<R = string, T extends Payload = Payload>(objs: T[], field: keyof T): R | undefined {
const expectedValue = objs.at(0)?.[field] as R
const didNotMatch = objs.reduce((prev, obj) => {
return prev || obj[field] !== expectedValue
}, false)
return didNotMatch ? undefined : expectedValue
}

protected contractInfoRequiredFields(callResults: CryptoContractFunctionCallResult[]): Promisable<Omit<ContractInfo, 'schema'>> {
protected contractInfoRequiredFields(callResults: CryptoContractFunctionCallResult[]): ContractInfo {
return {
address: assertEx(CryptoContractDiviner.matchingExistingField(callResults, 'address'), 'Mismatched address'),
chainId: assertEx(CryptoContractDiviner.matchingExistingField(callResults, 'chainId'), 'Mismatched chainId'),
schema: ContractInfoSchema,
}
}

protected override async divineHandler(inPayloads: CryptoContractFunctionCallResult[] = []): Promise<TContractInfo[]> {
protected override async divineHandler(inPayloads: CryptoContractFunctionCallResult[] = []): Promise<ContractInfo[]> {
const callResults = inPayloads.filter(isPayloadOfSchemaType<CryptoContractFunctionCallResult>(CryptoContractFunctionCallResultSchema))
const addresses = Object.keys(
callResults.reduce<Record<string, boolean>>((prev, result) => {
Expand All @@ -92,18 +87,21 @@ export abstract class CryptoContractDiviner<
const result = await Promise.all(
addresses.map(async (address) => {
const foundCallResults = callResults.filter((callResult) => callResult.address === address)
return {
...(await this.reduceResults(address, foundCallResults)),
const info: ContractInfo = {
...{ results: await this.reduceResults(foundCallResults) },
...this.contractInfoRequiredFields(foundCallResults),
} as TContractInfo
}
return info
}),
)

return result
}

protected abstract reduceResults(
address: string,
callResults: CryptoContractFunctionCallResult[],
): Promisable<Omit<TContractInfo, 'address' | 'chainId'>>
protected reduceResults(callResults: CryptoContractFunctionCallResult[]): Promisable<ContractInfo['results']> {
return callResults.reduce<Record<string, ContractFunctionResult>>((prev, callResult) => {
prev[callResult.functionName] = callResult.result
return prev
}, {})
}
}

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,34 @@
{
"config": {
"accountPath": "m/44'/60'/1'/1'/2'",
"call": {
"functionName": "tokenURI"
},
"labels": {
"network.xyo.crypto.contract.interface": "Erc721"
},
"language": "javascript",
"name": "Erc721TokenURIWitness",
"schema": "network.xyo.crypto.contract.function.read.config"
}
},
{
"config": {
"accountPath": "m/44'/60'/1'/1'/3'",
"call": {
"functionName": "ownerOf"
},
"labels": {
"network.xyo.crypto.contract.interface": "Erc721"
},
"language": "javascript",
"name": "Erc721OwnerOfWitness",
"schema": "network.xyo.crypto.contract.function.read.config"
}
},
{
"config": {
"accountPath": "m/44'/60'/1'/1'/4'",
"call": {
"functionName": "totalSupply",
"params": []
Expand All @@ -56,7 +84,21 @@
},
{
"config": {
"accountPath": "m/44'/60'/1'/1'/3'",
"accountPath": "m/44'/60'/1'/1'/5'",
"call": {
"functionName": "tokenByIndex"
},
"labels": {
"network.xyo.crypto.contract.interface": "Erc721Enumerable"
},
"language": "javascript",
"name": "Erc721TokenByIndexWitness",
"schema": "network.xyo.crypto.contract.function.read.config"
}
},
{
"config": {
"accountPath": "m/44'/60'/1'/1'/8'",
"call": {
"functionName": "uri",
"params": []
Expand All @@ -74,15 +116,15 @@
"accountPath": "m/44'/60'/1'/2'/0'",
"language": "javascript",
"name": "Erc721ContractInfoDiviner",
"schema": "network.xyo.crypto.contract.erc721.info.diviner.config"
"schema": "network.xyo.crypto.contract.diviner.config"
}
},
{
"config": {
"accountPath": "m/44'/60'/1'/2'/1'",
"language": "javascript",
"name": "Erc1155ContractInfoDiviner",
"schema": "network.xyo.crypto.contract.erc1155.info.diviner.config"
"schema": "network.xyo.crypto.contract.diviner.config"
}
},
{
Expand Down Expand Up @@ -125,6 +167,46 @@
}
]
}
},
{
"config": {
"accountPath": "m/44'/60'/1'/3'/1'",
"language": "javascript",
"name": "NftTokenInfoSentinel",
"schema": "network.xyo.sentinel.config",
"synchronous": "true",
"tasks": [
{
"input": true,
"module": "Erc721NameWitness"
},
{
"input": true,
"module": "Erc721SymbolWitness"
},
{
"input": true,
"module": "Erc721TotalSupplyWitness"
},
{
"input": true,
"module": "Erc1155UriWitness"
},
{
"input": [
"Erc721TokenURIWitness",
"Erc721OwnerOfWitness"
],
"module": "Erc721TokenContractInfoDiviner"
},
{
"input": [
"Erc1155UriWitness"
],
"module": "Erc1155TokenContractInfoDiviner"
}
]
}
}
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ export class CryptoContractFunctionReadWitness<
const fullCallPayload = { ...{ params: [] }, ...this.config.call, ...callPayload }
const { address, functionName, params } = fullCallPayload
const validatedAddress = assertEx(address, 'Missing address')
const validatedFunctionName = assertEx(functionName, 'Missing functionName')
const contract = this.params.factory(validatedAddress)
const func = assertEx(contract.callStatic[assertEx(functionName, 'missing functionName')], `functionName [${functionName}] not found`)
const func = assertEx(contract.callStatic[validatedFunctionName], `functionName [${validatedFunctionName}] not found`)
const rawResult = await func(...(params ?? []))
const result: CryptoContractFunctionCallResult['result'] = BigNumber.isBigNumber(rawResult)
? { type: 'BigNumber', value: rawResult.toHexString() }
Expand All @@ -47,6 +48,8 @@ export class CryptoContractFunctionReadWitness<
address: validatedAddress,
call: await PayloadHasher.hashAsync(fullCallPayload),
chainId: (await contract.provider.getNetwork()).chainId,
functionName: validatedFunctionName,
params,
result,
schema: CryptoContractFunctionCallResultSchema,
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,8 @@ import { isPayloadOfSchemaType } from '@xyo-network/payload-model'
import { asSentinelInstance } from '@xyo-network/sentinel-model'
import { asWitnessInstance } from '@xyo-network/witness-model'

import { CryptoContractErc721Diviner, Erc721ContractInfo, Erc721ContractInfoSchema } from '../Erc721Diviner'
import { ContractInfo, ContractInfoSchema, CryptoContractDiviner } from '../CryptoContractDiviner'
import erc721SentinelManifest from '../Erc721Sentinel.json'
import { CryptoContractErc1155Diviner } from '../Erc1155Diviner'
import { CryptoContractFunctionReadWitness } from '../Witness'

describeIf(process.env.INFURA_PROJECT_ID)('Erc721Sentinel', () => {
Expand All @@ -30,8 +29,7 @@ describeIf(process.env.INFURA_PROJECT_ID)('Erc721Sentinel', () => {
const mnemonic = 'later puppy sound rebuild rebuild noise ozone amazing hope broccoli crystal grief'
const wallet = await HDWallet.fromMnemonic(mnemonic)
const locator = new ModuleFactoryLocator()
locator.register(CryptoContractErc1155Diviner)
locator.register(CryptoContractErc721Diviner)
locator.register(CryptoContractDiviner)

locator.register(
new ModuleFactory(CryptoContractFunctionReadWitness, {
Expand Down Expand Up @@ -72,9 +70,9 @@ describeIf(process.env.INFURA_PROJECT_ID)('Erc721Sentinel', () => {
expect(diviner).toBeDefined()

const callPayload: CryptoContractFunctionCall = { address, schema: CryptoContractFunctionCallSchema }
const report = (await sentinel?.report([callPayload])) as Erc721ContractInfo[]
console.log(`Report: ${JSON.stringify(report, null, 2)}`)
expect(report.find(isPayloadOfSchemaType(Erc721ContractInfoSchema))?.symbol).toBe('HAAS')
const report = await sentinel?.report([callPayload])
const info = report?.find(isPayloadOfSchemaType(ContractInfoSchema)) as ContractInfo | undefined
expect(info?.results?.['symbol']?.value).toBe('HAAS')
})
})
})

0 comments on commit b8cf7dc

Please sign in to comment.