feat(check-deposits): check TRC20 addresses of recently active users only (#41)

TronGrid's free plan allows 10,000 requests a day. Checking every user's
TRC20 address on every five-minute run needs ~29,000 a day at 101 users,
so the key ran out at 03:00 UTC and every later run was refused.

get-deposit-info now records when a user opened the deposit screen.
check-deposits checks users seen in the last 24 hours on every run and
everyone in one run every six hours (00, 06, 12, 18 UTC): about 3,300
requests a day at 10 active users. A deposit made without opening the
screen within the past day waits for the next full scan.

The user-list query now reports its error instead of ending the TRC20
check as if there were no users.
This commit is contained in:
SatoshiQaziMuhammed authored and GitHub committed 2026-10-19 12:17:13 +04:00
1 parent 90955269df
commit 4f00d3f696
5 files changed
+87 -3

No files matched your search

+10 -3
View File
@@ -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
@@ -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<T extends Trc20User>(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;
});
}
@@ -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);
});
@@ -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
@@ -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;