From 795348b34b32acb25d7b2999d9b9498bbf73fb66 Mon Sep 17 00:00:00 2001 From: Cho Young-Hwi Date: Mon, 16 Mar 2026 11:37:33 +0000 Subject: [PATCH] [#154] Add receipt fetch retry to indexer routes Load-balanced RPC nodes may not have the receipt immediately after waitForTransactionReceipt completes on the client. Added getReceiptWithRetry helper (3 attempts, 1s backoff) and applied it to all three indexer routes (donation, storyline, plot). Fixes #154 Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/rpc.ts | 19 ++++++++++++++++++- src/app/api/index/donation/route.ts | 6 +++--- src/app/api/index/plot/route.ts | 6 +++--- src/app/api/index/storyline/route.ts | 6 +++--- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/lib/rpc.ts b/lib/rpc.ts index df6a9d5a..4a873171 100644 --- a/lib/rpc.ts +++ b/lib/rpc.ts @@ -1,4 +1,4 @@ -import { createPublicClient, http, fallback } from "viem"; +import { createPublicClient, http, fallback, type Hex } from "viem"; import { base, baseSepolia } from "viem/chains"; const chainId = Number(process.env.NEXT_PUBLIC_CHAIN_ID || "84532"); @@ -20,3 +20,20 @@ export const publicClient = createPublicClient({ chain, transport, }); + +/** + * Fetch a transaction receipt with retries and backoff. + * Load-balanced RPC nodes may not have the receipt immediately after + * `waitForTransactionReceipt` completes on the client side. + */ +export async function getReceiptWithRetry(hash: Hex, maxAttempts = 3) { + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + return await publicClient.getTransactionReceipt({ hash }); + } catch (err) { + if (attempt === maxAttempts) throw err; + await new Promise((r) => setTimeout(r, attempt * 1000)); + } + } + throw new Error("unreachable"); +} diff --git a/src/app/api/index/donation/route.ts b/src/app/api/index/donation/route.ts index b783ae53..415b73d1 100644 --- a/src/app/api/index/donation/route.ts +++ b/src/app/api/index/donation/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { type Hex, decodeEventLog, encodeEventTopics } from "viem"; -import { publicClient } from "../../../../../lib/viem"; +import { publicClient, getReceiptWithRetry } from "../../../../../lib/rpc"; import { createServerClient } from "../../../../../lib/supabase"; import { storyFactoryAbi, @@ -26,10 +26,10 @@ export async function POST(req: Request) { return error("Missing or invalid txHash"); } - // 1. Fetch receipt + // 1. Fetch receipt (with retry for load-balanced RPC nodes) let receipt; try { - receipt = await publicClient.getTransactionReceipt({ hash: txHash }); + receipt = await getReceiptWithRetry(txHash); } catch { return error("Failed to fetch transaction receipt", 502); } diff --git a/src/app/api/index/plot/route.ts b/src/app/api/index/plot/route.ts index 7a12dfbe..7c7814c7 100644 --- a/src/app/api/index/plot/route.ts +++ b/src/app/api/index/plot/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { type Hex, decodeEventLog, encodeEventTopics } from "viem"; -import { publicClient } from "../../../../../lib/viem"; +import { publicClient, getReceiptWithRetry } from "../../../../../lib/rpc"; import { createServerClient } from "../../../../../lib/supabase"; import { storyFactoryAbi, @@ -31,10 +31,10 @@ export async function POST(req: Request) { return error("Missing or invalid txHash"); } - // 1. Fetch receipt + // 1. Fetch receipt (with retry for load-balanced RPC nodes) let receipt; try { - receipt = await publicClient.getTransactionReceipt({ hash: txHash }); + receipt = await getReceiptWithRetry(txHash); } catch { return error("Failed to fetch transaction receipt", 502); } diff --git a/src/app/api/index/storyline/route.ts b/src/app/api/index/storyline/route.ts index 8aec030b..9bcc3b4d 100644 --- a/src/app/api/index/storyline/route.ts +++ b/src/app/api/index/storyline/route.ts @@ -1,6 +1,6 @@ import { NextResponse } from "next/server"; import { type Hex, decodeEventLog, encodeEventTopics } from "viem"; -import { publicClient } from "../../../../../lib/viem"; +import { publicClient, getReceiptWithRetry } from "../../../../../lib/rpc"; import { createServerClient } from "../../../../../lib/supabase"; import { storyFactoryAbi, @@ -32,10 +32,10 @@ export async function POST(req: Request) { return error("Missing or invalid txHash"); } - // 1. Fetch receipt + // 1. Fetch receipt (with retry for load-balanced RPC nodes) let receipt; try { - receipt = await publicClient.getTransactionReceipt({ hash: txHash }); + receipt = await getReceiptWithRetry(txHash); } catch { return error("Failed to fetch transaction receipt", 502); }