Реалізація dApp-інтерфейсу для смарт-контрактів на веб-сайті
dApp-інтерфейс — це фронтенд, який дозволяє користувачам взаємодіяти зі смарт-контрактами без знання Solidity або командного рядка. Кнопка "Stake" запускає транзакцію stake(amount). Форма "Swap" викликає swapExactTokensForTokens. Таблиця "Positions" читає дані контракту в реальному часі.
Складність залежить від контракту: одиночний контракт з 5 функціями — це одна історія; протокол з проксі, мультиконтрактними взаємодіями та offchain-ціноутворенням — зовсім інша.
Архітектура dApp-фронтенду
dapp/
├── abis/ # ABI-файли контрактів
│ ├── StakingPool.json
│ └── RewardToken.json
├── lib/
│ ├── contracts.ts # Типизовані інстанси контрактів
│ ├── wagmiConfig.ts # Налаштування wagmi + viem
│ └── multicall.ts # Батчинг read-запитів
├── hooks/
│ ├── useStakingPool.ts # Читання стану контракту
│ ├── useStake.ts # Транзакція stake
│ └── useApprove.ts # ERC-20 approve
└── components/
├── StakingWidget/
├── PositionsTable/
└── TransactionStatus/
Налаштування wagmi та клієнтів
// 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!),
},
});
Типизовані хуки для читання контракту
Замість роботи з сирими ABI-масивами — типізація через @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()],
});
Генерує типізовані хуки useReadStakingPool, useWriteStakingPool, useSimulateStakingPool. Помилки компіляції при неверних аргументах — замість runtime-помилок.
Читання стану з 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 + дія — стандартний двохшаговий флоу
Більшість DeFi-операцій вимагають спочатку approve токена, потім вызов контракту. Потрібно перевіряти поточний allowance та пропускати approve, якщо він достатній:
// 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);
// Крок 1: approve, якщо потрібен
if (!allowance || allowance < amountWei) {
const approveTx = await writeContractAsync({
address: STAKE_TOKEN,
abi: erc20Abi,
functionName: 'approve',
args: [STAKING_CONTRACT, amountWei],
});
// Чекаємо підтвердження approve
await waitForTransactionReceipt(wagmiConfig, { hash: approveTx });
await refetchAllowance();
}
// Крок 2: stake
const stakeTx = await writeContractAsync({
address: STAKING_CONTRACT,
abi: StakingPoolAbi,
functionName: 'stake',
args: [amountWei],
});
setTxHash(stakeTx);
};
return { stake, isWaiting, isSuccess, txHash };
}
Відстеження подій контракту
Оновлення в реальному часі через підписку на события — важливіше за поллінг:
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);
}
}
},
});
}
Симуляція транзакцій перед відправкою
import { useSimulateContract } from 'wagmi';
// Перевіряємо, що транзакція пройде, до підписи користувачем
const { data: simulation, error: simError } = useSimulateContract({
address: STAKING_CONTRACT,
abi: StakingPoolAbi,
functionName: 'stake',
args: [amountWei],
query: { enabled: amountWei > 0n },
});
// simError містить причину відказу контракту — показуємо користувачу
// до того, як він підписав і витратив gas
Часова шкала
Інтерфейс для одного контракту з 3–5 write-функціями, читанням стану та відображенням позицій користувача — 5–7 днів. Мультиконтрактний протокол з approve-флоу, підпиской на события, історією транзакцій та підтримкою кількох мереж — 2–3 тижні.







