feat(web,contracts): implement PR Merge Validation for anti-fraud payout
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s

This commit is contained in:
OG T
2026-06-07 22:01:28 +08:00
parent 874281725a
commit c3f21c1d85
3 changed files with 81 additions and 2 deletions

View File

@@ -645,12 +645,15 @@ export async function POST(request: NextRequest, props: { params: Promise<{ tool
// Update Task & Claim Status
const newTaskStatus = result.overall_result === JudgeOverallResult.PASS
? TaskStatus.COMPLETED
? TaskStatus.PENDING_REVIEW
: TaskStatus.FAILED_RETRYABLE;
await tx.task.update({
where: { id: submission.task_id },
data: { status: newTaskStatus }
data: {
status: newTaskStatus,
github_pr_url: parsed.github_pr_url
}
});
await tx.claim.update({

View File

@@ -0,0 +1,74 @@
import { NextRequest, NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { capturePayment } from "@/lib/payment";
import { TaskStatus } from "@agent-bounty/contracts";
export async function POST(request: NextRequest) {
try {
const eventType = request.headers.get("x-github-event");
// We only care about pull request events
if (eventType !== "pull_request") {
return NextResponse.json({ received: true });
}
const payload = await request.json();
// When a PR is merged, the action is "closed" and "merged" is true.
if (payload.action === "closed" && payload.pull_request) {
const pr = payload.pull_request;
if (pr.merged) {
const prUrl = pr.html_url;
// Find the Task associated with this PR
const task = await prisma.task.findFirst({
where: {
github_pr_url: prUrl,
status: TaskStatus.PENDING_REVIEW,
},
});
if (task) {
// Arbitrary point conversion: 1 USD cent = 1 Point
const points = task.reward_amount;
await prisma.$transaction(async (tx) => {
// Update Task to COMPLETED
await tx.task.update({
where: { id: task.id },
data: {
status: TaskStatus.COMPLETED,
reward_points: points,
},
});
// Find the active claim for this task
const claim = await tx.claim.findFirst({
where: { task_id: task.id, status: TaskStatus.PENDING_REVIEW },
orderBy: { created_at: "desc" }
});
if (claim) {
await tx.claim.update({
where: { id: claim.id },
data: { status: TaskStatus.COMPLETED },
});
// Process payout
await capturePayment(tx, task.id, `${claim.id}-capture-pr`);
}
});
console.log(`[GitHub Webhook] PR merged. Task ${task.id} COMPLETED and paid.`);
} else {
console.log(`[GitHub Webhook] No PENDING_REVIEW task found for PR: ${prUrl}`);
}
}
}
return NextResponse.json({ received: true });
} catch (error: any) {
console.error("[GitHub Webhook Error]", error);
return NextResponse.json({ error: "Internal Error" }, { status: 500 });
}
}

View File

@@ -169,6 +169,8 @@ export const SubmitSolutionRequestSchema = z.object({
/** 接案時取得的冪等憑證,防止重複提交 */
claim_token: z.string().uuid(),
deliverables: z.record(z.string(), z.string()),
/** 必須在目標專案開啟 Pull Request並提供該 PR 的網址以供人類審核 */
github_pr_url: z.string().url().optional(),
});
export const SubmitSolutionResponseSchema = z.object({