From 9294d2f868fad25108aea1cbe6055f75a63e74a6 Mon Sep 17 00:00:00 2001 From: adepojuemmanuel235 Date: Sat, 3 Oct 2026 16:59:22 +0000 Subject: [PATCH] Retry automatic payouts when the RPC lags the crediting block Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- app/lib/relay.ts | 58 +++++++++++++++++++++++++++------------- app/lib/webhook.ts | 2 ++ app/test/webhook.test.ts | 5 ++++ 3 files changed, 47 insertions(+), 18 deletions(-) diff --git a/app/lib/relay.ts b/app/lib/relay.ts index 13bb102..90166b0 100644 --- a/app/lib/relay.ts +++ b/app/lib/relay.ts @@ -147,7 +147,9 @@ async function write(chainId: number, request: unknown, attempts = 3): Promise; +export type RawLog = Pick & { + blockNumber?: bigint | null; +}; /** * Idempotent event processing shared by relayer receipts and the QuickNode webhook: @@ -155,7 +157,7 @@ export type RawLog = Pick { const { setlo } = getChain(chainId); - const toClaim = new Set
(); + const toClaim = new Map(); for (const log of logs) { if (log.address.toLowerCase() !== setlo.toLowerCase() || !log.transactionHash || log.logIndex == null) continue; let ev; @@ -182,28 +184,48 @@ export async function handleLogs(chainId: number, logs: readonly RawLog[]): Prom .select(); if (!inserted?.length) continue; - if (ev.eventName === "Credited") toClaim.add(ev.args.recipient); - else await notifyPayout(chainId, ev.args.recipient, ev.args.amount, log.transactionHash); + if (ev.eventName === "Credited") { + const block = log.blockNumber ?? BigInt(0); + const prev = toClaim.get(ev.args.recipient) ?? BigInt(0); + toClaim.set(ev.args.recipient, block > prev ? block : prev); + } else await notifyPayout(chainId, ev.args.recipient, ev.args.amount, log.transactionHash); } - for (const recipient of toClaim) await claimIfOwed(chainId, recipient); + for (const [recipient, block] of toClaim) await claimIfOwed(chainId, recipient, block); } -async function claimIfOwed(chainId: number, recipient: Address) { - const owed = await publicClient(chainId).readContract({ - address: getChain(chainId).setlo, - abi: setloAbi, - functionName: "claimable", - args: [recipient], - }); - if (owed === BigInt(0)) return; - try { - await submit(chainId, "claimFor", [recipient]); - } catch (e) { - // A frozen recipient must not block anyone else's claim; it can still `claim()` itself later. - console.error("claimFor failed", recipient, e); +const CLAIM_ATTEMPTS = 4; + +/** + * Load-balanced RPC nodes can lag the block that credited the recipient, so a read of `claimable` (or the + * `claimFor` simulation) can still see zero. Wait for the node to reach that block and retry on zero. + */ +async function claimIfOwed(chainId: number, recipient: Address, creditedAt: bigint) { + const client = publicClient(chainId); + for (let attempt = 1; attempt <= CLAIM_ATTEMPTS; attempt++) { + if (attempt > 1) await sleep(1000 * (attempt - 1)); + if ((await client.getBlockNumber({ cacheTime: 0 })) < creditedAt) continue; + const owed = await client.readContract({ + address: getChain(chainId).setlo, + abi: setloAbi, + functionName: "claimable", + args: [recipient], + }); + if (owed === BigInt(0)) continue; + try { + await submit(chainId, "claimFor", [recipient]); + return; + } catch (e) { + if (e instanceof HttpError && e.message === "NothingToClaim" && attempt < CLAIM_ATTEMPTS) continue; + // A frozen recipient must not block anyone else's claim; it can still `claim()` itself later. + console.error("claimFor failed", recipient, e); + return; + } } + console.error("claimFor skipped: nothing claimable after retries", recipient); } +const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); + async function notifyPayout(chainId: number, recipient: Address, amount: bigint, hash: Hash) { const { data } = await db() .from("profiles") diff --git a/app/lib/webhook.ts b/app/lib/webhook.ts index 8fb1e68..10e9b88 100644 --- a/app/lib/webhook.ts +++ b/app/lib/webhook.ts @@ -36,6 +36,8 @@ export function extractLogs(payload: unknown): RawLog[] { data: o.data as RawLog["data"], transactionHash: o.transactionHash as RawLog["transactionHash"], logIndex: typeof o.logIndex === "string" ? Number(BigInt(o.logIndex)) : (o.logIndex as number), + blockNumber: + typeof o.blockNumber === "string" || typeof o.blockNumber === "number" ? BigInt(o.blockNumber) : undefined, }); return; } diff --git a/app/test/webhook.test.ts b/app/test/webhook.test.ts index d26e77f..a3d1c3b 100644 --- a/app/test/webhook.test.ts +++ b/app/test/webhook.test.ts @@ -25,4 +25,9 @@ describe("extractLogs", () => { const logs = extractLogs({ data: [{ receipts: [{ logs: [log] }] }] }); expect(logs).toEqual([{ ...log, logIndex: 2 }]); }); + + it("parses hex block numbers", () => { + const log = { address: "0xabc", topics: ["0x1"], data: "0x", transactionHash: "0xdead", logIndex: 0, blockNumber: "0x10" }; + expect(extractLogs([log])[0].blockNumber).toBe(BigInt(16)); + }); });