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

Commit f4eded4

Browse files
committed
Polish audit event history UI
1 parent 6e54969 commit f4eded4

14 files changed

Lines changed: 1023 additions & 115 deletions

File tree

bun.lock

Lines changed: 89 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

next.config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ import type { NextConfig } from "next";
22

33
const nextConfig: NextConfig = {
44
output: "standalone",
5+
outputFileTracingRoot: __dirname,
6+
turbopack: {
7+
root: __dirname,
8+
},
59
};
610

711
export default nextConfig;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
"format": "biome format --write"
1111
},
1212
"dependencies": {
13+
"@aws-sdk/client-s3": "^3.1071.0",
1314
"next": "16.0.7",
1415
"react": "19.2.1",
1516
"react-dom": "19.2.1",

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,21 @@ import { type NextRequest, NextResponse } from "next/server";
22
import { type Block, createPublicClient, type Hash, http } from "viem";
33
import { mainnet } from "viem/chains";
44
import {
5+
bundleHistoryFromAuditEvents,
6+
getAuditEventsByBlockNumber,
57
getAuditEventsByTransactionHash,
68
meterBundleResponseFromAuditEvent,
79
transactionMetadataFromAuditEvents,
810
} from "@/lib/audit-events";
911
import type { BlockData, BlockTransaction } from "@/lib/transaction-data";
12+
import { getBlockFromCache } from "@/lib/s3";
13+
14+
const BLOCK_EVENT_TYPES = new Set([
15+
"BUILDER_FLASHBLOCK_STARTED",
16+
"BUILDER_FLASHBLOCK_PUBLISHED",
17+
"BUILDER_FLASHBLOCK_BUILD_STOPPED",
18+
"BUILDER_PAYLOAD_FINALIZED",
19+
]);
1020

1121
function serializeBlockData(block: BlockData) {
1222
return {
@@ -32,6 +42,7 @@ function serializeBlockData(block: BlockData) {
3242
metering,
3343
};
3444
}),
45+
eventHistory: block.eventHistory ?? [],
3546
};
3647
}
3748

@@ -121,6 +132,13 @@ async function buildAndCacheBlockData(
121132
number,
122133
timestamp: rpcBlock.timestamp,
123134
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 ?? [],
124142
gasUsed: rpcBlock.gasUsed,
125143
gasLimit: rpcBlock.gasLimit,
126144
cachedAt: Date.now(),
@@ -153,6 +171,10 @@ export async function GET(
153171

154172
const rpcBlock = await fetchBlockFromRpc(identifier);
155173
if (!rpcBlock || !rpcBlock.hash || !rpcBlock.number) {
174+
const cachedBlock = await getBlockFromCache(identifier);
175+
if (cachedBlock) {
176+
return NextResponse.json(serializeBlockData(cachedBlock));
177+
}
156178
return NextResponse.json({ error: "Block not found" }, { status: 404 });
157179
}
158180

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

Lines changed: 88 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,108 @@
11
import { type NextRequest, NextResponse } from "next/server";
2+
import { createPublicClient, type Hash, http } from "viem";
3+
import { mainnet } from "viem/chains";
24
import {
35
bundleHistoryFromAuditEvents,
4-
getAuditEventsByBundle,
6+
getJoinedAuditEventsByBundle,
57
} from "@/lib/audit-events";
6-
import type { BundleEvent } from "@/lib/transaction-data";
8+
import type { BundleEvent, BundleTransaction } from "@/lib/transaction-data";
9+
import { getBundleHistory } from "@/lib/s3";
10+
11+
const RPC_URL = process.env.TIPS_UI_RPC_URL || "http://localhost:8545";
12+
13+
const client = createPublicClient({
14+
chain: mainnet,
15+
transport: http(RPC_URL),
16+
});
717

818
export interface BundleHistoryResponse {
919
hash: string;
1020
history: BundleEvent[];
1121
}
1222

23+
function bigintToHex(value: bigint | null | undefined): string {
24+
return value === null || value === undefined ? "0x0" : `0x${value.toString(16)}`;
25+
}
26+
27+
function numberToHex(value: number | null | undefined): string {
28+
return value === null || value === undefined ? "0x0" : `0x${value.toString(16)}`;
29+
}
30+
31+
async function enrichBundleTransactionsFromRpc(
32+
history: BundleEvent[],
33+
): Promise<BundleEvent[]> {
34+
const hashes = Array.from(
35+
new Set(
36+
history.flatMap((event) =>
37+
event.data.bundle?.txs.map((tx) => tx.hash).filter(Boolean) ?? [],
38+
),
39+
),
40+
);
41+
const transactions = new Map<string, BundleTransaction>();
42+
43+
await Promise.all(
44+
hashes.map(async (hash) => {
45+
try {
46+
const tx = await client.getTransaction({ hash: hash as Hash });
47+
transactions.set(hash, {
48+
signer: tx.from,
49+
type: tx.typeHex ?? "",
50+
chainId: numberToHex(tx.chainId),
51+
nonce: numberToHex(tx.nonce),
52+
gas: bigintToHex(tx.gas),
53+
maxFeePerGas: bigintToHex(tx.maxFeePerGas ?? tx.gasPrice),
54+
maxPriorityFeePerGas: bigintToHex(tx.maxPriorityFeePerGas),
55+
to: tx.to,
56+
value: bigintToHex(tx.value),
57+
accessList: [...(tx.accessList ?? [])],
58+
input: tx.input,
59+
r: tx.r,
60+
s: tx.s,
61+
yParity: numberToHex(tx.yParity),
62+
v: bigintToHex(tx.v),
63+
hash: tx.hash,
64+
});
65+
} catch (error) {
66+
console.error(`Failed to fetch transaction ${hash} from RPC:`, error);
67+
}
68+
}),
69+
);
70+
71+
return history.map((event) => {
72+
if (!event.data.bundle) return event;
73+
return {
74+
...event,
75+
data: {
76+
...event.data,
77+
bundle: {
78+
...event.data.bundle,
79+
txs: event.data.bundle.txs.map((tx) => transactions.get(tx.hash) ?? tx),
80+
},
81+
},
82+
};
83+
});
84+
}
85+
1386
export async function GET(
1487
_request: NextRequest,
1588
{ params }: { params: Promise<{ hash: string }> },
1689
) {
1790
try {
1891
const { hash } = await params;
1992

20-
const bundle = bundleHistoryFromAuditEvents(
21-
hash,
22-
await getAuditEventsByBundle(hash),
23-
);
93+
const bundle = await getAuditBundleHistory(hash);
2494
if (!bundle) {
2595
return NextResponse.json({ error: "Bundle not found" }, { status: 404 });
2696
}
2797

28-
const history = bundle.history;
98+
const history = await enrichBundleTransactionsFromRpc(bundle.history);
2999
history.sort((lhs, rhs) =>
30100
lhs.data.timestamp < rhs.data.timestamp ? -1 : 1,
31101
);
32102

33103
const response: BundleHistoryResponse = {
34104
hash,
35-
history: bundle.history,
105+
history,
36106
};
37107

38108
return NextResponse.json(response);
@@ -44,3 +114,13 @@ export async function GET(
44114
);
45115
}
46116
}
117+
118+
async function getAuditBundleHistory(hash: string) {
119+
try {
120+
const events = await getJoinedAuditEventsByBundle(hash);
121+
return bundleHistoryFromAuditEvents(hash, events) ?? getBundleHistory(hash);
122+
} catch (error) {
123+
console.error("Falling back to S3 bundle history:", error);
124+
return getBundleHistory(hash);
125+
}
126+
}

src/app/api/rejected/route.ts

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,22 @@ import {
44
rejectedTransactionFromAuditEvent,
55
} from "@/lib/audit-events";
66
import type { RejectedTransaction } from "@/lib/transaction-data";
7+
import {
8+
getRejectedTransaction,
9+
listRejectedTransactions,
10+
} from "@/lib/s3";
711

812
export interface RejectedTransactionsResponse {
913
transactions: RejectedTransaction[];
1014
}
1115

1216
export async function GET() {
1317
try {
14-
const transactions = (await getAuditRejectedTransactionEvents(100))
15-
.map(rejectedTransactionFromAuditEvent)
16-
.filter((tx): tx is RejectedTransaction => tx !== null);
18+
const auditTransactions = await getAuditRejectedTransactions();
19+
const transactions =
20+
auditTransactions.length > 0
21+
? auditTransactions
22+
: await getS3RejectedTransactions();
1723

1824
const response: RejectedTransactionsResponse = { transactions };
1925
return NextResponse.json(response);
@@ -25,3 +31,24 @@ export async function GET() {
2531
);
2632
}
2733
}
34+
35+
async function getAuditRejectedTransactions(): Promise<RejectedTransaction[]> {
36+
try {
37+
return (await getAuditRejectedTransactionEvents(100))
38+
.map(rejectedTransactionFromAuditEvent)
39+
.filter((tx): tx is RejectedTransaction => tx !== null);
40+
} catch (error) {
41+
console.error("Falling back to S3 rejected transactions:", error);
42+
return [];
43+
}
44+
}
45+
46+
async function getS3RejectedTransactions(): Promise<RejectedTransaction[]> {
47+
const summaries = await listRejectedTransactions(100);
48+
const transactions = await Promise.all(
49+
summaries.map((summary) =>
50+
getRejectedTransaction(summary.blockNumber, summary.txHash),
51+
),
52+
);
53+
return transactions.filter((tx): tx is RejectedTransaction => tx !== null);
54+
}

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

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

1015
export interface TransactionEvent {
1116
type: string;
@@ -43,8 +48,12 @@ export async function GET(
4348
try {
4449
const { hash } = await params;
4550

46-
const events = await getAuditEventsByTransactionHash(hash);
47-
const metadata = transactionMetadataFromAuditEvents(events);
51+
const audit = await getAuditTransactionHistory(hash);
52+
if (audit) {
53+
return NextResponse.json(audit);
54+
}
55+
56+
const metadata = await getTransactionMetadataByHash(hash);
4857
if (!metadata) {
4958
return NextResponse.json(
5059
{ error: "Transaction not found" },
@@ -53,18 +62,15 @@ export async function GET(
5362
}
5463

5564
const bundleId = metadata.bundle_ids[0];
56-
const bundle =
57-
bundleId !== undefined
58-
? bundleHistoryFromAuditEvents(
59-
bundleId,
60-
await getAuditEventsByBundle(bundleId),
61-
)
62-
: { history: [] };
65+
const bundle = bundleId ? await getBundleHistory(bundleId) : null;
66+
if (!bundle) {
67+
return NextResponse.json({ error: "Bundle not found" }, { status: 404 });
68+
}
6369

6470
const response: TransactionHistoryResponse = {
6571
hash,
6672
bundle_ids: metadata.bundle_ids,
67-
history: bundle?.history ?? [],
73+
history: bundle.history,
6874
};
6975

7076
return NextResponse.json(response);
@@ -76,3 +82,35 @@ export async function GET(
7682
);
7783
}
7884
}
85+
86+
async function getAuditTransactionHistory(
87+
hash: string,
88+
): Promise<TransactionHistoryResponse | null> {
89+
try {
90+
const events = await getAuditEventsByTransactionHash(hash);
91+
const metadata = transactionMetadataFromAuditEvents(events);
92+
if (!metadata) return null;
93+
94+
const bundleId = metadata.bundle_ids[0];
95+
let history: BundleEvent[] = [];
96+
if (bundleId !== undefined) {
97+
const bundle = bundleHistoryFromAuditEvents(
98+
bundleId,
99+
mergeAuditEvents([
100+
await getJoinedAuditEventsByBundle(bundleId),
101+
events,
102+
]),
103+
);
104+
history = bundle?.history ?? [];
105+
}
106+
107+
return {
108+
hash,
109+
bundle_ids: metadata.bundle_ids,
110+
history,
111+
};
112+
} catch (error) {
113+
console.error("Falling back to S3 transaction history:", error);
114+
return null;
115+
}
116+
}

0 commit comments

Comments
 (0)