Web3 Integration Patterns in 2026: WalletConnect, ethers.js, and On-Chain Data with The Graph
Integrate Web3 into your application: WalletConnect v3, ethers.js contract interactions, on-chain data indexing with The Graph, transaction signing patterns, and hybrid Web2/Web3 architecture.
Web3 Integration Patterns in 2026: WalletConnect, ethers.js, and On-Chain Data with The Graph
Most "Web3 apps" in 2026 are actually hybrid: a traditional backend handling auth, payments, and business logic, with on-chain components for ownership, tokenization, and trustless settlement. Pure dApps that rely on the blockchain for everything are slow, expensive, and poor UX. Pragmatic hybrid architecture gives you the best of both worlds.
This post covers the integration patterns for connecting a standard TypeScript/Next.js app to the Ethereum ecosystem: wallet connection with WalletConnect v3, reading and writing to smart contracts with ethers.js, and indexing on-chain events efficiently with The Graph.
Hybrid Architecture: What Lives On-Chain vs Off-Chain
Off-chain (your backend): On-chain (blockchain):
├── User authentication ├── Asset ownership (NFTs, tokens)
├── Business logic ├── Trustless transfers
├── Payment processing ├── DAO governance votes
├── Notifications ├── DeFi protocol interactions
├── Search and filtering ├── Provenance / audit trail
└── Metadata storage └── Smart contract state
Rule: Put on-chain only what MUST be trustless or immutable.
Everything else is cheaper, faster, and better UX off-chain.
WalletConnect v3: Connecting Wallets
WalletConnect is the standard for connecting MetaMask, Rainbow, Coinbase Wallet, and hardware wallets to web apps. Use wagmi + WalletConnect's AppKit for the simplest integration:
npm install wagmi viem @walletconnect/appkit @tanstack/react-query
// src/lib/web3-config.ts
import { createAppKit } from '@walletconnect/appkit/react';
import { WagmiAdapter } from '@walletconnect/appkit-adapter-wagmi';
import { mainnet, sepolia, polygon } from 'viem/chains';
import { cookieStorage, createStorage } from 'wagmi';
const projectId = process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID!;
const networks = [mainnet, polygon, sepolia] as const;
export const wagmiAdapter = new WagmiAdapter({
storage: createStorage({ storage: cookieStorage }),
ssr: true, // Next.js SSR support
projectId,
networks,
});
// Initialize AppKit — provides the connect modal
createAppKit({
adapters: [wagmiAdapter],
networks,
projectId,
metadata: {
name: 'MyApp',
description: 'MyApp Web3 Integration',
url: 'https://myapp.com',
icons: ['https://myapp.com/icon.png'],
},
features: {
analytics: false,
email: false, // Disable email login
socials: [], // Disable social login
},
themeMode: 'light',
});
export const config = wagmiAdapter.wagmiConfig;
// src/providers/Web3Provider.tsx
'use client';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { WagmiProvider, type State } from 'wagmi';
import { config } from '@/lib/web3-config';
const queryClient = new QueryClient();
export function Web3Provider({
children,
initialState,
}: {
children: React.ReactNode;
initialState?: State;
}) {
return (
<WagmiProvider config={config} initialState={initialState}>
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
</WagmiProvider>
);
}
// src/components/ConnectButton.tsx
'use client';
import { useAppKit } from '@walletconnect/appkit/react';
import { useAccount, useDisconnect } from 'wagmi';
import { formatAddress } from '@/lib/utils';
export function ConnectButton() {
const { open } = useAppKit();
const { address, isConnected } = useAccount();
const { disconnect } = useDisconnect();
if (isConnected && address) {
return (
<div className="flex items-center gap-2">
<span className="font-mono text-sm">{formatAddress(address)}</span>
<button onClick={() => disconnect()} className="btn-secondary">
Disconnect
</button>
</div>
);
}
return (
<button onClick={() => open()} className="btn-primary">
Connect Wallet
</button>
);
}
// Format 0x1234...5678
function formatAddress(addr: string): string {
return `${addr.slice(0, 6)}...${addr.slice(-4)}`;
}
⛓️ Smart Contracts That Do Not Get Hacked
Every Solidity contract we deploy goes through static analysis, unit testing, and edge-case review. Security is not a checklist — it is built into every function.
- Solidity, Rust (Solana), Move (Aptos) smart contracts
- DeFi: DEX, lending, yield, staking protocols
- NFT platforms with on-chain and IPFS metadata
- DAO governance with multisig and timelock
Wallet-Based Authentication (SIWE)
Sign-In With Ethereum (EIP-4361) lets users authenticate by signing a message — proving wallet ownership without a password:
// src/api/auth/siwe.ts
import { SiweMessage, generateNonce } from 'siwe';
// Step 1: Generate nonce (prevent replay attacks)
export async function generateSiweNonce(address: string): Promise<string> {
const nonce = generateNonce();
// Store with TTL — nonces expire after 5 minutes
await redis.setex(`siwe:nonce:${address.toLowerCase()}`, 300, nonce);
return nonce;
}
// Step 2: Verify signed message
export async function verifySiweSignature(
message: string,
signature: string,
): Promise<{ address: string; valid: boolean }> {
const siweMessage = new SiweMessage(message);
// Verify cryptographic signature
const { success, data, error } = await siweMessage.verify({ signature });
if (!success || error) {
return { address: '', valid: false };
}
// Verify nonce matches what we issued
const storedNonce = await redis.get(`siwe:nonce:${data.address.toLowerCase()}`);
if (storedNonce !== data.nonce) {
return { address: '', valid: false };
}
// Delete nonce — single use
await redis.del(`siwe:nonce:${data.address.toLowerCase()}`);
return { address: data.address, valid: true };
}
// src/hooks/useSiweAuth.ts
'use client';
import { useSignMessage, useAccount } from 'wagmi';
import { SiweMessage } from 'siwe';
export function useSiweAuth() {
const { address, chainId } = useAccount();
const { signMessageAsync } = useSignMessage();
const signIn = async (): Promise<void> => {
if (!address || !chainId) throw new Error('Wallet not connected');
// Get nonce from your server
const { nonce } = await fetch('/api/auth/nonce', {
method: 'POST',
body: JSON.stringify({ address }),
}).then((r) => r.json());
// Create SIWE message
const message = new SiweMessage({
domain: window.location.host,
address,
statement: 'Sign in to MyApp',
uri: window.location.origin,
version: '1',
chainId,
nonce,
});
const messageString = message.prepareMessage();
const signature = await signMessageAsync({ message: messageString });
// Verify on server and get session token
const { token } = await fetch('/api/auth/verify', {
method: 'POST',
body: JSON.stringify({ message: messageString, signature }),
}).then((r) => r.json());
// Store session token (same as any other auth token)
localStorage.setItem('auth_token', token);
};
return { signIn };
}
Reading Smart Contracts with ethers.js / viem
// src/lib/contracts.ts
import { createPublicClient, http, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';
const publicClient = createPublicClient({
chain: mainnet,
transport: http(process.env.ETHEREUM_RPC_URL!),
});
// ERC-20 ABI (subset for balance checking)
const ERC20_ABI = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)',
'function symbol() view returns (string)',
'function totalSupply() view returns (uint256)',
]);
// NFT contract ABI
const ERC721_ABI = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function ownerOf(uint256 tokenId) view returns (address)',
'function tokenURI(uint256 tokenId) view returns (string)',
]);
export async function getTokenBalance(
tokenAddress: `0x${string}`,
walletAddress: `0x${string}`,
): Promise<{ balance: bigint; decimals: number; symbol: string }> {
const [balance, decimals, symbol] = await Promise.all([
publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [walletAddress],
}),
publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'decimals',
}),
publicClient.readContract({
address: tokenAddress,
abi: ERC20_ABI,
functionName: 'symbol',
}),
]);
return { balance, decimals, symbol };
}
// Format balance: 1000000000000000000 → "1.0 USDC"
export function formatTokenAmount(balance: bigint, decimals: number): string {
const divisor = BigInt(10 ** decimals);
const whole = balance / divisor;
const fraction = balance % divisor;
return `${whole}.${fraction.toString().padStart(decimals, '0').slice(0, 4)}`;
}
Writing to Contracts (Sending Transactions)
// src/hooks/useContractWrite.ts
'use client';
import { useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { parseEther } from 'viem';
const MY_CONTRACT_ABI = parseAbi([
'function mint(address to, uint256 amount) returns (bool)',
'function transfer(address to, uint256 amount) returns (bool)',
]);
const CONTRACT_ADDRESS = '0x...' as const;
export function useMintTokens() {
const { writeContract, data: hash, isPending, error } = useWriteContract();
const { isLoading: isConfirming, isSuccess: isConfirmed } =
useWaitForTransactionReceipt({ hash });
const mint = async (toAddress: `0x${string}`, amount: string) => {
writeContract({
address: CONTRACT_ADDRESS,
abi: MY_CONTRACT_ABI,
functionName: 'mint',
args: [toAddress, parseEther(amount)],
});
};
return {
mint,
hash,
isPending, // Waiting for user to sign in wallet
isConfirming, // Transaction submitted, waiting for block confirmation
isConfirmed, // Transaction included in block
error,
};
}
🔐 Already Have a Contract? Get It Audited.
Most hacks are preventable. Before you deploy to mainnet, let our team review your contracts for reentrancy, overflow, access control, and oracle manipulation.
- Manual line-by-line audit + automated Slither/Mythril scan
- Findings report with severity ratings and fix recommendations
- Audit certificate for your investors and community
- Post-audit re-check included
The Graph: Indexing On-Chain Events
Querying the blockchain directly for historical data is slow and expensive. The Graph indexes smart contract events into a queryable GraphQL API:
# subgraph/schema.graphql
type Transfer @entity {
id: ID! # tx_hash-log_index
from: Bytes! # address
to: Bytes! # address
value: BigInt! # token amount
blockNumber: BigInt!
timestamp: BigInt!
transactionHash: Bytes!
}
type Account @entity {
id: ID! # wallet address
balance: BigInt!
transfersOut: [Transfer!]! @derivedFrom(field: "from")
transfersIn: [Transfer!]! @derivedFrom(field: "to")
}
// subgraph/src/mapping.ts — event handler (AssemblyScript)
import { Transfer as TransferEvent } from '../generated/MyToken/MyToken';
import { Transfer, Account } from '../generated/schema';
import { BigInt } from '@graphprotocol/graph-ts';
export function handleTransfer(event: TransferEvent): void {
// Create transfer record
const id = event.transaction.hash.toHex() + '-' + event.logIndex.toString();
const transfer = new Transfer(id);
transfer.from = event.params.from;
transfer.to = event.params.to;
transfer.value = event.params.value;
transfer.blockNumber = event.block.number;
transfer.timestamp = event.block.timestamp;
transfer.transactionHash = event.transaction.hash;
transfer.save();
// Update sender balance
let sender = Account.load(event.params.from.toHex());
if (!sender) {
sender = new Account(event.params.from.toHex());
sender.balance = BigInt.fromI32(0);
}
sender.balance = sender.balance.minus(event.params.value);
sender.save();
// Update recipient balance
let recipient = Account.load(event.params.to.toHex());
if (!recipient) {
recipient = new Account(event.params.to.toHex());
recipient.balance = BigInt.fromI32(0);
}
recipient.balance = recipient.balance.plus(event.params.value);
recipient.save();
}
// src/lib/subgraph.ts — Query The Graph from your app
import { request, gql } from 'graphql-request';
const SUBGRAPH_URL = process.env.NEXT_PUBLIC_SUBGRAPH_URL!;
interface TransferHistory {
transfers: Array<{
id: string;
from: string;
to: string;
value: string;
timestamp: string;
transactionHash: string;
}>;
}
export async function getTransferHistory(
address: string,
limit = 20,
): Promise<TransferHistory['transfers']> {
const query = gql`
query GetTransfers($address: Bytes!, $limit: Int!) {
transfers(
where: { or: [{ from: $address }, { to: $address }] }
orderBy: timestamp
orderDirection: desc
first: $limit
) {
id
from
to
value
timestamp
transactionHash
}
}
`;
const data = await request<TransferHistory>(SUBGRAPH_URL, query, {
address: address.toLowerCase(),
limit,
});
return data.transfers;
}
// Token holder leaderboard
export async function getTopHolders(limit = 10) {
const query = gql`
query TopHolders($limit: Int!) {
accounts(
orderBy: balance
orderDirection: desc
first: $limit
where: { balance_gt: "0" }
) {
id
balance
}
}
`;
const { accounts } = await request<{ accounts: Array<{ id: string; balance: string }> }>(
SUBGRAPH_URL, query, { limit }
);
return accounts;
}
On-Chain Data Cost Comparison
| Method | Speed | Cost | Use Case |
|---|---|---|---|
| Direct RPC call (eth_call) | Fast | Free (read) | Current state: balance, owner |
| Event log scan (eth_getLogs) | Slow | Expensive | Historical events, small range |
| The Graph hosted | Fast | Free tier + paid | Most on-chain data queries |
| Self-hosted subgraph | Fast | Infra cost | High-volume, sensitive data |
| Alchemy/Moralis API | Fast | Subscription | NFT metadata, portfolio data |
Working With Viprasol
We integrate Web3 capabilities into existing applications — wallet authentication, smart contract interactions, and on-chain data indexing with The Graph.
What we deliver:
- WalletConnect v3 setup with wagmi and AppKit
- SIWE (Sign-In With Ethereum) authentication flow
- ethers.js / viem smart contract read/write integration
- The Graph subgraph development, deployment, and querying
- Hybrid Web2/Web3 architecture design
→ Discuss your Web3 integration → Blockchain development services
See Also
About the Author
Viprasol Tech Team
Custom Software Development Specialists
The Viprasol Tech team specialises in algorithmic trading software, AI agent systems, and SaaS development. With 100+ projects delivered across MT4/MT5 EAs, fintech platforms, and production AI systems, the team brings deep technical experience to every engagement. Based in India, serving clients globally.
Exploring Web3 & Blockchain?
Smart contracts, DApps, NFT platforms — built with security and audits included.
Free consultation • No commitment • Response within 24 hours
Need on-chain data pipelines or analytics?
We build blockchain data pipelines and analytics infrastructure — indexing on-chain events, building real-time dashboards, and turning raw blockchain data into actionable business intelligence.