From dcf1bbbb1e008a4755c49fcab33eba3002d711d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1n=20Jakub=20Nani=C5=A1ta?= Date: Wed, 13 Dec 2023 15:32:39 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9A=20OmniGraph=E2=84=A2=20Delusional?= =?UTF-8?q?=20code=20of=20the=20utterly=20deranged=20(#112)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/omnigraph/coordinates.ts | 6 +- .../utils-evm-hardhat/src/omnigraph/schema.ts | 5 +- .../src/omnigraph/transformations.ts | 10 +- .../utils-evm-hardhat/src/omnigraph/types.ts | 17 +- .../test/omnigraph/coordinates.test.ts | 12 +- .../test/omnigraph/transformations.test.ts | 52 +++-- packages/utils/jest.config.js | 1 + packages/utils/jest.setup.js | 3 + packages/utils/package.json | 2 + packages/utils/src/common/index.ts | 1 + packages/utils/src/common/promise.ts | 74 +++++++ packages/utils/src/index.ts | 1 + packages/utils/test/common/promise.test.ts | 194 ++++++++++++++++++ yarn.lock | 12 +- 14 files changed, 354 insertions(+), 36 deletions(-) create mode 100644 packages/utils/jest.setup.js create mode 100644 packages/utils/src/common/index.ts create mode 100644 packages/utils/src/common/promise.ts create mode 100644 packages/utils/test/common/promise.test.ts diff --git a/packages/utils-evm-hardhat/src/omnigraph/coordinates.ts b/packages/utils-evm-hardhat/src/omnigraph/coordinates.ts index 64745feb9..69e050ecc 100644 --- a/packages/utils-evm-hardhat/src/omnigraph/coordinates.ts +++ b/packages/utils-evm-hardhat/src/omnigraph/coordinates.ts @@ -1,11 +1,10 @@ import type { OmniPoint } from '@layerzerolabs/utils' -import type { HardhatRuntimeEnvironment } from 'hardhat/types' import pMemoize from 'p-memoize' import { OmniContract } from '@layerzerolabs/utils-evm' import { Contract } from '@ethersproject/contracts' import assert from 'assert' import { OmniContractFactoryHardhat, OmniDeployment } from './types' -import { createNetworkEnvironmentFactory, getDefaultRuntimeEnvironment } from '@/runtime' +import { createNetworkEnvironmentFactory } from '@/runtime' import { assertHardhatDeploy } from '@/internal/assertions' export const omniDeploymentToPoint = ({ eid, deployment }: OmniDeployment): OmniPoint => ({ @@ -19,8 +18,7 @@ export const omniDeploymentToContract = ({ eid, deployment }: OmniDeployment): O }) export const createContractFactory = ( - hre: HardhatRuntimeEnvironment = getDefaultRuntimeEnvironment(), - environmentFactory = createNetworkEnvironmentFactory(hre) + environmentFactory = createNetworkEnvironmentFactory() ): OmniContractFactoryHardhat => { return pMemoize(async ({ eid, address, contractName }) => { const env = await environmentFactory(eid) diff --git a/packages/utils-evm-hardhat/src/omnigraph/schema.ts b/packages/utils-evm-hardhat/src/omnigraph/schema.ts index f9bc4489b..41f47c63b 100644 --- a/packages/utils-evm-hardhat/src/omnigraph/schema.ts +++ b/packages/utils-evm-hardhat/src/omnigraph/schema.ts @@ -1,6 +1,6 @@ import { z } from 'zod' import { EndpointIdSchema, OmniPointSchema } from '@layerzerolabs/utils' -import type { OmniEdgeHardhat, OmniGraphHardhat, OmniNodeHardhat, OmniPointHardhat } from './types' +import type { OmniEdgeHardhat, OmniGraphHardhat, OmniNodeHardhat, OmniPointHardhat, WithContractName } from './types' export const OmniPointHardhatSchema: z.ZodSchema = z.object({ eid: EndpointIdSchema, @@ -57,3 +57,6 @@ export const createOmniGraphHardhatSchema = (value: T): value is WithContractName => + 'contractName' in value && typeof value.contractName === 'string' diff --git a/packages/utils-evm-hardhat/src/omnigraph/transformations.ts b/packages/utils-evm-hardhat/src/omnigraph/transformations.ts index c79fc3899..3bc5235d0 100644 --- a/packages/utils-evm-hardhat/src/omnigraph/transformations.ts +++ b/packages/utils-evm-hardhat/src/omnigraph/transformations.ts @@ -9,6 +9,7 @@ import type { OmniNodeHardhat, OmniPointHardhatTransformer, } from './types' +import { parallel } from '@layerzerolabs/utils' /** * Create a function capable of transforming `OmniPointHardhat` to a regular `OmniPoint` @@ -56,9 +57,10 @@ export const createOmniEdgeHardhatTransformer = export const createOmniGraphHardhatTransformer = ( nodeTransformer = createOmniNodeHardhatTransformer(), - edgeTransformer = createOmniEdgeHardhatTransformer() + edgeTransformer = createOmniEdgeHardhatTransformer(), + applicative = parallel ): OmniGraphHardhatTransformer => - async (graph) => ({ - contracts: await Promise.all(graph.contracts.map(nodeTransformer)), - connections: await Promise.all(graph.connections.map(edgeTransformer)), + async ({ contracts, connections }) => ({ + contracts: await applicative(contracts.map((contract) => () => nodeTransformer(contract))), + connections: await applicative(connections.map((connection) => () => edgeTransformer(connection))), }) diff --git a/packages/utils-evm-hardhat/src/omnigraph/types.ts b/packages/utils-evm-hardhat/src/omnigraph/types.ts index 843e8ccfc..76bd86a09 100644 --- a/packages/utils-evm-hardhat/src/omnigraph/types.ts +++ b/packages/utils-evm-hardhat/src/omnigraph/types.ts @@ -1,5 +1,5 @@ -import type { OmniGraph, OmniPoint, WithEid, WithOptionals } from '@layerzerolabs/utils' -import type { OmniContract } from '@layerzerolabs/utils-evm' +import type { Factory, OmniGraph, OmniPoint, WithEid, WithOptionals } from '@layerzerolabs/utils' +import type { OmniContractFactory } from '@layerzerolabs/utils-evm' import type { Deployment } from 'hardhat-deploy/dist/types' /** @@ -21,6 +21,8 @@ export type OmniPointHardhat = WithEid<{ address?: string | null }> +export type WithContractName = T & { contractName: string } + /** * Hardhat-specific variation of `OmniNode` that uses `OmniPointHardhat` * instead of `OmniPoint` to specify the contract coordinates @@ -49,10 +51,11 @@ export interface OmniGraphHardhat connections: OmniEdgeHardhat[] } -export type OmniContractFactoryHardhat = (point: OmniPointHardhat) => OmniContract | Promise +export type OmniContractFactoryHardhat = OmniContractFactory -export type OmniPointHardhatTransformer = (point: OmniPointHardhat | OmniPoint) => Promise +export type OmniPointHardhatTransformer = Factory<[OmniPointHardhat | OmniPoint], OmniPoint> -export type OmniGraphHardhatTransformer = ( - graph: OmniGraphHardhat -) => Promise> +export type OmniGraphHardhatTransformer = Factory< + [OmniGraphHardhat], + OmniGraph +> diff --git a/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts b/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts index e7d7c56a6..82f5ea9ac 100644 --- a/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts +++ b/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts @@ -43,7 +43,7 @@ describe('omnigraph/coordinates', () => { describe('createContractFactory', () => { describe('when called with OmniPointContractName', () => { it('should reject when eid does not exist', async () => { - const contractFactory = createContractFactory(hre) + const contractFactory = createContractFactory() await expect(() => contractFactory({ eid: EndpointId.CANTO_TESTNET, contractName: 'MyContract' }) @@ -51,7 +51,7 @@ describe('omnigraph/coordinates', () => { }) it('should reject when contract has not been deployed', async () => { - const contractFactory = createContractFactory(hre) + const contractFactory = createContractFactory() await expect(() => contractFactory({ eid: EndpointId.ETHEREUM_MAINNET, contractName: 'MyContract' }) @@ -60,7 +60,7 @@ describe('omnigraph/coordinates', () => { it('should resolve when contract has been deployed', async () => { const environmentFactory = createNetworkEnvironmentFactory(hre) - const contractFactory = createContractFactory(hre, environmentFactory) + const contractFactory = createContractFactory(environmentFactory) const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET) jest.spyOn(env.deployments, 'getOrNull').mockResolvedValue({ @@ -86,7 +86,7 @@ describe('omnigraph/coordinates', () => { it('should reject when eid does not exist', async () => { await fc.assert( fc.asyncProperty(evmAddressArbitrary, async (address) => { - const contractFactory = createContractFactory(hre) + const contractFactory = createContractFactory() await expect(() => contractFactory({ eid: EndpointId.CANTO_TESTNET, address }) @@ -98,7 +98,7 @@ describe('omnigraph/coordinates', () => { it('should reject when contract has not been deployed', async () => { await fc.assert( fc.asyncProperty(evmAddressArbitrary, async (address) => { - const contractFactory = createContractFactory(hre) + const contractFactory = createContractFactory() await expect(() => contractFactory({ eid: EndpointId.ETHEREUM_MAINNET, address }) @@ -111,7 +111,7 @@ describe('omnigraph/coordinates', () => { await fc.assert( fc.asyncProperty(evmAddressArbitrary, async (address) => { const environmentFactory = createNetworkEnvironmentFactory(hre) - const contractFactory = createContractFactory(hre, environmentFactory) + const contractFactory = createContractFactory(environmentFactory) const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET) jest.spyOn(env.deployments, 'getDeploymentsFromAddress').mockResolvedValue([ diff --git a/packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts b/packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts index 0b0713a8e..a2d3bc7a4 100644 --- a/packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts +++ b/packages/utils-evm-hardhat/test/omnigraph/transformations.test.ts @@ -8,7 +8,7 @@ import { } from '@/omnigraph/transformations' import { Contract } from '@ethersproject/contracts' import { endpointArbitrary, evmAddressArbitrary, nullableArbitrary, pointArbitrary } from '@layerzerolabs/test-utils' -import { isOmniPoint } from '@layerzerolabs/utils' +import { isOmniPoint, parallel, sequence } from '@layerzerolabs/utils' describe('omnigraph/transformations', () => { const pointHardhatArbitrary = fc.record({ @@ -17,6 +17,17 @@ describe('omnigraph/transformations', () => { address: nullableArbitrary(evmAddressArbitrary), }) + const nodeHardhatArbitrary = fc.record({ + contract: pointHardhatArbitrary, + config: fc.anything(), + }) + + const edgeHardhatArbitrary = fc.record({ + from: pointHardhatArbitrary, + to: pointHardhatArbitrary, + config: fc.anything(), + }) + describe('createOmniPointHardhatTransformer', () => { it('should pass the original value if contract is already an OmniPoint', async () => { await fc.assert( @@ -128,17 +139,6 @@ describe('omnigraph/transformations', () => { }) it('should call the nodeTransformer and edgeTransformer for every node and edge and return the result', async () => { - const nodeHardhatArbitrary = fc.record({ - contract: pointHardhatArbitrary, - config: fc.anything(), - }) - - const edgeHardhatArbitrary = fc.record({ - from: pointHardhatArbitrary, - to: pointHardhatArbitrary, - config: fc.anything(), - }) - await fc.assert( fc.asyncProperty( fc.array(nodeHardhatArbitrary), @@ -156,5 +156,33 @@ describe('omnigraph/transformations', () => { ) ) }) + + it('should support sequential applicative', async () => { + await fc.assert( + fc.asyncProperty( + fc.array(nodeHardhatArbitrary), + fc.array(edgeHardhatArbitrary), + async (contracts, connections) => { + const nodeTransformer = jest.fn().mockImplementation(async (node) => ({ node })) + const edgeTransformer = jest.fn().mockImplementation(async (edge) => ({ edge })) + const transformerSequential = createOmniGraphHardhatTransformer( + nodeTransformer, + edgeTransformer, + sequence + ) + const transformerParallel = createOmniGraphHardhatTransformer( + nodeTransformer, + edgeTransformer, + parallel + ) + + const graphSequential = await transformerSequential({ contracts, connections }) + const graphParallel = await transformerParallel({ contracts, connections }) + + expect(graphSequential).toEqual(graphParallel) + } + ) + ) + }) }) }) diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index 4851c7ff2..10f10f1ad 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -3,6 +3,7 @@ module.exports = { preset: 'ts-jest', cache: false, testEnvironment: 'node', + setupFilesAfterEnv: ['/jest.setup.js'], moduleNameMapper: { '^@/(.*)$': '/src/$1', }, diff --git a/packages/utils/jest.setup.js b/packages/utils/jest.setup.js new file mode 100644 index 000000000..84d638324 --- /dev/null +++ b/packages/utils/jest.setup.js @@ -0,0 +1,3 @@ +// add all jest-extended matchers +// eslint-disable-next-line @typescript-eslint/no-var-requires +expect.extend(require('jest-extended')); diff --git a/packages/utils/package.json b/packages/utils/package.json index 4bf2af289..5bd61fcce 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -34,7 +34,9 @@ "@layerzerolabs/lz-definitions": "~1.5.72", "@layerzerolabs/test-utils": "~0.0.1", "@types/jest": "^29.5.10", + "fast-check": "^3.14.0", "jest": "^29.7.0", + "jest-extended": "^4.0.2", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", "tslib": "~2.6.2", diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts new file mode 100644 index 000000000..d4aeb6d16 --- /dev/null +++ b/packages/utils/src/common/index.ts @@ -0,0 +1 @@ +export * from './promise' diff --git a/packages/utils/src/common/promise.ts b/packages/utils/src/common/promise.ts new file mode 100644 index 000000000..4dda47a63 --- /dev/null +++ b/packages/utils/src/common/promise.ts @@ -0,0 +1,74 @@ +import { Factory } from '@/types' +import assert from 'assert' + +/** + * Helper type for argumentless factories a.k.a. tasks + */ +type Task = Factory<[], T> + +/** + * Executes tasks in sequence, waiting for each one to finish before starting the next one + * + * Will resolve with the output of all tasks or reject with the first rejection. + * + * @param {Task[]} tasks + * @returns {Promise} + */ +export const sequence = async (tasks: Task[]): Promise => { + const collector: T[] = [] + + for (const task of tasks) { + collector.push(await task()) + } + + return collector +} + +/** + * Executes tasks in parallel + * + * Will resolve with the output of all tasks or reject with the any rejection. + * + * @param {Task[]} tasks + * @returns {Promise} + */ +export const parallel = async (tasks: Task[]): Promise => await Promise.all(tasks.map((task) => task())) + +/** + * Executes tasks in a sequence until one resolves. + * + * Will resolve with the output of the first task that resolves + * or reject with the last rejection. + * + * Will reject immediatelly if no tasks have been passed + * + * @param {Task[]} tasks + * @returns {Promise} + */ +export const first = async (tasks: Task[]): Promise => { + assert(tasks.length !== 0, `Must have at least one task for first()`) + + let lastError: unknown + + for (const task of tasks) { + try { + return await task() + } catch (error) { + lastError = error + } + } + + throw lastError +} + +/** + * Helper utility for currying first() - creating a function + * that behaves like first() but accepts arguments that will be passed to the factory functions + * + * @param {Factory[]} factories + * @returns {Factory} + */ +export const firstFactory = + (...factories: Factory[]): Factory => + async (...input) => + await first(factories.map((factory) => () => factory(...input))) diff --git a/packages/utils/src/index.ts b/packages/utils/src/index.ts index a8fe8c866..45a48c4b8 100644 --- a/packages/utils/src/index.ts +++ b/packages/utils/src/index.ts @@ -1,3 +1,4 @@ +export * from './common' export * from './omnigraph' export * from './transactions' export * from './types' diff --git a/packages/utils/test/common/promise.test.ts b/packages/utils/test/common/promise.test.ts new file mode 100644 index 000000000..8f958507f --- /dev/null +++ b/packages/utils/test/common/promise.test.ts @@ -0,0 +1,194 @@ +/// + +import fc from 'fast-check' +import { first, firstFactory, sequence } from '@/common/promise' + +describe('common/promise', () => { + const valueArbitrary = fc.anything() + const valuesArbitrary = fc.array(valueArbitrary) + + describe('sequence', () => { + it('should resolve with an empty array if called with an empty array', async () => { + expect(await sequence([])).toEqual([]) + }) + + it('should resolve if all resolve', async () => { + await fc.assert( + fc.asyncProperty(valuesArbitrary, async (values) => { + const tasks = values.map((value) => jest.fn().mockResolvedValue(value)) + + expect(await sequence(tasks)).toEqual(values) + + // Make sure all the tasks got called + for (const task of tasks) { + expect(task).toHaveBeenCalledTimes(1) + expect(task).toHaveBeenCalledWith() + } + }) + ) + }) + + it('should reject with the first rejection', async () => { + await fc.assert( + fc.asyncProperty(valuesArbitrary, valueArbitrary, valuesArbitrary, async (values1, error, values2) => { + const tasks1 = values1.map((value) => jest.fn().mockResolvedValue(value)) + const failingTask = jest.fn().mockRejectedValue(error) + const tasks2 = values2.map((value) => jest.fn().mockResolvedValue(value)) + const tasks = [...tasks1, failingTask, ...tasks2] + + await expect(sequence(tasks)).rejects.toBe(error) + + // Make sure the first batch got called + for (const task of tasks1) { + expect(task).toHaveBeenCalledTimes(1) + expect(task).toHaveBeenCalledWith() + } + + // Make sure the failing task got called + expect(failingTask).toHaveBeenCalledTimes(1) + expect(failingTask).toHaveBeenCalledWith() + + // Make sure the second batch didn't get called + for (const task of tasks2) { + expect(task).not.toHaveBeenCalled() + } + }) + ) + }) + + it('should execute one by one', async () => { + await fc.assert( + fc.asyncProperty(valuesArbitrary, async (values) => { + fc.pre(values.length > 0) + + const tasks = values.map((value) => jest.fn().mockResolvedValue(value)) + + await sequence(tasks) + + tasks.reduce((task1, task2) => { + return expect(task1).toHaveBeenCalledBefore(task2), task2 + }) + }) + ) + }) + }) + + describe('first', () => { + it('should reject if called with no factories', async () => { + await expect(first([])).rejects.toThrow('Must have at least one task for first()') + }) + + it('should resolve if the first task resolves', async () => { + await fc.assert( + fc.asyncProperty(valueArbitrary, valuesArbitrary, async (value, errors) => { + const task = jest.fn().mockResolvedValue(value) + const tasks = errors.map((error) => jest.fn().mockRejectedValue(error)) + + expect(await first([task, ...tasks])).toBe(value) + + // Make sure none of the other factories got called + for (const factory of tasks) { + expect(factory).not.toHaveBeenCalled() + } + }) + ) + }) + + it('should resolve with the first successful task', async () => { + await fc.assert( + fc.asyncProperty(valuesArbitrary, valueArbitrary, async (errors, value) => { + const tasks = errors.map((error) => jest.fn().mockRejectedValue(error)) + const task = jest.fn().mockResolvedValue(value) + + expect(await first([...tasks, task])).toBe(value) + + // Make sure all the tasks got called + for (const factory of tasks) { + expect(factory).toHaveBeenCalledTimes(1) + } + + expect(task).toHaveBeenCalledTimes(1) + }) + ) + }) + + it('should reject with the last rejected task error', async () => { + await fc.assert( + fc.asyncProperty(valuesArbitrary, valueArbitrary, async (errors, error) => { + const tasks = errors.map((error) => jest.fn().mockRejectedValue(error)) + const task = jest.fn().mockRejectedValue(error) + + await expect(first([...tasks, task])).rejects.toBe(error) + + // Make sure all the tasks got called + for (const factory of tasks) { + expect(factory).toHaveBeenCalledTimes(1) + } + + expect(task).toHaveBeenCalledTimes(1) + }) + ) + }) + }) + + describe('firstFactory', () => { + it('should throw an error if called with no factories', async () => { + await expect(firstFactory()).rejects.toThrow('Must have at least one task for first()') + }) + + it('should resolve if the first factory resolves', async () => { + await fc.assert( + fc.asyncProperty(valueArbitrary, valuesArbitrary, async (value, values) => { + const successful = jest.fn().mockResolvedValue(value) + const successfulOther = values.map((value) => jest.fn().mockResolvedValue(value)) + const factory = firstFactory(successful, ...successfulOther) + + expect(await factory()).toBe(value) + + // Make sure none of the other factories got called + for (const factory of successfulOther) { + expect(factory).not.toHaveBeenCalled() + } + }) + ) + }) + + it('should resolve with the first successful factory', async () => { + await fc.assert( + fc.asyncProperty(valuesArbitrary, valueArbitrary, async (errors, value) => { + const failing = errors.map((error) => jest.fn().mockRejectedValue(error)) + const successful = jest.fn().mockResolvedValue(value) + const factory = firstFactory(...failing, successful) + + expect(await factory()).toBe(value) + + // Make sure all the tasks got called + for (const factory of failing) { + expect(factory).toHaveBeenCalledTimes(1) + } + }) + ) + }) + + it('should pass the input to factories', async () => { + await fc.assert( + fc.asyncProperty(valuesArbitrary, valueArbitrary, valuesArbitrary, async (errors, value, args) => { + const failing = errors.map((error) => jest.fn().mockRejectedValue(error)) + const successful = jest.fn().mockResolvedValue(value) + const factory = firstFactory(...failing, successful) + + expect(await factory(...args)).toBe(value) + + // Make sure all the tasks got called with the correct arguments + for (const factory of failing) { + expect(factory).toHaveBeenCalledTimes(1) + expect(factory).toHaveBeenCalledWith(...args) + } + + expect(successful).toHaveBeenCalledTimes(1) + expect(successful).toHaveBeenCalledWith(...args) + }) + ) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 88a0a750a..2eadd23af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7833,7 +7833,7 @@ jest-config@^29.7.0: slash "^3.0.0" strip-json-comments "^3.1.1" -jest-diff@^29.7.0: +jest-diff@^29.0.0, jest-diff@^29.7.0: version "29.7.0" resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.7.0.tgz#017934a66ebb7ecf6f205e84699be10afd70458a" integrity sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw== @@ -7873,7 +7873,15 @@ jest-environment-node@^29.7.0: jest-mock "^29.7.0" jest-util "^29.7.0" -jest-get-type@^29.6.3: +jest-extended@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/jest-extended/-/jest-extended-4.0.2.tgz#d23b52e687cedf66694e6b2d77f65e211e99e021" + integrity sha512-FH7aaPgtGYHc9mRjriS0ZEHYM5/W69tLrFTIdzm+yJgeoCmmrSB/luSfMSqWP9O29QWHPEmJ4qmU6EwsZideog== + dependencies: + jest-diff "^29.0.0" + jest-get-type "^29.0.0" + +jest-get-type@^29.0.0, jest-get-type@^29.6.3: version "29.6.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.6.3.tgz#36f499fdcea197c1045a127319c0481723908fd1" integrity sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==