Реализация Token-Gated контента (доступ по владению токеном) на сайте
Token Gating — механизм, при котором доступ к контенту или функциям сайта открывается только пользователям, владеющим определёнными NFT или токенами в своём кошельке. Проверка баланса происходит через RPC-вызов к блокчейну.
Логика Token Gating
Пользователь подключает кошелёк
│
Сайт проверяет баланс токенов через RPC
│
[Есть токен?]
│ │
Да Нет
│ │
[Доступ [Предложить купить
открыт] токен / уведомление]
Проверка ERC-20 баланса
import { createPublicClient, http, parseAbi } from 'viem';
import { mainnet } from 'viem/chains';
const client = createPublicClient({
chain: mainnet,
transport: http(process.env.ETHEREUM_RPC_URL)
});
const ERC20_ABI = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function decimals() view returns (uint8)'
]);
async function checkERC20Balance(
walletAddress: string,
tokenContractAddress: `0x${string}`,
minBalance: bigint
): Promise<boolean> {
const balance = await client.readContract({
address: tokenContractAddress,
abi: ERC20_ABI,
functionName: 'balanceOf',
args: [walletAddress as `0x${string}`]
});
return balance >= minBalance;
}
// Пример: нужно >= 100 токенов EXAMPLE
const hasAccess = await checkERC20Balance(
userWalletAddress,
'0xYourTokenContract',
100n * 10n ** 18n // 100 токенов с 18 decimals
);
Проверка NFT (ERC-721)
const ERC721_ABI = parseAbi([
'function balanceOf(address owner) view returns (uint256)',
'function ownerOf(uint256 tokenId) view returns (address)'
]);
async function checkNFTOwnership(
walletAddress: string,
nftContract: `0x${string}`,
specificTokenId?: bigint
): Promise<boolean> {
if (specificTokenId !== undefined) {
// Проверить владение конкретным токеном
const owner = await client.readContract({
address: nftContract,
abi: ERC721_ABI,
functionName: 'ownerOf',
args: [specificTokenId]
});
return owner.toLowerCase() === walletAddress.toLowerCase();
}
// Проверить наличие хотя бы одного токена
const balance = await client.readContract({
address: nftContract,
abi: ERC721_ABI,
functionName: 'balanceOf',
args: [walletAddress as `0x${string}`]
});
return balance > 0n;
}
Middleware для защиты роутов
// Серверная проверка при каждом защищённом запросе
async function tokenGateMiddleware(req, res, next) {
const user = req.user; // JWT с walletAddress
if (!user?.walletAddress) {
return res.status(401).json({ error: 'Wallet not connected' });
}
// Кеширование результата проверки (5 минут) — RPC-вызовы платные
const cacheKey = `token_gate:${user.walletAddress}:${TOKEN_CONTRACT}`;
const cached = await redis.get(cacheKey);
if (cached !== null) {
if (cached === '0') return res.status(403).json({ error: 'Token required' });
return next();
}
const hasToken = await checkNFTOwnership(user.walletAddress, TOKEN_CONTRACT);
await redis.setex(cacheKey, 300, hasToken ? '1' : '0');
if (!hasToken) {
return res.status(403).json({
error: 'Access denied',
requiredToken: TOKEN_CONTRACT,
purchaseUrl: 'https://opensea.io/collection/your-nft'
});
}
next();
}
// Применение к роутам
app.get('/premium/content', authenticate, tokenGateMiddleware, getContent);
app.get('/members-only/*', authenticate, tokenGateMiddleware, handleMemberRoute);
Морально стойкий подход: кеширование с инвалидацией
Баланс токенов может измениться (пользователь продал NFT). Инвалидация кеша через блокчейн-события:
// Слушатель Transfer-событий NFT
const ERC721_TRANSFER_ABI = parseAbi([
'event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)'
]);
client.watchContractEvent({
address: TOKEN_CONTRACT,
abi: ERC721_TRANSFER_ABI,
eventName: 'Transfer',
onLogs: async (logs) => {
for (const log of logs) {
// Инвалидируем кеш для отправителя и получателя
await redis.del(`token_gate:${log.args.from}:${TOKEN_CONTRACT}`);
await redis.del(`token_gate:${log.args.to}:${TOKEN_CONTRACT}`);
}
}
});
Сроки
Token Gating с ERC-20/ERC-721 проверкой, кешированием и middleware — 3–5 дней.







