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 @@
+
+
+
+
+
+
+@layerzerolabs/utils-evm
+
+
+
+
+
+
+
+
+
+
+
+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'],
+ },
+])