Implementing dApp Interface for Smart Contracts on Website
dApp interface is frontend that lets users interact with smart contracts without knowing Solidity or command line. Button "Stake" runs stake(amount) transaction. Form "Swap" calls swapExactTokensForTokens. Table "Positions" reads contract data in real-time.
Complexity depends on contract: single contract with 5 functions is one story; protocol with proxy, multi-contract interactions, and offchain pricing — completely different.
dApp Frontend Architecture
dapp/
├── abis/ # Contract ABI files
│ ├── StakingPool.json
│ └── RewardToken.json
├── lib/
│ ├── contracts.ts # Typed contract instances
│ ├── wagmiConfig.ts # wagmi + viem setup
│ └── multicall.ts # Batch read requests
├── hooks/
│ ├── useStakingPool.ts # Read contract state
│ ├── useStake.ts # Stake transaction
│ └── useApprove.ts # ERC-20 approve
└── components/
├── StakingWidget/
├── PositionsTable/
└── TransactionStatus/
Wagmi and Client Setup
// lib/wagmiConfig.ts
import { createConfig, http } from 'wagmi';
import { mainnet, polygon, arbitrum } from 'wagmi/chains';
import { injected, walletConnect, coinbaseWallet } from 'wagmi/connectors';
export const wagmiConfig = createConfig({
chains: [mainnet, polygon, arbitrum],
connectors: [
injected(),
walletConnect({ projectId: process.env.NEXT_PUBLIC_WC_PROJECT_ID! }),
coinbaseWallet({ appName: 'MyDApp' }),
],
transports: {
[mainnet.id]: http(process.env.ETH_RPC_URL!),
[polygon.id]: http(process.env.POLYGON_RPC_URL!),
[arbitrum.id]: http(process.env.ARBITRUM_RPC_URL!),
},
});
Typed Hooks for Contract Reading
Instead of raw ABI arrays — typing via @wagmi/cli:
# wagmi.config.ts
npx wagmi generate
// wagmi.config.ts
import { defineConfig } from '@wagmi/cli';
import { react } from '@wagmi/cli/plugins';
export default defineConfig({
out: 'src/generated.ts',
contracts: [
{
name: 'StakingPool',
address: {
1: '0xContractOnMainnet',
137: '0xContractOnPolygon',
},
abi: StakingPoolAbi,
},
],
plugins: [react()],
});
Generates typed hooks useReadStakingPool, useWriteStakingPool, useSimulateStakingPool. Compilation errors for wrong arguments — instead of runtime errors.
Reading State with Multicall
// hooks/useStakingPool.ts
import { useAccount, useReadContracts } from 'wagmi';
import { StakingPoolAbi } from '@/abis/StakingPool';
const CONTRACT = '0xYourContract' as const;
export function useStakingPool() {
const { address } = useAccount();
const { data, isLoading } = useReadContracts({
contracts: [
{ address: CONTRACT, abi: StakingPoolAbi, functionName: 'totalStaked' },
{ address: CONTRACT, abi: StakingPoolAbi, functionName: 'rewardRate' },
{ address: CONTRACT, abi: StakingPoolAbi, functionName: 'periodFinish' },
...(address ? [
{ address: CONTRACT, abi: StakingPoolAbi, functionName: 'balanceOf', args: [address] },
{ address: CONTRACT, abi: StakingPoolAbi, functionName: 'earned', args: [address] },
] : []),
],
query: { refetchInterval: 12_000 },
});
return {
isLoading,
totalStaked: data?.[0].result as bigint | undefined,
rewardRate: data?.[1].result as bigint | undefined,
periodFinish: data?.[2].result as bigint | undefined,
userBalance: address ? data?.[3].result as bigint | undefined : undefined,
userEarned: address ? data?.[4].result as bigint | undefined : undefined,
};
}
ERC-20 Approve + Action — Standard Two-Step Flow
Most DeFi operations require approve token first, then call contract. Need to check current allowance and skip approve if sufficient:
// hooks/useStake.ts
import { useReadContract, useWriteContract, useWaitForTransactionReceipt } from 'wagmi';
import { erc20Abi, parseUnits } from 'viem';
const STAKING_CONTRACT = '0xStaking' as const;
const STAKE_TOKEN = '0xToken' as const;
export function useStake() {
const { address } = useAccount();
const { data: allowance, refetch: refetchAllowance } = useReadContract({
address: STAKE_TOKEN,
abi: erc20Abi,
functionName: 'allowance',
args: [address!, STAKING_CONTRACT],
query: { enabled: !!address },
});
const { writeContractAsync } = useWriteContract();
const [txHash, setTxHash] = useState<`0x${string}` | undefined>();
const { isLoading: isWaiting, isSuccess } = useWaitForTransactionReceipt({ hash: txHash });
const stake = async (amount: string, decimals: number) => {
const amountWei = parseUnits(amount, decimals);
// Step 1: approve if needed
if (!allowance || allowance < amountWei) {
const approveTx = await writeContractAsync({
address: STAKE_TOKEN,
abi: erc20Abi,
functionName: 'approve',
args: [STAKING_CONTRACT, amountWei],
});
// Wait for approve confirmation
await waitForTransactionReceipt(wagmiConfig, { hash: approveTx });
await refetchAllowance();
}
// Step 2: stake
const stakeTx = await writeContractAsync({
address: STAKING_CONTRACT,
abi: StakingPoolAbi,
functionName: 'stake',
args: [amountWei],
});
setTxHash(stakeTx);
};
return { stake, isWaiting, isSuccess, txHash };
}
Tracking Contract Events
Real-time updates via event subscription — more important than polling:
import { useWatchContractEvent } from 'wagmi';
export function useStakeEvents(onStake: (amount: bigint) => void) {
useWatchContractEvent({
address: STAKING_CONTRACT,
abi: StakingPoolAbi,
eventName: 'Staked',
onLogs(logs) {
for (const log of logs) {
if (log.args.user === address) {
onStake(log.args.amount as bigint);
}
}
},
});
}
Simulating Transactions Before Sending
import { useSimulateContract } from 'wagmi';
// Check transaction will pass before user signs
const { data: simulation, error: simError } = useSimulateContract({
address: STAKING_CONTRACT,
abi: StakingPoolAbi,
functionName: 'stake',
args: [amountWei],
query: { enabled: amountWei > 0n },
});
// simError contains reason for contract rejection — show to user
// before user signs and pays gas
Timeline
Interface for one contract with 3–5 write functions, state reading, and user position display — 5–7 days. Multi-contract protocol with approve flow, event subscription, transaction history, and multi-chain support — 2–3 weeks.







