From 795951b8e8e72155c1c6db7ab760bd1a0aa5bdf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A1n=20Jakub=20Nani=C5=A1ta?= Date: Thu, 7 Dec 2023 11:45:30 -0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=AA=9A=20OmniGraph=E2=84=A2=20address/byt?= =?UTF-8?q?es=20utilities=20&=20hasPeer=20OApp=20SDK=20method=20(#72)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../protocol-utils-evm/src/endpoint/sdk.ts | 11 +- packages/protocol-utils-evm/src/uln302/sdk.ts | 6 +- packages/test-utils/src/arbitraries.ts | 2 + .../contracts/DefaultOApp.sol | 4 +- packages/ua-utils-evm/src/oapp/sdk.ts | 14 +- packages/ua-utils-evm/test/oapp/sdk.test.ts | 123 ++++++++++++-- packages/ua-utils/src/oapp/config.ts | 10 +- packages/ua-utils/src/oapp/types.ts | 4 +- packages/utils-evm-hardhat/README.md | 28 ---- .../test/omnigraph/contracts.test.ts | 6 +- .../test/omnigraph/coordinates.test.ts | 6 +- packages/utils-evm/README.md | 28 ++++ packages/utils-evm/package.json | 1 + packages/utils-evm/src/address.ts | 59 ++++++- packages/utils-evm/test/address.test.ts | 151 +++++++++++++++++- packages/utils/src/types.ts | 2 + 16 files changed, 381 insertions(+), 74 deletions(-) diff --git a/packages/protocol-utils-evm/src/endpoint/sdk.ts b/packages/protocol-utils-evm/src/endpoint/sdk.ts index 6fd33e56e..3db2dc705 100644 --- a/packages/protocol-utils-evm/src/endpoint/sdk.ts +++ b/packages/protocol-utils-evm/src/endpoint/sdk.ts @@ -1,7 +1,7 @@ import type { IEndpoint } from '@layerzerolabs/protocol-utils' import { formatEid, type Address, type OmniTransaction } from '@layerzerolabs/utils' import type { EndpointId } from '@layerzerolabs/lz-definitions' -import { ignoreZero, makeZero, omniContractToPoint, type OmniContract } from '@layerzerolabs/utils-evm' +import { ignoreZero, makeZeroAddress, omniContractToPoint, type OmniContract } from '@layerzerolabs/utils-evm' export class Endpoint implements IEndpoint { constructor(public readonly contract: OmniContract) {} @@ -17,13 +17,13 @@ export class Endpoint implements IEndpoint { ): Promise { const data = this.contract.contract.interface.encodeFunctionData('setDefaultReceiveLibrary', [ eid, - makeZero(lib), + makeZeroAddress(lib), gracePeriod, ]) return { ...this.createTransaction(data), - description: `Setting default receive library for ${formatEid(eid)} to ${makeZero(lib)}`, + description: `Setting default receive library for ${formatEid(eid)} to ${makeZeroAddress(lib)}`, } } @@ -32,7 +32,10 @@ export class Endpoint implements IEndpoint { } async setDefaultSendLibrary(eid: EndpointId, lib: Address | null | undefined): Promise { - const data = this.contract.contract.interface.encodeFunctionData('setDefaultSendLibrary', [eid, makeZero(lib)]) + const data = this.contract.contract.interface.encodeFunctionData('setDefaultSendLibrary', [ + eid, + makeZeroAddress(lib), + ]) return { ...this.createTransaction(data), diff --git a/packages/protocol-utils-evm/src/uln302/sdk.ts b/packages/protocol-utils-evm/src/uln302/sdk.ts index 4117a9ee1..54928a94f 100644 --- a/packages/protocol-utils-evm/src/uln302/sdk.ts +++ b/packages/protocol-utils-evm/src/uln302/sdk.ts @@ -1,14 +1,14 @@ import type { EndpointId } from '@layerzerolabs/lz-definitions' import type { IUln302, Uln302ExecutorConfig, Uln302UlnConfig } from '@layerzerolabs/protocol-utils' import { Address, formatEid, type OmniTransaction } from '@layerzerolabs/utils' -import { omniContractToPoint, type OmniContract, makeZero } from '@layerzerolabs/utils-evm' +import { omniContractToPoint, type OmniContract, makeZeroAddress } from '@layerzerolabs/utils-evm' import { Uln302ExecutorConfigSchema, Uln302UlnConfigInputSchema, Uln302UlnConfigSchema } from './schema' export class Uln302 implements IUln302 { constructor(public readonly contract: OmniContract) {} async getUlnConfig(eid: EndpointId, address?: Address | null | undefined): Promise { - const config = await this.contract.contract.getUlnConfig(makeZero(address), eid) + const config = await this.contract.contract.getUlnConfig(makeZeroAddress(address), eid) // Now we convert the ethers-specific object into the common structure // @@ -18,7 +18,7 @@ export class Uln302 implements IUln302 { } async getExecutorConfig(eid: EndpointId, address?: Address | null | undefined): Promise { - const config = await this.contract.contract.getExecutorConfig(makeZero(address), eid) + const config = await this.contract.contract.getExecutorConfig(makeZeroAddress(address), eid) // Now we convert the ethers-specific object into the common structure // diff --git a/packages/test-utils/src/arbitraries.ts b/packages/test-utils/src/arbitraries.ts index fcc5f4d7c..58e16bd7e 100644 --- a/packages/test-utils/src/arbitraries.ts +++ b/packages/test-utils/src/arbitraries.ts @@ -6,6 +6,8 @@ export const addressArbitrary = fc.string() export const evmAddressArbitrary = fc.hexaString({ minLength: 40, maxLength: 40 }).map((address) => `0x${address}`) +export const evmBytes32Arbitrary = fc.hexaString({ minLength: 64, maxLength: 64 }).map((address) => `0x${address}`) + export const endpointArbitrary: fc.Arbitrary = fc.constantFrom(...ENDPOINT_IDS) export const stageArbitrary: fc.Arbitrary = fc.constantFrom(Stage.MAINNET, Stage.TESTNET, Stage.SANDBOX) diff --git a/packages/ua-utils-evm-hardhat-test/contracts/DefaultOApp.sol b/packages/ua-utils-evm-hardhat-test/contracts/DefaultOApp.sol index 1371b9aff..6dce66b71 100644 --- a/packages/ua-utils-evm-hardhat-test/contracts/DefaultOApp.sol +++ b/packages/ua-utils-evm-hardhat-test/contracts/DefaultOApp.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.22; contract DefaultOApp { - mapping(uint256 => address) public peers; + mapping(uint256 => bytes32) public peers; - function setPeer(uint256 eid, address peer) external { + function setPeer(uint256 eid, bytes32 peer) external { peers[eid] = peer; } } diff --git a/packages/ua-utils-evm/src/oapp/sdk.ts b/packages/ua-utils-evm/src/oapp/sdk.ts index f2e758d35..f919d9848 100644 --- a/packages/ua-utils-evm/src/oapp/sdk.ts +++ b/packages/ua-utils-evm/src/oapp/sdk.ts @@ -1,17 +1,21 @@ import type { IOApp } from '@layerzerolabs/ua-utils' -import type { Address, OmniTransaction } from '@layerzerolabs/utils' -import { omniContractToPoint, OmniContract, ignoreZero, makeZero } from '@layerzerolabs/utils-evm' +import type { Bytes32, Address, OmniTransaction } from '@layerzerolabs/utils' +import { omniContractToPoint, OmniContract, ignoreZero, makeBytes32, areBytes32Equal } from '@layerzerolabs/utils-evm' import type { EndpointId } from '@layerzerolabs/lz-definitions' export class OApp implements IOApp { constructor(public readonly contract: OmniContract) {} - async peers(eid: EndpointId): Promise { + async peers(eid: EndpointId): Promise { return ignoreZero(await this.contract.contract.peers(eid)) } - async setPeer(eid: EndpointId, address: Address | null | undefined): Promise { - const data = this.contract.contract.interface.encodeFunctionData('setPeer', [eid, makeZero(address)]) + async hasPeer(eid: EndpointId, address: Bytes32 | Address | null | undefined): Promise { + return areBytes32Equal(await this.peers(eid), address) + } + + async setPeer(eid: EndpointId, address: Bytes32 | Address | null | undefined): Promise { + const data = this.contract.contract.interface.encodeFunctionData('setPeer', [eid, makeBytes32(address)]) return this.createTransaction(data) } diff --git a/packages/ua-utils-evm/test/oapp/sdk.test.ts b/packages/ua-utils-evm/test/oapp/sdk.test.ts index 446b7a5bc..02fb76e22 100644 --- a/packages/ua-utils-evm/test/oapp/sdk.test.ts +++ b/packages/ua-utils-evm/test/oapp/sdk.test.ts @@ -2,9 +2,11 @@ import fc from 'fast-check' import { endpointArbitrary, evmAddressArbitrary } from '@layerzerolabs/test-utils' import { Contract } from '@ethersproject/contracts' import { OApp } from '@/oapp/sdk' -import { OmniContract, makeZero } from '@layerzerolabs/utils-evm' +import { OmniContract, isZero, makeZeroAddress } from '@layerzerolabs/utils-evm' +import { makeBytes32 } from '@layerzerolabs/utils-evm' describe('oapp/sdk', () => { + const nullishAddressArbitrary = fc.constantFrom(null, undefined, makeZeroAddress(), makeBytes32(makeZeroAddress())) const jestFunctionArbitrary = fc.anything().map(() => jest.fn()) const oappOmniContractArbitrary = fc.record({ @@ -35,13 +37,11 @@ describe('oapp/sdk', () => { }) it('should return undefined if peers() returns a zero address, null or undefined', async () => { - const peerArbitrary = fc.constantFrom(null, undefined, makeZero(null)) - await fc.assert( fc.asyncProperty( omniContractArbitrary, endpointArbitrary, - peerArbitrary, + nullishAddressArbitrary, async (omniContract, peerEid, peer) => { omniContract.contract.peers.mockResolvedValue(peer) @@ -54,25 +54,128 @@ describe('oapp/sdk', () => { }) it('should return undefined if peers() returns null or undefined', async () => { - const peerArbitrary = evmAddressArbitrary - await fc.assert( fc.asyncProperty( omniContractArbitrary, endpointArbitrary, - peerArbitrary, + evmAddressArbitrary, async (omniContract, peerEid, peer) => { omniContract.contract.peers.mockResolvedValue(peer) const sdk = new OApp(omniContract) - expect(sdk.peers(peerEid)).resolves.toBe(peer) + await expect(sdk.peers(peerEid)).resolves.toBe(peer) } ) ) }) }) + describe('hasPeer', () => { + describe('when called with zeroish address', () => { + it('should return true if peers returns a zero address', async () => { + await fc.assert( + fc.asyncProperty( + omniContractArbitrary, + endpointArbitrary, + nullishAddressArbitrary, + nullishAddressArbitrary, + async (omniContract, peerEid, peer, probePeer) => { + omniContract.contract.peers.mockResolvedValue(peer) + + const sdk = new OApp(omniContract) + + await expect(sdk.hasPeer(peerEid, probePeer)).resolves.toBe(true) + } + ) + ) + }) + + it('should return false if peers returns a non-zero address', async () => { + await fc.assert( + fc.asyncProperty( + omniContractArbitrary, + endpointArbitrary, + nullishAddressArbitrary, + evmAddressArbitrary, + async (omniContract, peerEid, peer, probePeer) => { + fc.pre(!isZero(probePeer)) + + omniContract.contract.peers.mockResolvedValue(peer) + + const sdk = new OApp(omniContract) + + await expect(sdk.hasPeer(peerEid, probePeer)).resolves.toBe(false) + } + ) + ) + }) + }) + + describe('when called non-zeroish address', () => { + it('should return false if peers() returns a zero address, null or undefined', async () => { + await fc.assert( + fc.asyncProperty( + omniContractArbitrary, + endpointArbitrary, + evmAddressArbitrary, + nullishAddressArbitrary, + async (omniContract, peerEid, peer, probePeer) => { + fc.pre(!isZero(peer)) + + omniContract.contract.peers.mockResolvedValue(peer) + + const sdk = new OApp(omniContract) + + await expect(sdk.hasPeer(peerEid, probePeer)).resolves.toBe(false) + await expect(sdk.hasPeer(peerEid, makeBytes32(probePeer))).resolves.toBe(false) + } + ) + ) + }) + + it('should return true if peers() returns a matching address', async () => { + await fc.assert( + fc.asyncProperty( + omniContractArbitrary, + endpointArbitrary, + evmAddressArbitrary, + async (omniContract, peerEid, peer) => { + fc.pre(!isZero(peer)) + + omniContract.contract.peers.mockResolvedValue(peer) + + const sdk = new OApp(omniContract) + + await expect(sdk.hasPeer(peerEid, peer)).resolves.toBe(true) + await expect(sdk.hasPeer(peerEid, makeBytes32(peer))).resolves.toBe(true) + } + ) + ) + }) + + it('should return true if peers() returns a matching bytes32', async () => { + await fc.assert( + fc.asyncProperty( + omniContractArbitrary, + endpointArbitrary, + evmAddressArbitrary, + async (omniContract, peerEid, peer) => { + fc.pre(!isZero(peer)) + + omniContract.contract.peers.mockResolvedValue(makeBytes32(peer)) + + const sdk = new OApp(omniContract) + + await expect(sdk.hasPeer(peerEid, peer)).resolves.toBe(true) + await expect(sdk.hasPeer(peerEid, makeBytes32(peer))).resolves.toBe(true) + } + ) + ) + }) + }) + }) + describe('setPeer', () => { it('should encode data for a setPeer call', async () => { await fc.assert( @@ -84,10 +187,12 @@ describe('oapp/sdk', () => { const sdk = new OApp(omniContract) const encodeFunctionData = omniContract.contract.interface.encodeFunctionData + ;(encodeFunctionData as jest.Mock).mockClear() + await sdk.setPeer(peerEid, peerAddress) expect(encodeFunctionData).toHaveBeenCalledTimes(1) - expect(encodeFunctionData).toHaveBeenCalledWith('setPeer', [peerEid, makeZero(peerAddress)]) + expect(encodeFunctionData).toHaveBeenCalledWith('setPeer', [peerEid, makeBytes32(peerAddress)]) } ) ) diff --git a/packages/ua-utils/src/oapp/config.ts b/packages/ua-utils/src/oapp/config.ts index 528da52d2..b47069a94 100644 --- a/packages/ua-utils/src/oapp/config.ts +++ b/packages/ua-utils/src/oapp/config.ts @@ -1,14 +1,14 @@ import type { OmniTransaction } from '@layerzerolabs/utils' import type { OAppFactory, OAppOmniGraph } from './types' -export const configureOApp = async (graph: OAppOmniGraph, factory: OAppFactory): Promise => { +export const configureOApp = async (graph: OAppOmniGraph, createSdk: OAppFactory): Promise => { const setPeers = await Promise.all( graph.connections.map(async ({ vector: { from, to } }): Promise => { - const instance = await factory(from) - const address = await instance.peers(to.eid) + const sdk = await createSdk(from) + const hasPeer = await sdk.hasPeer(to.eid, to.address) - if (to.address === address) return [] - return [await instance.setPeer(to.eid, to.address)] + if (hasPeer) return [] + return [await sdk.setPeer(to.eid, to.address)] }) ) diff --git a/packages/ua-utils/src/oapp/types.ts b/packages/ua-utils/src/oapp/types.ts index 6abbcdb8d..c443b4b40 100644 --- a/packages/ua-utils/src/oapp/types.ts +++ b/packages/ua-utils/src/oapp/types.ts @@ -1,10 +1,12 @@ import type { EndpointId } from '@layerzerolabs/lz-definitions' import type { Address, OmniGraph, OmniTransaction } from '@layerzerolabs/utils' +import { Bytes32 } from '@layerzerolabs/utils' import { OmniPointBasedFactory } from '@layerzerolabs/utils' export interface IOApp { peers(eid: EndpointId): Promise - setPeer(eid: EndpointId, peer: Address): Promise + hasPeer(eid: EndpointId, address: Bytes32 | Address | null | undefined): Promise + setPeer(eid: EndpointId, peer: Bytes32 | Address | null | undefined): Promise } export type OAppOmniGraph = OmniGraph diff --git a/packages/utils-evm-hardhat/README.md b/packages/utils-evm-hardhat/README.md index 3546518ee..c11c7f55f 100644 --- a/packages/utils-evm-hardhat/README.md +++ b/packages/utils-evm-hardhat/README.md @@ -62,31 +62,3 @@ const omniPoint: OmniPoint = { const omniContract = await omniContractFactory(omniPoint); ``` - -### Address utilities - -#### ignoreZero(address: Address | null | undefined) - -Turns EVM zero addresses to `undefined` - -```typescript -import { ignoreZero } from "@layerzerolabs/utils-evm"; - -ignoreZero("0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8"); // Returns '0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8' -ignoreZero("0x0000000000000000000000000000000000000000"); // Returns undefined -ignoreZero(undefined); // Returns undefined -ignoreZero(null); // Returns undefined -``` - -#### makeZero(address) - -Turns `null` and `undefined` into EVM zero address - -```typescript -import { makeZero } from "@layerzerolabs/utils-evm"; - -makeZero("0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8"); // Returns '0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8' -makeZero("0x0000000000000000000000000000000000000000"); // Returns '0x0000000000000000000000000000000000000000' -makeZero(undefined); // Returns '0x0000000000000000000000000000000000000000' -makeZero(null); // Returns '0x0000000000000000000000000000000000000000' -``` diff --git a/packages/utils-evm-hardhat/test/omnigraph/contracts.test.ts b/packages/utils-evm-hardhat/test/omnigraph/contracts.test.ts index f8e13ef52..cf45a2b71 100644 --- a/packages/utils-evm-hardhat/test/omnigraph/contracts.test.ts +++ b/packages/utils-evm-hardhat/test/omnigraph/contracts.test.ts @@ -4,7 +4,7 @@ import { JsonRpcProvider, Web3Provider } from '@ethersproject/providers' import { createConnectedContractFactory } from '@/omnigraph' import { pointArbitrary } from '@layerzerolabs/test-utils' import { Contract } from '@ethersproject/contracts' -import { makeZero } from '@layerzerolabs/utils-evm' +import { makeZeroAddress } from '@layerzerolabs/utils-evm' // Ethers calls the eth_chainId RPC method when initializing a provider so we mock the result jest.spyOn(Web3Provider.prototype, 'send').mockResolvedValue('1') @@ -28,7 +28,7 @@ describe('omnigraph/contracts', () => { await fc.assert( fc.asyncProperty(pointArbitrary, async (point) => { const error = new Error() - const contractFactory = jest.fn().mockResolvedValue(new Contract(makeZero(undefined), [])) + const contractFactory = jest.fn().mockResolvedValue(new Contract(makeZeroAddress(undefined), [])) const providerFactory = jest.fn().mockRejectedValue(error) const connectedContractFactory = createConnectedContractFactory(contractFactory, providerFactory) @@ -40,7 +40,7 @@ describe('omnigraph/contracts', () => { it('should return a connected contract', async () => { await fc.assert( fc.asyncProperty(pointArbitrary, async (point) => { - const contract = new Contract(makeZero(undefined), []) + const contract = new Contract(makeZeroAddress(undefined), []) const provider = new JsonRpcProvider() const contractFactory = jest.fn().mockResolvedValue({ eid: point.eid, contract }) const providerFactory = jest.fn().mockResolvedValue(provider) diff --git a/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts b/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts index e56857638..e7d7c56a6 100644 --- a/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts +++ b/packages/utils-evm-hardhat/test/omnigraph/coordinates.test.ts @@ -6,7 +6,7 @@ import { endpointArbitrary, evmAddressArbitrary } from '@layerzerolabs/test-util import { OmniDeployment, createContractFactory, omniDeploymentToContract, omniDeploymentToPoint } from '@/omnigraph' import { EndpointId } from '@layerzerolabs/lz-definitions' import { Contract } from '@ethersproject/contracts' -import { makeZero } from '@layerzerolabs/utils-evm' +import { makeZeroAddress } from '@layerzerolabs/utils-evm' import { createNetworkEnvironmentFactory } from '@/runtime' jest.spyOn(DeploymentsManager.prototype, 'getChainId').mockResolvedValue('1') @@ -64,7 +64,7 @@ describe('omnigraph/coordinates', () => { const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET) jest.spyOn(env.deployments, 'getOrNull').mockResolvedValue({ - address: makeZero(undefined), + address: makeZeroAddress(undefined), abi: [], }) @@ -116,7 +116,7 @@ describe('omnigraph/coordinates', () => { const env = await environmentFactory(EndpointId.ETHEREUM_MAINNET) jest.spyOn(env.deployments, 'getDeploymentsFromAddress').mockResolvedValue([ { - address: makeZero(undefined), + address: makeZeroAddress(undefined), abi: [], }, ]) diff --git a/packages/utils-evm/README.md b/packages/utils-evm/README.md index 8bedd5314..c18b77106 100644 --- a/packages/utils-evm/README.md +++ b/packages/utils-evm/README.md @@ -27,3 +27,31 @@ pnpm add @layerzerolabs/utils-evm npm install @layerzerolabs/utils-evm ``` + +### Address utilities + +#### ignoreZero(address: Address | null | undefined) + +Turns EVM zero addresses to `undefined` + +```typescript +import { ignoreZero } from "@layerzerolabs/utils-evm"; + +ignoreZero("0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8"); // Returns '0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8' +ignoreZero("0x0000000000000000000000000000000000000000"); // Returns undefined +ignoreZero(undefined); // Returns undefined +ignoreZero(null); // Returns undefined +``` + +#### makeZeroAddress(address) + +Turns `null` and `undefined` into EVM zero address + +```typescript +import { makeZeroAddress } from "@layerzerolabs/utils-evm"; + +makeZeroAddress("0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8"); // Returns '0xEe6cF2E1Bc7645F8439d241ce37820305F2BB3F8' +makeZeroAddress("0x0000000000000000000000000000000000000000"); // Returns '0x0000000000000000000000000000000000000000' +makeZeroAddress(undefined); // Returns '0x0000000000000000000000000000000000000000' +makeZeroAddress(null); // Returns '0x0000000000000000000000000000000000000000' +``` diff --git a/packages/utils-evm/package.json b/packages/utils-evm/package.json index 46f8f287d..dc7c51279 100644 --- a/packages/utils-evm/package.json +++ b/packages/utils-evm/package.json @@ -43,6 +43,7 @@ "@ethersproject/abstract-provider": "^5.7.0", "@ethersproject/abstract-signer": "^5.7.0", "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", "@ethersproject/contracts": "^5.7.0", "@ethersproject/providers": "^5.7.0", "@layerzerolabs/lz-definitions": "~1.5.69", diff --git a/packages/utils-evm/src/address.ts b/packages/utils-evm/src/address.ts index 468aad306..0ee9263a5 100644 --- a/packages/utils-evm/src/address.ts +++ b/packages/utils-evm/src/address.ts @@ -1,7 +1,58 @@ -import type { Address } from '@layerzerolabs/utils' +import type { Address, Bytes32 } from '@layerzerolabs/utils' +import { hexZeroPad } from '@ethersproject/bytes' import { AddressZero } from '@ethersproject/constants' -export const ignoreZero = (address: Address | null | undefined): string | undefined => - address === AddressZero ? undefined : address ?? undefined +/** + * Converts an address into Bytes32 by padding it with zeros. + * + * It will return zero bytes if passed `null`, `undefined` or an empty string. + * + * @param {Bytes32 | Address | null | undefined} address + * @returns {string} + */ +export const makeBytes32 = (address?: Bytes32 | Address | null | undefined): Bytes32 => hexZeroPad(address || '0x0', 32) -export const makeZero = (address: Address | null | undefined): string => address ?? AddressZero +/** + * Compares two Bytes32-like values by value (i.e. ignores casing on strings + * and string length) + * + * @param {Bytes32 | Address | null | undefined} a + * @param {Bytes32 | Address | null | undefined} b + * @returns {boolean} + */ +export const areBytes32Equal = ( + a: Bytes32 | Address | null | undefined, + b: Bytes32 | Address | null | undefined +): boolean => BigInt(makeBytes32(a)) === BigInt(makeBytes32(b)) + +/** + * Checks whether a value is a zero value. + * + * It will return true if passed `null`, `undefined`, empty bytes ('0x') or an empty string. + * + * It will throw an error if the value is not a valid numerical value. + * + * @param {Bytes32 | Address | null | undefined} value + * @returns {boolean} + */ +export const isZero = (value: Bytes32 | Address | null | undefined): boolean => + value === '0x' || BigInt(value || 0) === BigInt(0) + +/** + * Turns a potentially zero address into undefined + * + * @param {Bytes32 | Address | null | undefined} address + * + * @returns {string | undefined} + */ +export const ignoreZero = (value?: Bytes32 | Address | null | undefined): string | undefined => + isZero(value) ? undefined : value ?? undefined + +/** + * Turns a nullish value (`null` or `undefined`) into a zero address + * + * @param {Address | null | undefined} address + * + * @returns {string} + */ +export const makeZeroAddress = (address?: Address | null | undefined): string => address ?? AddressZero diff --git a/packages/utils-evm/test/address.test.ts b/packages/utils-evm/test/address.test.ts index d8a0ff693..f8d4aba31 100644 --- a/packages/utils-evm/test/address.test.ts +++ b/packages/utils-evm/test/address.test.ts @@ -1,9 +1,146 @@ import fc from 'fast-check' import { AddressZero } from '@ethersproject/constants' -import { evmAddressArbitrary } from '@layerzerolabs/test-utils' -import { ignoreZero, makeZero } from '@/address' +import { evmAddressArbitrary, evmBytes32Arbitrary } from '@layerzerolabs/test-utils' +import { areBytes32Equal, ignoreZero, isZero, makeBytes32, makeZeroAddress } from '@/address' describe('address', () => { + const ZERO_BYTES = '0x0000000000000000000000000000000000000000000000000000000000000000' + + describe('makeBytes32', () => { + it('should return zero value with empty bytes32', () => { + expect(makeBytes32(ZERO_BYTES)).toBe(ZERO_BYTES) + }) + + it('should return zero value with empty string', () => { + expect(makeBytes32('')).toBe(ZERO_BYTES) + }) + + it('should return zero value with zero address', () => { + expect(makeBytes32(AddressZero)).toBe(ZERO_BYTES) + }) + + it('should return zero value with undefined', () => { + expect(makeBytes32(undefined)).toBe(ZERO_BYTES) + }) + + it('should return zero value with null', () => { + expect(makeBytes32(null)).toBe(ZERO_BYTES) + }) + + it('should return zero value with empty bytes', () => { + expect(makeBytes32('0x')).toBe(ZERO_BYTES) + }) + + it('should return padded values for address', () => { + fc.assert( + fc.property(evmAddressArbitrary, (address) => { + const bytes = makeBytes32(address) + + expect(bytes.length).toBe(66) + expect(BigInt(bytes)).toBe(BigInt(address)) + }) + ) + }) + + it('should return identity for bytes32', () => { + fc.assert( + fc.property(evmBytes32Arbitrary, (bytes) => { + expect(makeBytes32(bytes)).toBe(bytes) + }) + ) + }) + }) + + describe('areBytes32Equal', () => { + const zeroishBytes32Arbitrary = fc.constantFrom(null, undefined, '0x', '0x0', makeZeroAddress(), ZERO_BYTES) + + it('should return true for two nullish values', () => { + fc.assert( + fc.property(zeroishBytes32Arbitrary, zeroishBytes32Arbitrary, (a, b) => { + expect(areBytes32Equal(a, b)).toBe(true) + }) + ) + }) + + it('should return true for two identical values', () => { + fc.assert( + fc.property(evmBytes32Arbitrary, (a) => { + expect(areBytes32Equal(a, a)).toBe(true) + }) + ) + }) + + it('should return true for an address and bytes', () => { + fc.assert( + fc.property(evmAddressArbitrary, (address) => { + expect(areBytes32Equal(address, makeBytes32(address))).toBe(true) + }) + ) + }) + + it('should return false for a zeroish value and a non-zeroish address', () => { + fc.assert( + fc.property(zeroishBytes32Arbitrary, evmAddressArbitrary, (bytes, address) => { + fc.pre(!isZero(address)) + + expect(areBytes32Equal(bytes, address)).toBe(false) + }) + ) + }) + + it('should return false for a zeroish value and a non-zeroish bytes', () => { + fc.assert( + fc.property(zeroishBytes32Arbitrary, evmBytes32Arbitrary, (a, b) => { + fc.pre(!isZero(b)) + + expect(areBytes32Equal(a, b)).toBe(false) + }) + ) + }) + }) + + describe('isZero', () => { + it('should return true with zero bytes32', () => { + expect(isZero(makeBytes32(AddressZero))).toBe(true) + }) + + it('should return true with zero bytes32', () => { + expect(isZero('0x')).toBe(true) + }) + + it('should return true with zero address', () => { + expect(isZero(AddressZero)).toBe(true) + }) + + it('should return true with undefined', () => { + expect(isZero(undefined)).toBe(true) + }) + + it('should return true with null', () => { + expect(isZero(null)).toBe(true) + }) + + it('should return false with non-zero address', () => { + fc.assert( + fc.property(evmAddressArbitrary, (address) => { + fc.pre(address !== AddressZero) + + expect(isZero(address)).toBe(false) + }) + ) + }) + + it('should return false with non-zero bytes32', () => { + fc.assert( + fc.property(evmBytes32Arbitrary, (address) => { + fc.pre(address !== ZERO_BYTES) + + expect(isZero(address)).toBe(false) + }) + ) + }) + }) + describe('ignoreZero', () => { it('should return address with non-zero address', () => { fc.assert( @@ -28,27 +165,27 @@ describe('address', () => { }) }) - describe('makeZero', () => { + describe('makeZeroAddress', () => { it('should return address with non-zero address', () => { fc.assert( fc.property(evmAddressArbitrary, (address) => { fc.pre(address !== AddressZero) - expect(makeZero(address)).toBe(address) + expect(makeZeroAddress(address)).toBe(address) }) ) }) it('should return undefined with zero address', () => { - expect(makeZero(AddressZero)).toBe(AddressZero) + expect(makeZeroAddress(AddressZero)).toBe(AddressZero) }) it('should return undefined with undefined', () => { - expect(makeZero(undefined)).toBe(AddressZero) + expect(makeZeroAddress(undefined)).toBe(AddressZero) }) it('should return undefined with null', () => { - expect(makeZero(null)).toBe(AddressZero) + expect(makeZeroAddress(null)).toBe(AddressZero) }) }) }) diff --git a/packages/utils/src/types.ts b/packages/utils/src/types.ts index a8c064fa4..c20eb6ef8 100644 --- a/packages/utils/src/types.ts +++ b/packages/utils/src/types.ts @@ -2,4 +2,6 @@ import type { EndpointId } from '@layerzerolabs/lz-definitions' export type Address = string +export type Bytes32 = string + export type EndpointBasedFactory = (eid: EndpointId) => TValue | Promise