feat: close wallet proposal revenue loop
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
All checks were successful
CI and Production Smoke / smoke (push) Successful in 7s
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
type TreasuryStats = {
|
||||
available?: {
|
||||
@@ -16,9 +16,12 @@ type TreasuryStats = {
|
||||
fiat?: number;
|
||||
};
|
||||
recent_transactions?: Array<{
|
||||
id?: string;
|
||||
type: string;
|
||||
source: string;
|
||||
amount: number;
|
||||
currency?: string;
|
||||
direction?: "in" | "out";
|
||||
date: string;
|
||||
}>;
|
||||
};
|
||||
@@ -31,26 +34,86 @@ export default function TreasuryDashboard() {
|
||||
const [withdrawType, setWithdrawType] = useState<"CRYPTO" | "FIAT">("CRYPTO");
|
||||
const [destination, setDestination] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
const [walletTaskId, setWalletTaskId] = useState("");
|
||||
const [walletReference, setWalletReference] = useState("");
|
||||
const [walletAmountUsd, setWalletAmountUsd] = useState("");
|
||||
const [walletNote, setWalletNote] = useState("");
|
||||
const [verifyingWallet, setVerifyingWallet] = useState(false);
|
||||
const [walletMessage, setWalletMessage] = useState("");
|
||||
const cents = (value: number | undefined) => value ?? 0;
|
||||
|
||||
useEffect(() => {
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/treasury/stats");
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || `Treasury stats request failed (${res.status})`);
|
||||
}
|
||||
setStats(data);
|
||||
} catch (error) {
|
||||
setLoadError(error instanceof Error ? error.message : "Unable to load treasury stats.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
const loadStats = useCallback(async () => {
|
||||
try {
|
||||
const res = await fetch("/api/admin/treasury/stats");
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok) {
|
||||
throw new Error(data.error || `Treasury stats request failed (${res.status})`);
|
||||
}
|
||||
};
|
||||
void loadStats();
|
||||
setStats(data);
|
||||
setLoadError("");
|
||||
} catch (error) {
|
||||
setLoadError(error instanceof Error ? error.message : "Unable to load treasury stats.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const timer = window.setTimeout(() => {
|
||||
void loadStats();
|
||||
}, 0);
|
||||
return () => window.clearTimeout(timer);
|
||||
}, [loadStats]);
|
||||
|
||||
const handleVerifyWalletReceipt = async () => {
|
||||
if (!walletTaskId.trim() || !walletReference.trim()) {
|
||||
setWalletMessage("Proposal ID and wallet reference are required.");
|
||||
return;
|
||||
}
|
||||
|
||||
const amountCents = walletAmountUsd.trim()
|
||||
? Math.round(Number.parseFloat(walletAmountUsd.replace(/[,$]/g, "")) * 100)
|
||||
: undefined;
|
||||
|
||||
if (walletAmountUsd.trim() && (!amountCents || amountCents <= 0)) {
|
||||
setWalletMessage("Amount must be a positive number.");
|
||||
return;
|
||||
}
|
||||
|
||||
setVerifyingWallet(true);
|
||||
setWalletMessage("");
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/admin/proposals/wallet/verify", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
task_id: walletTaskId.trim(),
|
||||
payment_reference: walletReference.trim(),
|
||||
amount_cents: amountCents,
|
||||
verification_note: walletNote.trim(),
|
||||
}),
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
if (!res.ok || !data.success) {
|
||||
throw new Error(data.error || `Wallet verification failed (${res.status})`);
|
||||
}
|
||||
|
||||
setWalletMessage(
|
||||
`Wallet receipt ${data.duplicate ? "was already recorded" : "verified"}; affiliate pending ${(data.affiliate_fee_cents / 100).toFixed(2)}.`
|
||||
);
|
||||
setWalletTaskId("");
|
||||
setWalletReference("");
|
||||
setWalletAmountUsd("");
|
||||
setWalletNote("");
|
||||
await loadStats();
|
||||
} catch (error) {
|
||||
setWalletMessage(error instanceof Error ? error.message : "Wallet verification failed.");
|
||||
} finally {
|
||||
setVerifyingWallet(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleWithdraw = async () => {
|
||||
if (!stats) {
|
||||
setMessage("⚠️ Treasury stats are unavailable.");
|
||||
@@ -176,6 +239,68 @@ export default function TreasuryDashboard() {
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* Wallet receipt verification */}
|
||||
<div className="lg:col-span-2 bg-[#111] border border-[#333] rounded-2xl p-8">
|
||||
<h2 className="text-2xl font-semibold mb-6 flex items-center gap-3">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M20 7H4"></path><path d="M4 7l2-3h12l2 3"></path><path d="M5 7v11h14V7"></path><path d="M9 11h6"></path></svg>
|
||||
Verify USDC Proposal Receipt
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Proposal ID</label>
|
||||
<input
|
||||
type="text"
|
||||
value={walletTaskId}
|
||||
onChange={(e) => setWalletTaskId(e.target.value)}
|
||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white font-mono focus:outline-none focus:border-emerald-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Tx hash or receipt reference</label>
|
||||
<input
|
||||
type="text"
|
||||
value={walletReference}
|
||||
onChange={(e) => setWalletReference(e.target.value)}
|
||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white font-mono focus:outline-none focus:border-emerald-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Amount override (USD/USDC)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={walletAmountUsd}
|
||||
onChange={(e) => setWalletAmountUsd(e.target.value)}
|
||||
placeholder="Leave blank to use proposal package"
|
||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white font-mono focus:outline-none focus:border-emerald-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm text-gray-400 mb-2">Verification note</label>
|
||||
<input
|
||||
type="text"
|
||||
value={walletNote}
|
||||
onChange={(e) => setWalletNote(e.target.value)}
|
||||
className="w-full bg-[#0a0a0a] border border-[#333] rounded-xl px-4 py-3 text-white focus:outline-none focus:border-emerald-500 transition-colors"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleVerifyWalletReceipt}
|
||||
disabled={verifyingWallet}
|
||||
className="w-full py-4 rounded-xl font-bold text-lg bg-emerald-600 hover:bg-emerald-500 disabled:opacity-50 disabled:cursor-not-allowed transition-all"
|
||||
>
|
||||
{verifyingWallet ? "Verifying Receipt..." : "Mark Wallet Proposal Fee Verified"}
|
||||
</button>
|
||||
|
||||
{walletMessage && (
|
||||
<div className={`mt-6 p-4 rounded-xl ${walletMessage.includes("failed") || walletMessage.includes("required") || walletMessage.includes("positive") ? 'bg-red-900/30 text-red-400 border border-red-500/30' : 'bg-green-900/30 text-green-400 border border-green-500/30'} font-mono text-sm`}>
|
||||
{walletMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Withdrawal Panel */}
|
||||
<div className="lg:col-span-2 bg-[#111] border border-[#333] rounded-2xl p-8">
|
||||
<h2 className="text-2xl font-semibold mb-6 flex items-center gap-3">
|
||||
@@ -238,17 +363,24 @@ export default function TreasuryDashboard() {
|
||||
<h3 className="text-lg font-semibold text-gray-300 mb-6">Recent Cashflows</h3>
|
||||
<div className="space-y-4">
|
||||
{stats.recent_transactions?.map((tx, idx) => (
|
||||
<div key={idx} className="flex justify-between items-center border-b border-[#222] pb-4 last:border-0">
|
||||
<div key={tx.id || idx} className="flex justify-between items-center border-b border-[#222] pb-4 last:border-0">
|
||||
<div>
|
||||
<p className="text-sm text-gray-300">{tx.type.replace('_', ' ')}</p>
|
||||
<p className="text-xs text-gray-500 font-mono mt-1">{tx.source}</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<p className="text-sm font-mono text-green-400">+${(tx.amount / 100).toFixed(2)}</p>
|
||||
<p className={`text-sm font-mono ${tx.direction === "out" ? "text-amber-300" : "text-green-400"}`}>
|
||||
{tx.direction === "out" ? "-" : "+"}${(tx.amount / 100).toFixed(2)} {tx.currency || "USD"}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">{new Date(tx.date).toLocaleTimeString()}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{!stats.recent_transactions?.length ? (
|
||||
<div className="rounded-xl border border-dashed border-[#333] p-4 text-sm text-gray-500">
|
||||
No verified cashflows yet.
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
112
apps/web/src/app/api/admin/proposals/wallet/verify/route.ts
Normal file
112
apps/web/src/app/api/admin/proposals/wallet/verify/route.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { adminUnauthorizedResponse, isAdminRequestAuthorized } from "@/lib/admin-auth";
|
||||
import { recordDemandProposalFeeCaptured } from "@/lib/proposal-payment-ledger";
|
||||
import { sanitizeAgentId } from "@/lib/a2a-growth";
|
||||
|
||||
function asRecord(value: unknown) {
|
||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function positiveInteger(value: unknown) {
|
||||
const parsed = typeof value === "number" ? value : Number.parseInt(String(value || ""), 10);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) return 0;
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
function normalizePaymentReference(value: string) {
|
||||
return value.trim().replace(/\s+/g, "").slice(0, 180);
|
||||
}
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!isAdminRequestAuthorized(request)) {
|
||||
return adminUnauthorizedResponse();
|
||||
}
|
||||
|
||||
try {
|
||||
const body = asRecord(await request.json().catch(() => ({})));
|
||||
const taskId = stringValue(body.task_id || body.taskId);
|
||||
const paymentReference = normalizePaymentReference(
|
||||
stringValue(body.payment_reference || body.tx_hash || body.txHash)
|
||||
);
|
||||
const overrideAmountCents = positiveInteger(body.amount_cents || body.amountCents);
|
||||
const verificationNote = stringValue(body.verification_note || body.note);
|
||||
|
||||
if (!taskId) {
|
||||
return NextResponse.json({ error: "task_id is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
if (!paymentReference) {
|
||||
return NextResponse.json({ error: "payment_reference or tx_hash is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: taskId },
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
referred_by_agent: true,
|
||||
scout_id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ error: "Proposal task not found" }, { status: 404 });
|
||||
}
|
||||
|
||||
const intakeEvent = await prisma.auditEvent.findFirst({
|
||||
where: {
|
||||
action: "DEMAND_PROPOSAL_INTAKE_CREATED",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { metadata: true },
|
||||
});
|
||||
|
||||
const metadata = asRecord(intakeEvent?.metadata);
|
||||
const feeCents = overrideAmountCents || positiveInteger(metadata.proposal_fee_cents);
|
||||
|
||||
if (!feeCents) {
|
||||
return NextResponse.json({
|
||||
error: "Unable to resolve proposal fee amount. Provide amount_cents.",
|
||||
}, { status: 400 });
|
||||
}
|
||||
|
||||
const referralAgent = sanitizeAgentId(
|
||||
task.referred_by_agent || task.scout_id || stringValue(metadata.referral_agent)
|
||||
);
|
||||
|
||||
const result = await recordDemandProposalFeeCaptured({
|
||||
taskId: task.id,
|
||||
referralAgent: referralAgent || null,
|
||||
proposalFeeCents: feeCents,
|
||||
paymentProvider: "wallet",
|
||||
paymentReference: `wallet:${paymentReference}`,
|
||||
idempotencyKey: `proposal-wallet-fee:${paymentReference}`,
|
||||
responseStatus: "verified",
|
||||
packageId: stringValue(metadata.package_id) || null,
|
||||
source: stringValue(metadata.source) || "wallet-verification",
|
||||
campaign: stringValue(metadata.campaign) || null,
|
||||
verificationNote: verificationNote || null,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
duplicate: result.duplicate,
|
||||
task_id: result.task_id,
|
||||
ledger_entry_id: result.ledger_entry_id,
|
||||
referral_agent: referralAgent || null,
|
||||
fee_cents: feeCents,
|
||||
affiliate_fee_cents: result.affiliate_fee_cents,
|
||||
payment_reference: paymentReference,
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
console.error("[Wallet Proposal Verification Error]", error);
|
||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,6 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { adminUnauthorizedResponse, isAdminRequestAuthorized } from "@/lib/admin-auth";
|
||||
|
||||
function getWithdrawalMetadata(metadata: unknown) {
|
||||
if (typeof metadata !== "object" || metadata === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = metadata as { currency?: unknown; amount?: unknown };
|
||||
return {
|
||||
currency: typeof value.currency === "string" ? value.currency : null,
|
||||
amount: typeof value.amount === "number" ? value.amount : 0,
|
||||
};
|
||||
}
|
||||
import { getTreasurySnapshot } from "@/lib/treasury-snapshot";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
if (!isAdminRequestAuthorized(request)) {
|
||||
@@ -20,65 +8,7 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
try {
|
||||
const allTasks = await prisma.task.findMany({
|
||||
select: { reward_amount: true, reward_currency: true, status: true }
|
||||
});
|
||||
|
||||
let totalGmvUsdc = 0;
|
||||
let totalGmvFiat = 0;
|
||||
let totalUsdcRevenue = 0;
|
||||
let totalFiatRevenue = 0;
|
||||
|
||||
for (const t of allTasks) {
|
||||
if (t.reward_currency === "USDC") {
|
||||
totalGmvUsdc += t.reward_amount;
|
||||
if (t.status === "COMPLETED") totalUsdcRevenue += t.reward_amount * 0.05;
|
||||
} else {
|
||||
totalGmvFiat += t.reward_amount;
|
||||
if (t.status === "COMPLETED") totalFiatRevenue += t.reward_amount * 0.05;
|
||||
}
|
||||
}
|
||||
|
||||
const arbitrations = await prisma.arbitration.count();
|
||||
totalUsdcRevenue += arbitrations * 5000; // 50 USDC court fee
|
||||
|
||||
// Get previous withdrawals
|
||||
const withdrawals = await prisma.auditEvent.findMany({
|
||||
where: { action: "TREASURY_WITHDRAWAL" }
|
||||
});
|
||||
|
||||
let withdrawnUsdc = 0;
|
||||
let withdrawnFiat = 0;
|
||||
|
||||
for (const w of withdrawals) {
|
||||
const data = getWithdrawalMetadata(w.metadata);
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
if (data.currency === "USDC") {
|
||||
withdrawnUsdc += data.amount;
|
||||
} else {
|
||||
withdrawnFiat += data.amount;
|
||||
}
|
||||
}
|
||||
|
||||
const availableUsdc = totalUsdcRevenue - withdrawnUsdc;
|
||||
const availableFiat = totalFiatRevenue - withdrawnFiat;
|
||||
|
||||
// Provide some fake recent history to make it look active
|
||||
const recentTransactions = [
|
||||
{ id: "tx-1", type: "FEE_COLLECTION", amount: 2500, currency: "USDC", source: "Task #epic-build-2", date: new Date().toISOString() },
|
||||
{ id: "tx-2", type: "ARBITRATION_FEE", amount: 5000, currency: "USDC", source: "Arbitration #arb-091", date: new Date(Date.now() - 3600000).toISOString() },
|
||||
{ id: "tx-3", type: "SANDBOX_COMPUTE", amount: 50, currency: "USD", source: "Sandbox VM-1928", date: new Date(Date.now() - 7200000).toISOString() },
|
||||
];
|
||||
|
||||
return NextResponse.json({
|
||||
gmv: { usdc: totalGmvUsdc, fiat: totalGmvFiat },
|
||||
revenue: { usdc: totalUsdcRevenue, fiat: totalFiatRevenue },
|
||||
withdrawn: { usdc: withdrawnUsdc, fiat: withdrawnFiat },
|
||||
available: { usdc: availableUsdc, fiat: availableFiat },
|
||||
recent_transactions: recentTransactions
|
||||
});
|
||||
return NextResponse.json(await getTreasurySnapshot());
|
||||
|
||||
} catch (error: unknown) {
|
||||
console.error("[Treasury Stats Error]", error);
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { adminUnauthorizedResponse, isAdminRequestAuthorized } from "@/lib/admin-auth";
|
||||
|
||||
function getWithdrawalMetadata(metadata: unknown) {
|
||||
if (typeof metadata !== "object" || metadata === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const value = metadata as { currency?: unknown; amount?: unknown };
|
||||
return {
|
||||
currency: typeof value.currency === "string" ? value.currency : null,
|
||||
amount: typeof value.amount === "number" ? value.amount : 0,
|
||||
};
|
||||
}
|
||||
import { getTreasurySnapshot } from "@/lib/treasury-snapshot";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
if (!isAdminRequestAuthorized(request)) {
|
||||
@@ -27,72 +16,40 @@ export async function POST(request: NextRequest) {
|
||||
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Calculate actual platform balance to ensure sufficient funds
|
||||
const completedTasks = await prisma.task.findMany({
|
||||
where: { status: "COMPLETED" },
|
||||
select: { reward_amount: true, reward_currency: true }
|
||||
});
|
||||
|
||||
let totalUsdcRevenue = 0;
|
||||
let totalFiatRevenue = 0;
|
||||
|
||||
for (const t of completedTasks) {
|
||||
const feeCut = t.reward_amount * 0.05; // 5% platform fee
|
||||
if (t.reward_currency === "USDC") {
|
||||
totalUsdcRevenue += feeCut;
|
||||
} else {
|
||||
totalFiatRevenue += feeCut;
|
||||
}
|
||||
const amountCents = Math.floor(Number(amount));
|
||||
if (!Number.isFinite(amountCents) || amountCents <= 0) {
|
||||
return NextResponse.json({ error: "Invalid amount" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Add revenue from Arbitrations (simulated)
|
||||
const arbitrations = await prisma.arbitration.count();
|
||||
totalUsdcRevenue += arbitrations * 5000; // 50 USDC court fee per arbitration
|
||||
const snapshot = await getTreasurySnapshot();
|
||||
|
||||
// Get previous withdrawals
|
||||
const withdrawals = await prisma.auditEvent.findMany({
|
||||
where: { action: "TREASURY_WITHDRAWAL" }
|
||||
});
|
||||
|
||||
for (const w of withdrawals) {
|
||||
const data = getWithdrawalMetadata(w.metadata);
|
||||
if (!data) continue;
|
||||
if (data.currency === "USDC") totalUsdcRevenue -= data.amount;
|
||||
else totalFiatRevenue -= data.amount;
|
||||
}
|
||||
|
||||
// Validate balance
|
||||
if (type === "CRYPTO" && amount > totalUsdcRevenue) {
|
||||
if (type === "CRYPTO" && amountCents > snapshot.available.usdc) {
|
||||
return NextResponse.json({ error: "Insufficient USDC balance in Treasury" }, { status: 400 });
|
||||
}
|
||||
if (type === "FIAT" && amount > totalFiatRevenue) {
|
||||
if (type === "FIAT" && amountCents > snapshot.available.fiat) {
|
||||
return NextResponse.json({ error: "Insufficient Fiat balance in Treasury" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Simulate blockchain/stripe transfer latency
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Record the withdrawal
|
||||
await prisma.auditEvent.create({
|
||||
data: {
|
||||
actorType: "ADMIN",
|
||||
actorId: "admin",
|
||||
action: "TREASURY_WITHDRAWAL",
|
||||
action: "TREASURY_WITHDRAWAL_REQUESTED",
|
||||
entityType: "TREASURY",
|
||||
entityId: "platform-treasury",
|
||||
metadata: {
|
||||
destination,
|
||||
type,
|
||||
amount,
|
||||
amount: amountCents,
|
||||
currency: type === "CRYPTO" ? "USDC" : "USD",
|
||||
txHash: type === "CRYPTO" ? "0x" + Math.random().toString(16).substring(2, 64) : `pi_${Math.random().toString(36).substring(2, 16)}`
|
||||
status: "PENDING_OPERATOR_EXECUTION",
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: `Successfully transferred ${(amount / 100).toFixed(2)} ${type === "CRYPTO" ? "USDC" : "USD"} to ${destination}`
|
||||
message: `Withdrawal request recorded for ${(amountCents / 100).toFixed(2)} ${type === "CRYPTO" ? "USDC" : "USD"} to ${destination}. Execute the actual transfer from the treasury wallet or Stripe dashboard, then record the completed withdrawal.`
|
||||
});
|
||||
|
||||
} catch (error: unknown) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { TaskStatus } from "@agent-bounty/contracts";
|
||||
import { broadcastFomoEvent } from "@/lib/x-broadcaster";
|
||||
import { sanitizeAgentId } from "@/lib/a2a-growth";
|
||||
import { resolveA2aTrafficContext } from "@/lib/a2a-traffic";
|
||||
import { recordDemandProposalFeeCaptured } from "@/lib/proposal-payment-ledger";
|
||||
|
||||
const stripe = process.env.STRIPE_SECRET_KEY ? new Stripe(process.env.STRIPE_SECRET_KEY, {
|
||||
apiVersion: "2026-05-27.dahlia",
|
||||
@@ -21,7 +22,6 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session, request
|
||||
const referralAgent = sanitizeAgentId(metadata.referral_agent);
|
||||
const proposalFeeCents = Number(metadata.proposal_fee_cents || session.amount_total || 0);
|
||||
const paymentIntentId = getStripeObjectId(session.payment_intent);
|
||||
const idempotencyKey = `proposal-fee:${session.id}`;
|
||||
const trafficContext = referralAgent
|
||||
? resolveA2aTrafficContext(requestHeaders, referralAgent)
|
||||
: null;
|
||||
@@ -37,137 +37,30 @@ async function handleDemandProposalFee(session: Stripe.Checkout.Session, request
|
||||
return;
|
||||
}
|
||||
|
||||
const existingCapture = await prisma.ledgerEntry.findUnique({
|
||||
where: { idempotency_key: idempotencyKey },
|
||||
const result = await recordDemandProposalFeeCaptured({
|
||||
taskId: task.id,
|
||||
referralAgent,
|
||||
proposalFeeCents,
|
||||
paymentProvider: "stripe",
|
||||
paymentReference: paymentIntentId || session.id,
|
||||
idempotencyKey: `proposal-fee:${session.id}`,
|
||||
responseStatus: "captured",
|
||||
packageId: metadata.package_id || null,
|
||||
source: metadata.source || null,
|
||||
campaign: metadata.campaign || null,
|
||||
trafficContext,
|
||||
});
|
||||
|
||||
if (existingCapture) {
|
||||
await prisma.task.update({
|
||||
where: { id: task.id },
|
||||
data: { stripe_checkout_session_id: session.id },
|
||||
});
|
||||
|
||||
if (result.duplicate) {
|
||||
console.log(`[Webhook] Duplicate proposal fee event ignored for session: ${session.id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.$transaction(async (tx) => {
|
||||
await tx.ledgerEntry.create({
|
||||
data: {
|
||||
task_id: task.id,
|
||||
phase: "proposal_fee_captured",
|
||||
idempotency_key: idempotencyKey,
|
||||
stripe_object_id: session.id,
|
||||
response_status: "captured",
|
||||
http_status: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: task.id },
|
||||
data: {
|
||||
stripe_checkout_session_id: session.id,
|
||||
stripe_payment_intent_id: paymentIntentId,
|
||||
status: TaskStatus.DRAFT,
|
||||
},
|
||||
});
|
||||
|
||||
if (referralAgent) {
|
||||
await tx.agentProfile.upsert({
|
||||
where: { agent_id: referralAgent },
|
||||
update: {
|
||||
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
|
||||
},
|
||||
create: {
|
||||
agent_id: referralAgent,
|
||||
type: "SCOUT",
|
||||
status: "PENDING",
|
||||
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
|
||||
capabilities: {
|
||||
growth_referral: true,
|
||||
source: metadata.source || "stripe-webhook",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.scoutReputation.upsert({
|
||||
where: { scout_id: referralAgent },
|
||||
update: {
|
||||
successful_conversions: { increment: 1 },
|
||||
},
|
||||
create: {
|
||||
scout_id: referralAgent,
|
||||
successful_conversions: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const existingAffiliate = await tx.affiliateLedger.findFirst({
|
||||
where: {
|
||||
scout_id: referralAgent,
|
||||
task_id: task.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingAffiliate && proposalFeeCents > 0) {
|
||||
await tx.affiliateLedger.create({
|
||||
data: {
|
||||
scout_id: referralAgent,
|
||||
task_id: task.id,
|
||||
amount: Math.floor(proposalFeeCents * 0.1),
|
||||
currency: "USD",
|
||||
status: "PENDING",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await tx.auditEvent.create({
|
||||
data: {
|
||||
actorType: "SYSTEM",
|
||||
actorId: "stripe-webhook",
|
||||
action: "DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
metadata: {
|
||||
stripe_session_id: session.id,
|
||||
stripe_payment_intent_id: paymentIntentId,
|
||||
fee_cents: proposalFeeCents,
|
||||
package_id: metadata.package_id || null,
|
||||
referral_agent: referralAgent || null,
|
||||
affiliate_fee_cents: referralAgent ? Math.floor(proposalFeeCents * 0.1) : 0,
|
||||
source: metadata.source || null,
|
||||
campaign: metadata.campaign || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (referralAgent && trafficContext) {
|
||||
await tx.auditEvent.create({
|
||||
data: {
|
||||
actorType: trafficContext.actor_type,
|
||||
actorId: trafficContext.actor_id,
|
||||
action: "EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
entityType: "TASK",
|
||||
entityId: task.id,
|
||||
reason: "referred_demand_proposal_payment_captured",
|
||||
metadata: {
|
||||
stripe_session_id: session.id,
|
||||
stripe_payment_intent_id: paymentIntentId,
|
||||
fee_cents: proposalFeeCents,
|
||||
package_id: metadata.package_id || null,
|
||||
referral_agent: referralAgent,
|
||||
affiliate_fee_cents: Math.floor(proposalFeeCents * 0.1),
|
||||
source: metadata.source || null,
|
||||
campaign: metadata.campaign || null,
|
||||
request_id: trafficContext.request_id,
|
||||
source_ip: trafficContext.source_ip,
|
||||
user_agent: trafficContext.user_agent,
|
||||
is_public_ip: trafficContext.is_public_ip,
|
||||
surface: "stripe/webhook",
|
||||
request_actor_headers: trafficContext.request_actor_headers,
|
||||
response_status: 200,
|
||||
response_summary: "demand_proposal_fee_captured",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
console.log(`[Webhook] Proposal fee captured for task ${task.id}. Payment Intent: ${paymentIntentId}`);
|
||||
}
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
|
||||
})
|
||||
: null;
|
||||
const stripeCaptured = payment === "stripe" && Boolean(task?.stripe_payment_intent_id);
|
||||
const walletCaptured = payment === "wallet" && Boolean(task?.stripe_payment_intent_id?.startsWith("wallet:"));
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-zinc-950 px-5 py-10 text-zinc-100">
|
||||
@@ -55,7 +56,9 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
|
||||
<div>
|
||||
<p className="text-sm font-medium text-emerald-200">
|
||||
{payment === "wallet"
|
||||
? "待 USDC 轉帳確認"
|
||||
? walletCaptured
|
||||
? "USDC 付款已確認"
|
||||
: "待 USDC 轉帳確認"
|
||||
: stripeCaptured
|
||||
? "提案付款已確認"
|
||||
: "付款返回成功,等待 webhook 入帳確認"}
|
||||
@@ -122,6 +125,12 @@ export default async function ProposalSuccessPage({ searchParams }: { searchPara
|
||||
{TREASURY_USDC_ADDRESS || `${TREASURY_WALLET_LABEL} is not configured yet`}
|
||||
</dd>
|
||||
</div>
|
||||
{walletCaptured ? (
|
||||
<div>
|
||||
<dt className="text-emerald-100/70">Receipt</dt>
|
||||
<dd className="mt-1 break-all text-emerald-50">{task?.stripe_payment_intent_id?.replace(/^wallet:/, "")}</dd>
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
{TREASURY_USDC_ADDRESS ? (
|
||||
<div className="mt-3 inline-flex items-center gap-2 rounded-md border border-emerald-300/30 px-3 py-2 text-xs leading-5 text-emerald-100">
|
||||
|
||||
@@ -211,13 +211,10 @@ export async function executePayout(
|
||||
where: { idempotency_key: idempotencyKey },
|
||||
});
|
||||
if (existing) {
|
||||
if (existing.response_status === "SUCCESS") return existing;
|
||||
if (existing.response_status === "SUCCESS" || existing.response_status.startsWith("PENDING_")) return existing;
|
||||
throw new Error(`Previous executePayout failed for idempotencyKey: ${idempotencyKey}`);
|
||||
}
|
||||
|
||||
let transferId = `mock_transfer_${taskId}`;
|
||||
|
||||
// If Stripe is configured and wallet looks like a Stripe Connect Account
|
||||
if (stripe && developerWallet.startsWith("acct_") && !ALLOW_MCP_CLAIM_WITHOUT_STRIPE) {
|
||||
try {
|
||||
const transfer = await stripe.transfers.create({
|
||||
@@ -226,7 +223,17 @@ export async function executePayout(
|
||||
destination: developerWallet,
|
||||
description: `Bounty Payout for Task ${taskId}`,
|
||||
}, { idempotencyKey });
|
||||
transferId = transfer.id;
|
||||
|
||||
return await tx.ledgerEntry.create({
|
||||
data: {
|
||||
task_id: taskId,
|
||||
phase: "PAYOUT",
|
||||
idempotency_key: idempotencyKey,
|
||||
stripe_object_id: transfer.id,
|
||||
response_status: "SUCCESS",
|
||||
http_status: 200,
|
||||
},
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
await tx.ledgerEntry.create({
|
||||
data: {
|
||||
@@ -247,8 +254,10 @@ export async function executePayout(
|
||||
task_id: taskId,
|
||||
phase: "PAYOUT",
|
||||
idempotency_key: idempotencyKey,
|
||||
stripe_object_id: transferId,
|
||||
response_status: "SUCCESS",
|
||||
stripe_object_id: developerWallet,
|
||||
response_status: developerWallet.startsWith("acct_")
|
||||
? "PENDING_STRIPE_CONNECT_TRANSFER"
|
||||
: "PENDING_MANUAL_WALLET_TRANSFER",
|
||||
http_status: 200,
|
||||
},
|
||||
});
|
||||
|
||||
212
apps/web/src/lib/proposal-payment-ledger.ts
Normal file
212
apps/web/src/lib/proposal-payment-ledger.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { asA2aAgentActorId } from "@/lib/a2a-traffic";
|
||||
import { Prisma } from "../../prisma/generated/client";
|
||||
import { TaskStatus } from "@agent-bounty/contracts";
|
||||
|
||||
type ProposalFeeTrafficContext = {
|
||||
actor_type: "AGENT" | "USER";
|
||||
actor_id: string;
|
||||
request_id: string;
|
||||
source_ip: string;
|
||||
user_agent: string;
|
||||
is_public_ip: boolean;
|
||||
request_actor_headers?: Record<string, string | number | boolean | null>;
|
||||
};
|
||||
|
||||
export type ProposalFeeCaptureInput = {
|
||||
taskId: string;
|
||||
referralAgent?: string | null;
|
||||
proposalFeeCents: number;
|
||||
paymentProvider: "stripe" | "wallet";
|
||||
paymentReference: string;
|
||||
idempotencyKey: string;
|
||||
responseStatus?: "captured" | "verified";
|
||||
packageId?: string | null;
|
||||
source?: string | null;
|
||||
campaign?: string | null;
|
||||
verificationNote?: string | null;
|
||||
trafficContext?: ProposalFeeTrafficContext | null;
|
||||
};
|
||||
|
||||
function positiveCents(value: number) {
|
||||
if (!Number.isFinite(value)) return 0;
|
||||
return Math.max(0, Math.floor(value));
|
||||
}
|
||||
|
||||
function externalTrafficContextForReferral(referralAgent: string): ProposalFeeTrafficContext {
|
||||
return {
|
||||
actor_type: "AGENT",
|
||||
actor_id: asA2aAgentActorId(referralAgent),
|
||||
request_id: `admin-wallet-${Date.now()}`,
|
||||
source_ip: "admin-verification",
|
||||
user_agent: "vibework-admin-wallet-verifier",
|
||||
is_public_ip: false,
|
||||
request_actor_headers: {},
|
||||
};
|
||||
}
|
||||
|
||||
export async function recordDemandProposalFeeCaptured(input: ProposalFeeCaptureInput) {
|
||||
const proposalFeeCents = positiveCents(input.proposalFeeCents);
|
||||
const affiliateFeeCents = input.referralAgent ? Math.floor(proposalFeeCents * 0.1) : 0;
|
||||
const responseStatus = input.responseStatus || (input.paymentProvider === "wallet" ? "verified" : "captured");
|
||||
const trafficContext =
|
||||
input.referralAgent && input.paymentProvider === "wallet" && !input.trafficContext
|
||||
? externalTrafficContextForReferral(input.referralAgent)
|
||||
: input.trafficContext;
|
||||
|
||||
const task = await prisma.task.findUnique({
|
||||
where: { id: input.taskId },
|
||||
select: { id: true },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
throw new Error(`Proposal task not found: ${input.taskId}`);
|
||||
}
|
||||
|
||||
const existingCapture = await prisma.ledgerEntry.findUnique({
|
||||
where: { idempotency_key: input.idempotencyKey },
|
||||
});
|
||||
|
||||
if (existingCapture) {
|
||||
return {
|
||||
task_id: input.taskId,
|
||||
duplicate: true,
|
||||
ledger_entry_id: existingCapture.id,
|
||||
affiliate_fee_cents: affiliateFeeCents,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await prisma.$transaction(async (tx: Prisma.TransactionClient) => {
|
||||
const ledger = await tx.ledgerEntry.create({
|
||||
data: {
|
||||
task_id: input.taskId,
|
||||
phase: "proposal_fee_captured",
|
||||
idempotency_key: input.idempotencyKey,
|
||||
stripe_object_id: input.paymentReference,
|
||||
response_status: responseStatus,
|
||||
http_status: 200,
|
||||
},
|
||||
});
|
||||
|
||||
await tx.task.update({
|
||||
where: { id: input.taskId },
|
||||
data: {
|
||||
stripe_payment_intent_id: input.paymentReference,
|
||||
status: TaskStatus.DRAFT,
|
||||
},
|
||||
});
|
||||
|
||||
if (input.referralAgent) {
|
||||
await tx.agentProfile.upsert({
|
||||
where: { agent_id: input.referralAgent },
|
||||
update: {
|
||||
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
|
||||
},
|
||||
create: {
|
||||
agent_id: input.referralAgent,
|
||||
type: "SCOUT",
|
||||
status: "PENDING",
|
||||
discovery_source: "DEMAND_PROPOSAL_PAID_REFERRAL",
|
||||
capabilities: {
|
||||
growth_referral: true,
|
||||
source: input.source || input.paymentProvider,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await tx.scoutReputation.upsert({
|
||||
where: { scout_id: input.referralAgent },
|
||||
update: {
|
||||
successful_conversions: { increment: 1 },
|
||||
},
|
||||
create: {
|
||||
scout_id: input.referralAgent,
|
||||
successful_conversions: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const existingAffiliate = await tx.affiliateLedger.findFirst({
|
||||
where: {
|
||||
scout_id: input.referralAgent,
|
||||
task_id: input.taskId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!existingAffiliate && affiliateFeeCents > 0) {
|
||||
await tx.affiliateLedger.create({
|
||||
data: {
|
||||
scout_id: input.referralAgent,
|
||||
task_id: input.taskId,
|
||||
amount: affiliateFeeCents,
|
||||
currency: input.paymentProvider === "wallet" ? "USDC" : "USD",
|
||||
status: "PENDING",
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await tx.auditEvent.create({
|
||||
data: {
|
||||
actorType: "SYSTEM",
|
||||
actorId: input.paymentProvider === "wallet" ? "wallet-verifier" : "stripe-webhook",
|
||||
action: "DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
entityType: "TASK",
|
||||
entityId: input.taskId,
|
||||
metadata: {
|
||||
payment_provider: input.paymentProvider,
|
||||
payment_reference: input.paymentReference,
|
||||
fee_cents: proposalFeeCents,
|
||||
currency: input.paymentProvider === "wallet" ? "USDC" : "USD",
|
||||
package_id: input.packageId || null,
|
||||
referral_agent: input.referralAgent || null,
|
||||
affiliate_fee_cents: affiliateFeeCents,
|
||||
source: input.source || null,
|
||||
campaign: input.campaign || null,
|
||||
verification_note: input.verificationNote || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (input.referralAgent && trafficContext) {
|
||||
await tx.auditEvent.create({
|
||||
data: {
|
||||
actorType: trafficContext.actor_type,
|
||||
actorId: trafficContext.actor_id,
|
||||
action: "EXTERNAL_DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
entityType: "TASK",
|
||||
entityId: input.taskId,
|
||||
reason: "referred_demand_proposal_payment_captured",
|
||||
metadata: {
|
||||
payment_provider: input.paymentProvider,
|
||||
payment_reference: input.paymentReference,
|
||||
fee_cents: proposalFeeCents,
|
||||
currency: input.paymentProvider === "wallet" ? "USDC" : "USD",
|
||||
package_id: input.packageId || null,
|
||||
referral_agent: input.referralAgent,
|
||||
affiliate_fee_cents: affiliateFeeCents,
|
||||
source: input.source || null,
|
||||
campaign: input.campaign || null,
|
||||
verification_note: input.verificationNote || null,
|
||||
request_id: trafficContext.request_id,
|
||||
source_ip: trafficContext.source_ip,
|
||||
user_agent: trafficContext.user_agent,
|
||||
is_public_ip: trafficContext.is_public_ip,
|
||||
surface: input.paymentProvider === "wallet" ? "admin/wallet-verification" : "stripe/webhook",
|
||||
request_actor_headers: trafficContext.request_actor_headers || {},
|
||||
response_status: 200,
|
||||
response_summary: "demand_proposal_fee_captured",
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return ledger;
|
||||
});
|
||||
|
||||
return {
|
||||
task_id: input.taskId,
|
||||
duplicate: false,
|
||||
ledger_entry_id: result.id,
|
||||
affiliate_fee_cents: affiliateFeeCents,
|
||||
};
|
||||
}
|
||||
158
apps/web/src/lib/treasury-snapshot.ts
Normal file
158
apps/web/src/lib/treasury-snapshot.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
type Bucket = {
|
||||
usdc: number;
|
||||
fiat: number;
|
||||
};
|
||||
|
||||
function emptyBucket(): Bucket {
|
||||
return { usdc: 0, fiat: 0 };
|
||||
}
|
||||
|
||||
function asRecord(value: unknown) {
|
||||
return typeof value === "object" && value !== null ? (value as Record<string, unknown>) : {};
|
||||
}
|
||||
|
||||
function numberValue(value: unknown) {
|
||||
const parsed = typeof value === "number" ? value : Number.parseInt(String(value || ""), 10);
|
||||
return Number.isFinite(parsed) ? Math.floor(parsed) : 0;
|
||||
}
|
||||
|
||||
function stringValue(value: unknown) {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function bucketKeyFromMetadata(metadata: Record<string, unknown>): keyof Bucket {
|
||||
const currency = stringValue(metadata.currency).toUpperCase();
|
||||
const paymentProvider = stringValue(metadata.payment_provider).toLowerCase();
|
||||
const withdrawalType = stringValue(metadata.type).toUpperCase();
|
||||
if (currency === "USDC" || paymentProvider === "wallet" || withdrawalType === "CRYPTO") {
|
||||
return "usdc";
|
||||
}
|
||||
return "fiat";
|
||||
}
|
||||
|
||||
function addToBucket(bucket: Bucket, key: keyof Bucket, amount: number) {
|
||||
bucket[key] += amount;
|
||||
}
|
||||
|
||||
function subtractBucket(left: Bucket, right: Bucket): Bucket {
|
||||
return {
|
||||
usdc: left.usdc - right.usdc,
|
||||
fiat: left.fiat - right.fiat,
|
||||
};
|
||||
}
|
||||
|
||||
export async function getTreasurySnapshot() {
|
||||
const [
|
||||
allTasks,
|
||||
completedTasks,
|
||||
arbitrationCount,
|
||||
proposalFeeEvents,
|
||||
withdrawalEvents,
|
||||
recentAuditEvents,
|
||||
] = await Promise.all([
|
||||
prisma.task.findMany({
|
||||
select: { reward_amount: true, reward_currency: true },
|
||||
}),
|
||||
prisma.task.findMany({
|
||||
where: { status: "COMPLETED" },
|
||||
select: { reward_amount: true, reward_currency: true },
|
||||
}),
|
||||
prisma.arbitration.count(),
|
||||
prisma.auditEvent.findMany({
|
||||
where: { action: "DEMAND_PROPOSAL_FEE_CAPTURED" },
|
||||
select: { id: true, entityId: true, metadata: true, createdAt: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
}),
|
||||
prisma.auditEvent.findMany({
|
||||
where: { action: "TREASURY_WITHDRAWAL" },
|
||||
select: { metadata: true },
|
||||
}),
|
||||
prisma.auditEvent.findMany({
|
||||
where: {
|
||||
action: {
|
||||
in: [
|
||||
"DEMAND_PROPOSAL_FEE_CAPTURED",
|
||||
"TREASURY_WITHDRAWAL",
|
||||
"TREASURY_WITHDRAWAL_REQUESTED",
|
||||
],
|
||||
},
|
||||
},
|
||||
select: { id: true, action: true, entityId: true, metadata: true, createdAt: true },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 20,
|
||||
}),
|
||||
]);
|
||||
|
||||
const gmv = emptyBucket();
|
||||
const taskPlatformFees = emptyBucket();
|
||||
const proposalFeeRevenue = emptyBucket();
|
||||
const arbitrationRevenue = emptyBucket();
|
||||
const withdrawn = emptyBucket();
|
||||
|
||||
for (const task of allTasks) {
|
||||
addToBucket(gmv, task.reward_currency === "USDC" ? "usdc" : "fiat", task.reward_amount);
|
||||
}
|
||||
|
||||
for (const task of completedTasks) {
|
||||
addToBucket(taskPlatformFees, task.reward_currency === "USDC" ? "usdc" : "fiat", Math.floor(task.reward_amount * 0.05));
|
||||
}
|
||||
|
||||
addToBucket(arbitrationRevenue, "usdc", arbitrationCount * 5000);
|
||||
|
||||
for (const event of proposalFeeEvents) {
|
||||
const metadata = asRecord(event.metadata);
|
||||
const amount = numberValue(metadata.fee_cents);
|
||||
if (amount <= 0) continue;
|
||||
addToBucket(proposalFeeRevenue, bucketKeyFromMetadata(metadata), amount);
|
||||
}
|
||||
|
||||
for (const event of withdrawalEvents) {
|
||||
const metadata = asRecord(event.metadata);
|
||||
const amount = numberValue(metadata.amount);
|
||||
if (amount <= 0) continue;
|
||||
addToBucket(withdrawn, bucketKeyFromMetadata(metadata), amount);
|
||||
}
|
||||
|
||||
const revenue = {
|
||||
usdc: taskPlatformFees.usdc + proposalFeeRevenue.usdc + arbitrationRevenue.usdc,
|
||||
fiat: taskPlatformFees.fiat + proposalFeeRevenue.fiat + arbitrationRevenue.fiat,
|
||||
};
|
||||
|
||||
const recentTransactions = recentAuditEvents.map((event) => {
|
||||
const metadata = asRecord(event.metadata);
|
||||
const isOutflow = event.action.startsWith("TREASURY_WITHDRAWAL");
|
||||
const amount = numberValue(metadata.amount) || numberValue(metadata.fee_cents);
|
||||
const currency = bucketKeyFromMetadata(metadata) === "usdc" ? "USDC" : "USD";
|
||||
const provider = stringValue(metadata.payment_provider);
|
||||
const reference = stringValue(metadata.payment_reference) || stringValue(metadata.txHash) || stringValue(metadata.destination);
|
||||
const source =
|
||||
event.action === "DEMAND_PROPOSAL_FEE_CAPTURED"
|
||||
? `Proposal ${event.entityId}`
|
||||
: reference || "Treasury";
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
type: event.action === "DEMAND_PROPOSAL_FEE_CAPTURED"
|
||||
? `${provider || "proposal"}_proposal_fee`
|
||||
: event.action.toLowerCase(),
|
||||
source,
|
||||
amount,
|
||||
currency,
|
||||
direction: isOutflow ? "out" : "in",
|
||||
date: event.createdAt.toISOString(),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
gmv,
|
||||
revenue,
|
||||
withdrawn,
|
||||
available: subtractBucket(revenue, withdrawn),
|
||||
proposal_fee_revenue: proposalFeeRevenue,
|
||||
task_platform_fee_revenue: taskPlatformFees,
|
||||
arbitration_fee_revenue: arbitrationRevenue,
|
||||
recent_transactions: recentTransactions,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user