wisdom-sdk
Version:
Core business logic and data access layer for prediction markets
436 lines (430 loc) • 14.7 kB
JavaScript
;
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