From bc533f79d89c89616274455d48b1f65f186b09cb Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 10:59:51 -0800 Subject: [PATCH 01/14] feat: Add OmniGraphBuilder --- packages/ua-utils/src/omnigraph/builder.ts | 110 +++++ packages/ua-utils/src/omnigraph/index.ts | 1 + .../ua-utils/test/__utils__/arbitraries.ts | 18 +- .../ua-utils/test/omnigraph/builder.test.ts | 452 ++++++++++++++++++ 4 files changed, 580 insertions(+), 1 deletion(-) create mode 100644 packages/ua-utils/src/omnigraph/builder.ts create mode 100644 packages/ua-utils/test/omnigraph/builder.test.ts diff --git a/packages/ua-utils/src/omnigraph/builder.ts b/packages/ua-utils/src/omnigraph/builder.ts new file mode 100644 index 000000000..12c28a8a3 --- /dev/null +++ b/packages/ua-utils/src/omnigraph/builder.ts @@ -0,0 +1,110 @@ +import assert from 'assert' +import { arePointsEqual, serializePoint, serializeVector } from './coordinates' +import type { OmniEdge, OmniGraph, OmniNode, OmniPoint, OmniVector } from './types' + +export class OmniGraphBuilder { + #nodes: Map> = new Map() + + #edges: Map> = new Map() + + #assertCanAddEdge(edge: OmniEdge): void { + const label = serializeVector(edge.vector) + const from = serializePoint(edge.vector.from) + const to = serializePoint(edge.vector.to) + + assert(this.getNodeAt(edge.vector.from), `Cannot add edge '${label}': '${from}' is not in the graph`) + assert(this.getNodeAt(edge.vector.to), `Cannot add edge '${label}': '${to}' is not in the graph`) + } + + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + // + // The builder methods + // + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + + addNodes(...nodes: OmniNode[]): this { + return nodes.forEach((node) => this.#nodes.set(serializePoint(node.point), node)), this + } + + addEdges(...edges: OmniEdge[]): this { + return ( + edges.forEach((edge) => { + // First we make sure we can add this edge + this.#assertCanAddEdge(edge) + + // Only then we add it + this.#edges.set(serializeVector(edge.vector), edge) + }), + this + ) + } + + removeNodeAt(point: OmniPoint): this { + return ( + // First we remove all edges between this node and any other nodes + [...this.getEdgesFrom(point), ...this.getEdgesTo(point)].forEach((edge) => this.removeEdgeAt(edge.vector)), + // Only then we remove the node itself + this.#nodes.delete(serializePoint(point)), + this + ) + } + + removeEdgeAt(vector: OmniVector): this { + return this.#edges.delete(serializeVector(vector)), this + } + + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + // + // The accessor methods + // + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + + getNodeAt(point: OmniPoint): OmniNode | undefined { + return this.#nodes.get(serializePoint(point)) + } + + getEdgeAt(vector: OmniVector): OmniEdge | undefined { + return this.#edges.get(serializeVector(vector)) + } + + getEdgesFrom(point: OmniPoint): OmniEdge[] { + return this.edges.filter(({ vector: { from } }) => arePointsEqual(point, from)) + } + + getEdgesTo(point: OmniPoint): OmniEdge[] { + return this.edges.filter(({ vector: { to } }) => arePointsEqual(point, to)) + } + + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + // + // The config accessors + // + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- + // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ + // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' + + get nodes(): OmniNode[] { + return Array.from(this.#nodes.values()) + } + + get edges(): OmniEdge[] { + return Array.from(this.#edges.values()) + } + + get graph(): OmniGraph { + return { + contracts: this.nodes, + connections: this.edges, + } + } +} diff --git a/packages/ua-utils/src/omnigraph/index.ts b/packages/ua-utils/src/omnigraph/index.ts index 6e12e12a8..904d9de85 100644 --- a/packages/ua-utils/src/omnigraph/index.ts +++ b/packages/ua-utils/src/omnigraph/index.ts @@ -1,3 +1,4 @@ +export * from './builder' export * from './coordinates' export * from './schema' export * from './types' diff --git a/packages/ua-utils/test/__utils__/arbitraries.ts b/packages/ua-utils/test/__utils__/arbitraries.ts index d9ce4a02e..8b33b2031 100644 --- a/packages/ua-utils/test/__utils__/arbitraries.ts +++ b/packages/ua-utils/test/__utils__/arbitraries.ts @@ -1,7 +1,7 @@ import fc from 'fast-check' import { EndpointId } from '@layerzerolabs/lz-definitions' import { ENDPOINT_IDS } from './constants' -import { OmniPoint, OmniVector } from '@/omnigraph/types' +import { OmniEdge, OmniNode, OmniPoint, OmniVector } from '@/omnigraph/types' export const addressArbitrary = fc.string() @@ -16,3 +16,19 @@ export const vectorArbitrary: fc.Arbitrary = fc.record({ from: pointArbitrary, to: pointArbitrary, }) + +export const createNodeArbitrary = ( + configArbitrary: fc.Arbitrary +): fc.Arbitrary> => + fc.record({ + point: pointArbitrary, + config: configArbitrary, + }) + +export const createEdgeArbitrary = ( + configArbitrary: fc.Arbitrary +): fc.Arbitrary> => + fc.record({ + vector: vectorArbitrary, + config: configArbitrary, + }) diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts new file mode 100644 index 000000000..e57362f78 --- /dev/null +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -0,0 +1,452 @@ +import fc from 'fast-check' +import { createNodeArbitrary, createEdgeArbitrary, pointArbitrary, vectorArbitrary } from '../__utils__/arbitraries' +import { OmniGraphBuilder } from '@/omnigraph/builder' +import { arePointsEqual, areVectorsEqual } from '@/omnigraph' + +describe('omnigraph/builder', () => { + const nodeConfigArbitrary = fc.anything() + const edgeConfigArbitrary = fc.anything() + const nodeArbitrary = createNodeArbitrary(nodeConfigArbitrary) + const nodesArbitrary = fc.array(nodeArbitrary) + const edgeArbitrary = createEdgeArbitrary(edgeConfigArbitrary) + const edgesArbitrary = fc.array(edgeArbitrary) + + describe('builder methods', () => { + describe('addNodes', () => { + it('should return self', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addNodes()).toBe(builder) + }) + + it('should do nothing if called with no nodes', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addNodes().nodes).toEqual([]) + }) + + it('should add a single node', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.addNodes(node) + expect(builder.nodes).toEqual([node]) + }) + ) + }) + + it('should not add a duplicate node', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.addNodes(node, node) + expect(builder.nodes).toEqual([node]) + }) + ) + }) + + it('should overwrite a node if the points are equal', () => { + fc.assert( + fc.property(pointArbitrary, nodeConfigArbitrary, nodeConfigArbitrary, (point, configA, configB) => { + const builder = new OmniGraphBuilder() + + const nodeA = { point, config: configA } + const nodeB = { point, config: configB } + + builder.addNodes(nodeA, nodeB) + expect(builder.nodes).toEqual([nodeB]) + }) + ) + }) + }) + + describe('removeNodeAt', () => { + it('should not do anything when there are no nodes', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.removeNodeAt(node.point) + expect(builder.nodes).toEqual([]) + }) + ) + }) + + it('should return self', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + expect(builder.removeNodeAt(node.point)).toBe(builder) + }) + ) + }) + + it('should remove a node at a specified point', () => { + fc.assert( + fc.property(nodeArbitrary, (node) => { + const builder = new OmniGraphBuilder() + + builder.addNodes(node) + builder.removeNodeAt(node.point) + expect(builder.nodes).toEqual([]) + }) + ) + }) + + it('should not remove nodes at different points', () => { + fc.assert( + fc.property(nodeArbitrary, nodeArbitrary, (nodeA, nodeB) => { + fc.pre(!arePointsEqual(nodeA.point, nodeB.point)) + + const builder = new OmniGraphBuilder() + + builder.addNodes(nodeA, nodeB) + builder.removeNodeAt(nodeA.point) + expect(builder.nodes).toEqual([nodeB]) + }) + ) + }) + + it('should remove all edges connected to the node', () => { + fc.assert( + fc.property( + nodeArbitrary, + nodeArbitrary, + nodeArbitrary, + edgeConfigArbitrary, + (nodeA, nodeB, nodeC, edgeConfig) => { + fc.pre(!arePointsEqual(nodeA.point, nodeB.point)) + fc.pre(!arePointsEqual(nodeA.point, nodeC.point)) + fc.pre(!arePointsEqual(nodeB.point, nodeC.point)) + + const builder = new OmniGraphBuilder() + + const edgeAB = { vector: { from: nodeA.point, to: nodeB.point }, config: edgeConfig } + const edgeAC = { vector: { from: nodeA.point, to: nodeC.point }, config: edgeConfig } + const edgeBA = { vector: { from: nodeB.point, to: nodeA.point }, config: edgeConfig } + const edgeBC = { vector: { from: nodeB.point, to: nodeC.point }, config: edgeConfig } + const edgeCA = { vector: { from: nodeC.point, to: nodeA.point }, config: edgeConfig } + const edgeCB = { vector: { from: nodeC.point, to: nodeB.point }, config: edgeConfig } + + builder + .addNodes(nodeA, nodeB, nodeC) + .addEdges(edgeAB, edgeAC, edgeBA, edgeBC, edgeCA, edgeCB) + .removeNodeAt(nodeA.point) + expect(builder.edges).toEqual([edgeBC, edgeCB]) + } + ) + ) + }) + }) + + describe('addEdges', () => { + it('should return self', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addEdges()).toBe(builder) + }) + + it('should do nothing if called with no edges', () => { + const builder = new OmniGraphBuilder() + + expect(builder.addEdges().edges).toEqual([]) + }) + + it('should fail if from is not in the graph', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder.addNodes({ point: edge.vector.to, config: nodeConfig }) + + expect(() => builder.addEdges(edge)).toThrow() + }) + ) + }) + + it('should fail if to is not in the graph', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder.addNodes({ point: edge.vector.from, config: nodeConfig }) + + expect(() => builder.addEdges(edge)).toThrow() + }) + ) + }) + + it('should add a single edge', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edge.vector.from, config: nodeConfig }) + .addNodes({ point: edge.vector.to, config: nodeConfig }) + .addEdges(edge) + expect(builder.edges).toEqual([edge]) + }) + ) + }) + + it('should not add a duplicate edge', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edge.vector.from, config: nodeConfig }) + .addNodes({ point: edge.vector.to, config: nodeConfig }) + .addEdges(edge, edge) + expect(builder.edges).toEqual([edge]) + }) + ) + }) + + it('should overwrite an edge if the points are equal', () => { + fc.assert( + fc.property( + vectorArbitrary, + edgeConfigArbitrary, + edgeConfigArbitrary, + nodeConfigArbitrary, + (vector, configA, configB, nodeConfig) => { + const builder = new OmniGraphBuilder() + + const edgeA = { vector, config: configA } + const edgeB = { vector, config: configB } + + builder + .addNodes({ point: vector.from, config: nodeConfig }) + .addNodes({ point: vector.to, config: nodeConfig }) + .addEdges(edgeA, edgeB) + expect(builder.edges).toEqual([edgeB]) + } + ) + ) + }) + }) + + describe('removeEdgeAt', () => { + it('should not do anything when there are no edges', () => { + fc.assert( + fc.property(edgeArbitrary, (edge) => { + const builder = new OmniGraphBuilder() + + builder.removeEdgeAt(edge.vector) + expect(builder.edges).toEqual([]) + }) + ) + }) + + it('should return self', () => { + fc.assert( + fc.property(edgeArbitrary, (edge) => { + const builder = new OmniGraphBuilder() + + expect(builder.removeEdgeAt(edge.vector)).toBe(builder) + }) + ) + }) + + it('should remove a edge at a specified vector', () => { + fc.assert( + fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edge.vector.from, config: nodeConfig }) + .addNodes({ point: edge.vector.to, config: nodeConfig }) + .addEdges(edge) + + builder.removeEdgeAt(edge.vector) + expect(builder.edges).toEqual([]) + }) + ) + }) + + it('should not remove edges at different vectors', () => { + fc.assert( + fc.property(edgeArbitrary, edgeArbitrary, nodeConfigArbitrary, (edgeA, edgeB, nodeConfig) => { + fc.pre(!areVectorsEqual(edgeA.vector, edgeB.vector)) + + const builder = new OmniGraphBuilder() + + builder + .addNodes({ point: edgeA.vector.from, config: nodeConfig }) + .addNodes({ point: edgeA.vector.to, config: nodeConfig }) + .addNodes({ point: edgeB.vector.from, config: nodeConfig }) + .addNodes({ point: edgeB.vector.to, config: nodeConfig }) + .addEdges(edgeA, edgeB) + builder.removeEdgeAt(edgeA.vector) + expect(builder.edges).toEqual([edgeB]) + }) + ) + }) + }) + }) + + describe('accessor methods', () => { + describe('getNodeAt', () => { + it('should return undefined when there are no nodes', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(pointArbitrary, (point) => { + expect(builder.getNodeAt(point)).toBeUndefined() + }) + ) + }) + + it('should return undefined when there are no nodes at a specified point', () => { + fc.assert( + fc.property(nodesArbitrary, (nodes) => { + const node = nodes.at(-1) + fc.pre(node != null) + + const builder = new OmniGraphBuilder() + + builder.addNodes(...nodes) + builder.removeNodeAt(node!.point) + expect(builder.getNodeAt(node!.point)).toBeUndefined() + }) + ) + }) + + it('should return node when there is a node at a specified point', () => { + fc.assert( + fc.property(nodesArbitrary, (nodes) => { + const node = nodes.at(-1) + fc.pre(node != null) + + const builder = new OmniGraphBuilder() + + builder.addNodes(...nodes) + expect(builder.getNodeAt(node!.point)).toBe(node) + }) + ) + }) + }) + + describe('getEdgeAt', () => { + it('should return undefined when there are no edges', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(vectorArbitrary, (vector) => { + expect(builder.getEdgeAt(vector)).toBeUndefined() + }) + ) + }) + + it('should return undefined when there are no edges at a specified vector', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder + .addNodes(...nodes) + .addEdges(...edges) + .removeEdgeAt(edge!.vector) + expect(builder.getEdgeAt(edge!.vector)).toBeUndefined() + }) + ) + }) + + it('should return edge when there is a edge at a specified vector', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder.addNodes(...nodes).addEdges(...edges) + expect(builder.getEdgeAt(edge!.vector)).toBe(edge) + }) + ) + }) + }) + + describe('getEdgesFrom', () => { + it('should return an empty array when there are no edges', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(pointArbitrary, (point) => { + expect(builder.getEdgesFrom(point)).toEqual([]) + }) + ) + }) + + it('should return all edges that originate at a specific point', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder.addNodes(...nodes).addEdges(...edges) + + const edgesFrom = builder.edges.filter(({ vector }) => + arePointsEqual(vector.from, edge!.vector.from) + ) + expect(builder.getEdgesFrom(edge!.vector.from)).toEqual(edgesFrom) + }) + ) + }) + }) + + describe('getEdgesTo', () => { + it('should return an empty array when there are no edges', () => { + const builder = new OmniGraphBuilder() + + fc.assert( + fc.property(pointArbitrary, (point) => { + expect(builder.getEdgesTo(point)).toEqual([]) + }) + ) + }) + + it('should return all edges that end at a specific point', () => { + fc.assert( + fc.property(edgesArbitrary, nodeConfigArbitrary, (edges, nodeConfig) => { + const edge = edges.at(-1) + fc.pre(edge != null) + + const builder = new OmniGraphBuilder() + const nodes = edges.flatMap(({ vector: { from, to } }) => [ + { point: from, config: nodeConfig }, + { point: to, config: nodeConfig }, + ]) + + builder.addNodes(...nodes).addEdges(...edges) + + const edgesTo = builder.edges.filter(({ vector }) => arePointsEqual(vector.to, edge!.vector.to)) + expect(builder.getEdgesTo(edge!.vector.to)).toEqual(edgesTo) + }) + ) + }) + }) + }) +}) From f4a6e5bbe6b1e345bf7568781dd40dc6a801fdc6 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 15:58:27 -0800 Subject: [PATCH 02/14] fix: Add missing tslib to ua-utils --- packages/ua-utils/package.json | 1 + yarn.lock | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/ua-utils/package.json b/packages/ua-utils/package.json index 7a33e3e5d..4412279a1 100644 --- a/packages/ua-utils/package.json +++ b/packages/ua-utils/package.json @@ -37,6 +37,7 @@ "jest": "^29.7.0", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", + "tslib": "~2.6.2", "tsup": "^7.2.0", "typescript": "^5.2.2", "zod": "^3.22.4" diff --git a/yarn.lock b/yarn.lock index 6cdcc8fca..f57119a33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11335,7 +11335,7 @@ tslib@^1.8.1, tslib@^1.9.3: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.0: +tslib@^2.0.0, tslib@^2.0.3, tslib@^2.4.1, tslib@^2.5.0, tslib@^2.6.0, tslib@~2.6.2: version "2.6.2" resolved "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz" integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== From 32a3b066a68f71406a701ea3f8755b5b7516c00c Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 15:58:56 -0800 Subject: [PATCH 03/14] chore: Add reconnect method to builder --- packages/ua-utils/src/omnigraph/builder.ts | 35 ++++++- .../ua-utils/test/omnigraph/builder.test.ts | 98 ++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/packages/ua-utils/src/omnigraph/builder.ts b/packages/ua-utils/src/omnigraph/builder.ts index 12c28a8a3..18697e47a 100644 --- a/packages/ua-utils/src/omnigraph/builder.ts +++ b/packages/ua-utils/src/omnigraph/builder.ts @@ -1,5 +1,5 @@ import assert from 'assert' -import { arePointsEqual, serializePoint, serializeVector } from './coordinates' +import { arePointsEqual, areSameEndpoint, serializePoint, serializeVector } from './coordinates' import type { OmniEdge, OmniGraph, OmniNode, OmniPoint, OmniVector } from './types' export class OmniGraphBuilder { @@ -57,6 +57,23 @@ export class OmniGraphBuilder { return this.#edges.delete(serializeVector(vector)), this } + reconnect(r: Reconnector): this { + const nodes = this.nodes + + return ( + nodes.forEach((fromNode) => + nodes.forEach((toNode) => { + const existingEdge = this.getEdgeAt({ from: fromNode.point, to: toNode.point }) + const newEdge = r(fromNode, toNode, existingEdge) + + if (newEdge != null) this.addEdges(newEdge) + else if (existingEdge != null) this.removeEdgeAt(existingEdge.vector) + }) + ), + this + ) + } + // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' @@ -108,3 +125,19 @@ export class OmniGraphBuilder { } } } + +export type Reconnector = ( + from: OmniNode, + to: OmniNode, + edge: OmniEdge | undefined +) => OmniEdge | undefined + +export const ignoreLoopback = + (r: Reconnector): Reconnector => + (from, to, edge) => + areSameEndpoint(from.point, to.point) ? undefined : r(from, to, edge) + +export const loopbackOnly = + (r: Reconnector): Reconnector => + (from, to, edge) => + areSameEndpoint(from.point, to.point) ? r(from, to, edge) : undefined diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts index e57362f78..e3581ae3f 100644 --- a/packages/ua-utils/test/omnigraph/builder.test.ts +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -1,6 +1,12 @@ import fc from 'fast-check' -import { createNodeArbitrary, createEdgeArbitrary, pointArbitrary, vectorArbitrary } from '../__utils__/arbitraries' -import { OmniGraphBuilder } from '@/omnigraph/builder' +import { + createNodeArbitrary, + createEdgeArbitrary, + pointArbitrary, + vectorArbitrary, + endpointArbitrary, +} from '../__utils__/arbitraries' +import { OmniGraphBuilder, ignoreLoopback, loopbackOnly } from '@/omnigraph/builder' import { arePointsEqual, areVectorsEqual } from '@/omnigraph' describe('omnigraph/builder', () => { @@ -449,4 +455,92 @@ describe('omnigraph/builder', () => { }) }) }) + + describe('reconnectors', () => { + describe('ignoreLoopback', () => { + describe('when eids match', () => { + it('should return undefined and not call reconnector', () => { + fc.assert( + fc.property(nodeArbitrary, nodeArbitrary, endpointArbitrary, (nodeA, nodeB, eid) => { + const reconnector = jest.fn() + + const nodeAWithEid = { ...nodeA, point: { ...nodeA.point, eid } } + const nodeBWithEid = { ...nodeB, point: { ...nodeB.point, eid } } + + const newEdge = ignoreLoopback(reconnector)(nodeAWithEid, nodeBWithEid, undefined) + expect(newEdge).toBeUndefined() + expect(reconnector).not.toHaveBeenCalled() + }) + ) + }) + }) + + describe('when eids differ', () => { + it('should call reconnector and return its value', () => { + fc.assert( + fc.property( + nodeArbitrary, + nodeArbitrary, + edgeArbitrary, + edgeArbitrary, + (nodeA, nodeB, edge, reconnectedEdge) => { + fc.pre(nodeA.point.eid !== nodeB.point.eid) + + const reconnector = jest.fn().mockReturnValue(reconnectedEdge) + const newEdge = ignoreLoopback(reconnector)(nodeA, nodeB, edge) + + expect(reconnector).toHaveBeenCalledWith(nodeA, nodeB, edge) + expect(newEdge).toBe(reconnectedEdge) + } + ) + ) + }) + }) + }) + + describe('loopbackOnly', () => { + describe('when eids match', () => { + it('should call reconnector and return its value', () => { + fc.assert( + fc.property( + nodeArbitrary, + nodeArbitrary, + endpointArbitrary, + edgeArbitrary, + edgeArbitrary, + (nodeA, nodeB, eid, edge, reconnectedEdge) => { + fc.pre(nodeA.point.eid !== nodeB.point.eid) + + const reconnector = jest.fn().mockReturnValue(reconnectedEdge) + const nodeAWithEid = { ...nodeA, point: { ...nodeA.point, eid } } + const nodeBWithEid = { ...nodeB, point: { ...nodeB.point, eid } } + + const newEdge = loopbackOnly(reconnector)(nodeAWithEid, nodeBWithEid, edge) + + expect(newEdge).toBe(reconnectedEdge) + expect(reconnector).toHaveBeenCalledWith(nodeAWithEid, nodeBWithEid, edge) + expect(newEdge).toBe(reconnectedEdge) + } + ) + ) + }) + }) + + describe('when eids differ', () => { + it('should return undefined and not call reconnector', () => { + fc.assert( + fc.property(nodeArbitrary, nodeArbitrary, (nodeA, nodeB) => { + fc.pre(nodeA.point.eid !== nodeB.point.eid) + + const reconnector = jest.fn() + + const newEdge = loopbackOnly(reconnector)(nodeA, nodeB, undefined) + expect(newEdge).toBeUndefined() + expect(reconnector).not.toHaveBeenCalled() + }) + ) + }) + }) + }) + }) }) From d1beef643bd5bc5fd20c7a55be629ff8fd932e3d Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 16:23:31 -0800 Subject: [PATCH 04/14] feat: Add reconnect method --- packages/ua-utils/src/omnigraph/builder.ts | 2 +- .../ua-utils/test/omnigraph/builder.test.ts | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/packages/ua-utils/src/omnigraph/builder.ts b/packages/ua-utils/src/omnigraph/builder.ts index 18697e47a..af4f9c1bf 100644 --- a/packages/ua-utils/src/omnigraph/builder.ts +++ b/packages/ua-utils/src/omnigraph/builder.ts @@ -66,8 +66,8 @@ export class OmniGraphBuilder { const existingEdge = this.getEdgeAt({ from: fromNode.point, to: toNode.point }) const newEdge = r(fromNode, toNode, existingEdge) + if (existingEdge != null) this.removeEdgeAt(existingEdge.vector) if (newEdge != null) this.addEdges(newEdge) - else if (existingEdge != null) this.removeEdgeAt(existingEdge.vector) }) ), this diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts index e3581ae3f..ab55dee92 100644 --- a/packages/ua-utils/test/omnigraph/builder.test.ts +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -294,6 +294,45 @@ describe('omnigraph/builder', () => { ) }) }) + + describe('reconnect', () => { + it('should return self', () => { + const builder = new OmniGraphBuilder() + const reconnector = jest.fn() + + expect(builder.reconnect(reconnector)).toBe(builder) + }) + + it('should not call reconnector when there are no nodes', () => { + const builder = new OmniGraphBuilder() + const reconnector = jest.fn() + + builder.reconnect(reconnector) + + expect(builder.nodes).toEqual([]) + expect(builder.edges).toEqual([]) + expect(reconnector).not.toHaveBeenCalled() + }) + + it('should call reconnector for every node combination', () => { + fc.assert( + fc.property(nodesArbitrary, (nodes) => { + const builder = new OmniGraphBuilder() + const reconnector = jest.fn() + + builder.addNodes(...nodes) + builder.reconnect(reconnector) + expect(reconnector).toHaveBeenCalledTimes(builder.nodes.length * builder.nodes.length) + + for (const nodeA of builder.nodes) { + for (const nodeB of builder.nodes) { + expect(reconnector).toHaveBeenCalledWith(nodeA, nodeB, undefined) + } + } + }) + ) + }) + }) }) describe('accessor methods', () => { From 62c5fda8db030f099f61530865c3a054c934fb8a Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 16:51:20 -0800 Subject: [PATCH 05/14] Add ua-utils-evm-hardhat-builder --- .../test/builder.test.ts | 93 +++++++++++++++++++ packages/ua-utils-evm-hardhat/package.json | 11 ++- packages/ua-utils-evm-hardhat/src/index.ts | 1 + .../src/internal/assertions.ts | 6 ++ .../src/omnigraph/builder.ts | 40 ++++++++ .../src/omnigraph/coordinates.ts | 27 ++++++ .../src/omnigraph/index.ts | 2 + packages/ua-utils-evm-hardhat/tsup.config.ts | 32 +++++-- 8 files changed, 200 insertions(+), 12 deletions(-) create mode 100644 packages/ua-utils-evm-hardhat-test/test/builder.test.ts create mode 100644 packages/ua-utils-evm-hardhat/src/index.ts create mode 100644 packages/ua-utils-evm-hardhat/src/internal/assertions.ts create mode 100644 packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts create mode 100644 packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts create mode 100644 packages/ua-utils-evm-hardhat/src/omnigraph/index.ts diff --git a/packages/ua-utils-evm-hardhat-test/test/builder.test.ts b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts new file mode 100644 index 000000000..9659c49ae --- /dev/null +++ b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts @@ -0,0 +1,93 @@ +import { expect } from 'chai' +import hre from 'hardhat' +import { describe } from 'mocha' +import { eidAndDeploymentToPoint, OmniGraphBuilderHardhat } from '@layerzerolabs/ua-utils-evm-hardhat' +import { getNetworkRuntimeEnvironment } from '@layerzerolabs/utils-evm-hardhat' +import { OmniPoint } from '@layerzerolabs/ua-utils' +import assert from 'assert' + +describe('builder', () => { + it('should collect all deployed DefaultOApp contracts', async () => { + const britneyEnv = await getNetworkRuntimeEnvironment('britney') + const vengaboysEnv = await getNetworkRuntimeEnvironment('vengaboys') + + const britneyDeployment = await britneyEnv.deployments.get('DefaultOApp') + const vengaboysDeployment = await vengaboysEnv.deployments.get('DefaultOApp') + + const britneyPoint: OmniPoint = eidAndDeploymentToPoint(britneyEnv.network.config.endpointId, britneyDeployment) + const vengaboysPoint: OmniPoint = eidAndDeploymentToPoint( + vengaboysEnv.network.config.endpointId, + vengaboysDeployment + ) + + const builder = await OmniGraphBuilderHardhat.fromDeployedContract(hre, 'DefaultOApp') + + expect(builder.graph).to.eql({ + contracts: [ + { + point: vengaboysPoint, + config: undefined, + }, + { + point: britneyPoint, + config: undefined, + }, + ], + connections: [ + { + vector: { from: vengaboysPoint, to: britneyPoint }, + config: undefined, + }, + { + vector: { from: britneyPoint, to: vengaboysPoint }, + config: undefined, + }, + ], + }) + }) + + it('should collect all newly deployed DefaultOApp contracts', async () => { + const britneyEnv = await getNetworkRuntimeEnvironment('britney') + const vengaboysEnv = await getNetworkRuntimeEnvironment('vengaboys') + + const [_, deployer] = await britneyEnv.getUnnamedAccounts() + assert(deployer, 'Missing deployer') + + const britneyDeployment = await britneyEnv.deployments.deploy('DefaultOApp', { + from: deployer, + skipIfAlreadyDeployed: false, + }) + const vengaboysDeployment = await vengaboysEnv.deployments.get('DefaultOApp') + + const britneyPoint: OmniPoint = eidAndDeploymentToPoint(britneyEnv.network.config.endpointId, britneyDeployment) + const vengaboysPoint: OmniPoint = eidAndDeploymentToPoint( + vengaboysEnv.network.config.endpointId, + vengaboysDeployment + ) + + const builder = await OmniGraphBuilderHardhat.fromDeployedContract(hre, 'DefaultOApp') + + expect(builder.graph).to.eql({ + contracts: [ + { + point: vengaboysPoint, + config: undefined, + }, + { + point: britneyPoint, + config: undefined, + }, + ], + connections: [ + { + vector: { from: vengaboysPoint, to: britneyPoint }, + config: undefined, + }, + { + vector: { from: britneyPoint, to: vengaboysPoint }, + config: undefined, + }, + ], + }) + }) +}) diff --git a/packages/ua-utils-evm-hardhat/package.json b/packages/ua-utils-evm-hardhat/package.json index 6bfc18bdf..47ecbc400 100644 --- a/packages/ua-utils-evm-hardhat/package.json +++ b/packages/ua-utils-evm-hardhat/package.json @@ -40,6 +40,8 @@ "@gnosis.pm/safe-ethers-lib": "^1.0.0", "@gnosis.pm/safe-service-client": "1.1.1", "@layerzerolabs/lz-definitions": "~1.5.62", + "@layerzerolabs/ua-utils": "~0.1.0", + "@layerzerolabs/utils-evm-hardhat": "~0.0.2", "@nomiclabs/hardhat-ethers": "^2.2.3", "@types/mocha": "^10.0.6", "cli-ux": "^6.0.9", @@ -48,8 +50,10 @@ "hardhat": "^2.19.0", "hardhat-deploy": "^0.11.22", "ts-node": "^10.9.1", + "tslib": "~2.6.2", "tsup": "^8.0.1", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "zod": "^3.22.4" }, "peerDependencies": { "@gnosis.pm/safe-core-sdk": "^2.0.0", @@ -57,9 +61,12 @@ "@gnosis.pm/safe-ethers-lib": "^1.0.0", "@gnosis.pm/safe-service-client": "1.1.1", "@layerzerolabs/lz-definitions": "~1.5.62", + "@layerzerolabs/ua-utils": "~0.1.0", + "@layerzerolabs/utils-evm-hardhat": "~0.0.2", "@nomiclabs/hardhat-ethers": "^2.2.3", "ethers": "^5.5.2", "hardhat": "^2.19.0", - "hardhat-deploy": "^0.11.22" + "hardhat-deploy": "^0.11.22", + "zod": "^3.22.4" } } \ No newline at end of file diff --git a/packages/ua-utils-evm-hardhat/src/index.ts b/packages/ua-utils-evm-hardhat/src/index.ts new file mode 100644 index 000000000..ac219fd3c --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/index.ts @@ -0,0 +1 @@ +export * from './omnigraph' diff --git a/packages/ua-utils-evm-hardhat/src/internal/assertions.ts b/packages/ua-utils-evm-hardhat/src/internal/assertions.ts new file mode 100644 index 000000000..2fa885515 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/internal/assertions.ts @@ -0,0 +1,6 @@ +import assert from 'assert' +import 'hardhat-deploy/dist/src/type-extensions' +import { HardhatRuntimeEnvironment } from 'hardhat/types' + +export const assertHardhatDeploy = (hre: HardhatRuntimeEnvironment) => + assert(hre.deployments, `You don't seem to be using hardhat-deploy in your project`) diff --git a/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts b/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts new file mode 100644 index 000000000..77900d500 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts @@ -0,0 +1,40 @@ +import 'hardhat-deploy/dist/src/type-extensions' +import type { HardhatRuntimeEnvironment } from 'hardhat/types' +import { OmniGraphBuilder } from '@layerzerolabs/ua-utils' +import { createNetworkLogger, getNetworkRuntimeEnvironment } from '@layerzerolabs/utils-evm-hardhat' +import { contractNameToPoint } from './coordinates' +import { vectorFromNodes } from '@layerzerolabs/ua-utils' +import { ignoreLoopback } from '@layerzerolabs/ua-utils' + +export class OmniGraphBuilderHardhat extends OmniGraphBuilder { + static async fromDeployedContract( + hre: HardhatRuntimeEnvironment, + contractName: string + ): Promise> { + const builder = new OmniGraphBuilder() + + for (const networkName of Object.keys(hre.config.networks)) { + const logger = createNetworkLogger(networkName) + const env = await getNetworkRuntimeEnvironment(networkName) + const point = await contractNameToPoint(env, contractName) + + if (point == null) { + logger.warn(`Could not find contract '${contractName}'`) + logger.warn(``) + logger.warn(`- Make sure the contract has been deployed`) + logger.warn(`- Make sure to include the endpointId in your hardhat networks config`) + + continue + } + + builder.addNodes({ point, config: undefined }) + } + + return builder.reconnect( + ignoreLoopback((from, to) => ({ + vector: vectorFromNodes(from, to), + config: undefined, + })) + ) + } +} diff --git a/packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts b/packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts new file mode 100644 index 000000000..5a5f0e534 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/omnigraph/coordinates.ts @@ -0,0 +1,27 @@ +import 'hardhat-deploy/dist/src/type-extensions' +import '@layerzerolabs/utils-evm-hardhat/type-extensions' +import type { EndpointId } from '@layerzerolabs/lz-definitions' +import type { OmniPoint } from '@layerzerolabs/ua-utils' +import type { Deployment } from 'hardhat-deploy/types' +import { HardhatRuntimeEnvironment } from 'hardhat/types' +import { assertHardhatDeploy } from '@/internal/assertions' + +export const contractNameToPoint = async ( + hre: HardhatRuntimeEnvironment, + contractName: string +): Promise => { + assertHardhatDeploy(hre) + + const eid = hre.network.config.endpointId + if (eid == null) return undefined + + const deployment = await hre.deployments.getOrNull(contractName) + if (deployment == null) return undefined + + return eidAndDeploymentToPoint(eid, deployment) +} + +export const eidAndDeploymentToPoint = (eid: EndpointId, { address }: Deployment): OmniPoint => ({ + eid, + address, +}) diff --git a/packages/ua-utils-evm-hardhat/src/omnigraph/index.ts b/packages/ua-utils-evm-hardhat/src/omnigraph/index.ts new file mode 100644 index 000000000..afcd7e800 --- /dev/null +++ b/packages/ua-utils-evm-hardhat/src/omnigraph/index.ts @@ -0,0 +1,2 @@ +export * from './builder' +export * from './coordinates' diff --git a/packages/ua-utils-evm-hardhat/tsup.config.ts b/packages/ua-utils-evm-hardhat/tsup.config.ts index 6af24d435..c1395151f 100644 --- a/packages/ua-utils-evm-hardhat/tsup.config.ts +++ b/packages/ua-utils-evm-hardhat/tsup.config.ts @@ -1,12 +1,24 @@ import { defineConfig } from 'tsup' -export default defineConfig({ - entry: ['src/tasks/index.ts'], - outDir: './dist/tasks', - clean: true, - dts: true, - sourcemap: true, - splitting: false, - treeshake: true, - format: ['esm', 'cjs'], -}) +export default defineConfig([ + { + entry: ['src/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, + { + entry: ['src/tasks/index.ts'], + outDir: './dist/tasks', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, +]) From 62ba70744cc088be990340fc85b179e311a02b55 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 17:07:05 -0800 Subject: [PATCH 06/14] chore: Add docs to reconnect method --- packages/ua-utils/src/omnigraph/builder.ts | 26 ++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/ua-utils/src/omnigraph/builder.ts b/packages/ua-utils/src/omnigraph/builder.ts index af4f9c1bf..82ff74bf3 100644 --- a/packages/ua-utils/src/omnigraph/builder.ts +++ b/packages/ua-utils/src/omnigraph/builder.ts @@ -57,6 +57,32 @@ export class OmniGraphBuilder { return this.#edges.delete(serializeVector(vector)), this } + /** + * Reconnect is the most complex method so far. It allows for + * reconnection of all the nodes - removing, adding and updating edges. + * + * At this point the interface is quite simple, a reconnector function is + * called for every node combination (including a loopback) and an optional + * existing edge. + * + * Reconnector function returns either an edge (to keep or edit a connection) or `undefined` + * (to delete a connection). + * + * The drawback of this approach is the fact that any edge can be returned, + * not only an edge between the two nodes passed in. This flexibility might not be + * desirable for most if not all the users. On the other hand of the flexibility spectrum + * is the fact that this function can only return one edge. + * + * To address these issues we could make this function: + * + * - Return an array of edges - empty for disconnection, filled to add/keep/edit connections + * - Return an edge config only. In this case we need to provide a specific value for disconnection + * (since `undefined` can be a valid config) - so we might need to turn the return value into + * a zero/one element tuple (`[TEdgeConfig] | []`) - btw nicely solvable with `Optional` types from functional languages + * + * @param r `Reconnector` + * @returns `this` + */ reconnect(r: Reconnector): this { const nodes = this.nodes From 35b16780028b998ca9c2b4f40bbf41705cbd4122 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 17:09:00 -0800 Subject: [PATCH 07/14] chore: Remove zod from peerDependencies --- packages/ua-utils-evm-hardhat/package.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/ua-utils-evm-hardhat/package.json b/packages/ua-utils-evm-hardhat/package.json index 47ecbc400..edfdae53c 100644 --- a/packages/ua-utils-evm-hardhat/package.json +++ b/packages/ua-utils-evm-hardhat/package.json @@ -66,7 +66,6 @@ "@nomiclabs/hardhat-ethers": "^2.2.3", "ethers": "^5.5.2", "hardhat": "^2.19.0", - "hardhat-deploy": "^0.11.22", - "zod": "^3.22.4" + "hardhat-deploy": "^0.11.22" } } \ No newline at end of file From 29ad88859595b259b292fdeacc44f5e0307c3f7d Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Fri, 24 Nov 2023 17:09:45 -0800 Subject: [PATCH 08/14] chore: Remove tslib from ua-utils-evm-hardhat --- packages/ua-utils-evm-hardhat/package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ua-utils-evm-hardhat/package.json b/packages/ua-utils-evm-hardhat/package.json index edfdae53c..73e43713f 100644 --- a/packages/ua-utils-evm-hardhat/package.json +++ b/packages/ua-utils-evm-hardhat/package.json @@ -50,7 +50,6 @@ "hardhat": "^2.19.0", "hardhat-deploy": "^0.11.22", "ts-node": "^10.9.1", - "tslib": "~2.6.2", "tsup": "^8.0.1", "typescript": "^5.2.2", "zod": "^3.22.4" From 15be95b14e63c29fd2456291fa341cb9814de3dc Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 27 Nov 2023 10:47:53 -0800 Subject: [PATCH 09/14] fix: Fix tests for OmniGraphBuilder --- packages/ua-utils/test/omnigraph/builder.test.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts index ab55dee92..d49d8a8ec 100644 --- a/packages/ua-utils/test/omnigraph/builder.test.ts +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -166,7 +166,12 @@ describe('omnigraph/builder', () => { fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { const builder = new OmniGraphBuilder() - builder.addNodes({ point: edge.vector.to, config: nodeConfig }) + builder + .addNodes( + { point: edge.vector.from, config: nodeConfig }, + { point: edge.vector.to, config: nodeConfig } + ) + .removeNodeAt(edge.vector.from) expect(() => builder.addEdges(edge)).toThrow() }) @@ -178,7 +183,12 @@ describe('omnigraph/builder', () => { fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { const builder = new OmniGraphBuilder() - builder.addNodes({ point: edge.vector.from, config: nodeConfig }) + builder + .addNodes( + { point: edge.vector.from, config: nodeConfig }, + { point: edge.vector.to, config: nodeConfig } + ) + .removeNodeAt(edge.vector.to) expect(() => builder.addEdges(edge)).toThrow() }) From 07261c7c0e6b53796516c9192adb86315eb09b14 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 27 Nov 2023 14:52:15 -0800 Subject: [PATCH 10/14] chore: Allow orphaned edge ends --- packages/ua-utils/src/omnigraph/builder.ts | 47 +----------------- .../ua-utils/test/omnigraph/builder.test.ts | 48 ++----------------- 2 files changed, 6 insertions(+), 89 deletions(-) diff --git a/packages/ua-utils/src/omnigraph/builder.ts b/packages/ua-utils/src/omnigraph/builder.ts index 82ff74bf3..179f9a442 100644 --- a/packages/ua-utils/src/omnigraph/builder.ts +++ b/packages/ua-utils/src/omnigraph/builder.ts @@ -10,10 +10,8 @@ export class OmniGraphBuilder { #assertCanAddEdge(edge: OmniEdge): void { const label = serializeVector(edge.vector) const from = serializePoint(edge.vector.from) - const to = serializePoint(edge.vector.to) assert(this.getNodeAt(edge.vector.from), `Cannot add edge '${label}': '${from}' is not in the graph`) - assert(this.getNodeAt(edge.vector.to), `Cannot add edge '${label}': '${to}' is not in the graph`) } // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- @@ -46,7 +44,7 @@ export class OmniGraphBuilder { removeNodeAt(point: OmniPoint): this { return ( // First we remove all edges between this node and any other nodes - [...this.getEdgesFrom(point), ...this.getEdgesTo(point)].forEach((edge) => this.removeEdgeAt(edge.vector)), + [...this.getEdgesFrom(point)].forEach((edge) => this.removeEdgeAt(edge.vector)), // Only then we remove the node itself this.#nodes.delete(serializePoint(point)), this @@ -57,49 +55,6 @@ export class OmniGraphBuilder { return this.#edges.delete(serializeVector(vector)), this } - /** - * Reconnect is the most complex method so far. It allows for - * reconnection of all the nodes - removing, adding and updating edges. - * - * At this point the interface is quite simple, a reconnector function is - * called for every node combination (including a loopback) and an optional - * existing edge. - * - * Reconnector function returns either an edge (to keep or edit a connection) or `undefined` - * (to delete a connection). - * - * The drawback of this approach is the fact that any edge can be returned, - * not only an edge between the two nodes passed in. This flexibility might not be - * desirable for most if not all the users. On the other hand of the flexibility spectrum - * is the fact that this function can only return one edge. - * - * To address these issues we could make this function: - * - * - Return an array of edges - empty for disconnection, filled to add/keep/edit connections - * - Return an edge config only. In this case we need to provide a specific value for disconnection - * (since `undefined` can be a valid config) - so we might need to turn the return value into - * a zero/one element tuple (`[TEdgeConfig] | []`) - btw nicely solvable with `Optional` types from functional languages - * - * @param r `Reconnector` - * @returns `this` - */ - reconnect(r: Reconnector): this { - const nodes = this.nodes - - return ( - nodes.forEach((fromNode) => - nodes.forEach((toNode) => { - const existingEdge = this.getEdgeAt({ from: fromNode.point, to: toNode.point }) - const newEdge = r(fromNode, toNode, existingEdge) - - if (existingEdge != null) this.removeEdgeAt(existingEdge.vector) - if (newEdge != null) this.addEdges(newEdge) - }) - ), - this - ) - } - // .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.-. .-.- // / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ \ / / \ // `-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' `-`-' diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts index d49d8a8ec..96666ab80 100644 --- a/packages/ua-utils/test/omnigraph/builder.test.ts +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -116,7 +116,7 @@ describe('omnigraph/builder', () => { ) }) - it('should remove all edges connected to the node', () => { + it('should remove all edges starting at the node', () => { fc.assert( fc.property( nodeArbitrary, @@ -141,7 +141,7 @@ describe('omnigraph/builder', () => { .addNodes(nodeA, nodeB, nodeC) .addEdges(edgeAB, edgeAC, edgeBA, edgeBC, edgeCA, edgeCB) .removeNodeAt(nodeA.point) - expect(builder.edges).toEqual([edgeBC, edgeCB]) + expect(builder.edges).toEqual([edgeBA, edgeBC, edgeCA, edgeCB]) } ) ) @@ -178,7 +178,7 @@ describe('omnigraph/builder', () => { ) }) - it('should fail if to is not in the graph', () => { + it('should not fail if to is not in the graph', () => { fc.assert( fc.property(edgeArbitrary, nodeConfigArbitrary, (edge, nodeConfig) => { const builder = new OmniGraphBuilder() @@ -189,8 +189,9 @@ describe('omnigraph/builder', () => { { point: edge.vector.to, config: nodeConfig } ) .removeNodeAt(edge.vector.to) + .addEdges(edge) - expect(() => builder.addEdges(edge)).toThrow() + expect(builder.edges).toEqual([edge]) }) ) }) @@ -304,45 +305,6 @@ describe('omnigraph/builder', () => { ) }) }) - - describe('reconnect', () => { - it('should return self', () => { - const builder = new OmniGraphBuilder() - const reconnector = jest.fn() - - expect(builder.reconnect(reconnector)).toBe(builder) - }) - - it('should not call reconnector when there are no nodes', () => { - const builder = new OmniGraphBuilder() - const reconnector = jest.fn() - - builder.reconnect(reconnector) - - expect(builder.nodes).toEqual([]) - expect(builder.edges).toEqual([]) - expect(reconnector).not.toHaveBeenCalled() - }) - - it('should call reconnector for every node combination', () => { - fc.assert( - fc.property(nodesArbitrary, (nodes) => { - const builder = new OmniGraphBuilder() - const reconnector = jest.fn() - - builder.addNodes(...nodes) - builder.reconnect(reconnector) - expect(reconnector).toHaveBeenCalledTimes(builder.nodes.length * builder.nodes.length) - - for (const nodeA of builder.nodes) { - for (const nodeB of builder.nodes) { - expect(reconnector).toHaveBeenCalledWith(nodeA, nodeB, undefined) - } - } - }) - ) - }) - }) }) describe('accessor methods', () => { From 861bf420bcf6259edc39aab0b9376f8b9b0a0146 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 27 Nov 2023 14:52:53 -0800 Subject: [PATCH 11/14] chore: Remove reconnectors --- packages/ua-utils/src/omnigraph/builder.ts | 18 +--- .../ua-utils/test/omnigraph/builder.test.ts | 90 +------------------ 2 files changed, 2 insertions(+), 106 deletions(-) diff --git a/packages/ua-utils/src/omnigraph/builder.ts b/packages/ua-utils/src/omnigraph/builder.ts index 179f9a442..b205bb34a 100644 --- a/packages/ua-utils/src/omnigraph/builder.ts +++ b/packages/ua-utils/src/omnigraph/builder.ts @@ -1,5 +1,5 @@ import assert from 'assert' -import { arePointsEqual, areSameEndpoint, serializePoint, serializeVector } from './coordinates' +import { arePointsEqual, serializePoint, serializeVector } from './coordinates' import type { OmniEdge, OmniGraph, OmniNode, OmniPoint, OmniVector } from './types' export class OmniGraphBuilder { @@ -106,19 +106,3 @@ export class OmniGraphBuilder { } } } - -export type Reconnector = ( - from: OmniNode, - to: OmniNode, - edge: OmniEdge | undefined -) => OmniEdge | undefined - -export const ignoreLoopback = - (r: Reconnector): Reconnector => - (from, to, edge) => - areSameEndpoint(from.point, to.point) ? undefined : r(from, to, edge) - -export const loopbackOnly = - (r: Reconnector): Reconnector => - (from, to, edge) => - areSameEndpoint(from.point, to.point) ? r(from, to, edge) : undefined diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts index 96666ab80..3f6b36531 100644 --- a/packages/ua-utils/test/omnigraph/builder.test.ts +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -6,7 +6,7 @@ import { vectorArbitrary, endpointArbitrary, } from '../__utils__/arbitraries' -import { OmniGraphBuilder, ignoreLoopback, loopbackOnly } from '@/omnigraph/builder' +import { OmniGraphBuilder } from '@/omnigraph/builder' import { arePointsEqual, areVectorsEqual } from '@/omnigraph' describe('omnigraph/builder', () => { @@ -466,92 +466,4 @@ describe('omnigraph/builder', () => { }) }) }) - - describe('reconnectors', () => { - describe('ignoreLoopback', () => { - describe('when eids match', () => { - it('should return undefined and not call reconnector', () => { - fc.assert( - fc.property(nodeArbitrary, nodeArbitrary, endpointArbitrary, (nodeA, nodeB, eid) => { - const reconnector = jest.fn() - - const nodeAWithEid = { ...nodeA, point: { ...nodeA.point, eid } } - const nodeBWithEid = { ...nodeB, point: { ...nodeB.point, eid } } - - const newEdge = ignoreLoopback(reconnector)(nodeAWithEid, nodeBWithEid, undefined) - expect(newEdge).toBeUndefined() - expect(reconnector).not.toHaveBeenCalled() - }) - ) - }) - }) - - describe('when eids differ', () => { - it('should call reconnector and return its value', () => { - fc.assert( - fc.property( - nodeArbitrary, - nodeArbitrary, - edgeArbitrary, - edgeArbitrary, - (nodeA, nodeB, edge, reconnectedEdge) => { - fc.pre(nodeA.point.eid !== nodeB.point.eid) - - const reconnector = jest.fn().mockReturnValue(reconnectedEdge) - const newEdge = ignoreLoopback(reconnector)(nodeA, nodeB, edge) - - expect(reconnector).toHaveBeenCalledWith(nodeA, nodeB, edge) - expect(newEdge).toBe(reconnectedEdge) - } - ) - ) - }) - }) - }) - - describe('loopbackOnly', () => { - describe('when eids match', () => { - it('should call reconnector and return its value', () => { - fc.assert( - fc.property( - nodeArbitrary, - nodeArbitrary, - endpointArbitrary, - edgeArbitrary, - edgeArbitrary, - (nodeA, nodeB, eid, edge, reconnectedEdge) => { - fc.pre(nodeA.point.eid !== nodeB.point.eid) - - const reconnector = jest.fn().mockReturnValue(reconnectedEdge) - const nodeAWithEid = { ...nodeA, point: { ...nodeA.point, eid } } - const nodeBWithEid = { ...nodeB, point: { ...nodeB.point, eid } } - - const newEdge = loopbackOnly(reconnector)(nodeAWithEid, nodeBWithEid, edge) - - expect(newEdge).toBe(reconnectedEdge) - expect(reconnector).toHaveBeenCalledWith(nodeAWithEid, nodeBWithEid, edge) - expect(newEdge).toBe(reconnectedEdge) - } - ) - ) - }) - }) - - describe('when eids differ', () => { - it('should return undefined and not call reconnector', () => { - fc.assert( - fc.property(nodeArbitrary, nodeArbitrary, (nodeA, nodeB) => { - fc.pre(nodeA.point.eid !== nodeB.point.eid) - - const reconnector = jest.fn() - - const newEdge = loopbackOnly(reconnector)(nodeA, nodeB, undefined) - expect(newEdge).toBeUndefined() - expect(reconnector).not.toHaveBeenCalled() - }) - ) - }) - }) - }) - }) }) From 2fcded44291e6bec56c6e877c83fc62bccb41ec4 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 27 Nov 2023 15:01:51 -0800 Subject: [PATCH 12/14] chore: Remove reconnection from OmniGraphBuilderHardhat --- .../test/builder.test.ts | 22 ++----------------- .../src/omnigraph/builder.ts | 9 +------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/packages/ua-utils-evm-hardhat-test/test/builder.test.ts b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts index 9659c49ae..9457e5a18 100644 --- a/packages/ua-utils-evm-hardhat-test/test/builder.test.ts +++ b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts @@ -33,16 +33,7 @@ describe('builder', () => { config: undefined, }, ], - connections: [ - { - vector: { from: vengaboysPoint, to: britneyPoint }, - config: undefined, - }, - { - vector: { from: britneyPoint, to: vengaboysPoint }, - config: undefined, - }, - ], + connections: [], }) }) @@ -78,16 +69,7 @@ describe('builder', () => { config: undefined, }, ], - connections: [ - { - vector: { from: vengaboysPoint, to: britneyPoint }, - config: undefined, - }, - { - vector: { from: britneyPoint, to: vengaboysPoint }, - config: undefined, - }, - ], + connections: [], }) }) }) diff --git a/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts b/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts index 77900d500..2f8777417 100644 --- a/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts +++ b/packages/ua-utils-evm-hardhat/src/omnigraph/builder.ts @@ -3,8 +3,6 @@ import type { HardhatRuntimeEnvironment } from 'hardhat/types' import { OmniGraphBuilder } from '@layerzerolabs/ua-utils' import { createNetworkLogger, getNetworkRuntimeEnvironment } from '@layerzerolabs/utils-evm-hardhat' import { contractNameToPoint } from './coordinates' -import { vectorFromNodes } from '@layerzerolabs/ua-utils' -import { ignoreLoopback } from '@layerzerolabs/ua-utils' export class OmniGraphBuilderHardhat extends OmniGraphBuilder { static async fromDeployedContract( @@ -30,11 +28,6 @@ export class OmniGraphBuilderHardhat extends OmniGraph builder.addNodes({ point, config: undefined }) } - return builder.reconnect( - ignoreLoopback((from, to) => ({ - vector: vectorFromNodes(from, to), - config: undefined, - })) - ) + return builder } } From a54b8c8e364c55feef5087546d74ceebe9bd8b0a Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 27 Nov 2023 16:01:59 -0800 Subject: [PATCH 13/14] chore: More explicit test for builder when deployments change --- .../test/builder.test.ts | 44 ++++++++++++++----- .../ua-utils/test/omnigraph/builder.test.ts | 8 +--- 2 files changed, 35 insertions(+), 17 deletions(-) diff --git a/packages/ua-utils-evm-hardhat-test/test/builder.test.ts b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts index 9457e5a18..9d9b2f10a 100644 --- a/packages/ua-utils-evm-hardhat-test/test/builder.test.ts +++ b/packages/ua-utils-evm-hardhat-test/test/builder.test.ts @@ -3,7 +3,7 @@ import hre from 'hardhat' import { describe } from 'mocha' import { eidAndDeploymentToPoint, OmniGraphBuilderHardhat } from '@layerzerolabs/ua-utils-evm-hardhat' import { getNetworkRuntimeEnvironment } from '@layerzerolabs/utils-evm-hardhat' -import { OmniPoint } from '@layerzerolabs/ua-utils' +import { arePointsEqual, OmniPoint } from '@layerzerolabs/ua-utils' import assert from 'assert' describe('builder', () => { @@ -11,6 +11,9 @@ describe('builder', () => { const britneyEnv = await getNetworkRuntimeEnvironment('britney') const vengaboysEnv = await getNetworkRuntimeEnvironment('vengaboys') + assert(britneyEnv.network.config.endpointId, 'Missing endpointId on britney network') + assert(vengaboysEnv.network.config.endpointId, 'Missing endpointId on vengaboys network') + const britneyDeployment = await britneyEnv.deployments.get('DefaultOApp') const vengaboysDeployment = await vengaboysEnv.deployments.get('DefaultOApp') @@ -41,31 +44,52 @@ describe('builder', () => { const britneyEnv = await getNetworkRuntimeEnvironment('britney') const vengaboysEnv = await getNetworkRuntimeEnvironment('vengaboys') + assert(britneyEnv.network.config.endpointId, 'Missing endpointId on britney network') + assert(vengaboysEnv.network.config.endpointId, 'Missing endpointId on vengaboys network') + + const oldBritneyDeployment = await britneyEnv.deployments.get('DefaultOApp') + const oldVengaboysDeployment = await vengaboysEnv.deployments.get('DefaultOApp') + + const oldBritneyPoint: OmniPoint = eidAndDeploymentToPoint( + britneyEnv.network.config.endpointId, + oldBritneyDeployment + ) + const oldVengaboysPoint: OmniPoint = eidAndDeploymentToPoint( + vengaboysEnv.network.config.endpointId, + oldVengaboysDeployment + ) + + // First we create a builder using the redeployed contracts + const oldBuilder = await OmniGraphBuilderHardhat.fromDeployedContract(hre, 'DefaultOApp') + + // Now we redeploy one of the contracts const [_, deployer] = await britneyEnv.getUnnamedAccounts() assert(deployer, 'Missing deployer') - const britneyDeployment = await britneyEnv.deployments.deploy('DefaultOApp', { + await britneyEnv.deployments.delete('DefaultOApp') + const newBritneyDeployment = await britneyEnv.deployments.deploy('DefaultOApp', { from: deployer, - skipIfAlreadyDeployed: false, }) - const vengaboysDeployment = await vengaboysEnv.deployments.get('DefaultOApp') - const britneyPoint: OmniPoint = eidAndDeploymentToPoint(britneyEnv.network.config.endpointId, britneyDeployment) - const vengaboysPoint: OmniPoint = eidAndDeploymentToPoint( - vengaboysEnv.network.config.endpointId, - vengaboysDeployment + const newBritneyPoint: OmniPoint = eidAndDeploymentToPoint( + britneyEnv.network.config.endpointId, + newBritneyDeployment ) + // As a sanity check, we make sure the deployment has actually changed + expect(arePointsEqual(newBritneyPoint, oldBritneyPoint)).to.be.false + const builder = await OmniGraphBuilderHardhat.fromDeployedContract(hre, 'DefaultOApp') + expect(oldBuilder.graph).not.to.eql(builder.graph) expect(builder.graph).to.eql({ contracts: [ { - point: vengaboysPoint, + point: oldVengaboysPoint, config: undefined, }, { - point: britneyPoint, + point: newBritneyPoint, config: undefined, }, ], diff --git a/packages/ua-utils/test/omnigraph/builder.test.ts b/packages/ua-utils/test/omnigraph/builder.test.ts index 3f6b36531..ed6ff01aa 100644 --- a/packages/ua-utils/test/omnigraph/builder.test.ts +++ b/packages/ua-utils/test/omnigraph/builder.test.ts @@ -1,11 +1,5 @@ import fc from 'fast-check' -import { - createNodeArbitrary, - createEdgeArbitrary, - pointArbitrary, - vectorArbitrary, - endpointArbitrary, -} from '../__utils__/arbitraries' +import { createNodeArbitrary, createEdgeArbitrary, pointArbitrary, vectorArbitrary } from '../__utils__/arbitraries' import { OmniGraphBuilder } from '@/omnigraph/builder' import { arePointsEqual, areVectorsEqual } from '@/omnigraph' From 614bbd0cb1fc026c350d47e8ed5a92a700da7eb9 Mon Sep 17 00:00:00 2001 From: Jan Nanista Date: Mon, 27 Nov 2023 16:28:19 -0800 Subject: [PATCH 14/14] chore: Speed up docker EVM node healthcheck --- .../ua-utils-evm-hardhat-test/docker-compose.templates.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml b/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml index bd0db5985..eafb845dd 100644 --- a/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml +++ b/packages/ua-utils-evm-hardhat-test/docker-compose.templates.yaml @@ -30,4 +30,6 @@ services: - 8545 command: ["npx", "hardhat", "node", "--hostname", "0.0.0.0", "--no-deploy"] healthcheck: + interval: 2s + retries: 10 test: ["CMD", "curl", "-f", "http://0.0.0.0:8545/"]