Skip to content
This repository was archived by the owner on Jul 15, 2026. It is now read-only.

Commit ff4e880

Browse files
committed
fix: preserve s3 fallback when audit is unavailable
Treat the audit JSON-RPC path as optional so block, transaction, bundle, and rejection views continue serving legacy S3 data during rollout or audit outages. Add a bounded audit request timeout and remove the remaining untyped metering access so the PR passes local lint and type checks.
1 parent 095191c commit ff4e880

8 files changed

Lines changed: 165 additions & 59 deletions

File tree

src/app/api/block/[hash]/route.ts

Lines changed: 89 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,20 @@ import {
55
bundleHistoryFromAuditEvents,
66
getAuditEventsByBlockNumber,
77
getAuditEventsByTransactionHash,
8+
isAuditConfigured,
89
meterBundleResponseFromAuditEvent,
910
transactionMetadataFromAuditEvents,
1011
} from "@/lib/audit-events";
11-
import type { BlockData, BlockTransaction } from "@/lib/transaction-data";
12-
import { getBlockFromCache } from "@/lib/s3";
12+
import {
13+
getBlockFromCache,
14+
getBundleHistory,
15+
getTransactionMetadataByHash,
16+
} from "@/lib/s3";
17+
import type {
18+
BlockData,
19+
BlockTransaction,
20+
BundleEvent,
21+
} from "@/lib/transaction-data";
1322

1423
const BLOCK_EVENT_TYPES = new Set([
1524
"BUILDER_FLASHBLOCK_STARTED",
@@ -87,26 +96,91 @@ async function fetchBlockFromRpcByNumber(
8796
}
8897
}
8998

90-
async function enrichTransactionWithBundleData(txHash: string): Promise<{
99+
async function enrichTransactionFromS3(txHash: string): Promise<{
91100
bundleId: string | null;
92101
meterBundleResponse: Record<string, unknown> | null;
93102
}> {
94-
const events = await getAuditEventsByTransactionHash(txHash);
95-
const metadata = transactionMetadataFromAuditEvents(events);
96-
const accepted = events.find(
97-
(event) => event.event_type === "SIMULATION_SUCCEEDED",
103+
const metadata = await getTransactionMetadataByHash(txHash);
104+
if (!metadata || metadata.bundle_ids.length === 0) {
105+
return { bundleId: null, meterBundleResponse: null };
106+
}
107+
108+
const bundleId = metadata.bundle_ids[0];
109+
const bundleHistory = await getBundleHistory(bundleId);
110+
if (!bundleHistory) {
111+
return { bundleId, meterBundleResponse: null };
112+
}
113+
114+
const receivedEvent = bundleHistory.history.find(
115+
(event) => event.event === "Received",
98116
);
117+
if (!receivedEvent?.data?.bundle?.meter_bundle_response) {
118+
return { bundleId, meterBundleResponse: null };
119+
}
120+
99121
return {
100-
bundleId: metadata?.bundle_ids[0] ?? null,
101-
meterBundleResponse: accepted
102-
? (meterBundleResponseFromAuditEvent(accepted) as unknown as Record<
103-
string,
104-
unknown
105-
>)
106-
: null,
122+
bundleId,
123+
meterBundleResponse: receivedEvent.data.bundle
124+
.meter_bundle_response as unknown as Record<string, unknown>,
107125
};
108126
}
109127

128+
async function enrichTransactionWithBundleData(txHash: string): Promise<{
129+
bundleId: string | null;
130+
meterBundleResponse: Record<string, unknown> | null;
131+
}> {
132+
if (!isAuditConfigured()) {
133+
return enrichTransactionFromS3(txHash);
134+
}
135+
136+
try {
137+
const events = await getAuditEventsByTransactionHash(txHash);
138+
const metadata = transactionMetadataFromAuditEvents(events);
139+
const bundleId = metadata?.bundle_ids[0] ?? null;
140+
if (bundleId !== null) {
141+
const accepted = events.find(
142+
(event) => event.event_type === "SIMULATION_SUCCEEDED",
143+
);
144+
return {
145+
bundleId,
146+
meterBundleResponse: accepted
147+
? (meterBundleResponseFromAuditEvent(accepted) as unknown as Record<
148+
string,
149+
unknown
150+
>)
151+
: null,
152+
};
153+
}
154+
} catch {
155+
// Audit is an optional read path; retain the S3-backed behavior on errors.
156+
}
157+
158+
return enrichTransactionFromS3(txHash);
159+
}
160+
161+
async function getBlockEventHistory(
162+
hash: Hash,
163+
number: bigint,
164+
): Promise<BundleEvent[]> {
165+
if (!isAuditConfigured()) {
166+
return [];
167+
}
168+
169+
try {
170+
return (
171+
bundleHistoryFromAuditEvents(
172+
hash,
173+
(await getAuditEventsByBlockNumber(Number(number))).filter((event) =>
174+
BLOCK_EVENT_TYPES.has(event.event_type),
175+
),
176+
)?.history ?? []
177+
);
178+
} catch (error) {
179+
console.error("Failed to fetch block event history from audit RPC:", error);
180+
return [];
181+
}
182+
}
183+
110184
async function buildAndCacheBlockData(
111185
rpcBlock: Block<bigint, true>,
112186
hash: Hash,
@@ -132,13 +206,7 @@ async function buildAndCacheBlockData(
132206
number,
133207
timestamp: rpcBlock.timestamp,
134208
transactions,
135-
eventHistory:
136-
bundleHistoryFromAuditEvents(
137-
hash,
138-
(await getAuditEventsByBlockNumber(Number(number))).filter((event) =>
139-
BLOCK_EVENT_TYPES.has(event.event_type),
140-
),
141-
)?.history ?? [],
209+
eventHistory: await getBlockEventHistory(hash, number),
142210
gasUsed: rpcBlock.gasUsed,
143211
gasLimit: rpcBlock.gasLimit,
144212
cachedAt: Date.now(),

src/app/api/bundle/[hash]/route.ts

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import { mainnet } from "viem/chains";
44
import {
55
bundleHistoryFromAuditEvents,
66
getJoinedAuditEventsByBundle,
7+
isAuditConfigured,
78
} from "@/lib/audit-events";
8-
import type { BundleEvent, BundleTransaction } from "@/lib/transaction-data";
99
import { getBundleHistory } from "@/lib/s3";
10+
import type { BundleEvent, BundleTransaction } from "@/lib/transaction-data";
1011

1112
const RPC_URL = process.env.TIPS_UI_RPC_URL || "http://localhost:8545";
1213

@@ -21,20 +22,25 @@ export interface BundleHistoryResponse {
2122
}
2223

2324
function bigintToHex(value: bigint | null | undefined): string {
24-
return value === null || value === undefined ? "0x0" : `0x${value.toString(16)}`;
25+
return value === null || value === undefined
26+
? "0x0"
27+
: `0x${value.toString(16)}`;
2528
}
2629

2730
function numberToHex(value: number | null | undefined): string {
28-
return value === null || value === undefined ? "0x0" : `0x${value.toString(16)}`;
31+
return value === null || value === undefined
32+
? "0x0"
33+
: `0x${value.toString(16)}`;
2934
}
3035

3136
async function enrichBundleTransactionsFromRpc(
3237
history: BundleEvent[],
3338
): Promise<BundleEvent[]> {
3439
const hashes = Array.from(
3540
new Set(
36-
history.flatMap((event) =>
37-
event.data.bundle?.txs.map((tx) => tx.hash).filter(Boolean) ?? [],
41+
history.flatMap(
42+
(event) =>
43+
event.data.bundle?.txs.map((tx) => tx.hash).filter(Boolean) ?? [],
3844
),
3945
),
4046
);
@@ -76,7 +82,9 @@ async function enrichBundleTransactionsFromRpc(
7682
...event.data,
7783
bundle: {
7884
...event.data.bundle,
79-
txs: event.data.bundle.txs.map((tx) => transactions.get(tx.hash) ?? tx),
85+
txs: event.data.bundle.txs.map(
86+
(tx) => transactions.get(tx.hash) ?? tx,
87+
),
8088
},
8189
},
8290
};
@@ -116,6 +124,10 @@ export async function GET(
116124
}
117125

118126
async function getAuditBundleHistory(hash: string) {
127+
if (!isAuditConfigured()) {
128+
return getBundleHistory(hash);
129+
}
130+
119131
try {
120132
const events = await getJoinedAuditEventsByBundle(hash);
121133
return bundleHistoryFromAuditEvents(hash, events) ?? getBundleHistory(hash);

src/app/api/rejected/route.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
import { NextResponse } from "next/server";
22
import {
33
getAuditRejectedTransactionEvents,
4+
isAuditConfigured,
45
rejectedTransactionFromAuditEvent,
56
} from "@/lib/audit-events";
7+
import { getRejectedTransaction, listRejectedTransactions } from "@/lib/s3";
68
import type { RejectedTransaction } from "@/lib/transaction-data";
7-
import {
8-
getRejectedTransaction,
9-
listRejectedTransactions,
10-
} from "@/lib/s3";
119

1210
export interface RejectedTransactionsResponse {
1311
transactions: RejectedTransaction[];
@@ -33,6 +31,10 @@ export async function GET() {
3331
}
3432

3533
async function getAuditRejectedTransactions(): Promise<RejectedTransaction[]> {
34+
if (!isAuditConfigured()) {
35+
return [];
36+
}
37+
3638
try {
3739
return (await getAuditRejectedTransactionEvents(100))
3840
.map(rejectedTransactionFromAuditEvent)

src/app/api/txn/[hash]/route.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import { type NextRequest, NextResponse } from "next/server";
22
import {
33
bundleHistoryFromAuditEvents,
4-
getJoinedAuditEventsByBundle,
54
getAuditEventsByTransactionHash,
5+
getJoinedAuditEventsByBundle,
6+
isAuditConfigured,
67
mergeAuditEvents,
78
transactionMetadataFromAuditEvents,
89
} from "@/lib/audit-events";
10+
import { getBundleHistory, getTransactionMetadataByHash } from "@/lib/s3";
911
import type { BundleEvent } from "@/lib/transaction-data";
10-
import {
11-
getBundleHistory,
12-
getTransactionMetadataByHash,
13-
} from "@/lib/s3";
1412

1513
export interface TransactionEvent {
1614
type: string;
@@ -86,10 +84,14 @@ export async function GET(
8684
async function getAuditTransactionHistory(
8785
hash: string,
8886
): Promise<TransactionHistoryResponse | null> {
87+
if (!isAuditConfigured()) {
88+
return null;
89+
}
90+
8991
try {
9092
const events = await getAuditEventsByTransactionHash(hash);
9193
const metadata = transactionMetadataFromAuditEvents(events);
92-
if (!metadata) return null;
94+
if (!metadata || metadata.bundle_ids.length === 0) return null;
9395

9496
const bundleId = metadata.bundle_ids[0];
9597
let history: BundleEvent[] = [];

src/app/block/[hash]/page.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,26 @@ import type { BlockData, BlockTransaction } from "@/lib/transaction-data";
77

88
const BLOCK_EXPLORER_URL = process.env.NEXT_PUBLIC_BLOCK_EXPLORER_URL;
99

10+
interface SerializedMetering {
11+
transaction?: {
12+
executionTimeUs?: number;
13+
gasUsed?: number;
14+
} | null;
15+
bundle?: {
16+
stateRootTimeUs?: number;
17+
} | null;
18+
}
19+
1020
// The API serializes metering as { transaction, bundle } from the raw
11-
// meterBundleResponse. The page receives this shape via JSON.
12-
// biome-ignore lint/suspicious/noExplicitAny: opaque metering JSON
13-
function metering(tx: BlockTransaction): any {
14-
return (tx as any).metering;
21+
// meterBundleResponse. The page receives this shape via JSON.
22+
function metering(tx: BlockTransaction): SerializedMetering | null {
23+
return (
24+
(
25+
tx as BlockTransaction & {
26+
metering?: SerializedMetering | null;
27+
}
28+
).metering ?? null
29+
);
1530
}
1631

1732
function executionTimeUs(tx: BlockTransaction): number | null {

src/app/event-history-row.tsx

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,9 @@ import Link from "next/link";
44
import { useState } from "react";
55
import type { BundleEvent } from "@/lib/transaction-data";
66

7-
function EventChip({
8-
children,
9-
}: {
10-
children: React.ReactNode;
11-
}) {
7+
function EventChip({ children }: { children: React.ReactNode }) {
128
return (
13-
<span
14-
className="inline-flex items-center rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-xs font-medium text-gray-600"
15-
>
9+
<span className="inline-flex items-center rounded-md border border-gray-200 bg-gray-50 px-2 py-0.5 text-xs font-medium text-gray-600">
1610
{children}
1711
</span>
1812
);

src/lib/audit-events.test.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@ import {
66
bundleKeysFromAuditEvents,
77
mergeAuditEvents,
88
rejectedTransactionFromAuditEvent,
9-
transactionMetadataFromAuditEvents,
109
transactionHashesFromAuditEvents,
10+
transactionMetadataFromAuditEvents,
1111
} from "./audit-events";
1212

1313
const acceptedEvent: AuditTransactionEventRecord = {
@@ -105,12 +105,12 @@ describe("audit event route adapters", () => {
105105
},
106106
]);
107107

108-
assert.equal(payloadEvent?.history[0]?.data.originalEvent?.payload_id, "0xpayload");
109-
assert.equal(payloadEvent?.history[0]?.data.originalEvent?.block_number, 123);
110-
assert.equal(
111-
payloadEvent?.history[0]?.data.originalEvent?.data.flashblock_index,
112-
2,
113-
);
108+
const originalEvent = payloadEvent?.history[0]?.data.originalEvent as
109+
| AuditTransactionEventRecord
110+
| undefined;
111+
assert.equal(originalEvent?.payload_id, "0xpayload");
112+
assert.equal(originalEvent?.block_number, 123);
113+
assert.equal(originalEvent?.data.flashblock_index, 2);
114114
});
115115

116116
test("does not decorate audit history with derived display latencies", () => {

src/lib/audit-events.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,12 @@ import type {
88
TransactionMetadata,
99
} from "@/lib/transaction-data";
1010

11-
const AUDIT_RPC_URL =
12-
process.env.TIPS_UI_AUDIT_RPC_URL || "http://localhost:8080";
11+
const AUDIT_RPC_URL = process.env.TIPS_UI_AUDIT_RPC_URL;
12+
const AUDIT_RPC_TIMEOUT_MS = 3000;
13+
14+
export function isAuditConfigured(): boolean {
15+
return Boolean(AUDIT_RPC_URL);
16+
}
1317

1418
export interface AuditTransactionEventRecord {
1519
schema_version: string;
@@ -33,9 +37,14 @@ interface JsonRpcResponse<T> {
3337
}
3438

3539
async function auditRpc<T>(method: string, params: unknown[]): Promise<T> {
40+
if (!AUDIT_RPC_URL) {
41+
throw new Error("audit RPC URL is not configured");
42+
}
43+
3644
const response = await fetch(AUDIT_RPC_URL, {
3745
method: "POST",
3846
headers: { "Content-Type": "application/json" },
47+
signal: AbortSignal.timeout(AUDIT_RPC_TIMEOUT_MS),
3948
body: JSON.stringify({
4049
jsonrpc: "2.0",
4150
id: 1,
@@ -182,7 +191,11 @@ function payloadFlashblockContextsFromAuditEvents(
182191
const payloadId = stringField(event.payload_id);
183192
const blockNumber = numberField(event.block_number);
184193
const flashblockIndex = numberField(event.data.flashblock_index);
185-
if (payloadId === null || blockNumber === null || flashblockIndex === null) {
194+
if (
195+
payloadId === null ||
196+
blockNumber === null ||
197+
flashblockIndex === null
198+
) {
186199
continue;
187200
}
188201

0 commit comments

Comments
 (0)