import { encodeEventTopics, parseAbiItem, type RpcLog } from 'viem' import * as z from 'zod/mini' import * as Transfers from '../apps/data/routes/transfers.js' import * as Schema from './Schema.js' import * as Value from './Value.js' const event = parseAbiItem(Transfers.signature) const topic = encodeEventTopics({ abi: [event] })[0] /** Parsed transfer filter predicates. */ export type Filters = z.output /** One decoded TIP-20 Transfer row, as the read endpoint returns it. */ export type EventData = { address: string amount: string blockNumber: number logIndex: number recipient: string sender: string timestamp: string transactionHash: string } /** True when an address is in Tempo's deterministic TIP-20 contract range. */ function isTip20Address(address: string): boolean { const value = address.toLowerCase() return value >= Transfers.tip20AddressRange[0] && value < Transfers.tip20AddressRange[1] } /** Decodes one strict TIP-20 Transfer wire payload. */ export function decode(log: RpcLog, timestamp: string): EventData | undefined { if (!isTip20Address(log.address)) return undefined if ( log.topics.length !== 3 || log.topics[0]?.toLowerCase() !== topic.toLowerCase() || !/^0x0{24}[0-9a-fA-F]{40}$/.test(log.topics[1] ?? '') || !/^0x0{24}[0-9a-fA-F]{40}$/.test(log.topics[2] ?? '') || !/^0x[0-9a-fA-F]{64}$/.test(log.data) ) return undefined const address = Schema.TokenAddress.safeParse(log.address) const recipient = Schema.Address.safeParse(`0x${log.topics[2]!.slice(-40)}`) const sender = Schema.Address.safeParse(`0x${log.topics[1]!.slice(-40)}`) const transactionHash = Schema.Hash.safeParse(log.transactionHash) const amount = Value.fromHex(log.data) const blockNumber = Value.hexToNumber(log.blockNumber) const logIndex = Value.hexToNumber(log.logIndex) if ( !address.success || !recipient.success || !sender.success || !transactionHash.success || amount === undefined || blockNumber === undefined || logIndex === undefined ) return undefined return { address: address.data, amount, blockNumber, logIndex, recipient: recipient.data, sender: sender.data, timestamp, transactionHash: transactionHash.data, } } /** Re-checks every transfer filter after topic-level matching. */ export function matches(data: EventData, filters: Filters): boolean { if (filters.address && data.sender !== filters.address && data.recipient !== filters.address) return false if (filters.recipient && data.recipient !== filters.recipient) return false if (filters.sender && data.sender !== filters.sender) return false if (filters.token && data.address !== filters.token) return false return true }