0.7.0 • Published 10 months ago
@pavlovcik/permit2-rpc-manager v0.7.0
Permit2 RPC Manager
An intelligent RPC manager for EVM-compatible chains that automatically selects the fastest, valid RPC endpoint from a curated whitelist.
Features
- Automatic RPC Selection: Dynamically tests whitelisted RPCs for latency, sync status (
eth_syncing), and specific contract bytecode (Permit2 viaeth_getCode) to find the best endpoint. Uses an intelligent fallback system that adapts to operation requirements:- For standard operations: Can use any responsive RPC in order of preference: fully synced > wrong Permit2 bytecode > syncing
- For Permit2-related operations: Only uses RPCs with correct Permit2 bytecode
- Whitelisting: Uses a configurable
src/rpc-whitelist.jsonto manage the pool of RPCs to test. - Caching: Caches detailed latency test results (including status/errors) in
.rpc-cache.json(Node.js) orlocalStorage(browser) to speed up subsequent requests (default 1-hour TTL). - Robust Fallback: Automatically iterates through the ranked list of available RPCs if the initial attempt fails. Uses round-robin selection for initial attempts across concurrent requests to distribute load. Includes a runtime cooldown mechanism (
runtimeFailureCooldownMs) to temporarily skip RPCs that fail during operation, preventing repeated attempts on temporarily faulty endpoints. - Contract Interaction: Includes a
readContracthelper function (usingviem) for easy read-only smart contract calls (requires user-provided ABI). - Configurable Logging: Control log verbosity (
debug,info,warn,error,none) via constructor options. Default iswarn. - Isomorphic: Designed for both Node.js/Bun backend and browser/worker environments. Uses
localStoragefor caching in browsers and defaults to a temporary file in Node.js (configurable path). - TypeScript: Written in TypeScript with type definitions and source maps included in the build.
Installation
bun install # Or npm install / yarn installUsage
Basic RPC Calls (eth_blockNumber, etc.)
import { Permit2RpcManager } from "./src/index.ts"; // Adjust import path as needed
async function example() {
// Optionally configure timeouts and cache TTL
const manager = new Permit2RpcManager({
latencyTimeoutMs: 5000, // Timeout for latency tests
requestTimeoutMs: 10000, // Timeout for actual RPC calls
// cacheTtlMs: 60 * 60 * 1000, // Default is 1 hour
// logLevel: 'info', // Default is 'warn'
// runtimeFailureCooldownMs: 60000, // Default is 60s. Temporarily skip RPCs that fail during runtime.
// nodeCachePath: '/path/to/my/cache.json', // Optional: Specify cache file path for Node.js
// initialRpcData: { rpcs: { '1': ['https://my-custom-rpc.com'] } } // Optional: Override default whitelist
});
const chainId = 1; // Ethereum
try {
const blockNumberHex = await manager.send<string>(chainId, "eth_blockNumber");
const blockNumber = parseInt(blockNumberHex, 16);
console.log(`Latest block number on chain ${chainId}: ${blockNumber}`);
// Example: Get balance
// const balanceHex = await manager.send<string>(chainId, 'eth_getBalance', [address, 'latest']);
// console.log(`Balance: ${balanceHex}`);
} catch (error) {
console.error(`Error fetching data for chain ${chainId}:`, error);
}
}
example();Smart Contract Calls (readContract)
import { Permit2RpcManager, readContract } from "./src/index.ts"; // Adjust import path
import type { Address, Abi } from "viem";
// Define your contract ABI (e.g., ERC20 subset)
const erc20Abi = [
{
inputs: [],
name: "symbol",
outputs: [{ type: "string" }],
stateMutability: "view",
type: "function",
},
{
inputs: [{ name: "account", type: "address" }],
name: "balanceOf",
outputs: [{ type: "uint256" }],
stateMutability: "view",
type: "function",
},
] as const; // Use 'as const'
const manager = new Permit2RpcManager();
const chainId = 1; // Ethereum
const usdcAddress: Address = "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48";
const someAccount: Address = "0x47ac0Fb4F2D84898e4D9E7b4DaB3C24507a6D503";
async function getContractInfo() {
try {
const symbol = await readContract<string>({
manager,
chainId,
address: usdcAddress,
abi: erc20Abi,
functionName: "symbol",
});
console.log(`Token Symbol: ${symbol}`);
const balance = await readContract<bigint>({
manager,
chainId,
address: usdcAddress,
abi: erc20Abi,
functionName: "balanceOf",
args: [someAccount],
});
console.log(`Balance of ${someAccount}: ${balance.toString()}`);
} catch (error) {
console.error("Contract read error:", error);
}
}
getContractInfo();Development
- Build (Node Target):
bun run build(For publishing to npm) - Build (Browser Target):
bun run build:browser(For direct browser usage/testing) - Test:
bun test - Watch & Rebuild (Node):
bun run watch - Watch & Rebuild + Live Server (Browser Test):
bun run dev:browser(Opensindex.html) - Update Whitelist from Submodule:
bun run whitelist:update(Requireschainlist:generateto be run first if submodule changed) - Test Whitelist Connectivity:
bun run whitelist:test - Format:
bun run format - Release New Version:
npm run release:patch(orminor/major) - Requires clean git state & NPM_TOKEN env var.
Whitelist
Modify src/rpc-whitelist.json to add/remove RPC endpoints for specific chain IDs. The manager will only test URLs listed in this file.
Latency Testing & Selection
The LatencyTester performs the following checks concurrently for each whitelisted RPC:
- Permit2 Bytecode: Sends
eth_getCodeto the Permit2 address (0x000000000022D473030F116dDEE9F6B43aC78BA3) and verifies the returned bytecode matches the first 13995 bytes. The prefix check ensures the Permit2 contract is correctly deployed, but allows for potential minor deployment differences across chains. The byte comparison is exact, and any mismatch results instatus: 'wrong_bytecode'. - Sync Status: Sends
eth_syncingand verifies the result isfalse. Failure results instatus: 'syncing'. - Connectivity/Timeout: Checks for network errors, HTTP errors, RPC errors, or timeouts during the above calls.
The RpcSelector uses these test results to select an endpoint based on operation needs:
- Priority 1: RPCs with
status: 'ok'(fully synced, correct bytecode) - sorted by latency - Priority 2: RPCs with
status: 'wrong_bytecode'(synced but incorrect Permit2 bytecode) - sorted by latency- These RPCs are fully functional for most operations
- Only excluded when Permit2-specific functionality is needed
- Priority 3: RPCs with
status: 'syncing'(not fully synced) - sorted by latency- May have correct bytecode but need time to sync
- Useful as last resort for basic calls
- Excluded: RPCs with network errors (including CORS errors in browser), timeouts, HTTP errors, or authentication failures (
rpc_errorfrom test).
This prioritization ensures:
- Basic operations (like
eth_callfor token symbol) work reliably by using any responsive RPC - Permit2-related operations only use RPCs with exact bytecode match
- Performance is optimized by selecting the fastest RPC within each priority level.
- Maximum availability through iterative fallback across the entire ranked list of usable RPCs, including skipping RPCs that have recently failed during runtime (within the
runtimeFailureCooldownMs). - Load distribution across RPCs for concurrent requests via round-robin starting point selection.
Note: For browser usage, the effectiveness relies on the rpc-whitelist.json containing RPCs with permissive CORS headers. The library will filter out non-CORS-friendly RPCs during latency testing.