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

🪚 OmniGraph™ Delusional code of the utterly deranged #112

Merged
merged 8 commits into from
Dec 13, 2023
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 @@ -2,6 +2,7 @@
module.exports = {
preset: 'ts-jest',
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