From a665df2647f745674224dc75dd1217d9540585c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1n=20Jakub=20Nani=C5=A1ta?= Date: Fri, 8 Dec 2023 12:05:43 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9A=20OmniGraph=E2=84=A2=20Transformat?= =?UTF-8?q?ions=20&=20schemas=20(#76)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/test-utils/src/arbitraries.ts | 4 + .../test/__utils__/endpoint.ts | 6 +- .../test/oapp/config.test.ts | 4 +- .../ua-utils-evm-hardhat/src/oapp/index.ts | 1 + .../ua-utils-evm-hardhat/src/oapp/types.ts | 3 + .../src/omnigraph/builder.ts | 28 +-- .../utils-evm-hardhat/src/omnigraph/index.ts | 2 + .../utils-evm-hardhat/src/omnigraph/schema.ts | 59 +++++ .../src/omnigraph/transformations.ts | 43 ++++ .../utils-evm-hardhat/src/omnigraph/types.ts | 8 +- .../test/omnigraph/transformations.test.ts | 206 ++++++++++++++++++ packages/utils/src/omnigraph/builder.ts | 16 +- packages/utils/src/omnigraph/schema.ts | 31 ++- 13 files changed, 374 insertions(+), 37 deletions(-) create mode 100644 packages/ua-utils-evm-hardhat/src/oapp/index.ts create mode 100644 packages/ua-utils-evm-hardhat/src/oapp/types.ts create mode 100644 packages/utils-evm-hardhat/src/omnigraph/schema.ts create mode 100644 packages/utils-evm-hardhat/src/omnigraph/transformations.ts create mode 100644 packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts diff --git a/packages/test-utils/src/arbitraries.ts b/packages/test-utils/src/arbitraries.ts index 58e16bd7e..bc1c6fb22 100644 --- a/packages/test-utils/src/arbitraries.ts +++ b/packages/test-utils/src/arbitraries.ts @@ -2,6 +2,10 @@ import fc from 'fast-check' import { EndpointId, Stage } from '@layerzerolabs/lz-definitions' import { ENDPOINT_IDS } from './constants' +export const nullishArbitrary = fc.constantFrom(null, undefined) + +export const nullableArbitrary = (a: fc.Arbitrary) => fc.oneof(a, nullishArbitrary) + export const addressArbitrary = fc.string() export const evmAddressArbitrary = fc.hexaString({ minLength: 40, maxLength: 40 }).map((address) => `0x${address}`) diff --git a/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts b/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts index 0a989fd92..059418636 100644 --- a/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts +++ b/packages/ua-utils-evm-hardhat-test/test/__utils__/endpoint.ts @@ -144,11 +144,11 @@ export const setupDefaultEndpoint = async (): Promise => { } // Now we compile a list of all the transactions that need to be executed for the ULNs and Endpoints - const builderEndpoint = await OmniGraphBuilderHardhat.fromConfig(config, contractFactory) + const builderEndpoint = await OmniGraphBuilderHardhat.fromConfig(config) const endpointTransactions = await configureEndpoint(builderEndpoint.graph, endpointSdkFactory) - const builderSendUln = await OmniGraphBuilderHardhat.fromConfig(sendUlnConfig, contractFactory) + const builderSendUln = await OmniGraphBuilderHardhat.fromConfig(sendUlnConfig) const sendUlnTransactions = await configureUln302(builderSendUln.graph, ulnSdkFactory) - const builderReceiveUln = await OmniGraphBuilderHardhat.fromConfig(receiveUlnConfig, contractFactory) + const builderReceiveUln = await OmniGraphBuilderHardhat.fromConfig(receiveUlnConfig) const receiveUlnTransactions = await configureUln302(builderReceiveUln.graph, ulnSdkFactory) const transactions = [...sendUlnTransactions, ...receiveUlnTransactions, ...endpointTransactions] diff --git a/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts b/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts index 975b13bf5..1f22868e7 100644 --- a/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts +++ b/packages/ua-utils-evm-hardhat-test/test/oapp/config.test.ts @@ -54,7 +54,7 @@ describe('oapp/config', () => { // This is the required tooling we need to set up const contractFactory = createContractFactory() const connectedContractFactory = createConnectedContractFactory(contractFactory) - const builder = await OmniGraphBuilderHardhat.fromConfig(config, contractFactory) + const builder = await OmniGraphBuilderHardhat.fromConfig(config) // This so far the only non-oneliner, a function that returns an SDK for a contract on a network const sdkFactory = async (point: OmniPoint) => new OApp(await connectedContractFactory(point)) @@ -79,7 +79,7 @@ describe('oapp/config', () => { // This is the required tooling we need to set up const contractFactory = createContractFactory() const connectedContractFactory = createConnectedContractFactory(contractFactory) - const builder = await OmniGraphBuilderHardhat.fromConfig(config, contractFactory) + const builder = await OmniGraphBuilderHardhat.fromConfig(config) // This so far the only non-oneliner, a function that returns an SDK for a contract on a network const sdkFactory = async (point: OmniPoint) => new OApp(await connectedContractFactory(point)) diff --git a/packages/ua-utils-evm-hardhat/src/oapp/index.ts b/packages/ua-utils-evm-hardhat/src/oapp/index.ts new file mode 100644 index 000000000..c9f6f047d --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/oapp/index.ts @@ -0,0 +1 @@ +export * from './types' diff --git a/packages/ua-utils-evm-hardhat/src/oapp/types.ts b/packages/ua-utils-evm-hardhat/src/oapp/types.ts new file mode 100644 index 000000000..3cf297c40 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/oapp/types.ts @@ -0,0 +1,3 @@ +import { OmniGraphHardhat } from '@layerzerolabs/utils-evm-hardhat' + +export type OAppOmniGraphHardhat = OmniGraphHardhat diff --git a/packages/utils-evm-hardhat/src/omnigraph/builder.ts b/packages/utils-evm-hardhat/src/omnigraph/builder.ts index 8346f2bcb..7a426a238 100644 --- a/packages/utils-evm-hardhat/src/omnigraph/builder.ts +++ b/packages/utils-evm-hardhat/src/omnigraph/builder.ts @@ -1,8 +1,7 @@ -import type { OmniEdge, OmniNode } from '@layerzerolabs/utils' -import type { OmniContractFactoryHardhat, OmniGraphHardhat } from './types' +import type { OmniGraphHardhat } from './types' import { OmniGraphBuilder } from '@layerzerolabs/utils' -import { omniContractToPoint } from '@layerzerolabs/utils-evm' import assert from 'assert' +import { OmniGraphHardhatTransformer, createOmniGraphHardhatTransformer } from './transformations' /** * OmniGraphBuilderHardhat houses all hardhat-specific utilities for building OmniGraphs @@ -12,28 +11,9 @@ import assert from 'assert' export class OmniGraphBuilderHardhat { static async fromConfig( graph: OmniGraphHardhat, - contractFactory: OmniContractFactoryHardhat + transform: OmniGraphHardhatTransformer = createOmniGraphHardhatTransformer() ): Promise> { - const builder = new OmniGraphBuilder() - - const nodes: OmniNode[] = await Promise.all( - graph.contracts.map(async ({ contract, config }) => ({ - point: omniContractToPoint(await contractFactory(contract)), - config, - })) - ) - - const edges: OmniEdge[] = await Promise.all( - graph.connections.map(async ({ from, to, config }) => ({ - vector: { - from: omniContractToPoint(await contractFactory(from)), - to: omniContractToPoint(await contractFactory(to)), - }, - config, - })) - ) - - return builder.addNodes(...nodes).addEdges(...edges) + return OmniGraphBuilder.fromGraph(await transform(graph)) } constructor() { diff --git a/packages/utils-evm-hardhat/src/omnigraph/index.ts b/packages/utils-evm-hardhat/src/omnigraph/index.ts index 037cfacf9..02402959c 100644 --- a/packages/utils-evm-hardhat/src/omnigraph/index.ts +++ b/packages/utils-evm-hardhat/src/omnigraph/index.ts @@ -1,4 +1,6 @@ export * from './builder' export * from './contracts' export * from './coordinates' +export * from './schema' +export * from './transformations' export * from './types' diff --git a/packages/utils-evm-hardhat/src/omnigraph/schema.ts b/packages/utils-evm-hardhat/src/omnigraph/schema.ts new file mode 100644 index 000000000..ab30d05d1 --- /dev/null +++ b/packages/utils-evm-hardhat/src/omnigraph/schema.ts @@ -0,0 +1,59 @@ +import { z } from 'zod' +import { EndpointIdSchema, OmniPointSchema } from '@layerzerolabs/utils' +import type { OmniEdgeHardhat, OmniGraphHardhat, OmniNodeHardhat, OmniPointHardhat } from './types' + +export const OmniPointHardhatSchema: z.ZodSchema = z.object({ + eid: EndpointIdSchema, + contractName: z.string().nullish(), + address: z.string().nullish(), +}) + +const OmniPointOrOmniPointHardhatSchema = z.union([OmniPointHardhatSchema, OmniPointSchema]) + +/** + * Factory for OmniNodeHardhat schemas + * + * @param configSchema Schema of the config contained in the node + * + * @returns {z.ZodSchema>} schema for a node with the particular config type + */ +export const createOmniNodeHardhatSchema = ( + configSchema: z.ZodSchema +): z.ZodSchema, z.ZodTypeDef, unknown> => + z.object({ + contract: OmniPointOrOmniPointHardhatSchema, + config: configSchema, + }) as z.ZodSchema, z.ZodTypeDef, unknown> + +/** + * Factory for OmniEdgeHardhat schemas + * + * @param {z.ZodSchema} configSchema Schema of the config contained in the edge + * + * @returns {z.ZodSchema>} Schema for an edge with the particular config type + */ +export const createOmniEdgeHardhatSchema = ( + configSchema: z.ZodSchema +): z.ZodSchema, z.ZodTypeDef, unknown> => + z.object({ + from: OmniPointOrOmniPointHardhatSchema, + to: OmniPointOrOmniPointHardhatSchema, + config: configSchema, + }) as z.ZodSchema, z.ZodTypeDef, unknown> + +/** + * Factory for OmniGraphHardhat schemas + * + * @param {z.ZodSchema>} nodeSchema + * @param {z.ZodSchema>} edgeSchema + * + * @returns {z.ZodSchema>} + */ +export const createOmniGraphHardhatSchema = ( + nodeSchema: z.ZodSchema, z.ZodTypeDef, unknown>, + edgeSchema: z.ZodSchema, z.ZodTypeDef, unknown> +): z.ZodSchema, z.ZodTypeDef, unknown> => + z.object({ + contracts: z.array(nodeSchema), + connections: z.array(edgeSchema), + }) diff --git a/packages/utils-evm-hardhat/src/omnigraph/transformations.ts b/packages/utils-evm-hardhat/src/omnigraph/transformations.ts new file mode 100644 index 000000000..d245177d9 --- /dev/null +++ b/packages/utils-evm-hardhat/src/omnigraph/transformations.ts @@ -0,0 +1,43 @@ +import type { OmniEdge, OmniGraph, OmniNode } from '@layerzerolabs/utils' +import { isOmniPoint } from '@layerzerolabs/utils' +import { omniContractToPoint } from '@layerzerolabs/utils-evm' +import { createContractFactory } from './coordinates' +import type { OmniContractFactoryHardhat, OmniEdgeHardhat, OmniGraphHardhat, OmniNodeHardhat } from './types' + +export const createOmniNodeHardhatTransformer = + (contractFactory: OmniContractFactoryHardhat = createContractFactory()) => + async ({ + contract, + config, + }: OmniNodeHardhat): Promise> => { + const point = isOmniPoint(contract) ? contract : omniContractToPoint(await contractFactory(contract)) + + return { point, config } + } + +export const createOmniEdgeHardhatTransformer = + (contractFactory: OmniContractFactoryHardhat = createContractFactory()) => + async ({ + from: fromContract, + to: toContract, + config, + }: OmniEdgeHardhat): Promise> => { + const from = isOmniPoint(fromContract) ? fromContract : omniContractToPoint(await contractFactory(fromContract)) + const to = isOmniPoint(toContract) ? toContract : omniContractToPoint(await contractFactory(toContract)) + + return { vector: { from, to }, config } + } + +export type OmniGraphHardhatTransformer = ( + graph: OmniGraphHardhat +) => Promise> + +export const createOmniGraphHardhatTransformer = + ( + nodeTransformer = createOmniNodeHardhatTransformer(), + edgeTransformer = createOmniEdgeHardhatTransformer() + ): OmniGraphHardhatTransformer => + async (graph) => ({ + contracts: await Promise.all(graph.contracts.map(nodeTransformer)), + connections: await Promise.all(graph.connections.map(edgeTransformer)), + }) diff --git a/packages/utils-evm-hardhat/src/omnigraph/types.ts b/packages/utils-evm-hardhat/src/omnigraph/types.ts index 93a44b045..31684adb0 100644 --- a/packages/utils-evm-hardhat/src/omnigraph/types.ts +++ b/packages/utils-evm-hardhat/src/omnigraph/types.ts @@ -4,8 +4,8 @@ import type { OmniContract } from '@layerzerolabs/utils-evm' export interface OmniPointHardhat { eid: EndpointId - contractName?: string - address?: string + contractName?: string | null + address?: string | null } export interface OmniNodeHardhat { @@ -14,8 +14,8 @@ export interface OmniNodeHardhat { } export interface OmniEdgeHardhat { - from: OmniPointHardhat - to: OmniPointHardhat + from: OmniPointHardhat | OmniPoint + to: OmniPointHardhat | OmniPoint config: TEdgeConfig } diff --git a/packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts b/packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts new file mode 100644 index 000000000..685f843fe --- /dev/null +++ b/packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts @@ -0,0 +1,206 @@ +import fc from 'fast-check' +import { OmniPointHardhat } from '@/omnigraph' +import { + createOmniEdgeHardhatTransformer, + createOmniGraphHardhatTransformer, + createOmniNodeHardhatTransformer, +} from '@/omnigraph/transformations' +import { Contract } from '@ethersproject/contracts' +import { endpointArbitrary, evmAddressArbitrary, nullableArbitrary, pointArbitrary } from '@layerzerolabs/test-utils' +import { isOmniPoint } from '@layerzerolabs/utils' + +describe('omnigraph/transformations', () => { + const pointHardhatArbitrary = fc.record({ + eid: endpointArbitrary, + contractName: nullableArbitrary(fc.string()), + address: nullableArbitrary(evmAddressArbitrary), + }) + + describe('createOmniNodeHardhatTransformer', () => { + it('should pass the original value if contract is already an OmniPoint', async () => { + await fc.assert( + fc.asyncProperty(pointArbitrary, fc.anything(), async (point, config) => { + const contractFactory = jest.fn().mockRejectedValue('Oh no') + const transformer = createOmniNodeHardhatTransformer(contractFactory) + + const node = await transformer({ contract: point, config }) + + expect(node).toEqual({ point, config }) + expect(contractFactory).not.toHaveBeenCalled() + }) + ) + }) + + it('should call the contractFactory if contract is not an OmniPoint', async () => { + await fc.assert( + fc.asyncProperty( + pointHardhatArbitrary, + evmAddressArbitrary, + fc.anything(), + async (point, address, config) => { + fc.pre(!isOmniPoint(point)) + + const contract = new Contract(address, []) + const contractFactory = jest + .fn() + .mockImplementation(async (point: OmniPointHardhat) => ({ eid: point.eid, contract })) + const transformer = createOmniNodeHardhatTransformer(contractFactory) + + const node = await transformer({ contract: point, config }) + + expect(node).toEqual({ point: { eid: point.eid, address }, config }) + expect(contractFactory).toHaveBeenCalledTimes(1) + expect(contractFactory).toHaveBeenCalledWith(point) + } + ) + ) + }) + }) + + describe('createOmniEdgeHardhatTransformer', () => { + it('should pass the original values if from and to are already OmniPoints', async () => { + await fc.assert( + fc.asyncProperty(pointArbitrary, pointArbitrary, fc.anything(), async (from, to, config) => { + const contractFactory = jest.fn().mockRejectedValue('Oh no') + const transformer = createOmniEdgeHardhatTransformer(contractFactory) + + const edge = await transformer({ from, to, config }) + + expect(edge).toEqual({ vector: { from, to }, config }) + expect(contractFactory).not.toHaveBeenCalled() + }) + ) + }) + + it('should call the contractFactory if from is not an OmniPoint', async () => { + await fc.assert( + fc.asyncProperty( + pointHardhatArbitrary, + pointArbitrary, + evmAddressArbitrary, + fc.anything(), + async (from, to, address, config) => { + fc.pre(!isOmniPoint(from)) + + const contract = new Contract(address, []) + const contractFactory = jest + .fn() + .mockImplementation(async (point: OmniPointHardhat) => ({ eid: point.eid, contract })) + const transformer = createOmniEdgeHardhatTransformer(contractFactory) + + const edge = await transformer({ from, to, config }) + + expect(edge).toEqual({ vector: { from: { eid: from.eid, address }, to }, config }) + expect(contractFactory).toHaveBeenCalledTimes(1) + expect(contractFactory).toHaveBeenCalledWith(from) + } + ) + ) + }) + + it('should call the contractFactory if to is not an OmniPoint', async () => { + await fc.assert( + fc.asyncProperty( + pointArbitrary, + pointHardhatArbitrary, + evmAddressArbitrary, + fc.anything(), + async (from, to, address, config) => { + fc.pre(!isOmniPoint(to)) + + const contract = new Contract(address, []) + const contractFactory = jest + .fn() + .mockImplementation(async (point: OmniPointHardhat) => ({ eid: point.eid, contract })) + const transformer = createOmniEdgeHardhatTransformer(contractFactory) + + const edge = await transformer({ from, to, config }) + + expect(edge).toEqual({ vector: { from, to: { eid: to.eid, address } }, config }) + expect(contractFactory).toHaveBeenCalledTimes(1) + expect(contractFactory).toHaveBeenCalledWith(to) + } + ) + ) + }) + + it('should call the contractFactory if from & to are not OmniPoints', async () => { + await fc.assert( + fc.asyncProperty( + pointHardhatArbitrary, + pointHardhatArbitrary, + evmAddressArbitrary, + fc.anything(), + async (from, to, address, config) => { + fc.pre(!isOmniPoint(from)) + fc.pre(!isOmniPoint(to)) + + const contract = new Contract(address, []) + const contractFactory = jest + .fn() + .mockImplementation(async (point: OmniPointHardhat) => ({ eid: point.eid, contract })) + const transformer = createOmniEdgeHardhatTransformer(contractFactory) + + const edge = await transformer({ from, to, config }) + + expect(edge).toEqual({ + vector: { from: { eid: from.eid, address }, to: { eid: to.eid, address } }, + config, + }) + expect(contractFactory).toHaveBeenCalledTimes(2) + expect(contractFactory).toHaveBeenCalledWith(from) + expect(contractFactory).toHaveBeenCalledWith(to) + } + ) + ) + }) + }) + + describe('createOmniGraphHardhatTransformer', () => { + it('should return an empty graph if called with an empty graph', async () => { + const nodeTransformer = jest.fn().mockRejectedValue('Oh node') + const edgeTransformer = jest.fn().mockRejectedValue('Oh edge') + const transformer = createOmniGraphHardhatTransformer(nodeTransformer, edgeTransformer) + + expect( + await transformer({ + contracts: [], + connections: [], + }) + ).toEqual({ + contracts: [], + connections: [], + }) + }) + + 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), + fc.array(edgeHardhatArbitrary), + async (contracts, connections) => { + const nodeTransformer = jest.fn().mockImplementation(async (node) => ({ node })) + const edgeTransformer = jest.fn().mockImplementation(async (edge) => ({ edge })) + const transformer = createOmniGraphHardhatTransformer(nodeTransformer, edgeTransformer) + + const graph = await transformer({ contracts, connections }) + + expect(graph.contracts).toEqual(contracts.map((node) => ({ node }))) + expect(graph.connections).toEqual(connections.map((edge) => ({ edge }))) + } + ) + ) + }) + }) +}) diff --git a/packages/utils/src/omnigraph/builder.ts b/packages/utils/src/omnigraph/builder.ts index fc4cf44c1..6b1b5b377 100644 --- a/packages/utils/src/omnigraph/builder.ts +++ b/packages/utils/src/omnigraph/builder.ts @@ -3,7 +3,21 @@ import { arePointsEqual, isVectorPossible, serializePoint, serializeVector } fro import type { OmniEdge, OmniGraph, OmniNode, OmniPoint, OmniVector } from './types' import { formatOmniPoint, formatOmniVector } from './format' -export class OmniGraphBuilder { +export class OmniGraphBuilder { + /** + * Syntactic sugar utility for cloning graphs + * + * @param {OmniGraph} graph + * @returns {OmniGraph} + */ + static fromGraph( + graph: OmniGraph + ): OmniGraphBuilder { + return new OmniGraphBuilder() + .addNodes(...graph.contracts) + .addEdges(...graph.connections) + } + #nodes: Map> = new Map() #edges: Map> = new Map() diff --git a/packages/utils/src/omnigraph/schema.ts b/packages/utils/src/omnigraph/schema.ts index e3cdab1a0..cf21b1ad3 100644 --- a/packages/utils/src/omnigraph/schema.ts +++ b/packages/utils/src/omnigraph/schema.ts @@ -1,6 +1,6 @@ import { EndpointId } from '@layerzerolabs/lz-definitions' import { z } from 'zod' -import type { OmniPoint, OmniNode, OmniVector, OmniEdge } from './types' +import type { OmniPoint, OmniNode, OmniVector, OmniEdge, OmniGraph } from './types' export const AddressSchema = z.string() @@ -28,6 +28,14 @@ export const EmptyOmniEdgeSchema = z.object({ config: z.unknown(), }) +/** + * Helper assertion utility for `OmniPoint` instances + * + * @param {unknown} value + * @returns {boolean} `true` if the value is an `OmniPoint`, `false` otherwise + */ +export const isOmniPoint = (value: unknown): value is OmniPoint => OmniPointSchema.safeParse(value).success + /** * Factory for OmniNode schemas * @@ -45,9 +53,9 @@ export const createOmniNodeSchema = ( /** * Factory for OmniEdge schemas * - * @param configSchema `z.ZodSchema` Schema of the config contained in the edge + * @param {z.ZodSchema} configSchema Schema of the config contained in the edge * - * @returns `z.ZodSchema>` schema for an edge with the particular config type + * @returns {z.ZodSchema>} Schema for an edge with the particular config type */ export const createOmniEdgeSchema = ( configSchema: z.ZodSchema @@ -55,3 +63,20 @@ export const createOmniEdgeSchema = ( EmptyOmniEdgeSchema.extend({ config: configSchema, }) as z.ZodSchema, z.ZodTypeDef, unknown> + +/** + * Factory for OmniGraph schemas + * + * @param {z.ZodSchema, z.ZodTypeDef, unknown>} nodeSchema + * @param {z.ZodSchema, z.ZodTypeDef, unknown>} edgeSchema + * + * @returns {z.ZodSchema, z.ZodTypeDef, unknown>} + */ +export const createOmniGraphSchema = ( + nodeSchema: z.ZodSchema, z.ZodTypeDef, unknown>, + edgeSchema: z.ZodSchema, z.ZodTypeDef, unknown> +): z.ZodSchema, z.ZodTypeDef, unknown> => + z.object({ + contracts: z.array(nodeSchema), + connections: z.array(edgeSchema), + })