diff --git a/packages/utils-evm/.eslintignore b/packages/utils-evm/.eslintignore new file mode 100644 index 000000000..db4c6d9b6 --- /dev/null +++ b/packages/utils-evm/.eslintignore @@ -0,0 +1,2 @@ +dist +node_modules \ No newline at end of file diff --git a/packages/utils-evm/.prettierignore b/packages/utils-evm/.prettierignore new file mode 100644 index 000000000..763301fc0 --- /dev/null +++ b/packages/utils-evm/.prettierignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ \ No newline at end of file diff --git a/packages/utils-evm/README.md b/packages/utils-evm/README.md new file mode 100644 index 000000000..8bedd5314 --- /dev/null +++ b/packages/utils-evm/README.md @@ -0,0 +1,29 @@ +

+ + LayerZero + +

+ +

@layerzerolabs/utils-evm

+ + +

+ + NPM Version + + Downloads + + NPM License +

+ +Utilities for working with LayerZero EVM contracts. + +## Installation + +```bash +yarn add @layerzerolabs/utils-evm + +pnpm add @layerzerolabs/utils-evm + +npm install @layerzerolabs/utils-evm +``` diff --git a/packages/utils-evm/jest.config.js b/packages/utils-evm/jest.config.js new file mode 100644 index 000000000..16148cfb1 --- /dev/null +++ b/packages/utils-evm/jest.config.js @@ -0,0 +1,8 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, +}; diff --git a/packages/utils-evm/package.json b/packages/utils-evm/package.json new file mode 100644 index 000000000..d59cf7248 --- /dev/null +++ b/packages/utils-evm/package.json @@ -0,0 +1,57 @@ +{ + "name": "@layerzerolabs/utils-evm", + "version": "0.0.1", + "private": true, + "description": "Utilities for LayerZero EVM projects", + "repository": { + "type": "git", + "url": "git+https://github.com/LayerZero-Labs/lz-utils.git", + "directory": "packages/utils-evm" + }, + "license": "MIT", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "require": "./dist/index.js", + "import": "./dist/index.mjs" + }, + "./*": { + "types": "./dist/*.d.ts", + "require": "./dist/*.js", + "import": "./dist/*.mjs" + } + }, + "main": "./dist/index.js", + "module": "./dist/index.mjs", + "types": "./dist/index.d.ts", + "files": [ + "./dist/index.*" + ], + "scripts": { + "prebuild": "npx tsc --noEmit -p tsconfig.build.json", + "build": "npx tsup", + "clean": "rm -rf dist", + "dev": "npx tsup --watch", + "lint": "npx eslint '**/*.{js,ts,json}'", + "test": "jest" + }, + "dependencies": { + "p-memoize": "~4.0.1" + }, + "devDependencies": { + "@ethersproject/providers": "^5.7.0", + "@layerzerolabs/lz-definitions": "~1.5.62", + "@layerzerolabs/test-utils": "~0.0.1", + "@types/jest": "^29.5.10", + "fast-check": "^3.14.0", + "jest": "^29.7.0", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "tsup": "~8.0.1", + "typescript": "^5.2.2" + }, + "peerDependencies": { + "@ethersproject/providers": "^5.7.0", + "@layerzerolabs/lz-definitions": "~1.5.62" + } +} \ No newline at end of file diff --git a/packages/utils-evm/src/index.ts b/packages/utils-evm/src/index.ts new file mode 100644 index 000000000..6f29423e2 --- /dev/null +++ b/packages/utils-evm/src/index.ts @@ -0,0 +1 @@ +export * from './provider' diff --git a/packages/utils-evm/src/provider/factory.ts b/packages/utils-evm/src/provider/factory.ts new file mode 100644 index 000000000..d237137f6 --- /dev/null +++ b/packages/utils-evm/src/provider/factory.ts @@ -0,0 +1,6 @@ +import pMemoize from 'p-memoize' +import { ProviderFactory, RpcUrlFactory } from './types' +import { JsonRpcProvider } from '@ethersproject/providers' + +export const createProviderFactory = (urlFactory: RpcUrlFactory): ProviderFactory => + pMemoize(async (eid) => new JsonRpcProvider(await urlFactory(eid))) diff --git a/packages/utils-evm/src/provider/index.ts b/packages/utils-evm/src/provider/index.ts new file mode 100644 index 000000000..97a7b5991 --- /dev/null +++ b/packages/utils-evm/src/provider/index.ts @@ -0,0 +1,2 @@ +export * from './factory' +export * from './types' diff --git a/packages/utils-evm/src/provider/types.ts b/packages/utils-evm/src/provider/types.ts new file mode 100644 index 000000000..8b5beab29 --- /dev/null +++ b/packages/utils-evm/src/provider/types.ts @@ -0,0 +1,10 @@ +import type { BaseProvider } from '@ethersproject/providers' +import type { EndpointId } from '@layerzerolabs/lz-definitions' + +export type Provider = BaseProvider + +export type EndpointBasedFactory = (eid: EndpointId) => TValue | Promise + +export type RpcUrlFactory = EndpointBasedFactory + +export type ProviderFactory = EndpointBasedFactory diff --git a/packages/utils-evm/test/provider/factory.test.ts b/packages/utils-evm/test/provider/factory.test.ts new file mode 100644 index 000000000..b9491c10f --- /dev/null +++ b/packages/utils-evm/test/provider/factory.test.ts @@ -0,0 +1,61 @@ +import fc from 'fast-check' +import { createProviderFactory } from '@/provider/factory' +import { endpointArbitrary } from '@layerzerolabs/test-utils' +import { JsonRpcProvider } from '@ethersproject/providers' + +describe('provider/factory', () => { + describe('createProviderFactory', () => { + const errorArbitrary = fc.anything() + const urlArbitrary = fc.webUrl() + + it('should reject if urlFactory throws', async () => { + await fc.assert( + fc.asyncProperty(errorArbitrary, endpointArbitrary, async (error, eid) => { + const urlFactory = jest.fn().mockImplementation(() => { + throw error + }) + const providerFactory = createProviderFactory(urlFactory) + + await expect(providerFactory(eid)).rejects.toBe(error) + }) + ) + }) + + it('should reject if urlFactory rejects', async () => { + await fc.assert( + fc.asyncProperty(errorArbitrary, endpointArbitrary, async (error, eid) => { + const urlFactory = jest.fn().mockRejectedValue(error) + const providerFactory = createProviderFactory(urlFactory) + + await expect(providerFactory(eid)).rejects.toBe(error) + }) + ) + }) + + it('should resolve with JsonRpcProvider if urlFactory returns a URL', async () => { + await fc.assert( + fc.asyncProperty(urlArbitrary, endpointArbitrary, async (url, eid) => { + const urlFactory = jest.fn().mockReturnValue(url) + const providerFactory = createProviderFactory(urlFactory) + const provider = await providerFactory(eid) + + expect(provider).toBeInstanceOf(JsonRpcProvider) + expect(provider.connection.url).toBe(url) + }) + ) + }) + + it('should resolve with JsonRpcProvider if urlFactory resolves with a URL', async () => { + await fc.assert( + fc.asyncProperty(urlArbitrary, endpointArbitrary, async (url, eid) => { + const urlFactory = jest.fn().mockResolvedValue(url) + const providerFactory = createProviderFactory(urlFactory) + const provider = await providerFactory(eid) + + expect(provider).toBeInstanceOf(JsonRpcProvider) + expect(provider.connection.url).toBe(url) + }) + ) + }) + }) +}) diff --git a/packages/utils-evm/tsconfig.build.json b/packages/utils-evm/tsconfig.build.json new file mode 100644 index 000000000..0507620e8 --- /dev/null +++ b/packages/utils-evm/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "test"] +} diff --git a/packages/utils-evm/tsconfig.json b/packages/utils-evm/tsconfig.json new file mode 100644 index 000000000..acecf2754 --- /dev/null +++ b/packages/utils-evm/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "exclude": ["dist", "node_modules"], + "include": ["src", "test", "*.config.ts"], + "compilerOptions": { + "types": ["node", "jest"], + "paths": { + "@/*": ["./src/*"] + } + } +} diff --git a/packages/utils-evm/tsup.config.ts b/packages/utils-evm/tsup.config.ts new file mode 100644 index 000000000..7ef46a5ad --- /dev/null +++ b/packages/utils-evm/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup' + +export default defineConfig([ + { + entry: ['src/index.ts'], + outDir: './dist', + clean: true, + dts: true, + sourcemap: true, + splitting: false, + treeshake: true, + format: ['esm', 'cjs'], + }, +])