import type { Hex } from 'ox' import * as WebhookTransfer from './WebhookTransfer.js' const token: Hex.Hex = '0x20c0000000000000000000000000000000000001' const sender: Hex.Hex = `0x${'aa'.repeat(20)}` const recipient: Hex.Hex = `0x${'bb'.repeat(20)}` const other: Hex.Hex = `0x${'cc'.repeat(20)}` const transactionHash = `0x${'ef'.repeat(32)}` const transferTopic = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' const timestamp = '2026-01-01T00:00:00.000Z' const addressTopic = (address: string) => `0x${'0'.repeat(24)}${address.slice(2)}` function transferLog(overrides: Record = {}) { return { address: token, blockNumber: '0x7b', data: `0x${(10n ** 18n).toString(16).padStart(64, '0')}`, logIndex: '0x4', topics: [transferTopic, addressTopic(sender), addressTopic(recipient)], transactionHash, transactionIndex: '0x0', ...overrides, } as never } describe('decode', () => { test('decodes a strict TIP-20 Transfer log', () => { expect(WebhookTransfer.decode(transferLog(), timestamp)).toMatchInlineSnapshot(` { "address": "0x20c0000000000000000000000000000000000001", "amount": "1000000000000000000", "blockNumber": 123, "logIndex": 4, "recipient": "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "sender": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "timestamp": "2026-01-01T00:00:00.000Z", "transactionHash": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef", } `) }) test('rejects non-TIP-20 addresses and malformed payloads', () => { const cases: readonly Record[] = [ // Outside the deterministic TIP-20 address range. { address: other }, // Missing recipient topic. { topics: [transferTopic, addressTopic(sender)] }, // Wrong event selector. { topics: [`0x${'12'.repeat(32)}`, addressTopic(sender), addressTopic(recipient)] }, // Sender topic without the address left-padding. { topics: [transferTopic, `0x${'ff'.repeat(32)}`, addressTopic(recipient)] }, // Amount payload is not one word. { data: '0x01' }, // Pending-log positions cannot form a cursor. { blockNumber: null }, { logIndex: null }, ] for (const overrides of cases) expect( WebhookTransfer.decode(transferLog(overrides), timestamp), JSON.stringify(overrides), ).toBeUndefined() }) }) describe('matches', () => { const data = WebhookTransfer.decode(transferLog(), timestamp)! test('AND-combines side, either-side address, and token filters', () => { const cases: readonly [WebhookTransfer.Filters, boolean][] = [ [{}, true], [{ sender }, true], [{ sender: recipient }, false], [{ recipient }, true], [{ recipient: sender }, false], [{ address: sender }, true], [{ address: recipient }, true], [{ address: other }, false], [{ token }, true], [{ token: '0x20c0000000000000000000000000000000000002' }, false], [{ recipient, sender, token }, true], [{ recipient: sender, sender, token }, false], ] for (const [filters, expected] of cases) expect(WebhookTransfer.matches(data, filters), JSON.stringify(filters)).toBe(expected) }) })