blockchainsv
Web3 Integration & APIs·July 13, 2026·8 min read

Web3 development: how frontend libraries talk to blockchains

A Web3 dApp does not "connect" to a blockchain. It issues signed JSON-RPC calls to a node operator, waits for a state-changing transaction to be mined, and renders the resulting log.

Web3 development: how frontend libraries talk to blockchains

The Pipe Is the Perimeter: Why Frontend Integration Is a Security Problem

Every abstraction layer between the user's browser and the EVM execution environment is a trust boundary. Most exploits attributed to "smart contract bugs" actually originate upstream — in the library that signs the transaction, in the indexer that returns the wrong balance, in the oracle that fails to update during a crash.

This article dissects the modern frontend integration stack with the same rigor applied to on-chain code. The goal is not a feature comparison. The goal is to make the data flow explicit, identify where state can be falsified or stale, and establish the minimum set of guarantees a production dApp must enforce.

The browser is untrusted. The RPC endpoint is untrusted. The Graph node is untrusted. The oracle is a probabilistic, not deterministic, dependency.

The JSON-RPC Boundary: Where Most Attacks Begin

The browser never speaks to the chain directly. It speaks to a node — usually hosted by Infura, Alchemy, QuickNode, or a self-hosted Erigon/Geth instance. That node can lie. It can return a fabricated eth_call result, censor transactions, or withhold events. A frontend that trusts the node's responses without cross-checking is not a Web3 application. It is a thin client to a server that happens to have a JSON-RPC interface.

The practical implications:

  • View calls (eth_call) are advisory. They simulate a state transition. The actual transaction may revert, or the state may shift before the user signs. The library's job is to surface this clearly, not hide it.
  • Log filters (eth_getLogs) are lossy. Public nodes impose strict range limits (often 5,000–10,000 blocks) and may drop events under load. A frontend displaying "no swaps in the last hour" may be showing the wrong absence.
  • Transaction receipts are not final until finality. On Ethereum L1, that is roughly 12–19 minutes depending on chain head. Confirmations counted by the library are probabilistic, not absolute.

The role of a frontend integration library is to make these failure modes visible to the developer. A library that hides finality assumptions, retries silently, or fabricates reasonable defaults is a source of bugs, not a productivity tool.

Viem and Ethers.js v6: Type Safety as a Security Primitive

Two libraries dominate the contract interaction layer. The choice between them is not aesthetic. It is a trade-off between abstraction, bundle size, and compile-time guarantees.

Viem is a low-level TypeScript interface. It exposes the EVM as a set of strongly-typed functions. Every contract call returns a typed result; every event is parsed against a defined ABI; every transaction request is validated against a schema before signature. Bundle size sits in the 15–20kb range when tree-shaken. Viem has no concept of a "provider" in the legacy sense — clients are explicit, and actions are composed. This makes it harder to accidentally call provider.send with a method the underlying node does not support.

Ethers.js v6 is a complete rewrite of the v5 architecture, modularized into sub-packages: @ethersproject/abi, @ethersproject/providers, @ethersproject/contracts, and so on. The benefit is granular bundle control. The cost is fragmentation: a developer wiring the library manually must understand the boundary between the provider, the signer, and the contract factory. Misconfiguration here is a frequent source of "transaction sent to wrong chain" or "signature requested on disconnected account" incidents.

Neither library is "better." Viem is the default for TypeScript-native React stacks because its types are end-to-end and the API surface is smaller. Ethers v6 remains a reasonable choice for Node.js backend services, scripts, and projects migrating from v5 where the migration cost is non-trivial. What matters is that the choice is deliberate, and the library's failure modes are documented in the team's runbooks.

If the function signature on the contract is wrong, the library should refuse to encode the call. A runtime revert is a bug. A compile error is a feature.

Wagmi: Hooks, Caching, and the Problem of Stale State

Wagmi is a React Hooks library built on Viem. It abstracts wallet connection, account state, and contract reads/writes into a uniform hook-based interface. This is not a UX convenience. It is a state-management primitive with specific security implications.

The core pattern:

  • useAccount exposes the connected address, chain ID, and connection status.
  • useReadContract issues a cached eth_call with automatic refetch on block updates and on wallet switch.
  • useWriteContract and useTransaction manage the lifecycle from signature request through receipt.

The critical invariant Wagmi enforces — when used correctly — is that the UI cannot render a contract read result for an account not currently connected, or for a chain different from the one the dApp targets. Race conditions where a user disconnects mid-render, or switches chains while a read is in flight, are handled by the hook layer rather than scattered across components.

The danger is misuse. Three patterns break the invariant:

1. Stale closure capture. Storing a useReadContract result in a useEffect dependency without including the reactive address. The displayed balance belongs to the previous user.

2. Manual refetch suppression. Calling refetch() on a custom interval while ignoring query.error. The UI shows the last successful read indefinitely.

3. Cross-chain assumption. Reading a contract address valid on mainnet while the wallet is connected to a testnet. The contract does not exist. The read returns zero, not an error. The UI displays "0 tokens."

Wagmi does not prevent these. It provides the primitives. The dApp's invariant enforcement is the developer's responsibility.

The Graph: Indexing as a Performance and Trust Boundary

RPC is a synchronization protocol. It is not a database. Querying historical event data through eth_getLogs is slow, rate-limited, and breaks down on chains with deep reorg histories. The Graph solves this by maintaining a separate index — a subgraph — that processes events into a queryable GraphQL schema.

The architecture:

  • A subgraph manifest (subgraph.yaml) declares the data sources, networks, and event handlers.
  • Event handlers, written in AssemblyScript, transform on-chain events into entities.
  • GraphQL queries, issued by the frontend via graphql-request or Apollo, retrieve entities.

This is not "free performance." The Graph node is a separate service operator. It can serve stale data, lag behind chain head, or — in the case of decentralized indexers — disagree with the canonical chain. The frontend must display the subgraph's block and treat any query result as a snapshot, not a real-time read.

A secure integration with The Graph follows three rules:

  • Pin the endpoint. Use a specific subgraph deployment ID. Do not query "the latest" version, which may change ABI.
  • Display data freshness. Show the block number of the index. Users acting on stale data have been liquidated.
  • Reconcile on critical paths. For high-value operations (borrows, swaps, governance votes), cross-check the indexed state against a direct RPC read before signing. The latency cost is worth the correctness guarantee.

Price oracles are the most lied-about component in Web3. The interface is simple: read a function, get a number. The reality is that the number is a median of many data feeds, updated only when the price moves more than a deviation threshold (typically 0.5% for ETH/USD) or after a heartbeat interval (typically 1 hour for less liquid feeds).

The invariants a frontend must enforce:

  • The price can be stale by up to one heartbeat. For a 1-hour heartbeat, an asset can move 5% in that window and the oracle will still report the old price. The dApp's on-chain logic must handle this. The frontend must not assume freshness.
  • The oracle can fail. A Chainlink feed can return the last good price, revert, or return an invalid value during an aggregator outage. The frontend must detect a reverted call and refuse to render dependent UI.
  • The feed address is chain-specific and asset-specific. Using the mainnet ETH/USD address on Optimism is a misconfiguration, not a degradation. The library should validate chainId and feedAddress at the type level.

Chainlink Data Feeds are the industry standard because they are decentralized at the data source level. They are not trustless. They are trust-minimized at a price. The frontend is responsible for surfacing that price to the user.

The Frontend Invariant Checklist

A Web3 frontend is a distributed system with three untrusted dependencies: the RPC node, the indexer, and the oracle. The libraries discussed here — Viem, Ethers, Wagmi, The Graph, Chainlink — provide primitives. The invariants are the developer's contract with the user.

Minimum guarantees before any production deployment:

  • Validate chainId before every contract call. Reject the call if the wallet is on the wrong network.
  • Display data freshness for all off-chain or indexed data. Block number, oracle round ID, or timestamp must be visible.
  • Reconcile indexed state with RPC for high-value operations. Do not rely on The Graph as the sole source of truth for liquidations or settlements.
  • Treat eth_call results as advisory. Surface revert reasons; do not catch and suppress them.
  • Pin every external dependency version exactly. A subgraph upgrade, a Chainlink feed address change, or a library minor version bump is a potential state change.

The complexity is not the enemy. Unacknowledged complexity is.

FAQ

Why should I treat RPC node responses as untrusted?
RPC nodes can return fabricated results, censor transactions, or withhold events. A frontend that trusts these responses without cross-checking acts only as a thin client to a potentially compromised server.
What is the main difference between Viem and Ethers.js v6?
Viem is a low-level, strongly-typed TypeScript interface with a smaller API surface, while Ethers.js v6 is a modular architecture that offers granular bundle control but requires more manual configuration of providers and signers.
How can I prevent stale state when using Wagmi hooks?
Avoid stale closure captures by ensuring all reactive dependencies are included in hooks, avoid manual refetch suppression that ignores errors, and always verify that the connected chain matches the dApp's target chain.
Is it safe to rely solely on The Graph for dApp data?
No, The Graph is an indexer that can lag behind the chain head or serve stale data. For high-value operations like liquidations or settlements, you should cross-check indexed state against a direct RPC read.
What are the risks of using Chainlink price oracles in a frontend?
Price data can be stale due to heartbeat intervals or deviation thresholds, and the oracle feed itself can fail or return invalid values. The frontend must detect reverted calls and display the data's freshness to the user.

By Caleb North