diff --git a/packages/ua-utils/src/coordinates.ts b/packages/ua-utils/src/coordinates.ts new file mode 100644 index 000000000..c876c71cd --- /dev/null +++ b/packages/ua-utils/src/coordinates.ts @@ -0,0 +1,6 @@ +import { OmnichainGraphCoordinate } from "./types" + +export const areSameCoordinates = (a: OmnichainGraphCoordinate, b: OmnichainGraphCoordinate): boolean => + a.address === b.address && a.eid === b.eid + +export const serializeCoordinate = ({ address, eid }: OmnichainGraphCoordinate): string => `${eid}|${address}` diff --git a/packages/ua-utils/src/index.ts b/packages/ua-utils/src/index.ts index 8827fed6e..3dec05993 100644 --- a/packages/ua-utils/src/index.ts +++ b/packages/ua-utils/src/index.ts @@ -1,2 +1,3 @@ +export * from "./coordinates" export * from "./schema" export * from "./types" diff --git a/packages/ua-utils/src/schema.ts b/packages/ua-utils/src/schema.ts index 84fa47845..f382f04c2 100644 --- a/packages/ua-utils/src/schema.ts +++ b/packages/ua-utils/src/schema.ts @@ -1,16 +1,20 @@ import { EndpointId } from "@layerzerolabs/lz-definitions" import { z } from "zod" -import type { ContractSpec } from "./types" +import type { OmnichainGraphCoordinate, OmnichainGraphNode } from "./types" export const AddressSchema = z.string() export const EndpointIdSchema = z.nativeEnum(EndpointId).pipe(z.number()) -export const createContractSpecSchema = ( +export const OmnichainGraphCoordinateSchema: z.ZodSchema = z.object({ + address: AddressSchema, + eid: EndpointIdSchema, +}) + +export const createOmnichainGraphNodeSchema = ( configSchema: z.ZodSchema -): z.ZodSchema, z.ZodTypeDef, unknown> => +): z.ZodSchema, z.ZodTypeDef, unknown> => z.object({ - address: AddressSchema, - endpointId: EndpointIdSchema, + coordinate: OmnichainGraphCoordinateSchema, config: configSchema, - }) as z.ZodSchema, z.ZodTypeDef, unknown> + }) as z.ZodSchema, z.ZodTypeDef, unknown> diff --git a/packages/ua-utils/src/types.ts b/packages/ua-utils/src/types.ts index 2cf3862f8..974e5cd8d 100644 --- a/packages/ua-utils/src/types.ts +++ b/packages/ua-utils/src/types.ts @@ -2,19 +2,48 @@ import type { EndpointId } from "@layerzerolabs/lz-definitions" export type Address = string -export interface ContractSpec { - endpointId: EndpointId +/** + * OmnichainGraphCoordinate identifies a point in omniverse, an omnichain universe. + * + * In layman terms this is a contract deployed on a particular network (represented by an endpoint). + */ +export interface OmnichainGraphCoordinate { + eid: EndpointId address: Address +} + +/** + * OmnichainGraphNode represents a point in omniverse + * with an additional piece of information attached + */ +export interface OmnichainGraphNode { + coordinate: OmnichainGraphCoordinate config: TConfig } -export interface ContractConnectionSpec { - from: EndpointId - to: EndpointId +/** + * OmnichainGraphEdge represents a connection between two points in omniverse + * with an additional piece of information attached + * + * TODO There is a certain asymetry now between OmnichainGraphNode and OmnichainGraphEdge - + * OmnichainGraphNode contains a coordinate and config whereas OmnichainGraphEdge has no wrapper + * for the coordinate part - the coordinate (which in this case is a tuple [from, to]) is spread directly + * on its type. This asymetry looks fine - the sacrifice of verbosity is made in the name of smaller boilerplate + */ +export interface OmnichainGraphEdge { + from: OmnichainGraphCoordinate + to: OmnichainGraphCoordinate config: TConfig } -export interface OmnichainSpec { - contracts: ContractSpec[] - connections: ContractConnectionSpec[] +/** + * OmnichainGraph is a collection of nodes and edges of omniverse + * that together represent an omnichain app a.k.a. OApp. + * + * For purposes of readability and to avoid overabstraction on the user end, + * the names are set to be `contracts` rather than `nodes` and `connections` rather than `edges` + */ +export interface OmnichainGraph { + contracts: OmnichainGraphNode[] + connections: OmnichainGraphEdge[] } diff --git a/packages/ua-utils/test/coordinates.test.ts b/packages/ua-utils/test/coordinates.test.ts new file mode 100644 index 000000000..0a892db7c --- /dev/null +++ b/packages/ua-utils/test/coordinates.test.ts @@ -0,0 +1,45 @@ +import { areSameCoordinates } from "@/coordinates" +import { OmnichainGraphCoordinate } from "@/types" +import { EndpointId } from "@layerzerolabs/lz-definitions" + +describe("coordinates", () => { + describe("areSameCoordinates", () => { + type TestCase = [OmnichainGraphCoordinate, OmnichainGraphCoordinate] + + const GOOD: TestCase[] = [ + [ + { eid: EndpointId.BSC_TESTNET, address: "0x0" }, + { eid: EndpointId.BSC_TESTNET, address: "0x0" }, + ], + [ + { eid: EndpointId.SOLANA_MAINNET, address: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So" }, + { eid: EndpointId.SOLANA_MAINNET, address: "mSoLzYCxHdYgdzU16g5QSh3i5K3z3KZK7ytfqcJm7So" }, + ], + ] + + it.each(GOOD)(`should be true for %j and %j`, (a, b) => { + expect(areSameCoordinates(a, b)).toBeTruthy() + expect(areSameCoordinates(a, a)).toBeTruthy() + expect(areSameCoordinates(b, b)).toBeTruthy() + }) + + const BAD: TestCase[] = [ + [ + { eid: EndpointId.ETHEREUM_MAINNET, address: "0x0" }, + { eid: EndpointId.BSC_TESTNET, address: "0x0" }, + ], + [ + { eid: EndpointId.ETHEREUM_MAINNET, address: "0x0" }, + { eid: EndpointId.ETHEREUM_MAINNET, address: "0x1" }, + ], + [ + { eid: EndpointId.ETHEREUM_MAINNET, address: "0xa" }, + { eid: EndpointId.ETHEREUM_MAINNET, address: "0xA" }, + ], + ] + + it.each(BAD)(`should be false for %j and %j`, (a, b) => { + expect(areSameCoordinates(a, b)).toBeFalsy() + }) + }) +}) diff --git a/packages/ua-utils/test/schema.test.ts b/packages/ua-utils/test/schema.test.ts index edc60d965..2c0ce6b98 100644 --- a/packages/ua-utils/test/schema.test.ts +++ b/packages/ua-utils/test/schema.test.ts @@ -1,56 +1,50 @@ -import { createContractSpecSchema } from "@/schema" +import { createOmnichainGraphNodeSchema } from "@/schema" +import { OmnichainGraphCoordinate } from "@/types" import { EndpointId } from "@layerzerolabs/lz-definitions" import { expect } from "chai" import { z } from "zod" describe("schema", () => { - describe("createContractSpecSchema", () => { + describe("createOmnichainGraphNodeSchema", () => { interface TestCase { - schema: z.ZodSchema + configSchema: z.ZodSchema good: unknown[] bad: unknown[] } const TEST_CASES: TestCase[] = [ { - schema: z.string(), + configSchema: z.string(), good: ["config", ""], bad: [false, null, undefined, 1, {}], }, { - schema: z.object({ a: z.number().nonnegative() }), + configSchema: z.object({ a: z.number().nonnegative() }), good: [{ a: 0 }, { a: 1 }], bad: [{ a: -1 }, false, "", []], }, ] - TEST_CASES.forEach(({ schema, good, bad }, index) => { - const contractSpecSchema = createContractSpecSchema(schema) + describe.each(TEST_CASES)(`schema`, ({ configSchema, good, bad }) => { + const schema = createOmnichainGraphNodeSchema(configSchema) + const coordinate: OmnichainGraphCoordinate = { eid: EndpointId.APTOS_MAINNET, address: "0x0" } - describe(`case ${index}`, () => { - good.forEach((config) => { - it(`should work for ${JSON.stringify(config)}`, () => { - const spec = { - endpointId: EndpointId.APTOS_MAINNET, - address: "0x0", - config, - } + it.each(good)(`should pass with %j`, (config) => { + const node = { + coordinate, + config, + } - expect(contractSpecSchema.parse(spec)).to.eql(spec) - }) - }) + expect(schema.parse(node)).to.eql(node) + }) - bad.forEach((config) => { - it(`should not work for ${JSON.stringify(config)}`, () => { - const spec = { - endpointId: EndpointId.APTOS_MAINNET, - address: "0x0", - config, - } + it.each(bad)(`should fail with %j`, (config) => { + const node = { + coordinate, + config, + } - expect(() => contractSpecSchema.parse(spec)).to.throw() - }) - }) + expect(() => schema.parse(node)).to.throw() }) }) })