Anvil Custom RPC Methods UI
Ubiquity Dollar · 2023 · First open source contribution: exposed 33+ Anvil custom RPC methods in a safe, configurable UI to speed blockchain test setup. Led to core team position and 2-year tenure.
Problem
When developers wanted to test PRs in the Ubiquity Dollar DApp at https://uad.ubq.fi/, they lacked the ability to update blockchain state for testing purposes. Essential debugging operations like account impersonation, balance modification, state dumps, and mempool manipulation required CLI access to Anvil (Foundry’s local node), creating significant friction during PR reviews and testing workflows.
Approach
- Build a browser-based UI exposing Anvil’s 33+ custom RPC methods with production safety guards
- Implement config-driven architecture where
method-configs.ts
serves as single source of truth - Type-safe parameter conversion from form inputs to blockchain-specific types
- Organized method categorization (chain/user/utility) for intuitive developer experience
- Selective file downloads for large payload methods (state dumps, snapshots)
System diagram
flowchart LR User[User] --> DAppUI[DApp UI] DAppUI --> CustomRPCButton[Custom RPC Button] CustomRPCButton --> MethodSelection[Method Selection] MethodSelection --> ParameterInput[Parameter Input] ParameterInput --> RPCCall[RPC Call] RPCCall --> AnvilNode[Anvil Node] AnvilNode --> StateModification[State Modification] StateModification --> ResponseDownload[Response or Download]
Implementation showcase
Closed state — Debug panel accessible via footer button
Open state — Method selection with organized categories and parameter inputs
Active debugging — Browser console showing transaction hash alongside Anvil node receiving RPC calls
Outcome
- Method expansion: Delivered 33+ methods vs. original requirement of 8 (4× scope increase)
- Enhanced developer productivity: Eliminated CLI dependency for blockchain testing workflows
- Production-safe deployment: Hidden button implementation prevents accidental use in live environment
- Successful onboarding: First contribution that established foundation for 2-year OSS trajectory at Ubiquity
- Budget success: Expanded from $100 to $400 budget based on delivered value and scope extension
Constraints
- Development-only visibility (hidden in production builds via environment guards)
- Monorepo compliance with existing dapp structure and linting conventions
- Ethers.js integration (Viem recommendation declined by maintainers)
- Zero external dependencies beyond existing stack
Design choices
- Config-driven architecture: Single
method-configs.ts
file defines all available methods, parameters, and UI behavior - Type-safe parameter handling: String form inputs converted to appropriate blockchain types (BigInt, hex, boolean)
- Selective response handling: Large payloads trigger file downloads; small responses display inline
- Production safety: Environment-based feature flags prevent accidental production exposure
- Categorized organization: Methods grouped by utility (chain operations, user actions, debugging utilities)
Proof
Code excerpt — JSON-RPC call implementation with error handling
const handleFetch = async (method: string, params: unknown[]) => {
try {
const response = await fetch("http://localhost:8545", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
jsonrpc: "2.0",
id: 1,
method,
params
}),
});
const result = await response.json();
if (result.error) {
throw new Error(`RPC Error: ${result.error.message}`);
}
return result;
} catch (error) {
console.error(`Failed to call ${method}:`, error);
throw error;
}
};
Code excerpt — type-safe parameter conversion
function convertToArgType(arg: string, type: string): unknown {
// Trim input to handle whitespace
const trimmedArg = arg.trim();
switch (type) {
case "bigint":
return BigInt(trimmedArg);
case "number":
return Number(trimmedArg);
case "boolean":
return Boolean(trimmedArg);
case "string":
return String(trimmedArg);
default:
return trimmedArg;
}
}
// Usage in form handler
const typedArgs = method.params.map((param, index) =>
convertToArgType(methodArgs[param.name], param.type)
);
Code excerpt — config-driven method definitions
export const methodConfigs = [
{
name: "Impersonate Account",
methodName: "anvil_impersonateAccount",
description: "Start impersonating an account for subsequent calls",
params: [{ name: "address", type: "string" }],
type: "user"
},
{
name: "Set Balance",
methodName: "anvil_setBalance",
description: "Set the balance of an account in wei",
params: [
{ name: "address", type: "string" },
{ name: "balance", type: "bigint" }
],
type: "user"
},
{
name: "Get Block Number",
methodName: "eth_blockNumber",
description: "Returns the current block number",
params: [],
download: true,
type: "utility"
}
// ... 30+ additional methods
];
Code excerpt — production safety guard
// Component only renders in development
if (process.env.NODE_ENV === 'production') {
return null;
}
return (
<div className="anvil-debug-panel">
{/* UI implementation */}
</div>
);