Skip to content

Commit 2cd06eb

Browse files
eabdelmoneimclaude
andcommitted
feat: extend backfill fallback to support /transaction/status endpoint
- Add BackfillEntry interface with status field ("mined" | "errored") - Update TransactionDB to store/retrieve JSON format for backfill entries - Add backfill fallback lookup to /transaction/status routes - Update /transaction/logs to use new getBackfill method - Update admin backfill schema to accept status field - Maintain backwards compatibility for plain string tx hash entries Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent a8a64bd commit 2cd06eb

4 files changed

Lines changed: 138 additions & 10 deletions

File tree

src/server/routes/admin/backfill.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,15 @@ const loadRequestBodySchema = Type.Object({
2020
entries: Type.Array(
2121
Type.Object({
2222
queueId: Type.String({ description: "Queue ID (UUID)" }),
23-
transactionHash: Type.String({ description: "Transaction hash (0x...)" }),
23+
status: Type.Union([Type.Literal("mined"), Type.Literal("errored")], {
24+
description: "Transaction status: 'mined' for successful transactions, 'errored' for failed ones",
25+
}),
26+
transactionHash: Type.Optional(
27+
Type.String({ description: "Transaction hash (0x...). Required for mined transactions." }),
28+
),
2429
}),
2530
{
26-
description: "Array of queueId to transactionHash mappings",
31+
description: "Array of queueId to status/transactionHash mappings",
2732
maxItems: 10000,
2833
},
2934
),

src/server/routes/transaction/blockchain/get-logs.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,9 +167,9 @@ export async function getTransactionLogs(fastify: FastifyInstance) {
167167
// see https://github.com/thirdweb-dev/solutions-customer-scripts/blob/main/amex/scripts/load-backfill-via-api.ts
168168
// Fallback to backfill table if enabled and not found
169169
if (!hash && env.ENABLE_TX_BACKFILL_FALLBACK) {
170-
const backfillHash = await TransactionDB.getBackfillHash(queueId);
171-
if (backfillHash && isHex(backfillHash)) {
172-
hash = backfillHash as Hex;
170+
const backfill = await TransactionDB.getBackfill(queueId);
171+
if (backfill?.status === "mined" && backfill.transactionHash && isHex(backfill.transactionHash)) {
172+
hash = backfill.transactionHash as Hex;
173173
}
174174
}
175175
} else if (transactionHash) {

src/server/routes/transaction/status.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,85 @@ import { type Static, Type } from "@sinclair/typebox";
22
import type { FastifyInstance } from "fastify";
33
import { StatusCodes } from "http-status-codes";
44
import { TransactionDB } from "../../../shared/db/transactions/db";
5+
import { env } from "../../../shared/utils/env";
56
import { createCustomError } from "../../middleware/error";
67
import { standardResponseSchema } from "../../schemas/shared-api-schemas";
78
import {
89
TransactionSchema,
910
toTransactionSchema,
1011
} from "../../schemas/transaction";
1112

13+
/**
14+
* Creates a minimal transaction response from backfill data.
15+
* Used when the transaction is not found in Redis but exists in the backfill table.
16+
*/
17+
const createBackfillResponse = (
18+
queueId: string,
19+
backfill: { status: "mined" | "errored"; transactionHash?: string },
20+
): Static<typeof TransactionSchema> => {
21+
const baseResponse: Static<typeof TransactionSchema> = {
22+
queueId,
23+
status: backfill.status,
24+
chainId: null,
25+
fromAddress: null,
26+
toAddress: null,
27+
data: null,
28+
extension: null,
29+
value: null,
30+
nonce: null,
31+
gasLimit: null,
32+
gasPrice: null,
33+
maxFeePerGas: null,
34+
maxPriorityFeePerGas: null,
35+
transactionType: null,
36+
transactionHash: null,
37+
queuedAt: null,
38+
sentAt: null,
39+
minedAt: null,
40+
cancelledAt: null,
41+
deployedContractAddress: null,
42+
deployedContractType: null,
43+
errorMessage: null,
44+
sentAtBlockNumber: null,
45+
blockNumber: null,
46+
retryCount: 0,
47+
retryGasValues: null,
48+
retryMaxFeePerGas: null,
49+
retryMaxPriorityFeePerGas: null,
50+
signerAddress: null,
51+
accountAddress: null,
52+
accountSalt: null,
53+
accountFactoryAddress: null,
54+
target: null,
55+
sender: null,
56+
initCode: null,
57+
callData: null,
58+
callGasLimit: null,
59+
verificationGasLimit: null,
60+
preVerificationGas: null,
61+
paymasterAndData: null,
62+
userOpHash: null,
63+
functionName: null,
64+
functionArgs: null,
65+
onChainTxStatus: null,
66+
onchainStatus: null,
67+
effectiveGasPrice: null,
68+
cumulativeGasUsed: null,
69+
batchOperations: null,
70+
};
71+
72+
if (backfill.status === "mined" && backfill.transactionHash) {
73+
return {
74+
...baseResponse,
75+
transactionHash: backfill.transactionHash,
76+
onchainStatus: "success",
77+
onChainTxStatus: 1,
78+
};
79+
}
80+
81+
return baseResponse;
82+
};
83+
1284
// INPUT
1385
const requestSchema = Type.Object({
1486
queueId: Type.String({
@@ -75,6 +147,16 @@ export async function getTransactionStatusRoute(fastify: FastifyInstance) {
75147

76148
const transaction = await TransactionDB.get(queueId);
77149
if (!transaction) {
150+
// Fallback to backfill table if enabled
151+
if (env.ENABLE_TX_BACKFILL_FALLBACK) {
152+
const backfill = await TransactionDB.getBackfill(queueId);
153+
if (backfill) {
154+
return reply.status(StatusCodes.OK).send({
155+
result: createBackfillResponse(queueId, backfill),
156+
});
157+
}
158+
}
159+
78160
throw createCustomError(
79161
"Transaction not found.",
80162
StatusCodes.BAD_REQUEST,
@@ -122,6 +204,16 @@ export async function getTransactionStatusQueryParamRoute(
122204

123205
const transaction = await TransactionDB.get(queueId);
124206
if (!transaction) {
207+
// Fallback to backfill table if enabled
208+
if (env.ENABLE_TX_BACKFILL_FALLBACK) {
209+
const backfill = await TransactionDB.getBackfill(queueId);
210+
if (backfill) {
211+
return reply.status(StatusCodes.OK).send({
212+
result: createBackfillResponse(queueId, backfill),
213+
});
214+
}
215+
}
216+
125217
throw createCustomError(
126218
"Transaction not found.",
127219
StatusCodes.BAD_REQUEST,

src/shared/db/transactions/db.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,15 @@ import superjson from "superjson";
22
import { MAX_REDIS_BATCH_SIZE, redis } from "../../utils/redis/redis";
33
import type { AnyTransaction } from "../../utils/transaction/types";
44

5+
/**
6+
* Backfill entry stored as JSON in Redis.
7+
* Used for transaction status and logs fallback lookup.
8+
*/
9+
export interface BackfillEntry {
10+
status: "mined" | "errored";
11+
transactionHash?: string; // Only present for mined transactions
12+
}
13+
514
/**
615
* Schemas
716
*
@@ -211,10 +220,30 @@ export class TransactionDB {
211220
};
212221

213222
/**
223+
* Gets backfill entry from backfill table.
224+
* Returns parsed JSON or handles backwards compatibility for plain string tx hashes.
225+
*/
226+
static getBackfill = async (queueId: string): Promise<BackfillEntry | null> => {
227+
const val = await redis.get(this.backfillKey(queueId));
228+
if (!val) return null;
229+
try {
230+
return JSON.parse(val) as BackfillEntry;
231+
} catch {
232+
// Backwards compatibility: treat plain string as mined tx hash
233+
return { status: "mined", transactionHash: val };
234+
}
235+
};
236+
237+
/**
238+
* @deprecated Use getBackfill instead
214239
* Gets transaction hash from backfill table.
215240
*/
216241
static getBackfillHash = async (queueId: string): Promise<string | null> => {
217-
return redis.get(this.backfillKey(queueId));
242+
const backfill = await this.getBackfill(queueId);
243+
if (backfill?.status === "mined" && backfill.transactionHash) {
244+
return backfill.transactionHash;
245+
}
246+
return null;
218247
};
219248

220249
/**
@@ -225,9 +254,10 @@ export class TransactionDB {
225254
queueId: string,
226255
transactionHash: string,
227256
): Promise<boolean> => {
257+
const entry: BackfillEntry = { status: "mined", transactionHash };
228258
const result = await redis.setnx(
229259
this.backfillKey(queueId),
230-
transactionHash,
260+
JSON.stringify(entry),
231261
);
232262
return result === 1;
233263
};
@@ -237,14 +267,15 @@ export class TransactionDB {
237267
* @returns { inserted: number, skipped: number }
238268
*/
239269
static bulkSetBackfill = async (
240-
entries: Array<{ queueId: string; transactionHash: string }>,
270+
entries: Array<{ queueId: string; status: "mined" | "errored"; transactionHash?: string }>,
241271
): Promise<{ inserted: number; skipped: number }> => {
242272
let inserted = 0;
243273
let skipped = 0;
244274

245275
const pipeline = redis.pipeline();
246-
for (const { queueId, transactionHash } of entries) {
247-
pipeline.setnx(this.backfillKey(queueId), transactionHash);
276+
for (const { queueId, status, transactionHash } of entries) {
277+
const entry: BackfillEntry = { status, transactionHash };
278+
pipeline.setnx(this.backfillKey(queueId), JSON.stringify(entry));
248279
}
249280

250281
const results = await pipeline.exec();

0 commit comments

Comments
 (0)