Skip to content

Commit

Permalink
🪚 OmniGraph™ Delusional code of the utterly deranged (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored Dec 13, 2023
1 parent 0c28371 commit dcf1bbb
Show file tree
Hide file tree
Showing 14 changed files with 354 additions and 36 deletions.
6 changes: 2 additions & 4 deletions packages/utils-evm-hardhat/src/omnigraph/coordinates.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import type { OmniPoint } from '@layerzerolabs/utils'
import type { HardhatRuntimeEnvironment } from 'hardhat/types'
import pMemoize from 'p-memoize'
import { OmniContract } from '@layerzerolabs/utils-evm'
import { Contract } from '@ethersproject/contracts'
import assert from 'assert'
import { OmniContractFactoryHardhat, OmniDeployment } from './types'
import { createNetworkEnvironmentFactory, getDefaultRuntimeEnvironment } from '@/runtime'
import { createNetworkEnvironmentFactory } from '@/runtime'
import { assertHardhatDeploy } from '@/internal/assertions'

export const omniDeploymentToPoint = ({ eid, deployment }: OmniDeployment): OmniPoint => ({
Expand All @@ -19,8 +18,7 @@ export const omniDeploymentToContract = ({ eid, deployment }: OmniDeployment): O
})

export const createContractFactory = (
hre: HardhatRuntimeEnvironment = getDefaultRuntimeEnvironment(),
environmentFactory = createNetworkEnvironmentFactory(hre)
environmentFactory = createNetworkEnvironmentFactory()
): OmniContractFactoryHardhat => {
return pMemoize(async ({ eid, address, contractName }) => {
const env = await environmentFactory(eid)
Expand Down
5 changes: 4 additions & 1 deletion packages/utils-evm-hardhat/src/omnigraph/schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { z } from 'zod'
import { EndpointIdSchema, OmniPointSchema } from '@layerzerolabs/utils'
import type { OmniEdgeHardhat, OmniGraphHardhat, OmniNodeHardhat, OmniPointHardhat } from './types'
import type { OmniEdgeHardhat, OmniGraphHardhat, OmniNodeHardhat, OmniPointHardhat, WithContractName } from './types'

export const OmniPointHardhatSchema: z.ZodSchema<OmniPointHardhat, z.ZodTypeDef, unknown> = z.object({
eid: EndpointIdSchema,
Expand Down Expand Up @@ -57,3 +57,6 @@ export const createOmniGraphHardhatSchema = <TNodeConfig = unknown, TEdgeConfig
contracts: z.array(nodeSchema),
connections: z.array(edgeSchema),
})

export const hasContractName = <T extends object>(value: T): value is WithContractName<T> =>
'contractName' in value && typeof value.contractName === 'string'
10 changes: 6 additions & 4 deletions packages/utils-evm-hardhat/src/omnigraph/transformations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type {
OmniNodeHardhat,
OmniPointHardhatTransformer,
} from './types'
import { parallel } from '@layerzerolabs/utils'

/**
* Create a function capable of transforming `OmniPointHardhat` to a regular `OmniPoint`
Expand Down Expand Up @@ -56,9 +57,10 @@ export const createOmniEdgeHardhatTransformer =
export const createOmniGraphHardhatTransformer =
<TNodeConfig, TEdgeConfig>(
nodeTransformer = createOmniNodeHardhatTransformer(),
edgeTransformer = createOmniEdgeHardhatTransformer()
edgeTransformer = createOmniEdgeHardhatTransformer(),
applicative = parallel
): OmniGraphHardhatTransformer<TNodeConfig, TEdgeConfig> =>
async (graph) => ({
contracts: await Promise.all(graph.contracts.map(nodeTransformer)),
connections: await Promise.all(graph.connections.map(edgeTransformer)),
async ({ contracts, connections }) => ({
contracts: await applicative(contracts.map((contract) => () => nodeTransformer(contract))),
connections: await applicative(connections.map((connection) => () => edgeTransformer(connection))),
})
17 changes: 10 additions & 7 deletions packages/utils-evm-hardhat/src/omnigraph/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { OmniGraph, OmniPoint, WithEid, WithOptionals } from '@layerzerolabs/utils'
import type { OmniContract } from '@layerzerolabs/utils-evm'
import type { Factory, OmniGraph, OmniPoint, WithEid, WithOptionals } from '@layerzerolabs/utils'
import type { OmniContractFactory } from '@layerzerolabs/utils-evm'
import type { Deployment } from 'hardhat-deploy/dist/types'

/**
Expand All @@ -21,6 +21,8 @@ export type OmniPointHardhat = WithEid<{
address?: string | null
}>

export type WithContractName<T> = T & { contractName: string }

/**
* Hardhat-specific variation of `OmniNode` that uses `OmniPointHardhat`
* instead of `OmniPoint` to specify the contract coordinates
Expand Down Expand Up @@ -49,10 +51,11 @@ export interface OmniGraphHardhat<TNodeConfig = unknown, TEdgeConfig = unknown>
connections: OmniEdgeHardhat<TEdgeConfig>[]
}

export type OmniContractFactoryHardhat = (point: OmniPointHardhat) => OmniContract | Promise<OmniContract>
export type OmniContractFactoryHardhat = OmniContractFactory<OmniPointHardhat>

export type OmniPointHardhatTransformer = (point: OmniPointHardhat | OmniPoint) => Promise<OmniPoint>
export type OmniPointHardhatTransformer = Factory<[OmniPointHardhat | OmniPoint], OmniPoint>

export type OmniGraphHardhatTransformer<TNodeConfig = unknown, TEdgeConfig = unknown> = (
graph: OmniGraphHardhat<TNodeConfig, TEdgeConfig>
) => Promise<OmniGraph<TNodeConfig, TEdgeConfig>>
export type OmniGraphHardhatTransformer<TNodeConfig = unknown, TEdgeConfig = unknown> = Factory<
[OmniGraphHardhat<TNodeConfig, TEdgeConfig>],
OmniGraph<TNodeConfig, TEdgeConfig>
>
12 changes: 6 additions & 6 deletions packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ describe('omnigraph/coordinates', () => {
describe('createContractFactory', () => {
describe('when called with OmniPointContractName', () => {
it('should reject when eid does not exist', async () => {
const contractFactory = createContractFactory(hre)
const contractFactory = createContractFactory()

await expect(() =>
contractFactory({ eid: EndpointId.CANTO_TESTNET, contractName: 'MyContract' })
).rejects.toBeTruthy()
})

it('should reject when contract has not been deployed', async () => {
const contractFactory = createContractFactory(hre)
const contractFactory = createContractFactory()

await expect(() =>
contractFactory({ eid: EndpointId.ETHEREUM_MAINNET, contractName: 'MyContract' })
Expand All @@ -60,7 +60,7 @@ describe('omnigraph/coordinates', () => {

it('should resolve when contract has been deployed', async () => {
const environmentFactory = createNetworkEnvironmentFactory(hre)
const contractFactory = createContractFactory(hre, environmentFactory)
const contractFactory = createContractFactory(environmentFactory)

const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET)
jest.spyOn(env.deployments, 'getOrNull').mockResolvedValue({
Expand All @@ -86,7 +86,7 @@ describe('omnigraph/coordinates', () => {
it('should reject when eid does not exist', async () => {
await fc.assert(
fc.asyncProperty(evmAddressArbitrary, async (address) => {
const contractFactory = createContractFactory(hre)
const contractFactory = createContractFactory()

await expect(() =>
contractFactory({ eid: EndpointId.CANTO_TESTNET, address })
Expand All @@ -98,7 +98,7 @@ describe('omnigraph/coordinates', () => {
it('should reject when contract has not been deployed', async () => {
await fc.assert(
fc.asyncProperty(evmAddressArbitrary, async (address) => {
const contractFactory = createContractFactory(hre)
const contractFactory = createContractFactory()

await expect(() =>
contractFactory({ eid: EndpointId.ETHEREUM_MAINNET, address })
Expand All @@ -111,7 +111,7 @@ describe('omnigraph/coordinates', () => {
await fc.assert(
fc.asyncProperty(evmAddressArbitrary, async (address) => {
const environmentFactory = createNetworkEnvironmentFactory(hre)
const contractFactory = createContractFactory(hre, environmentFactory)
const contractFactory = createContractFactory(environmentFactory)

const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET)
jest.spyOn(env.deployments, 'getDeploymentsFromAddress').mockResolvedValue([
Expand Down
52 changes: 40 additions & 12 deletions packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
} from '@/omnigraph/transformations'
import { Contract } from '@ethersproject/contracts'
import { endpointArbitrary, evmAddressArbitrary, nullableArbitrary, pointArbitrary } from '@layerzerolabs/test-utils'
import { isOmniPoint } from '@layerzerolabs/utils'
import { isOmniPoint, parallel, sequence } from '@layerzerolabs/utils'

describe('omnigraph/transformations', () => {
const pointHardhatArbitrary = fc.record({
Expand All @@ -17,6 +17,17 @@ describe('omnigraph/transformations', () => {
address: nullableArbitrary(evmAddressArbitrary),
})

const nodeHardhatArbitrary = fc.record({
contract: pointHardhatArbitrary,
config: fc.anything(),
})

const edgeHardhatArbitrary = fc.record({
from: pointHardhatArbitrary,
to: pointHardhatArbitrary,
config: fc.anything(),
})

describe('createOmniPointHardhatTransformer', () => {
it('should pass the original value if contract is already an OmniPoint', async () => {
await fc.assert(
Expand Down Expand Up @@ -128,17 +139,6 @@ describe('omnigraph/transformations', () => {
})

it('should call the nodeTransformer and edgeTransformer for every node and edge and return the result', async () => {
const nodeHardhatArbitrary = fc.record({
contract: pointHardhatArbitrary,
config: fc.anything(),
})

const edgeHardhatArbitrary = fc.record({
from: pointHardhatArbitrary,
to: pointHardhatArbitrary,
config: fc.anything(),
})

await fc.assert(
fc.asyncProperty(
fc.array(nodeHardhatArbitrary),
Expand All @@ -156,5 +156,33 @@ describe('omnigraph/transformations', () => {
)
)
})

it('should support sequential applicative', async () => {
await fc.assert(
fc.asyncProperty(
fc.array(nodeHardhatArbitrary),
fc.array(edgeHardhatArbitrary),
async (contracts, connections) => {
const nodeTransformer = jest.fn().mockImplementation(async (node) => ({ node }))
const edgeTransformer = jest.fn().mockImplementation(async (edge) => ({ edge }))
const transformerSequential = createOmniGraphHardhatTransformer(
nodeTransformer,
edgeTransformer,
sequence
)
const transformerParallel = createOmniGraphHardhatTransformer(
nodeTransformer,
edgeTransformer,
parallel
)

const graphSequential = await transformerSequential({ contracts, connections })
const graphParallel = await transformerParallel({ contracts, connections })

expect(graphSequential).toEqual(graphParallel)
}
)
)
})
})
})
1 change: 1 addition & 0 deletions packages/utils/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ module.exports = {
preset: 'ts-jest',
cache: false,
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
},
Expand Down
3 changes: 3 additions & 0 deletions packages/utils/jest.setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// add all jest-extended matchers
// eslint-disable-next-line @typescript-eslint/no-var-requires
expect.extend(require('jest-extended'));
2 changes: 2 additions & 0 deletions packages/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@
"@layerzerolabs/lz-definitions": "~1.5.72",
"@layerzerolabs/test-utils": "~0.0.1",
"@types/jest": "^29.5.10",
"fast-check": "^3.14.0",
"jest": "^29.7.0",
"jest-extended": "^4.0.2",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"tslib": "~2.6.2",
Expand Down
1 change: 1 addition & 0 deletions packages/utils/src/common/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './promise'
74 changes: 74 additions & 0 deletions packages/utils/src/common/promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Factory } from '@/types'
import assert from 'assert'

/**
* Helper type for argumentless factories a.k.a. tasks
*/
type Task<T> = Factory<[], T>

/**
* Executes tasks in sequence, waiting for each one to finish before starting the next one
*
* Will resolve with the output of all tasks or reject with the first rejection.
*
* @param {Task<T>[]} tasks
* @returns {Promise<T[]>}
*/
export const sequence = async <T>(tasks: Task<T>[]): Promise<T[]> => {
const collector: T[] = []

for (const task of tasks) {
collector.push(await task())
}

return collector
}

/**
* Executes tasks in parallel
*
* Will resolve with the output of all tasks or reject with the any rejection.
*
* @param {Task<T>[]} tasks
* @returns {Promise<T[]>}
*/
export const parallel = async <T>(tasks: Task<T>[]): Promise<T[]> => await Promise.all(tasks.map((task) => task()))

/**
* Executes tasks in a sequence until one resolves.
*
* Will resolve with the output of the first task that resolves
* or reject with the last rejection.
*
* Will reject immediatelly if no tasks have been passed
*
* @param {Task<T>[]} tasks
* @returns {Promise<T>}
*/
export const first = async <T>(tasks: Task<T>[]): Promise<T> => {
assert(tasks.length !== 0, `Must have at least one task for first()`)

let lastError: unknown

for (const task of tasks) {
try {
return await task()
} catch (error) {
lastError = error
}
}

throw lastError
}

/**
* Helper utility for currying first() - creating a function
* that behaves like first() but accepts arguments that will be passed to the factory functions
*
* @param {Factory<TInput, TOutput>[]} factories
* @returns {Factory<TInput, TOutput>}
*/
export const firstFactory =
<TInput extends unknown[], TOutput>(...factories: Factory<TInput, TOutput>[]): Factory<TInput, TOutput> =>
async (...input) =>
await first(factories.map((factory) => () => factory(...input)))
1 change: 1 addition & 0 deletions packages/utils/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './common'
export * from './omnigraph'
export * from './transactions'
export * from './types'
Loading

0 comments on commit dcf1bbb

Please sign in to comment.