Skip to content

Commit

Permalink
Merge pull request #905 from XYOracleNetwork/feature/nft-info-diviner
Browse files Browse the repository at this point in the history
Hosted NFT info diviner
  • Loading branch information
JoelBCarter committed Aug 1, 2023
2 parents a47c66b + e6a253e commit 928e184
Show file tree
Hide file tree
Showing 22 changed files with 157 additions and 54 deletions.
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@xyo-network/node-model": "workspace:~",
"@xyo-network/query-payload-plugin": "workspace:~",
"bip39": "^3.1.0",
"dotenv": "^16.3.1",
"lodash": "^4.17.21",
"tail": "^2.2.6",
"terminal-kit": "^3.0.0",
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/command/commands/util/getModuleFromArgs.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { HttpBridge } from '@xyo-network/http-bridge'
import { asModuleInstance, ModuleInstance } from '@xyo-network/module-model'

import { printError } from '../../../lib'
import { getBridgeConfig } from '../../util'
import { getBridge } from '../../util'
import { ModuleArguments } from '../ModuleArguments'
import { getModuleFilterFromArgs } from './getModuleFilterFromArgs'

export const getModuleFromArgs = async (args: ModuleArguments): Promise<ModuleInstance> => {
const { verbose } = args
try {
const config = await getBridgeConfig(args)
const bridge = await HttpBridge.create({ config })
const bridge = await getBridge(args)
const filter = getModuleFilterFromArgs(args)
const resolved = await bridge.resolve(filter)
return asModuleInstance(resolved.pop(), `Failed to load module from filter [${filter}]`)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
import { HttpBridge } from '@xyo-network/http-bridge'
import { asModule, Module, ModuleFilter } from '@xyo-network/module-model'

import { printError } from '../../../lib'
import { BaseArguments } from '../../BaseArguments'
import { getBridgeConfig } from '../../util'
import { getBridge } from '../../util'

export const getModuleFromModuleFilter = async (args: BaseArguments, filter: ModuleFilter): Promise<Module> => {
const { verbose } = args
try {
const config = await getBridgeConfig(args)
const bridge = await HttpBridge.create({ config })
const bridge = await getBridge(args)
const resolved = await bridge.resolve(filter)
return asModule(resolved.pop(), `Failed to load module from filter [${filter}]`)
} catch (error) {
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/command/util/getBridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { HDWallet } from '@xyo-network/account'
import { HttpBridge } from '@xyo-network/http-bridge'

import { printError } from '../../lib'
import { BaseArguments } from '../BaseArguments'
import { getBridgeConfig } from './getBridgeConfig'

// TODO: Grab from config, rethink default path, use hardened path?
const accountDerivationPath = "m/44'/60'/0"
let wallet: HDWallet | undefined = undefined

export const getBridge = async (args: BaseArguments): Promise<HttpBridge> => {
const { verbose } = args
try {
if (!wallet) {
const mnemonic = process.env.MNEMONIC
wallet = mnemonic ? await HDWallet.fromMnemonic(mnemonic) : await HDWallet.random()
}
const config = await getBridgeConfig(args)
const bridge = await HttpBridge.create({ accountDerivationPath, config, wallet })
return bridge
} catch (error) {
if (verbose) printError(JSON.stringify(error))
throw new Error('Unable to obtain bridge')
}
}
6 changes: 2 additions & 4 deletions packages/cli/src/command/util/getNode.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
import { assertEx } from '@xylabs/assert'
import { HttpBridge } from '@xyo-network/http-bridge'
import { asNodeInstance, NodeInstance } from '@xyo-network/node-model'

import { printError } from '../../lib'
import { BaseArguments } from '../BaseArguments'
import { getBridgeConfig } from './getBridgeConfig'
import { getBridge } from './getBridge'

export const getNode = async (args: BaseArguments): Promise<NodeInstance> => {
const { verbose } = args
try {
const config = await getBridgeConfig(args)
const bridge = await HttpBridge.create({ config })
const bridge = await getBridge(args)
const node = assertEx((await bridge.resolve({ address: [await bridge.getRootAddress()] }))?.pop(), 'Failed to resolve rootNode')
return asNodeInstance(node, 'Not a NodeModule')
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/command/util/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './getBridge'
export * from './getBridgeConfig'
export * from './getNode'
export * from './output'
3 changes: 3 additions & 0 deletions packages/cli/src/xyo.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { config } from 'dotenv'
import yargs from 'yargs'
// eslint-disable-next-line import/no-internal-modules
import { hideBin } from 'yargs/helpers'

import { opts, OutputType } from './command'

config()

void yargs(hideBin(process.argv))
.commandDir('./command/commands', opts)
.wrap(yargs.terminalWidth())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export class HttpBridge<TParams extends HttpBridgeParams = HttpBridgeParams, TEv
}

protected override async startHandler() {
const start = Date.now()
// const start = Date.now()
await super.startHandler()
const rootAddress = await this.initRootAddress()
this.downResolver.addResolver(this.targetDownResolver())
Expand All @@ -177,7 +177,7 @@ export class HttpBridge<TParams extends HttpBridgeParams = HttpBridgeParams, TEv
//notify parents of child modules
//TODO: this needs to be thought through. If this the correct direction for data flow and how do we 'un-attach'?
parentNodes.forEach((node) => children.forEach((child) => node.emit('moduleAttached', { module: child })))
console.log(`Started HTTP Bridge in ${Date.now() - start}ms`)
// console.log(`Started HTTP Bridge in ${Date.now() - start}ms`)
return true
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,6 @@ export const WALLET_PATHS = {
Witnesses: {
Witness: `${ModulePath.Witness}/0'` as const,
Prometheus: `${ModulePath.Witness}/1'` as const,
CryptoWalletNftWitness: `${ModulePath.Witness}/2'` as const,
} as const,
} as const
1 change: 1 addition & 0 deletions packages/node/packages/core/packages/types/src/witness.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export const WITNESS_TYPES = {
CryptoWalletNftWitness: Symbol('CryptoWalletNftWitness'),
PrometheusWitness: Symbol('PrometheusWitness'),
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
"@xyo-network/archivist": "workspace:~",
"@xyo-network/archivist-model": "workspace:~",
"@xyo-network/core": "workspace:~",
"@xyo-network/crypto-wallet-nft-payload-plugin": "workspace:~",
"@xyo-network/crypto-wallet-nft-plugin": "workspace:~",
"@xyo-network/diviner-address-history": "workspace:~",
"@xyo-network/diviner-address-space": "workspace:~",
"@xyo-network/diviner-boundwitness": "workspace:~",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
import { HDWallet } from '@xyo-network/account'
import { CryptoWalletNftWitness } from '@xyo-network/crypto-wallet-nft-plugin'
import { CreatableModuleDictionary, ModuleFactory } from '@xyo-network/module-model'
import { TYPES, WALLET_PATHS } from '@xyo-network/node-core-types'
import { PrometheusNodeWitness } from '@xyo-network/prometheus-node-plugin'
import { Container } from 'inversify'

const getPrometheusNodeWitness = async (container: Container) => {
const getWallet = (container: Container) => {
const mnemonic = container.get<string>(TYPES.AccountMnemonic)
const account = await (await HDWallet.fromMnemonic(mnemonic)).derivePath?.(WALLET_PATHS.Witnesses.Prometheus)
return HDWallet.fromMnemonic(mnemonic)
}

const getCryptoWalletNftWitness = async (container: Container) => {
const wallet = await getWallet(container)
return new ModuleFactory(CryptoWalletNftWitness, {
accountDerivationPath: WALLET_PATHS.Witnesses.CryptoWalletNftWitness,
config: { name: TYPES.CryptoWalletNftWitness.description, schema: CryptoWalletNftWitness.configSchema },
wallet,
})
}

const getPrometheusNodeWitness = async (container: Container) => {
const wallet = await getWallet(container)
return new ModuleFactory(PrometheusNodeWitness, {
account,
accountDerivationPath: WALLET_PATHS.Witnesses.Prometheus,
config: { name: TYPES.PrometheusWitness.description, schema: PrometheusNodeWitness.configSchema },
wallet,
})
}

export const addWitnessModuleFactories = async (container: Container) => {
const dictionary = container.get<CreatableModuleDictionary>(TYPES.CreatableModuleDictionary)
dictionary[CryptoWalletNftWitness.configSchema] = await getCryptoWalletNftWitness(container)
dictionary[PrometheusNodeWitness.configSchema] = await getPrometheusNodeWitness(container)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { exists } from '@xylabs/exists'
import { Account } from '@xyo-network/account'
import { ArchivistConfigSchema, ArchivistInsertQuerySchema, isArchivistInstance, withArchivistInstance } from '@xyo-network/archivist-model'
import { PayloadHasher } from '@xyo-network/core'
import { NftWitnessConfigSchema } from '@xyo-network/crypto-wallet-nft-payload-plugin'
import {
AddressHistoryDivinerConfigSchema,
AddressSpaceBatchDivinerConfigSchema,
Expand Down Expand Up @@ -38,7 +39,10 @@ const diviners: ModuleConfigWithVisibility[] = [
[{ schema: SchemaListDivinerConfigSchema }, true],
[{ schema: SchemaStatsDivinerConfigSchema }, true],
]
const witnesses: ModuleConfigWithVisibility[] = [[{ schema: PrometheusNodeWitnessConfigSchema }, true]] // TODO: If we set this to false the visible modules stop resolving
const witnesses: ModuleConfigWithVisibility[] = [
[{ schema: NftWitnessConfigSchema }, true],
[{ schema: PrometheusNodeWitnessConfigSchema }, false],
]

const configs: ModuleConfigWithVisibility[] = [...archivists, ...diviners, ...witnesses]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export type NftAnalysis = {
}

export type NftScorePayload = Payload<{ schema: NftScoreSchema } & Partial<Omit<NftAnalysis, 'schema'>>>
export const isNftScorePayload = (x?: Payload | null): x is NftScorePayload => x?.schema === NftScoreSchema
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Payload } from '@xyo-network/payload-model'

import { NftWitnessQuerySchema } from './Schema'

export type NftWitnessQueryPayload = Payload<{
address?: string
chainId?: number
schema: NftWitnessQuerySchema
}>
export const isNftWitnessQueryPayload = (x?: Payload | null): x is NftWitnessQueryPayload => x?.schema === NftWitnessQuerySchema
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@ import { NftSchema } from '../Schema'

export type NftWitnessConfigSchema = `${NftSchema}.witness.config`
export const NftWitnessConfigSchema: NftWitnessConfigSchema = `${NftSchema}.witness.config`

export type NftWitnessQuerySchema = `${NftSchema}.witness.query`
export const NftWitnessQuerySchema: NftWitnessQuerySchema = `${NftSchema}.witness.query`
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './Config'
export * from './Query'
export * from './Schema'
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import type { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers'
import { assertEx } from '@xylabs/assert'
import { CryptoWalletNftWitnessConfig, NftInfoPayload, NftSchema, NftWitnessConfigSchema } from '@xyo-network/crypto-wallet-nft-payload-plugin'
import {
CryptoWalletNftWitnessConfig,
isNftWitnessQueryPayload,
NftInfoPayload,
NftSchema,
NftWitnessConfigSchema,
} from '@xyo-network/crypto-wallet-nft-payload-plugin'
import { AnyConfigSchema } from '@xyo-network/module'
import { Payload } from '@xyo-network/payload-model'
import { AbstractWitness, WitnessParams } from '@xyo-network/witness'
Expand All @@ -23,14 +29,20 @@ export class CryptoWalletNftWitness<TParams extends CryptoWalletNftWitnessParams
return assertEx(this.params.provider, 'Provider Required')
}

protected override async observeHandler(): Promise<Payload[]> {
protected override async observeHandler(payloads?: Payload[]): Promise<Payload[]> {
await this.started('throw')
const address = assertEx(this.config.address, 'params.address is required')
const chainId = assertEx(this.config.chainId, 'params.chainId is required')
const nfts = await getNftsOwnedByAddress(address, chainId, this.provider)
const payloads = nfts.map<NftInfoPayload>((nft) => {
return { ...nft, schema }
})
return payloads
const queries = payloads?.filter(isNftWitnessQueryPayload) ?? []
const observations = await Promise.all(
queries.map(async (query) => {
const address = assertEx(query?.address || this.config.address, 'params.address is required')
const chainId = assertEx(query?.chainId || this.config.chainId, 'params.chainId is required')
const nfts = await getNftsOwnedByAddress(address, chainId, this.account.private.hex)
const observation = nfts.map<NftInfoPayload>((nft) => {
return { ...nft, schema }
})
return observation
}),
)
return observations.flat()
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { ExternalProvider, JsonRpcFetchFunc } from '@ethersproject/providers'
import { Auth, SDK } from '@infura/sdk'
import { NftInfo } from '@xyo-network/crypto-wallet-nft-payload-plugin'

Expand All @@ -18,10 +17,14 @@ export const getNftsOwnedByAddress = async (
* The chain ID (1 = Ethereum Mainnet, 4 = Rinkeby, etc.) of the chain to search for NFTs on
*/
chainId: number,
// /**
// * The ethers provider to use to search for NFTs
// */
// provider: ExternalProvider | JsonRpcFetchFunc,
/**
* The ethers provider to use to search for NFTs
* The private key of the wallet to use to search for NFTs
*/
provider: ExternalProvider | JsonRpcFetchFunc,
privateKey: string,
/**
* The maximum number of NFTs to return. Configurable to prevent
* large wallets from exhausting Infura API credits.
Expand All @@ -32,13 +35,14 @@ export const getNftsOwnedByAddress = async (
const sdk = new SDK(
new Auth({
chainId,
// privateKey,
privateKey,
projectId: process.env.INFURA_PROJECT_ID,
// ipfs: {
// apiKeySecret: process.env.INFURA_IPFS_PROJECT_SECRET,
// projectId: process.env.INFURA_IPFS_PROJECT_ID,
// },
provider,
// provider,
// NOTE: rpcUrl is not required if chainId & projectId are provided
// rpcUrl: process.env.EVM_RPC_URL,
secretId: process.env.INFURA_PROJECT_SECRET,
}),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
import { describeIf } from '@xylabs/jest-helpers'
import { HttpProvider } from 'web3-providers-http'
import { HDWallet } from '@xyo-network/account'

import { getExternalProviderFromHttpProvider } from '../getExternalProviderFromHttpProvider'
import { getNftsOwnedByAddress } from '../getNftsOwnedByAddress'

describeIf(process.env.INFURA_PROJECT_ID)('getNftsOwnedByAddress', () => {
const address = '0xacdaEEb57ff6886fC8e203B9Dd4C2b241DF89b7a'
const chainId = 1
const network = 'homestead'
const apiKey = process.env.INFURA_PROJECT_ID
const provider = getExternalProviderFromHttpProvider(new HttpProvider(`https://${network}.infura.io/v3/${apiKey}`))
test('observe', async () => {
const nfts = await getNftsOwnedByAddress(address, chainId, provider)
expect(nfts.length).toBeGreaterThan(1)
test('gets NFTs owned by the address', async () => {
const { privateKey } = await HDWallet.random()
const nfts = await getNftsOwnedByAddress(address, chainId, privateKey)
expect(nfts.length).toBeGreaterThan(0)
for (let i = 0; i < nfts.length; i++) {
const nft = nfts[i]
expect(nft.contract).toBeString()
Expand Down
Loading

0 comments on commit 928e184

Please sign in to comment.