@tuwaio/orbit-evm
@tuwaio/orbit-evm is the EVM Layer 2 (L2) package of Orbit Utils, the Stage 1 primitives layer of the TUWA ecosystem. It wraps viem and @wagmi/core into small, framework-agnostic helpers: cached public clients, chain switching, ENS resolution, and ERC-4337 smart accounts built on Solady with a Pimlico bundler and paymaster. It does not use ethers.js or web3.js.
🏛️ Core Capabilities
- Public clients:
createViemClientreturns a cachedviempublic client per chain, so repeated reads do not create new clients. - Chain alignment:
checkAndSwitchChainasks the connected wallet to switch networks only when it is on a different chain, and throws a readable error when the user rejects. - ENS resolution:
getName,getAvatarandgetAddressalways resolve against Ethereum Mainnet and cache results in memory;isEnsNametells names and addresses apart. - ERC-4337 smart accounts:
createPimlicoSmartAccountClientsets up a Solady smart account (EntryPoint v0.7), a bundler client and an optional gas-sponsoring paymaster in one call. Bundler and paymaster clients are cached in memory per endpoint. The building blocks (createSoladySmartAccount,createBundlerRpcClient,createPimlicoPaymasterClient,createPimlicoRpcUrl,clearBundlerCache) are exported for custom setups. - Chain lists:
getEvmChainsandisEvmChainListextract and detect EVM chain IDs in mixed multi-chain configurations.
💾 Installation
pnpm add @tuwaio/orbit-evm @wagmi/core viem[!IMPORTANT]
viem(2.x) and@wagmi/core(3.x) are peer dependencies and must be installed alongside@tuwaio/orbit-evm.
🚀 Usage
Cached public client
import { createViemClient } from '@tuwaio/orbit-evm';
import { mainnet, sepolia } from 'viem/chains';
const chains = [mainnet, sepolia] as const;
// Returns undefined (and logs a warning) if the chain ID is not in `chains`.
const client = createViemClient(sepolia.id, chains);
const blockNumber = await client?.getBlockNumber();The client uses the chain’s default RPC URL. The cache is keyed by chain ID and is rebuilt when that URL changes.
Chain switching
import { type Config } from '@wagmi/core';
import { checkAndSwitchChain } from '@tuwaio/orbit-evm';
declare const wagmiConfig: Config;
try {
await checkAndSwitchChain(11155111, wagmiConfig);
} catch (error) {
// "User rejected the request to switch network." or a generic switch error (original error in `cause`).
console.error(error);
}ENS resolution
import { getAddress, getAvatar, getName, isEnsName } from '@tuwaio/orbit-evm';
import { mainnet } from 'viem/chains';
const chains = [mainnet] as const;
const name = await getName('0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', chains); // "vitalik.eth" or null
const avatar = name ? await getAvatar(name, chains) : null;
const input = 'vitalik.eth';
const address = isEnsName(input) ? await getAddress(input, chains) : null; // lowercased address or nullLookups use the mainnet entry of chains when present (so your own RPC URL is used), otherwise viem’s default mainnet RPC.
ERC-4337 smart account with Pimlico
import { type Config } from '@wagmi/core';
import { createPimlicoSmartAccountClient } from '@tuwaio/orbit-evm';
import { sepolia } from 'viem/chains';
declare const wagmiConfig: Config;
const { account, bundlerClient } = await createPimlicoSmartAccountClient({
chain: sepolia,
wagmiConfig, // or pass `walletClient` directly
apiKey: process.env.NEXT_PUBLIC_PIMLICO_API_KEY, // or `bundlerUrl` for your own bundler endpoint
});
const userOpHash = await bundlerClient.sendUserOperation({
account,
calls: [{ to: '0x0000000000000000000000000000000000000000', value: 0n }],
});
const receipt = await bundlerClient.waitForUserOperationReceipt({ hash: userOpHash });- A connected wallet is required: the EOA signs for the smart account.
- Gas sponsorship (
sponsor) is on by default whenapiKeyorbundlerUrlis set. Without either, the public Pimlico endpoint is used and no paymaster is attached. - The smart account uses viem’s Solady implementation with its default EntryPoint v0.7 (
entryPoint07Address). - The account salt defaults to the owner address right-padded to 32 bytes (
pad(owner, { dir: 'right', size: 32 })). This satisfies the Solady factory’s owner-prefix check (SaltDoesNotStartWith()) and gives every owner a deterministic, unique account address. Passsaltto override it.
[!WARNING] The Pimlico API key becomes part of the bundler URL used by the browser. Use a key restricted to your domains and sponsorship policies, or proxy the bundler through your backend and pass its address as
bundlerUrl.
Low-level bundler building blocks
import {
clearBundlerCache,
createBundlerRpcClient,
createPimlicoPaymasterClient,
createPimlicoRpcUrl,
} from '@tuwaio/orbit-evm';
// Resolution order: bundlerUrl → https://api.pimlico.io/v2/{chainId}/rpc?apikey=… → https://public.pimlico.io/v2/{chainId}/rpc
const rpcUrl = createPimlicoRpcUrl({ chainId: 11155111, apiKey: process.env.PIMLICO_API_KEY });
// Cached by resolved URL.
const paymasterClient = createPimlicoPaymasterClient({ chainId: 11155111, apiKey: process.env.PIMLICO_API_KEY });
// Cached by resolved URL, paymaster presence and client presence.
const bundlerClient = createBundlerRpcClient({
chainId: 11155111,
apiKey: process.env.PIMLICO_API_KEY,
paymaster: paymasterClient,
});
// Drops cached URLs, bundler and paymaster clients (e.g. after rotating the API key).
clearBundlerCache();📚 API Reference
Every export, with signatures and types generated from the source, is documented at orbit.docs.tuwa.io/packages/orbit-evm . For a full walkthrough of sovereign account abstraction with Orbit, Pulsar and Quasar, see the ERC-4337 guide .
📄 License
Licensed under the Apache-2.0 License. See the LICENSE file for details.
Interfaces
- CreateSoladySmartAccountParams
- PimlicoSmartAccountClientConfig
- PimlicoSmartAccountClientResult
- PimlicoUrlConfig