Реализация 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 };
}
Отслеживание событий контракта
Реальное время обновлений через event-подписку — важнее поллинга:
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-флоу, event-подпиской, историей транзакций и поддержкой нескольких сетей — 2–3 недели.







