diff --git a/supabase/functions/check-deposits/index.ts b/supabase/functions/check-deposits/index.ts index 719d6eb..47730ca 100644 --- a/supabase/functions/check-deposits/index.ts +++ b/supabase/functions/check-deposits/index.ts @@ -13,6 +13,7 @@ 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'; +import { selectTrc20Users } from './trc20.ts'; const ALLOWED_ORIGINS = ['https://telegram.pezkuwichain.io', 'https://telegram.pezkiwi.app']; const MIN_DEPOSIT = 10; // Minimum 10 USDT @@ -304,14 +305,20 @@ async function checkTrc20Deposits( // platform admin account and doubles as the treasury sweep destination - // excluded here so internal sweep transfers landing on it are never // mistaken for a customer deposit. - const { data: users } = await supabase + const { data: allUsers, error: usersError } = await supabase .from('tg_users') - .select('id, deposit_index') + .select('id, deposit_index, deposit_info_seen_at') .not('deposit_index', 'is', null) .neq('deposit_index', 0) .order('deposit_index', { ascending: true }); - if (!users || users.length === 0) return result; + if (usersError) { + result.errors.push(`TRC20 user list error: ${usersError.message}`); + return result; + } + type Trc20Row = { id: string; deposit_index: number; deposit_info_seen_at: string | null }; + const users = selectTrc20Users((allUsers ?? []) as Trc20Row[], new Date()); + if (users.length === 0) return result; // TronGrid allows a key 15 requests a second and, past that, refuses every // request for about 30 s ("The key exceeds the frequency limit(15), and the diff --git a/supabase/functions/check-deposits/trc20.ts b/supabase/functions/check-deposits/trc20.ts new file mode 100644 index 0000000..3e05d71 --- /dev/null +++ b/supabase/functions/check-deposits/trc20.ts @@ -0,0 +1,32 @@ +// The TRC20 check costs one TronGrid request per user, and the free plan +// allows 10,000 a day ("Exceed the user daily usage (10000)", logged +// 2026-10-19). Checking every user every five minutes needs ~29,000 a day at +// 101 users, so only users who opened the deposit screen recently are checked +// on every run; everyone else is checked by a full scan every six hours. + +export const ACTIVE_WINDOW_MS = 24 * 60 * 60 * 1000; +export const FULL_SCAN_EVERY_HOURS = 6; +// The cron runs every five minutes, so exactly one run per full-scan hour +// starts inside this window. +export const FULL_SCAN_WINDOW_MINUTES = 5; + +export function isFullScanRun(now: Date): boolean { + return ( + now.getUTCHours() % FULL_SCAN_EVERY_HOURS === 0 && + now.getUTCMinutes() < FULL_SCAN_WINDOW_MINUTES + ); +} + +export interface Trc20User { + deposit_info_seen_at: string | null; +} + +export function selectTrc20Users(users: T[], now: Date): T[] { + if (isFullScanRun(now)) return users; + const since = now.getTime() - ACTIVE_WINDOW_MS; + return users.filter((user) => { + if (!user.deposit_info_seen_at) return false; + const seen = Date.parse(user.deposit_info_seen_at); + return !Number.isNaN(seen) && seen >= since; + }); +} diff --git a/supabase/functions/check-deposits/trc20_test.ts b/supabase/functions/check-deposits/trc20_test.ts new file mode 100644 index 0000000..780bb99 --- /dev/null +++ b/supabase/functions/check-deposits/trc20_test.ts @@ -0,0 +1,32 @@ +import { assertEquals } from 'jsr:@std/assert@1'; +import { isFullScanRun, selectTrc20Users } from './trc20.ts'; + +const at = (iso: string) => new Date(iso); +const users = [ + { id: 'recent', deposit_info_seen_at: '2026-10-19T09:00:00Z' }, + { id: 'stale', deposit_info_seen_at: '2026-10-17T09:00:00Z' }, + { id: 'never', deposit_info_seen_at: null }, + { id: 'garbled', deposit_info_seen_at: 'not a date' }, +]; +const ids = (list: { id: string }[]) => list.map((u) => u.id); + +Deno.test('an ordinary run checks only users seen in the last 24 hours', () => { + assertEquals(ids(selectTrc20Users(users, at('2026-10-19T10:35:00Z'))), ['recent']); +}); + +Deno.test('a user seen exactly 24 hours ago is still checked', () => { + const edge = [{ id: 'edge', deposit_info_seen_at: '2026-10-18T10:35:00Z' }]; + assertEquals(ids(selectTrc20Users(edge, at('2026-10-19T10:35:00Z'))), ['edge']); +}); + +Deno.test('the full-scan run checks everyone', () => { + assertEquals(ids(selectTrc20Users(users, at('2026-10-19T12:00:20Z'))), ids(users)); +}); + +Deno.test('exactly one five-minute run per full-scan hour is a full scan', () => { + assertEquals(isFullScanRun(at('2026-10-19T00:00:15Z')), true); + assertEquals(isFullScanRun(at('2026-10-19T06:04:59Z')), true); + assertEquals(isFullScanRun(at('2026-10-19T06:05:00Z')), false); + assertEquals(isFullScanRun(at('2026-10-19T07:00:15Z')), false); + assertEquals(isFullScanRun(at('2026-10-19T18:00:15Z')), true); +}); diff --git a/supabase/functions/get-deposit-info/index.ts b/supabase/functions/get-deposit-info/index.ts index 09e25c6..90068de 100644 --- a/supabase/functions/get-deposit-info/index.ts +++ b/supabase/functions/get-deposit-info/index.ts @@ -255,6 +255,15 @@ serve(async (req) => { userId = newUser.id; } + // check-deposits checks this user's TRC20 address on every run for the + // next 24 hours, so a deposit made from this screen is found within + // minutes rather than at the next six-hourly full scan. + const { error: seenError } = await supabase + .from('tg_users') + .update({ deposit_info_seen_at: new Date().toISOString() }) + .eq('id', userId); + if (seenError) console.error('Failed to record deposit_info_seen_at:', seenError.message); + // Get or create deposit code let depositCode: string; const { data: existingCode } = await supabase diff --git a/supabase/migrations/20261019_tg_users_deposit_info_seen_at.sql b/supabase/migrations/20261019_tg_users_deposit_info_seen_at.sql new file mode 100644 index 0000000..a048584 --- /dev/null +++ b/supabase/migrations/20261019_tg_users_deposit_info_seen_at.sql @@ -0,0 +1,4 @@ +-- When the user last opened the deposit screen. check-deposits checks the +-- TRC20 addresses of users seen in the last 24 hours on every run and all +-- users only every six hours, to stay inside TronGrid's free daily quota. +ALTER TABLE tg_users ADD COLUMN IF NOT EXISTS deposit_info_seen_at timestamptz;