Skip to content

Commit 7a33293

Browse files
optimize transaction search with parallel RPC/explorer calls
1 parent 711350f commit 7a33293

1 file changed

Lines changed: 127 additions & 128 deletions

File tree

src/app/api/v1/search/[hash]/route.ts

Lines changed: 127 additions & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,118 @@ import { fetchTxFromExplorer } from '@/lib/explorer';
1414
import { getServerSession } from '@/lib/auth-server';
1515
import { getRpcUrlForChainOptimized } from '@/lib/public-network-utils';
1616

17+
type Pair = { key?: string; rpcUrl: string };
18+
19+
/**
20+
* Function to search transactions and contracts across multiple networks
21+
* TODO: mairjamijailovic think we do not need both GET and POST
22+
*/
23+
async function searchTransactionsAndContracts(
24+
pairs: Pair[],
25+
hashAsHash: Hash,
26+
hashAsAddress: Address | undefined,
27+
authSession: AuthType['session'] | null
28+
): Promise<{ transactions: SearchData[]; contracts: SearchData[] }> {
29+
// Search transactions in parallel - RPC and explorer run concurrently
30+
const transactionPromises = pairs.map(
31+
async ({ rpcUrl, key }): Promise<SearchData | undefined> => {
32+
const client = createPublicClient({ transport: http(rpcUrl) });
33+
34+
// Run RPC and explorer in parallel for faster results
35+
const rpcPromise = (async () => {
36+
try {
37+
const transaction = await client.getTransaction({ hash: hashAsHash });
38+
let resolvedChainId: string;
39+
if (key) {
40+
resolvedChainId = key;
41+
} else {
42+
const numeric = await client.getChainId();
43+
const chainKey = resolveChainId(numeric, authSession);
44+
resolvedChainId = chainKey || numeric.toString();
45+
}
46+
return {
47+
source: { chainId: resolvedChainId, rpcUrl: undefined },
48+
hash: transaction.hash
49+
};
50+
} catch (error) {
51+
return undefined;
52+
}
53+
})();
54+
55+
// Explorer fallback - only if we have a key, run in parallel with RPC
56+
const explorerPromise = key
57+
? (async () => {
58+
try {
59+
const explorer = await fetchTxFromExplorer(key, hashAsHash);
60+
if (explorer?.found) {
61+
return {
62+
source: { chainId: key, rpcUrl: undefined },
63+
hash: hashAsHash
64+
};
65+
}
66+
return undefined;
67+
} catch (error) {
68+
return undefined;
69+
}
70+
})()
71+
: Promise.resolve(undefined);
72+
73+
// Race between RPC and explorer - return first successful result
74+
const [rpcResult, explorerResult] = await Promise.allSettled([rpcPromise, explorerPromise]);
75+
76+
if (rpcResult.status === 'fulfilled' && rpcResult.value) {
77+
return rpcResult.value;
78+
}
79+
if (explorerResult.status === 'fulfilled' && explorerResult.value) {
80+
return explorerResult.value;
81+
}
82+
83+
return undefined;
84+
}
85+
);
86+
87+
// Use allSettled to avoid one slow network blocking others
88+
const transactionResults = await Promise.allSettled(transactionPromises);
89+
const transactions: SearchData[] = transactionResults
90+
.map((result) => (result.status === 'fulfilled' ? result.value : undefined))
91+
.filter((tx): tx is SearchData => tx !== undefined);
92+
93+
// Search contracts in parallel
94+
const contracts: SearchData[] = hashAsAddress
95+
? (
96+
await Promise.allSettled(
97+
pairs.map(async ({ rpcUrl, key }): Promise<SearchData | undefined> => {
98+
const client = createPublicClient({ transport: http(rpcUrl) });
99+
try {
100+
const bytecode = await client.getCode({ address: hashAsAddress });
101+
if (bytecode && bytecode !== '0x') {
102+
let resolvedChainId: string;
103+
if (key) {
104+
resolvedChainId = key;
105+
} else {
106+
const numeric = await client.getChainId();
107+
const chainKey = resolveChainId(numeric, authSession);
108+
resolvedChainId = chainKey || numeric.toString();
109+
}
110+
return {
111+
source: { chainId: resolvedChainId, rpcUrl: undefined },
112+
hash: hashAsAddress
113+
};
114+
}
115+
} catch (error) {
116+
// Ignore errors silently
117+
}
118+
return undefined;
119+
})
120+
)
121+
)
122+
.map((result) => (result.status === 'fulfilled' ? result.value : undefined))
123+
.filter((contract): contract is SearchData => contract !== undefined)
124+
: [];
125+
126+
return { transactions, contracts };
127+
}
128+
17129
export const GET = async (
18130
request: NextRequest,
19131
{ params }: { params: Promise<{ hash: string }> }
@@ -34,8 +146,8 @@ export const GET = async (
34146
const { hash } = await params;
35147
const hashAsHash = hash as Hash;
36148
const hashAsAddress = isAddress(hash) ? (hash as Address) : undefined;
149+
37150
// Prefer chain keys sent via ?chains=KEY1,KEY2; fallback to rpc_urls; default to all enabled.
38-
type Pair = { key?: string; rpcUrl: string };
39151
let pairs: Pair[] = [];
40152
if (chainsParam) {
41153
const keys = chainsParam
@@ -45,7 +157,6 @@ export const GET = async (
45157
const built: Pair[] = [];
46158
for (const k of keys) {
47159
try {
48-
// Use optimized RPC URL resolution
49160
const url = getRpcUrlForChainOptimized(k, authSession?.session || null);
50161
built.push({ key: k, rpcUrl: url });
51162
} catch (error) {
@@ -98,72 +209,17 @@ export const GET = async (
98209
}
99210
}
100211

101-
const transactions: (SearchData | undefined)[] = await Promise.all(
102-
pairs.map(async ({ rpcUrl, key }) => {
103-
const client = createPublicClient({ transport: http(rpcUrl) });
104-
try {
105-
const transaction = await client.getTransaction({ hash: hashAsHash });
106-
let resolvedChainId: string;
107-
if (key) {
108-
resolvedChainId = key;
109-
} else {
110-
const numeric = await client.getChainId();
111-
// Use resolveChainId to support both static and tenant networks
112-
const chainKey = resolveChainId(numeric, authSession?.session || null);
113-
resolvedChainId = chainKey || numeric.toString();
114-
}
115-
return {
116-
source: { chainId: resolvedChainId, rpcUrl: undefined },
117-
hash: transaction.hash
118-
};
119-
} catch (error) {
120-
// RPC miss → try explorer fallback for chains with explorer configured
121-
if (key) {
122-
const explorer = await fetchTxFromExplorer(key, hashAsHash);
123-
if (explorer?.found) {
124-
return {
125-
source: { chainId: key, rpcUrl: undefined },
126-
hash: hashAsHash
127-
};
128-
}
129-
}
130-
const { name, message } = error as GetTransactionErrorType;
131-
console.error(name, message);
132-
}
133-
})
212+
const { transactions, contracts } = await searchTransactionsAndContracts(
213+
pairs,
214+
hashAsHash,
215+
hashAsAddress,
216+
authSession?.session || null
134217
);
135-
const contracts: (SearchData | undefined)[] = hashAsAddress
136-
? await Promise.all(
137-
pairs.map(async ({ rpcUrl, key }) => {
138-
const client = createPublicClient({ transport: http(rpcUrl) });
139-
try {
140-
const bytecode = await client.getCode({ address: hashAsAddress });
141-
if (bytecode && bytecode !== '0x') {
142-
let resolvedChainId: string;
143-
if (key) {
144-
resolvedChainId = key;
145-
} else {
146-
const numeric = await client.getChainId();
147-
// Use resolveChainId to support both static and tenant networks
148-
const chainKey = resolveChainId(numeric, authSession?.session || null);
149-
resolvedChainId = chainKey || numeric.toString();
150-
}
151-
return {
152-
source: { chainId: resolvedChainId, rpcUrl: undefined },
153-
hash: hashAsAddress
154-
};
155-
}
156-
} catch (error) {
157-
console.error('getCode error', error);
158-
}
159-
})
160-
)
161-
: [];
162218

163219
const response: SearchDataResponse = {
164-
transactions: transactions.filter((transaction: SearchData | undefined) => !!transaction),
220+
transactions,
165221
classes: [],
166-
contracts: contracts.filter((contract: SearchData | undefined) => !!contract)
222+
contracts
167223
};
168224
return NextResponse.json(response);
169225
};
@@ -184,7 +240,6 @@ export const POST = async (
184240
const body = (await request.json()) as { chains?: string[] } | undefined;
185241
const chains = (body?.chains ?? []).map((c) => c.trim()).filter(Boolean);
186242

187-
type Pair = { key?: string; rpcUrl: string };
188243
let pairs: Pair[] = [];
189244
if (chains.length > 0) {
190245
const built: Pair[] = [];
@@ -228,73 +283,17 @@ export const POST = async (
228283
}
229284
}
230285

231-
const transactions: (SearchData | undefined)[] = await Promise.all(
232-
pairs.map(async ({ rpcUrl, key }) => {
233-
const client = createPublicClient({ transport: http(rpcUrl) });
234-
try {
235-
const transaction = await client.getTransaction({ hash: hashAsHash });
236-
let resolvedChainId: string;
237-
if (key) {
238-
resolvedChainId = key;
239-
} else {
240-
const numeric = await client.getChainId();
241-
// Use resolveChainId to support both static and tenant networks
242-
const chainKey = resolveChainId(numeric, authSession?.session || null);
243-
resolvedChainId = chainKey || numeric.toString();
244-
}
245-
return {
246-
source: { chainId: resolvedChainId, rpcUrl: undefined },
247-
hash: transaction.hash
248-
};
249-
} catch (error) {
250-
if (key) {
251-
const explorer = await fetchTxFromExplorer(key, hashAsHash);
252-
if (explorer?.found) {
253-
return {
254-
source: { chainId: key, rpcUrl: undefined },
255-
hash: hashAsHash
256-
};
257-
}
258-
}
259-
const { name, message } = error as GetTransactionErrorType;
260-
console.error(name, message);
261-
}
262-
})
286+
const { transactions, contracts } = await searchTransactionsAndContracts(
287+
pairs,
288+
hashAsHash,
289+
hashAsAddress,
290+
authSession.session
263291
);
264292

265-
const contracts: (SearchData | undefined)[] =
266-
hashAsAddress && pairs.length > 0
267-
? await Promise.all(
268-
pairs.map(async ({ rpcUrl, key }) => {
269-
const client = createPublicClient({ transport: http(rpcUrl) });
270-
try {
271-
const bytecode = await client.getCode({ address: hashAsAddress });
272-
if (bytecode && bytecode !== '0x') {
273-
let resolvedChainId: string;
274-
if (key) {
275-
resolvedChainId = key;
276-
} else {
277-
const numeric = await client.getChainId();
278-
// Use resolveChainId to support both static and tenant networks
279-
const chainKey = resolveChainId(numeric, authSession?.session || null);
280-
resolvedChainId = chainKey || numeric.toString();
281-
}
282-
return {
283-
source: { chainId: resolvedChainId, rpcUrl: undefined },
284-
hash: hashAsAddress
285-
};
286-
}
287-
} catch (error) {
288-
console.error('getCode error', error);
289-
}
290-
})
291-
)
292-
: [];
293-
294293
const response: SearchDataResponse = {
295-
transactions: transactions.filter((transaction: SearchData | undefined) => !!transaction),
294+
transactions,
296295
classes: [],
297-
contracts: contracts.filter((contract: SearchData | undefined) => !!contract)
296+
contracts
298297
};
299298
return NextResponse.json(response);
300299
} catch (e) {

0 commit comments

Comments
 (0)