Разработка системы уведомлений для dApp через Push Protocol
Email-уведомления в DeFi не работают — у пользователей нет email в протоколе, только адрес кошелька. Телеграм-боты требуют отдельной подписки. Push Protocol (ранее EPNS) решает это: децентрализованный messaging layer, где уведомления привязаны к адресу кошелька, пользователь подписывается on-chain на каналы и получает уведомления в Push App, браузерный extension или прямо в dApp через SDK.
Как работает Push Protocol
Channel — это адрес, который отправляет уведомления. Создаётся один раз, требует стейкинга 50 PUSH на Ethereum (или Polygon). Протокол — платный в части создания канала, но рассылка уведомлений бесплатна.
Subscriber — пользователь, который opt-in в канал. Подписка — on-chain транзакция (или off-chain через gasless механизм на Polygon).
Notification — JSON payload, хранится в IPFS, индексируется узлами Push Protocol сети. Типы: Broadcast (всем подписчикам), Targeted (конкретному адресу), Subset (списку адресов).
Создание канала и отправка уведомлений
npm install @pushprotocol/restapi @pushprotocol/socket ethers
Создание канала — через Push dApp (app.push.org). Программная отправка уведомлений с сервера:
import * as PushAPI from '@pushprotocol/restapi'
import { ethers } from 'ethers'
const CHANNEL_PRIVATE_KEY = process.env.PUSH_CHANNEL_PRIVATE_KEY!
const signer = new ethers.Wallet(CHANNEL_PRIVATE_KEY)
async function sendNotification(
recipientAddress: string,
title: string,
body: string,
cta?: string
) {
await PushAPI.payloads.sendNotification({
signer,
type: 3, // targeted
identityType: 2, // direct payload
notification: { title, body },
payload: {
title,
body,
cta: cta ?? '',
img: '',
},
recipients: `eip155:1:${recipientAddress}`,
channel: `eip155:1:${CHANNEL_ADDRESS}`,
env: 'prod',
})
}
Типичные триггеры для DeFi уведомлений
| Событие | Тип уведомления | Задержка |
|---|---|---|
| Liquidation risk (health < 1.2) | Targeted, HIGH urgency | Real-time |
| Position liquidated | Targeted | Real-time |
| Yield harvest available | Targeted | Hourly |
| Governance proposal created | Broadcast | On-chain event |
| Voting deadline в 24h | Broadcast | Scheduled |
| Large price movement (>10%) | Broadcast | Price oracle |
Для real-time триггеров нужен on-chain event listener:
import { createPublicClient, http, parseAbiItem } from 'viem'
const client = createPublicClient({ chain: mainnet, transport: http(RPC_URL) })
// Смотрим события ликвидации
client.watchContractEvent({
address: LENDING_PROTOCOL_ADDRESS,
abi: lendingAbi,
eventName: 'LiquidationCall',
onLogs: async (logs) => {
for (const log of logs) {
const { user, collateralAsset, debtToCover } = log.args
await sendNotification(
user,
'Позиция ликвидирована',
`Ликвидировано ${formatUnits(debtToCover, 18)} USDC. Проверьте ваши позиции.`,
`https://app.protocol.xyz/positions`
)
}
}
})
Отображение уведомлений в dApp
Fetch уведомлений для пользователя
import * as PushAPI from '@pushprotocol/restapi'
import { useAccount } from 'wagmi'
import { useQuery } from '@tanstack/react-query'
export function useNotifications() {
const { address } = useAccount()
return useQuery({
queryKey: ['push-notifications', address],
queryFn: async () => {
const feeds = await PushAPI.user.getFeeds({
user: `eip155:1:${address}`,
limit: 20,
env: 'prod',
})
return feeds
},
enabled: !!address,
refetchInterval: 30_000, // polling каждые 30 секунд
})
}
Real-time через WebSocket
import { createSocketConnection, EVENTS } from '@pushprotocol/socket'
function usePushSocket(address: string | undefined) {
const [socket, setSocket] = useState<any>(null)
const queryClient = useQueryClient()
useEffect(() => {
if (!address) return
const sdkSocket = createSocketConnection({
user: `eip155:1:${address}`,
env: 'prod',
socketOptions: { autoConnect: true }
})
sdkSocket.on(EVENTS.CONNECT, () => console.log('Push socket connected'))
sdkSocket.on(EVENTS.USER_FEEDS, (feedItem: any) => {
// Инвалидируем кэш при новом уведомлении
queryClient.invalidateQueries({ queryKey: ['push-notifications', address] })
// Показываем toast
showToast(feedItem.payload.notification.title)
})
setSocket(sdkSocket)
return () => sdkSocket?.disconnect()
}, [address])
}
Компонент уведомлений
function NotificationBell() {
const { data: notifications = [], isLoading } = useNotifications()
const unread = notifications.filter(n => !n.epoch || n.epoch > lastRead)
return (
<Popover>
<PopoverTrigger>
<Bell className="h-5 w-5" />
{unread.length > 0 && (
<span className="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-red-500 text-xs flex items-center justify-center">
{unread.length}
</span>
)}
</PopoverTrigger>
<PopoverContent className="w-80">
{notifications.map(n => (
<NotificationItem key={n.sid} notification={n} />
))}
</PopoverContent>
</Popover>
)
}
Проверка подписки и opt-in
Перед отправкой targeted уведомлений нужно проверить, подписан ли пользователь:
async function isSubscribed(userAddress: string): Promise<boolean> {
const subscriptions = await PushAPI.user.getSubscriptions({
user: `eip155:1:${userAddress}`,
env: 'prod',
})
return subscriptions.some(
(sub: any) => sub.channel.toLowerCase() === CHANNEL_ADDRESS.toLowerCase()
)
}
Gasless opt-in через Push SDK — пользователь подписывается через off-chain подпись (EIP-712), без gas. Важно для onboarding — требовать gas за подписку на уведомления отпугивает пользователей.
const user = await PushAPI.initialize(signer, { env: 'prod' })
await user.notification.subscribe(`eip155:1:${CHANNEL_ADDRESS}`) // gasless через delegated signing







