Back to Blog

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.

Viprasol Tech Team
August 20, 2026
14 min read

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

MethodSpeedCostUse Case
Direct RPC call (eth_call)FastFree (read)Current state: balance, owner
Event log scan (eth_getLogs)SlowExpensiveHistorical events, small range
The Graph hostedFastFree tier + paidMost on-chain data queries
Self-hosted subgraphFastInfra costHigh-volume, sensitive data
Alchemy/Moralis APIFastSubscriptionNFT 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 integrationBlockchain development services


See Also

Share this article:

About the Author

V

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.

MT4/MT5 EA DevelopmentAI Agent SystemsSaaS DevelopmentAlgorithmic Trading

Exploring Web3 & Blockchain?

Smart contracts, DApps, NFT platforms — built with security and audits included.

Free consultation • No commitment • Response within 24 hours

Viprasol · Big Data & Analytics

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.