Skip to content

Commit

Permalink
🪚 OmniGraph™ formatting & checking utilities (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored Nov 30, 2023
1 parent 1549879 commit ef9f5f7
Show file tree
Hide file tree
Showing 15 changed files with 717 additions and 75 deletions.
4 changes: 3 additions & 1 deletion packages/test-utils/src/arbitraries.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import fc from 'fast-check'
import { EndpointId } from '@layerzerolabs/lz-definitions'
import { EndpointId, Stage } from '@layerzerolabs/lz-definitions'
import { ENDPOINT_IDS } from './constants'

export const addressArbitrary = fc.string()

export const evmAddressArbitrary = fc.hexaString({ minLength: 40, maxLength: 40 }).map((address) => `0x${address}`)

export const endpointArbitrary: fc.Arbitrary<EndpointId> = fc.constantFrom(...ENDPOINT_IDS)

export const stageArbitrary: fc.Arbitrary<Stage> = fc.constantFrom(Stage.MAINNET, Stage.TESTNET, Stage.SANDBOX)
21 changes: 7 additions & 14 deletions packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,8 @@ import { describe } from 'mocha'
import hre from 'hardhat'
import { configureOApp, OmniPoint } from '@layerzerolabs/ua-utils'
import { OApp } from '@layerzerolabs/ua-utils-evm'
import { connectOmniContract } from '@layerzerolabs/utils-evm'
import {
createDeploymentFactory,
omniDeploymentToContract,
omniDeploymentToPoint,
OmniGraphHardhat,
OmniGraphBuilderHardhat,
} from '@layerzerolabs/ua-utils-evm-hardhat'
import { omniContractToPoint, connectOmniContract } from '@layerzerolabs/utils-evm'
import { createContractFactory, OmniGraphHardhat, OmniGraphBuilderHardhat } from '@layerzerolabs/ua-utils-evm-hardhat'
import { EndpointId } from '@layerzerolabs/lz-definitions'

describe('oapp/config', () => {
Expand Down Expand Up @@ -47,14 +41,13 @@ describe('oapp/config', () => {

// This is the required tooling we need to set up
const providerFactory = createProviderFactory(hre)
const deploymentFactory = createDeploymentFactory(hre)
const builder = await OmniGraphBuilderHardhat.fromConfig(config, deploymentFactory)
const contractFactory = createContractFactory(hre)
const builder = await OmniGraphBuilderHardhat.fromConfig(config, contractFactory)

// This so far the only non-oneliner, a function that returns an SDK for a contract on a network
const sdkFactory = async (point: OmniPoint) => {
const provider = await providerFactory(point.eid)
const deployment = await deploymentFactory(point)
const contract = omniDeploymentToContract(deployment)
const contract = await contractFactory(point)

return new OApp(connectOmniContract(contract, provider))
}
Expand All @@ -63,10 +56,10 @@ describe('oapp/config', () => {
const transactions = await configureOApp(builder.graph, sdkFactory)

// And finally the test assertions
const ethPoint = omniDeploymentToPoint(await deploymentFactory(ethContract))
const ethPoint = omniContractToPoint(await contractFactory(ethContract))
const ethSdk = await sdkFactory(ethPoint)

const avaxPoint = omniDeploymentToPoint(await deploymentFactory(avaxContract))
const avaxPoint = omniContractToPoint(await contractFactory(avaxContract))
const avaxSdk = await sdkFactory(avaxPoint)

expect(transactions).to.eql([
Expand Down
12 changes: 6 additions & 6 deletions packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { type OmniEdge, OmniGraphBuilder, type OmniNode } from '@layerzerolabs/ua-utils'
import type { OmniDeploymentFactory, OmniGraphHardhat } from './types'
import { omniDeploymentToPoint } from './coordinates'
import type { OmniContractFactory, OmniGraphHardhat } from './types'
import { omniContractToPoint } from '@layerzerolabs/utils-evm'

export class OmniGraphBuilderHardhat<TNodeConfig, TEdgeConfig> extends OmniGraphBuilder<TNodeConfig, TEdgeConfig> {
static async fromConfig<TNodeConfig, TEdgeConfig>(
graph: OmniGraphHardhat<TNodeConfig, TEdgeConfig>,
deploymentFactory: OmniDeploymentFactory
contractFactory: OmniContractFactory
): Promise<OmniGraphBuilderHardhat<TNodeConfig, TEdgeConfig>> {
const builder = new OmniGraphBuilderHardhat<TNodeConfig, TEdgeConfig>()

const nodes: OmniNode<TNodeConfig>[] = await Promise.all(
graph.contracts.map(async ({ contract, config }) => ({
point: omniDeploymentToPoint(await deploymentFactory(contract)),
point: omniContractToPoint(await contractFactory(contract)),
config,
}))
)

const edges: OmniEdge<TEdgeConfig>[] = await Promise.all(
graph.connections.map(async ({ from, to, config }) => ({
vector: {
from: omniDeploymentToPoint(await deploymentFactory(from)),
to: omniDeploymentToPoint(await deploymentFactory(to)),
from: omniContractToPoint(await contractFactory(from)),
to: omniContractToPoint(await contractFactory(to)),
},
config,
}))
Expand Down
50 changes: 27 additions & 23 deletions packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import pMemoize from 'p-memoize'
import { OmniContract } from '@layerzerolabs/utils-evm'
import { Contract } from '@ethersproject/contracts'
import assert from 'assert'
import { OmniDeploymentFactory, OmniPointContractName, OmniPointHardhat } from './types'
import { OmniContractFactory } from './types'
import { assertHardhatDeploy, createNetworkEnvironmentFactory } from '@layerzerolabs/utils-evm-hardhat'

export interface OmniDeployment {
Expand All @@ -21,36 +21,40 @@ export const omniDeploymentToContract = ({ eid, deployment }): OmniContract => (
contract: new Contract(deployment.address, deployment.abi),
})

export const isOmniPointContractName = (point: OmniPointHardhat): point is OmniPointContractName =>
'contractName' in point && typeof point.contractName === 'string'

export const createDeploymentFactory = (hre: HardhatRuntimeEnvironment): OmniDeploymentFactory => {
assertHardhatDeploy(hre)

export const createContractFactory = (hre: HardhatRuntimeEnvironment): OmniContractFactory => {
const environmentFactory = createNetworkEnvironmentFactory(hre)

return pMemoize(async (point) => {
const env = await environmentFactory(point.eid)
return pMemoize(async ({ eid, address, contractName }) => {
const env = await environmentFactory(eid)
assertHardhatDeploy(env)

let deployment: Deployment | null
assert(
contractName != null || address != null,
'At least one of contractName, address must be specified for OmniPointHardhat'
)

// If we have both the contract name & address, we go off artifacts
if (contractName != null && address != null) {
const artifact = await env.deployments.getArtifact(contractName)
const contract = new Contract(address, artifact.abi)

if (isOmniPointContractName(point)) {
deployment = await env.deployments.getOrNull(point.contractName)
return { eid, contract }
}

assert(
deployment,
`Could not find a deployment for contract '${point.contractName}' on endpoint ${point.eid}`
)
} else {
;[deployment] = await env.deployments.getDeploymentsFromAddress(point.address)
// If we have the contract name but no address, we need to get it from the deployments by name
if (contractName != null && address == null) {
const deployment = await env.deployments.getOrNull(contractName)
assert(deployment != null, `Could not find a deployment for contract '${contractName}`)

assert(
deployment,
`Could not find a deployment for on address '${point.address}' and endpoint ${point.eid}`
)
return omniDeploymentToContract({ eid, deployment })
}

return { eid: point.eid, deployment }
// And if we only have the address, we need to go get it from deployments by address
if (address != null) {
const [deployment] = await env.deployments.getDeploymentsFromAddress(address)
assert(deployment != null, `Could not find a deployment for address '${address}`)

return omniDeploymentToContract({ eid, deployment })
}
})
}
10 changes: 5 additions & 5 deletions packages/ua-utils-evm-hardhat/src/omnigraph/types.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import type { EndpointId } from '@layerzerolabs/lz-definitions'
import { OmniDeployment } from './coordinates'
import { OmniPoint } from '@layerzerolabs/ua-utils'
import { OmniContract } from '@layerzerolabs/utils-evm'

export type OmniPointHardhat = OmniPoint | OmniPointContractName

export interface OmniPointContractName {
export interface OmniPointHardhat {
eid: EndpointId
contractName: string
contractName?: string
address?: string
}

export interface OmniNodeHardhat<TNodeConfig> {
Expand All @@ -25,4 +25,4 @@ export interface OmniGraphHardhat<TNodeConfig = unknown, TEdgeConfig = unknown>
connections: OmniEdgeHardhat<TEdgeConfig>[]
}

export type OmniDeploymentFactory = (point: OmniPointHardhat) => OmniDeployment | Promise<OmniDeployment>
export type OmniContractFactory = (point: OmniPointHardhat) => OmniContract | Promise<OmniContract>
34 changes: 17 additions & 17 deletions packages/ua-utils-evm-hardhat/test/omnigraph/coordinates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ import fc from 'fast-check'
import hre from 'hardhat'
import { Deployment, DeploymentSubmission } from 'hardhat-deploy/dist/types'
import { endpointArbitrary, evmAddressArbitrary } from '@layerzerolabs/test-utils'
import { OmniDeployment, createDeploymentFactory, omniDeploymentToContract, omniDeploymentToPoint } from '@/omnigraph'
import { OmniDeployment, createContractFactory, omniDeploymentToContract, omniDeploymentToPoint } from '@/omnigraph'
import { EndpointId } from '@layerzerolabs/lz-definitions'
import { createNetworkEnvironmentFactory } from '@layerzerolabs/utils-evm-hardhat'
import { HardhatRuntimeEnvironment } from 'hardhat/types'
import { Contract } from '@ethersproject/contracts'
import { makeZero } from '@layerzerolabs/utils-evm'

describe('omnigraph/coordinates', () => {
describe('omniDeploymentToPoint', () => {
Expand Down Expand Up @@ -36,15 +38,15 @@ describe('omnigraph/coordinates', () => {
})
})

describe('createDeploymentFactory', () => {
describe('createContractFactory', () => {
// Hardhat deploy will try to get the chain ID from the RPC so we can't let it
const mockSend = (env: HardhatRuntimeEnvironment) => {
env.network.provider.send = jest.fn().mockResolvedValue(1)
}

describe('when called with OmniPointContractName', () => {
it('should reject when eid does not exist', async () => {
const deploymentFactory = createDeploymentFactory(hre)
const deploymentFactory = createContractFactory(hre)

await expect(() =>
deploymentFactory({ eid: EndpointId.CANTO_TESTNET, contractName: 'MyContract' })
Expand All @@ -53,7 +55,7 @@ describe('omnigraph/coordinates', () => {

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

const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET)
mockSend(env)
Expand All @@ -65,13 +67,16 @@ describe('omnigraph/coordinates', () => {

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

const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET)
mockSend(env)

// We'll create a dummy deployment first
await env.deployments.save('MyContract', {} as DeploymentSubmission)
await env.deployments.save('MyContract', {
address: makeZero(undefined),
abi: [],
} as DeploymentSubmission)

// Then check whether the factory will get it for us
const deployment = await deploymentFactory({
Expand All @@ -81,9 +86,7 @@ describe('omnigraph/coordinates', () => {

expect(deployment).toEqual({
eid: EndpointId.ETHEREUM_MAINNET,
deployment: {
numDeployments: expect.any(Number),
},
contract: expect.any(Contract),
})
})
})
Expand All @@ -92,7 +95,7 @@ describe('omnigraph/coordinates', () => {
it('should reject when eid does not exist', async () => {
await fc.assert(
fc.asyncProperty(evmAddressArbitrary, async (address) => {
const deploymentFactory = createDeploymentFactory(hre)
const deploymentFactory = createContractFactory(hre)

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

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

const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET)
mockSend(env)

// We'll create a dummy deployment with the specified address first
await env.deployments.save('MyContract', { address } as DeploymentSubmission)
await env.deployments.save('MyContract', { address, abi: [] } as DeploymentSubmission)

// Then check whether the factory will get it for us
const deployment = await deploymentFactory({
Expand All @@ -133,10 +136,7 @@ describe('omnigraph/coordinates', () => {

expect(deployment).toEqual({
eid: EndpointId.ETHEREUM_MAINNET,
deployment: {
address,
numDeployments: expect.any(Number),
},
contract: expect.any(Contract),
})
})
)
Expand Down
10 changes: 6 additions & 4 deletions packages/ua-utils/src/omnigraph/builder.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import assert from 'assert'
import { arePointsEqual, serializePoint, serializeVector } from './coordinates'
import { arePointsEqual, isVectorPossible, serializePoint, serializeVector } from './coordinates'
import type { OmniEdge, OmniGraph, OmniNode, OmniPoint, OmniVector } from './types'
import { formatOmniPoint, formatOmniVector } from './format'

export class OmniGraphBuilder<TNodeConfig, TEdgeConfig> {
#nodes: Map<string, OmniNode<TNodeConfig>> = new Map()

#edges: Map<string, OmniEdge<TEdgeConfig>> = new Map()

#assertCanAddEdge(edge: OmniEdge<TEdgeConfig>): void {
const label = serializeVector(edge.vector)
const from = serializePoint(edge.vector.from)
const label = formatOmniVector(edge.vector)
const from = formatOmniPoint(edge.vector.from)

assert(this.getNodeAt(edge.vector.from), `Cannot add edge '${label}': '${from}' is not in the graph`)
assert(isVectorPossible(edge.vector), `Cannot add edge ${label}: cannot connect the two endpoints`)
assert(this.getNodeAt(edge.vector.from), `Cannot add edge ${label}: ${from} is not in the graph`)
}

// .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-
Expand Down
12 changes: 12 additions & 0 deletions packages/ua-utils/src/omnigraph/coordinates.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { endpointIdToStage } from '@layerzerolabs/lz-definitions'
import { OmniVector, OmniPoint, OmniNode } from './types'

/**
Expand Down Expand Up @@ -31,6 +32,17 @@ export const areSameEndpoint = (a: OmniPoint, b: OmniPoint): boolean => a.eid ==
export const areVectorsEqual = (a: OmniVector, b: OmniVector): boolean =>
arePointsEqual(a.from, b.from) && arePointsEqual(a.to, b.to)

/**
* Checks that a vector is _possible_ - i.e. connects two endpoints
* that can be connected in reality.
*
* @param vector `OmniVector`
*
* @returns `true` if two points of the vector can be connected in reality
*/
export const isVectorPossible = ({ from, to }: OmniVector): boolean =>
endpointIdToStage(from.eid) === endpointIdToStage(to.eid)

/**
* Serializes a point. Useful for when points need to be used in Map
* where we cannot adjust the default behavior of using a reference equality
Expand Down
9 changes: 9 additions & 0 deletions packages/ua-utils/src/omnigraph/format.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { EndpointId } from '@layerzerolabs/lz-definitions'
import type { OmniPoint, OmniVector } from './types'

export const formatEid = (eid: EndpointId): string => EndpointId[eid] ?? `Unknown EndpointId (${eid})`

export const formatOmniPoint = ({ eid, address }: OmniPoint): string => `[${address} @ ${formatEid(eid)}]`

export const formatOmniVector = ({ from, to }: OmniVector): string =>
`${formatOmniPoint(from)} → ${formatOmniPoint(to)}`
1 change: 1 addition & 0 deletions packages/ua-utils/src/omnigraph/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './builder'
export * from './coordinates'
export * from './format'
export * from './schema'
export * from './types'
Loading

0 comments on commit ef9f5f7

Please sign in to comment.