|
| 1 | +import { and, eq } from "drizzle-orm"; |
| 2 | +import { getDb } from "../db/client"; |
| 3 | +import { claims, markets, predictions, users } from "../db/schema"; |
| 4 | + |
| 5 | +export interface PortfolioExportMarket { |
| 6 | + marketId: string; |
| 7 | + question: string; |
| 8 | + status: string; |
| 9 | + resolutionTime: string; |
| 10 | + outcome: string; |
| 11 | + predictions: number; |
| 12 | + totalStaked: string; |
| 13 | + claimable: string; |
| 14 | + latestPredictionAt: string; |
| 15 | +} |
| 16 | + |
| 17 | +export interface PortfolioExportSummary { |
| 18 | + totalMarketsParticipated: number; |
| 19 | + totalPredictions: number; |
| 20 | + totalStaked: string; |
| 21 | + totalClaimable: string; |
| 22 | + outcomes: { |
| 23 | + won: number; |
| 24 | + lost: number; |
| 25 | + pending: number; |
| 26 | + confirmed: number; |
| 27 | + claimed: number; |
| 28 | + }; |
| 29 | +} |
| 30 | + |
| 31 | +export interface PortfolioExportSnapshot { |
| 32 | + version: 1; |
| 33 | + exportedAt: string; |
| 34 | + address: string; |
| 35 | + summary: PortfolioExportSummary; |
| 36 | + markets: PortfolioExportMarket[]; |
| 37 | +} |
| 38 | + |
| 39 | +function parseAmount(amount: string | null | undefined): bigint { |
| 40 | + if (!amount || !/^\d+$/.test(amount)) return 0n; |
| 41 | + return BigInt(amount); |
| 42 | +} |
| 43 | + |
| 44 | +function addDecimalStrings(a: string, b: string): string { |
| 45 | + return (parseAmount(a) + parseAmount(b)).toString(); |
| 46 | +} |
| 47 | + |
| 48 | +export async function getPortfolioExport(address: string): Promise<PortfolioExportSnapshot | null> { |
| 49 | + const db = getDb(); |
| 50 | + |
| 51 | + const userRows = await db |
| 52 | + .select({ id: users.id, stellarAddress: users.stellarAddress }) |
| 53 | + .from(users) |
| 54 | + .where(eq(users.stellarAddress, address)) |
| 55 | + .limit(1); |
| 56 | + const user = userRows[0]; |
| 57 | + if (!user) return null; |
| 58 | + |
| 59 | + const [predictionRows, claimRows] = await Promise.all([ |
| 60 | + db |
| 61 | + .select({ |
| 62 | + id: predictions.id, |
| 63 | + marketId: predictions.marketId, |
| 64 | + question: markets.question, |
| 65 | + marketStatus: markets.status, |
| 66 | + resolutionTime: markets.resolutionTime, |
| 67 | + outcome: predictions.outcome, |
| 68 | + amount: predictions.amount, |
| 69 | + status: predictions.status, |
| 70 | + createdAt: predictions.createdAt, |
| 71 | + }) |
| 72 | + .from(predictions) |
| 73 | + .innerJoin(markets, eq(predictions.marketId, markets.id)) |
| 74 | + .where(eq(predictions.userId, user.id)), |
| 75 | + db |
| 76 | + .select({ marketId: claims.marketId, amount: claims.amount }) |
| 77 | + .from(claims) |
| 78 | + .where(and(eq(claims.userId, user.id), eq(claims.status, "pending"))), |
| 79 | + ]); |
| 80 | + |
| 81 | + const claimableByMarket = new Map<string, string>(); |
| 82 | + for (const row of claimRows) { |
| 83 | + claimableByMarket.set( |
| 84 | + row.marketId, |
| 85 | + addDecimalStrings(claimableByMarket.get(row.marketId) ?? "0", row.amount), |
| 86 | + ); |
| 87 | + } |
| 88 | + |
| 89 | + const byMarket = new Map<string, PortfolioExportMarket>(); |
| 90 | + const summary: PortfolioExportSummary = { |
| 91 | + totalMarketsParticipated: 0, |
| 92 | + totalPredictions: 0, |
| 93 | + totalStaked: "0", |
| 94 | + totalClaimable: "0", |
| 95 | + outcomes: { |
| 96 | + won: 0, |
| 97 | + lost: 0, |
| 98 | + pending: 0, |
| 99 | + confirmed: 0, |
| 100 | + claimed: 0, |
| 101 | + }, |
| 102 | + }; |
| 103 | + |
| 104 | + for (const row of predictionRows) { |
| 105 | + summary.totalPredictions += 1; |
| 106 | + summary.totalStaked = addDecimalStrings(summary.totalStaked, row.amount); |
| 107 | + |
| 108 | + const status = row.status as keyof typeof summary.outcomes; |
| 109 | + if (status in summary.outcomes) { |
| 110 | + summary.outcomes[status] += 1; |
| 111 | + } |
| 112 | + |
| 113 | + const createdAt = row.createdAt.toISOString(); |
| 114 | + const existing = byMarket.get(row.marketId); |
| 115 | + if (existing) { |
| 116 | + existing.predictions += 1; |
| 117 | + existing.totalStaked = addDecimalStrings(existing.totalStaked, row.amount); |
| 118 | + if (createdAt > existing.latestPredictionAt) { |
| 119 | + existing.latestPredictionAt = createdAt; |
| 120 | + } |
| 121 | + } else { |
| 122 | + byMarket.set(row.marketId, { |
| 123 | + marketId: row.marketId, |
| 124 | + question: row.question, |
| 125 | + status: row.marketStatus, |
| 126 | + resolutionTime: row.resolutionTime.toISOString(), |
| 127 | + outcome: row.outcome, |
| 128 | + predictions: 1, |
| 129 | + totalStaked: row.amount, |
| 130 | + claimable: claimableByMarket.get(row.marketId) ?? "0", |
| 131 | + latestPredictionAt: createdAt, |
| 132 | + }); |
| 133 | + } |
| 134 | + } |
| 135 | + |
| 136 | + summary.totalMarketsParticipated = byMarket.size; |
| 137 | + for (const amount of claimableByMarket.values()) { |
| 138 | + summary.totalClaimable = addDecimalStrings(summary.totalClaimable, amount); |
| 139 | + } |
| 140 | + |
| 141 | + return { |
| 142 | + version: 1, |
| 143 | + exportedAt: new Date().toISOString(), |
| 144 | + address: user.stellarAddress, |
| 145 | + summary, |
| 146 | + markets: [...byMarket.values()].sort((a, b) => |
| 147 | + b.latestPredictionAt.localeCompare(a.latestPredictionAt), |
| 148 | + ), |
| 149 | + }; |
| 150 | +} |
0 commit comments