UNPKG

wisdom-sdk

Version:

Core business logic and data access layer for prediction markets

436 lines (430 loc) 14.7 kB
'use strict'; var kv = require('@vercel/kv'); // src/kv-store.ts // src/logger.ts var createLogger = () => { const logLevel = process.env.LOG_LEVEL || "info"; const logLevels = { debug: 0, info: 1, warn: 2, error: 3 }; const currentLevelValue = logLevel in logLevels ? logLevels[logLevel] : logLevels.info; const formatLog = (level, obj, msg) => { const timestamp = (/* @__PURE__ */ new Date()).toISOString(); const service = "wisdom-sdk"; const objStr = JSON.stringify(obj); return `[${timestamp}] ${level.toUpperCase()} [${service}] ${msg || ""} ${objStr}`; }; return { debug: (obj, msg) => { if (currentLevelValue <= 0) { console.debug(formatLog("debug", obj, msg)); } }, info: (obj, msg) => { if (currentLevelValue <= 1) { console.info(formatLog("info", obj, msg)); } }, warn: (obj, msg) => { if (currentLevelValue <= 2) { console.warn(formatLog("warn", obj, msg)); } }, error: (obj, msg) => { if (currentLevelValue <= 3) { console.error(formatLog("error", obj, msg)); } }, child: (bindings) => { const childLogger = createLogger(); return { debug: (obj, msg) => childLogger.debug({ ...obj, ...bindings }, msg), info: (obj, msg) => childLogger.info({ ...obj, ...bindings }, msg), warn: (obj, msg) => childLogger.warn({ ...obj, ...bindings }, msg), error: (obj, msg) => childLogger.error({ ...obj, ...bindings }, msg), child: (nestedBindings) => childLogger.child({ ...bindings, ...nestedBindings }) }; } }; }; var logger = createLogger(); function getContextLogger(context) { return logger.child({ context }); } var AppError = class extends Error { constructor({ message, context = "general", code = "INTERNAL_ERROR", originalError, data }) { super(message); this.name = "AppError"; this.context = context; this.code = code; this.originalError = originalError; this.data = data; Error.captureStackTrace(this, this.constructor); } // Logs this error with appropriate context and returns it log() { const contextLogger = getContextLogger(this.context); const logObj = { code: this.code, error: this.message, ...this.originalError && { originalError: this.originalError.message }, ...this.data && { data: this.data } }; contextLogger.error(logObj, this.message); return this; } }; // src/kv-store.ts var kvLogger = logger.child({ context: "kv-store" }); var KV_PREFIXES = { MARKET: "market", MARKET_IDS: "market_ids", USER_MARKETS: "user_markets", MARKET_PARTICIPANTS: "market_participants", MARKET_CATEGORY: "market_category", // Index for markets by category MARKET_STATUS: "market_status", // Index for markets by status PREDICTION: "prediction", USER_PREDICTIONS: "user_predictions", MARKET_PREDICTIONS: "market_predictions", PREDICTION_NFT: "prediction_nft", USER_BALANCE: "user_balance", USER_STATS: "user_stats", LEADERBOARD: "leaderboard", LEADERBOARD_EARNINGS: "leaderboard_earnings", LEADERBOARD_ACCURACY: "leaderboard_accuracy", BUG_REPORT: "bug_report", BUG_REPORT_IDS: "bug_report_ids", USER_BUG_REPORTS: "user_bug_reports", // Transaction custody-related prefixes CUSTODY_TRANSACTION: "custody_transaction", CUSTODY_TRANSACTION_IDS: "custody_transaction_ids", USER_TRANSACTIONS: "user_transactions", SIGNER_TRANSACTIONS: "signer_transactions", MARKET_TRANSACTIONS: "market_transactions", CUSTODY_NFT_RECEIPT: "custody_nft_receipt", // Claim reward transaction prefixes CLAIM_REWARD_TRANSACTION: "claim_reward_transaction", USER_CLAIM_REWARDS: "user_claim_rewards" }; function getKey(entityType, id) { const prefix = KV_PREFIXES[entityType]; if (entityType === "MARKET_IDS" && !id) { return prefix; } return id ? `${prefix}:${id}` : prefix; } async function storeEntity(entityType, id, data) { try { const key = getKey(entityType, id); await kv.kv.set(key, JSON.stringify(data)); return data; } catch (error) { throw new AppError({ message: `Failed to store ${entityType} with ID ${id}`, context: "kv-store", code: "KV_STORE_ERROR", originalError: error instanceof Error ? error : new Error(String(error)), data: { entityType, id, operation: "store" } }).log(); } } async function getEntity(entityType, id) { try { const key = getKey(entityType, id); let data = await kv.kv.get(key); if (!data && entityType === "MARKET") { data = await kv.kv.get(`markets:${id}`); } if (!data) { kvLogger.debug({ entityType, id }, `Entity not found: ${entityType}:${id}`); return null; } if (typeof data !== "string") { return data; } try { return JSON.parse(data); } catch (e) { throw new AppError({ message: `Error parsing JSON for ${entityType} with ID ${id}`, context: "kv-store", code: "KV_JSON_PARSE_ERROR", originalError: e instanceof Error ? e : new Error(String(e)), data: { entityType, id, operation: "parse" } }).log(); } } catch (error) { if (error instanceof AppError) { throw error; } throw new AppError({ message: `Failed to retrieve ${entityType} with ID ${id}`, context: "kv-store", code: "KV_RETRIEVE_ERROR", originalError: error instanceof Error ? error : new Error(String(error)), data: { entityType, id, operation: "get" } }).log(); } } async function addToSortedSet(setType, memberId, score) { try { const key = getKey(setType); await kv.kv.zadd(key, { score, member: memberId }); return true; } catch (error) { console.error(`Error adding to sorted set ${setType}:`, error); return false; } } async function getTopFromSortedSet(setType, limit = 10, reverse = true) { try { const key = getKey(setType); return await kv.kv.zrange(key, 0, limit - 1, { rev: reverse }); } catch (error) { console.error(`Error getting top members from sorted set ${setType}:`, error); return []; } } async function getScoresFromSortedSet(setType, memberIds) { try { const key = getKey(setType); const result = {}; const batchSize = 50; for (let i = 0; i < memberIds.length; i += batchSize) { const batch = memberIds.slice(i, i + batchSize); const batchScores = await Promise.all( batch.map(async (memberId) => { const score = await kv.kv.zscore(key, memberId); return { memberId, score: score ? Number(score) : null }; }) ); batchScores.forEach(({ memberId, score }) => { if (score !== null) { result[memberId] = score; } }); } return result; } catch (error) { console.error(`Error getting scores from sorted set ${setType}:`, error); return {}; } } // src/user-stats-store.ts var userStatsStore = { // Helper to calculate user score consistently across the app calculateUserScore(stats) { const accuracyComponent = stats.totalPredictions >= 5 ? stats.accuracy : 0; const normalizedEarnings = stats.totalEarnings / 100; const volumeFactor = stats.totalPredictions > 0 ? Math.log10(stats.totalPredictions + 1) * 10 : 0; const consistencyFactor = stats.totalPredictions >= 10 ? accuracyComponent * Math.min(stats.totalPredictions / 20, 1.5) : accuracyComponent; return consistencyFactor * 0.4 + normalizedEarnings * 0.3 + Math.min(volumeFactor, 25) * 0.3; }, // Get user stats for a specific user async getUserStats(userId) { try { if (!userId) return null; const stats = await getEntity("USER_STATS", userId); return stats || null; } catch (error) { console.error(`Error getting user stats for ${userId}:`, error); return null; } }, // Update user stats when a prediction is made async updateStatsForNewPrediction(userId, prediction) { try { const currentStats = await this.getUserStats(userId) || { userId, totalPredictions: 0, correctPredictions: 0, accuracy: 0, totalAmount: 0, totalEarnings: 0, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; const updatedStats = { ...currentStats, totalPredictions: currentStats.totalPredictions + 1, totalAmount: currentStats.totalAmount + prediction.amount, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; updatedStats.accuracy = updatedStats.totalPredictions > 0 ? updatedStats.correctPredictions / updatedStats.totalPredictions * 100 : 0; await storeEntity("USER_STATS", userId, updatedStats); await this.updateLeaderboardEntries(updatedStats); return updatedStats; } catch (error) { console.error(`Error updating stats for user ${userId}:`, error); throw error; } }, // Update user stats when a prediction is resolved async updateStatsForResolvedPrediction(userId, prediction, isCorrect, earnings) { try { if (!userId || userId === "anonymous") { console.log("Skipping stats update for anonymous user"); return { userId, totalPredictions: 1, correctPredictions: isCorrect ? 1 : 0, accuracy: isCorrect ? 100 : 0, totalAmount: prediction?.amount || 0, totalEarnings: earnings, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; } const currentStats = await this.getUserStats(userId); if (!currentStats) { console.log(`Creating new stats for user ${userId}`); const newStats = { userId, totalPredictions: 1, correctPredictions: isCorrect ? 1 : 0, accuracy: isCorrect ? 100 : 0, totalAmount: prediction?.amount || 0, totalEarnings: earnings, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; await storeEntity("USER_STATS", userId, newStats); await this.updateLeaderboardEntries(newStats); return newStats; } const updatedStats = { ...currentStats, correctPredictions: isCorrect ? currentStats.correctPredictions + 1 : currentStats.correctPredictions, totalEarnings: currentStats.totalEarnings + earnings, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; updatedStats.accuracy = updatedStats.totalPredictions > 0 ? updatedStats.correctPredictions / updatedStats.totalPredictions * 100 : 0; await storeEntity("USER_STATS", userId, updatedStats); await this.updateLeaderboardEntries(updatedStats); return updatedStats; } catch (error) { console.error(`Error updating stats for resolved prediction, user ${userId}:`, error); throw error; } }, // Update user's username (when available from auth provider) async updateUsername(userId, username) { try { const stats = await this.getUserStats(userId); if (!stats) return null; const updatedStats = { ...stats, username, lastUpdated: (/* @__PURE__ */ new Date()).toISOString() }; await storeEntity("USER_STATS", userId, updatedStats); await this.updateLeaderboardEntries(updatedStats); return updatedStats; } catch (error) { console.error(`Error updating username for user ${userId}:`, error); return null; } }, // Update leaderboard sorted sets for efficient querying async updateLeaderboardEntries(stats) { try { await addToSortedSet( "LEADERBOARD_EARNINGS", stats.userId, stats.totalEarnings ); const accuracyScore = stats.totalPredictions >= 5 ? stats.accuracy : 0; await addToSortedSet( "LEADERBOARD_ACCURACY", stats.userId, accuracyScore ); const compositeScore = this.calculateUserScore(stats); await addToSortedSet( "LEADERBOARD", stats.userId, compositeScore ); } catch (error) { console.error("Error updating leaderboard entries:", error); throw error; } }, // Get top leaderboard entries by earnings async getTopEarners(limit = 10) { try { const userIds = await getTopFromSortedSet("LEADERBOARD_EARNINGS", limit); const leaderboard = await this.getUserStatsForIds(userIds); const scoresMap = await getScoresFromSortedSet("LEADERBOARD_EARNINGS", userIds); return leaderboard.map((entry, index) => { return { ...entry, rank: index + 1, score: scoresMap[entry.userId] || this.calculateUserScore(entry) }; }); } catch (error) { console.error("Error getting top earners:", error); return []; } }, // Get top leaderboard entries by accuracy async getTopAccurate(limit = 10) { try { const userIds = await getTopFromSortedSet("LEADERBOARD_ACCURACY", limit); const leaderboard = await this.getUserStatsForIds(userIds); const scoresMap = await getScoresFromSortedSet("LEADERBOARD_ACCURACY", userIds); return leaderboard.map((entry, index) => { return { ...entry, rank: index + 1, score: scoresMap[entry.userId] || this.calculateUserScore(entry) }; }); } catch (error) { console.error("Error getting top accuracy:", error); return []; } }, // Get top leaderboard entries by combined score async getTopUsers(limit = 10) { try { const userIds = await getTopFromSortedSet("LEADERBOARD", limit); const leaderboard = await this.getUserStatsForIds(userIds); const scoresMap = await getScoresFromSortedSet("LEADERBOARD", userIds); return leaderboard.map((entry, index) => { return { ...entry, rank: index + 1, score: scoresMap[entry.userId] || this.calculateUserScore(entry) }; }); } catch (error) { console.error("Error getting leaderboard:", error); return []; } }, // Helper to get multiple user stats by IDs async getUserStatsForIds(userIds) { try { if (userIds.length === 0) return []; const statsPromises = userIds.map((id) => this.getUserStats(id)); const statsResults = await Promise.all(statsPromises); return statsResults.filter(Boolean); } catch (error) { console.error("Error getting user stats for IDs:", error); return []; } } }; exports.userStatsStore = userStatsStore; //# sourceMappingURL=user-stats-store.cjs.map //# sourceMappingURL=user-stats-store.cjs.map