Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -99,15 +99,37 @@ async function handler(req: NextApiRequestWithSponsor, res: NextApiResponse) {
});
}

const allExistingTxIds = submissions
.flatMap((sub) =>
((sub.paymentDetails as any[]) || []).map((payment) => payment.txId),
)
.filter(Boolean);

const alreadyUsedTxIds = txIds.filter((txId) =>
allExistingTxIds.includes(txId),
);
// Check globally across ALL submissions (paid and unpaid) to prevent
// transaction replay attacks where a txId from a paid submission is reused
// for a different winner.
if (txIds.length === 0) {
return res.status(400).json({ error: 'No valid transaction IDs provided' });
}

const submissionsUsingTxIds: Array<{
id: string;
paymentDetails: unknown;
}> = await prisma.submission.findMany({
where: {
OR: txIds.map((txId) => ({
paymentDetails: { string_contains: txId },
})),
},
select: { id: true, paymentDetails: true },
});

const alreadyUsedTxIds = [
...new Set(
submissionsUsingTxIds
.flatMap((sub): (string | undefined)[] =>
(
(sub.paymentDetails as Array<{ txId?: string }> | null) ?? []
).map((p) => p.txId),
)
.filter((txId): txId is string => !!txId && txIds.includes(txId)),
),
];

if (alreadyUsedTxIds.length > 0) {
return res.status(400).json({
error: `Transaction IDs already used: ${alreadyUsedTxIds.join(', ')}`,
Expand Down