Keyrxng

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.

So what?
33+ RPC endpoints methods supported · 3 days review cycle · 4× original requirements scope expansion
Role
Engineer (First OSS Contribution)
Year
2023
Stack
React, TypeScript, Next.js, Ethers.js, Foundry/Anvil, JSON-RPC
Read narrative
33+ RPC endpoints methods supported3 days review cycle4× original requirements scope expansion

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

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 Anvil RPC UI Closed

Open state — Method selection with organized categories and parameter inputs Anvil RPC UI Open

Active debugging — Browser console showing transaction hash alongside Anvil node receiving RPC calls Anvil RPC UI Active

Outcome

Constraints

Design choices

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>
);

References