diff --git a/supabase/functions/check-deposits/index.ts b/supabase/functions/check-deposits/index.ts index 7843306..ec8107b 100644 --- a/supabase/functions/check-deposits/index.ts +++ b/supabase/functions/check-deposits/index.ts @@ -12,6 +12,7 @@ import { keccak_256 } from 'https://esm.sh/@noble/hashes@1.3.2/sha3'; import { sha256 } from 'https://esm.sh/@noble/hashes@1.3.2/sha256'; import { bytesToHex } from 'https://esm.sh/@noble/hashes@1.3.2/utils'; import { secp256k1 } from 'https://esm.sh/@noble/curves@1.2.0/secp256k1'; +import { isIncomingTonTransfer } from './ton.ts'; const ALLOWED_ORIGINS = ['https://telegram.pezkuwichain.io', 'https://telegram.pezkiwi.app']; const MIN_DEPOSIT = 10; // Minimum 10 USDT @@ -138,6 +139,17 @@ async function checkTonDeposits( const result = { found: 0, processed: 0, errors: [] as string[] }; try { + // tonapi reports recipients in raw form; the configured address may be + // user-friendly, so resolve it before comparing. + const parsed = await fetch(`${TON_API}/address/${depositAddress}/parse`, { + headers: tonHeaders(), + }); + if (!parsed.ok) { + result.errors.push(await apiError('TON address parse error:', parsed)); + return result; + } + const depositRawAddress: string = (await parsed.json()).raw_form; + // Get recent transactions const response = await fetch(`${TON_API}/accounts/${depositAddress}/events?limit=50`, { headers: tonHeaders(), @@ -156,6 +168,7 @@ async function checkTonDeposits( for (const action of event.actions || []) { if (action.type !== 'JettonTransfer') continue; if (action.JettonTransfer?.jetton?.address !== TON_USDT_MASTER) continue; + if (!isIncomingTonTransfer(action.JettonTransfer, depositRawAddress)) continue; const txHash = event.event_id; const amount = parseInt(action.JettonTransfer.amount) / 1e6; // USDT has 6 decimals diff --git a/supabase/functions/check-deposits/ton.ts b/supabase/functions/check-deposits/ton.ts new file mode 100644 index 0000000..524829d --- /dev/null +++ b/supabase/functions/check-deposits/ton.ts @@ -0,0 +1,13 @@ +// A deposit address's event feed also carries the transfers it sends, such as +// sweeps to the hot wallet. Only a transfer whose recipient is the deposit +// address itself is a deposit. +export interface TonJettonTransfer { + recipient?: { address?: string }; +} + +export function isIncomingTonTransfer( + transfer: TonJettonTransfer | undefined, + depositRawAddress: string +): boolean { + return transfer?.recipient?.address === depositRawAddress; +} diff --git a/supabase/functions/check-deposits/ton_test.ts b/supabase/functions/check-deposits/ton_test.ts new file mode 100644 index 0000000..8d2ab3f --- /dev/null +++ b/supabase/functions/check-deposits/ton_test.ts @@ -0,0 +1,21 @@ +import { assert, assertFalse } from 'jsr:@std/assert@1'; +import { isIncomingTonTransfer } from './ton.ts'; + +const DEPOSIT = '0:c35f028ad76a7003bcb164decaec61230a77ad05c724856409e9445014b0b7be'; + +Deno.test('a transfer to the deposit address is a deposit', () => { + assert(isIncomingTonTransfer({ recipient: { address: DEPOSIT } }, DEPOSIT)); +}); + +Deno.test('a sweep out of the deposit address is not a deposit', () => { + // 2026-02-13: 41.9 USDT sent from the deposit address, no memo. + const sweep = { + recipient: { address: '0:ef28248f185d0000000000000000000000000000000000000000000000000000' }, + }; + assertFalse(isIncomingTonTransfer(sweep, DEPOSIT)); +}); + +Deno.test('a transfer with no recipient is not a deposit', () => { + assertFalse(isIncomingTonTransfer({}, DEPOSIT)); + assertFalse(isIncomingTonTransfer(undefined, DEPOSIT)); +});