feat(web,contracts): implement PR Merge Validation for anti-fraud payout
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
Some checks failed
Deploy to 110 WOOO Server / deploy (push) Failing after 6s
This commit is contained in:
@@ -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({
|
||||
|
||||
74
apps/web/src/app/api/webhooks/github/route.ts
Normal file
74
apps/web/src/app/api/webhooks/github/route.ts
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user