Skip to main content
The Block Explorer API service resolves contract ABIs from block explorers and returns callable contract instances.

Quick Start

import { Effect, Layer } from "effect"
import {
  Provider,
  HttpTransport,
  mainnet,
  BlockExplorerApi,
  BlockExplorerApiService
} from "voltaire-effect"

const AppLayer = Layer.mergeAll(
  Provider.pipe(Layer.provide(HttpTransport("https://eth.llamarpc.com"))),
  mainnet,
  BlockExplorerApi.fromEnv()
)

const program = Effect.gen(function* () {
  const explorer = yield* BlockExplorerApiService

  // Get a callable contract from just an address
  const usdc = yield* explorer.getContract("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48")

  // Call read methods
  const symbol = yield* usdc.read.symbol()
  const decimals = yield* usdc.read.decimals()

  return { symbol, decimals }
}).pipe(Effect.provide(AppLayer))

await Effect.runPromise(program)

Contract Instance

getContract() returns a contract with callable methods:
interface ExplorerContractInstance {
  address: `0x${string}`           // Resolved implementation address
  requestedAddress: `0x${string}`  // Original input address
  abi: ReadonlyArray<AbiItem>      // Resolved ABI
  resolution: AbiResolution        // How ABI was resolved

  // Read-only methods (view/pure)
  read: Record<string, (...args) => Effect<unknown, ContractCallError, ProviderService>>

  // Simulate state-changing methods
  simulate: Record<string, (...args) => Effect<unknown, ContractCallError, ProviderService>>

  // State-changing methods
  write: Record<string, (...args) => Effect<`0x${string}`, ContractWriteError, SignerService>>

  // Explicit signature-based access
  call: (signature: string, args: unknown[]) => Effect<unknown, ContractCallError, ProviderService>
}

Method Access

By Name

const contract = yield* explorer.getContract(address)
const name = yield* contract.read.name()
const balance = yield* contract.read.balanceOf(account)

By Signature

For overloaded functions, use the full signature:
// ERC721 has two safeTransferFrom overloads
yield* contract.write["safeTransferFrom(address,address,uint256)"](from, to, tokenId)
yield* contract.write["safeTransferFrom(address,address,uint256,bytes)"](from, to, tokenId, data)

Resolution Modes

ModeBehavior
verified-onlyOnly verified ABIs. Fails if not found.
verified-firstTry verified sources in order. Fails if all miss. (Default)
best-effortFalls back to ABI recovery. For inspection only.
// Strict - production use
yield* explorer.getContract(address, { resolution: "verified-only" })

// Best-effort - tooling/inspection
yield* explorer.getContract(address, { resolution: "best-effort" })

Configuration

Environment

// Reads ETHERSCAN_API_KEY, BLOCKSCOUT_API_KEY
const ExplorerLayer = BlockExplorerApi.fromEnv()

Explicit

const ExplorerLayer = BlockExplorerApi({
  sources: {
    sourcify: { enabled: true },
    etherscanV2: { enabled: true, apiKey: "..." },
    blockscout: { enabled: true }
  },
  sourceOrder: ["sourcify", "etherscanV2", "blockscout"],
  cache: { enabled: true, ttlMillis: 300_000 }
})

Proxy Resolution

const contract = yield* explorer.getContract(proxyAddress, {
  followProxies: true
})

// contract.address = implementation address
// contract.requestedAddress = original proxy address

Error Handling

const program = explorer.getContract(address).pipe(
  Effect.catchTag("BlockExplorerNotFoundError", () =>
    Effect.succeed(null)
  ),
  Effect.catchTag("BlockExplorerRateLimitError", (e) =>
    Effect.fail(new Error(`Retry in ${e.retryAfterSeconds}s`))
  )
)
ErrorWhen
BlockExplorerNotFoundErrorNo ABI found
BlockExplorerRateLimitErrorRate limited
BlockExplorerConfigErrorBad config

Convenience Methods

// Just get the ABI
const abi = yield* explorer.getAbi(address)

// Get source files
const sources = yield* explorer.getSources(address)

See Also