UNPKG

wisdom-sdk

Version:

Core business logic and data access layer for prediction markets

516 lines (514 loc) 18.8 kB
import { marketStore } from './chunk-IA2KNKYT.js'; import { userStatsStore } from './chunk-ZJ2UKXRV.js'; import { isAdmin, generateUUID } from './chunk-7CBIP22I.js'; import { userBalanceStore } from './chunk-SL5BKWK6.js'; import { storeEntity, deleteEntity, removeFromSet, getEntity, getSetMembers, startTransaction } from './chunk-FIJAO3BQ.js'; import { logger, AppError } from './chunk-2OHF4QSJ.js'; // src/prediction-store.ts var predictionLogger = logger.child({ context: "prediction-store" }); var predictionStore = { // Create a new prediction async createPrediction(data) { try { if (!data.marketId || !data.userId || data.amount <= 0) { throw new AppError({ message: "Invalid prediction data", context: "prediction-store", code: "PREDICTION_VALIDATION_ERROR", data: { hasMarketId: !!data.marketId, hasUserId: !!data.userId, amount: data.amount } }).log(); } const id = generateUUID(); const now = (/* @__PURE__ */ new Date()).toISOString(); predictionLogger.debug( { marketId: data.marketId, userId: data.userId, amount: data.amount }, "Creating new prediction" ); const nftReceipt = { id: generateUUID(), tokenId: `${data.marketId}-${data.userId}-${now}`, image: this.generateNftImage(data.marketName, data.outcomeName, data.amount), predictionId: id, marketName: data.marketName, outcomeName: data.outcomeName, amount: data.amount, createdAt: now }; const prediction = { id, marketId: data.marketId, outcomeId: data.outcomeId, outcomeName: data.outcomeName, userId: data.userId, amount: data.amount, createdAt: now, nftReceipt, status: "active" }; const tx = await startTransaction(); try { await tx.addEntity("PREDICTION", id, prediction); await tx.addEntity("PREDICTION_NFT", nftReceipt.id, nftReceipt); await tx.addToSetInTransaction("USER_PREDICTIONS", data.userId, id); await tx.addToSetInTransaction("MARKET_PREDICTIONS", data.marketId, id); const success = await tx.execute(); if (!success) { throw new AppError({ message: "Failed to create prediction - transaction failed", context: "prediction-store", code: "PREDICTION_TRANSACTION_ERROR", data: { predictionId: id } }).log(); } await marketStore.updateMarketStats( data.marketId, data.outcomeId, data.amount, data.userId ); predictionLogger.info( { predictionId: id, userId: data.userId }, "Prediction created successfully" ); return prediction; } catch (error) { if (error instanceof AppError) { throw error; } else { throw new AppError({ message: "Error during prediction creation transaction", context: "prediction-store", code: "PREDICTION_CREATE_ERROR", originalError: error instanceof Error ? error : new Error(String(error)), data: { marketId: data.marketId, userId: data.userId } }).log(); } } } catch (error) { if (error instanceof AppError) { throw error; } else { throw new AppError({ message: "Failed to create prediction", context: "prediction-store", code: "PREDICTION_CREATE_ERROR", originalError: error instanceof Error ? error : new Error(String(error)), data: { marketId: data.marketId, userId: data.userId, amount: data.amount } }).log(); } } }, // Get all predictions for a user async getUserPredictions(userId) { try { if (!userId) return []; const predictionIds = await getSetMembers("USER_PREDICTIONS", userId); if (predictionIds.length === 0) { return []; } const predictions = await Promise.all( predictionIds.map((id) => this.getPrediction(id)) ); return predictions.filter(Boolean); } catch (error) { console.error("Error getting user predictions:", error); return []; } }, // Get all predictions for a market async getMarketPredictions(marketId) { try { if (!marketId) return []; const predictionIds = await getSetMembers("MARKET_PREDICTIONS", marketId); if (predictionIds.length === 0) { return []; } const predictions = await Promise.all( predictionIds.map((id) => this.getPrediction(id)) ); return predictions.filter(Boolean); } catch (error) { console.error("Error getting market predictions:", error); return []; } }, // Get a specific prediction by ID async getPrediction(id) { try { if (!id) return void 0; const prediction = await getEntity("PREDICTION", id); return prediction || void 0; } catch (error) { console.error(`Error getting prediction ${id}:`, error); return void 0; } }, // Get a specific NFT receipt async getNFTReceipt(id) { try { if (!id) return void 0; const receipt = await getEntity("PREDICTION_NFT", id); return receipt || void 0; } catch (error) { console.error(`Error getting NFT receipt ${id}:`, error); return void 0; } }, // Update prediction status async updatePredictionStatus(id, status) { try { const prediction = await this.getPrediction(id); if (!prediction) return void 0; const updatedPrediction = { ...prediction, status }; await storeEntity("PREDICTION", id, updatedPrediction); return updatedPrediction; } catch (error) { console.error(`Error updating prediction ${id}:`, error); return void 0; } }, // Generate a placeholder image URL for the NFT // In a real app, this would generate or reference an actual image generateNftImage(marketName, outcomeName, amount) { const bgColor = "#1a2026"; const textColor = "#ffffff"; const sanitizedOutcome = outcomeName.replace(/[<>&"']/g, ""); const sanitizedMarket = marketName.substring(0, 30).replace(/[<>&"']/g, ""); const svg = ` <svg width="600" height="400" xmlns="http://www.w3.org/2000/svg"> <rect width="600" height="400" fill="${bgColor}" /> <text x="300" y="170" font-family="Arial, sans-serif" font-size="24" text-anchor="middle" fill="${textColor}">Prediction Receipt</text> <text x="300" y="210" font-family="Arial, sans-serif" font-size="20" text-anchor="middle" fill="${textColor}">${sanitizedOutcome}</text> <text x="300" y="250" font-family="Arial, sans-serif" font-size="16" text-anchor="middle" fill="${textColor}">${sanitizedMarket}</text> <text x="300" y="280" font-family="Arial, sans-serif" font-size="18" text-anchor="middle" fill="${textColor}">$${amount.toFixed(2)}</text> </svg>`; return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`; }, /** * Create a prediction with balance update * This function handles the complete process of: * 1. Checking user balance and deducting funds * 2. Creating the prediction and NFT receipt * 3. Updating market stats * 4. Updating user stats for leaderboard * * @param data Prediction data * @returns Object with created prediction or error message */ async createPredictionWithBalanceUpdate(data) { try { const opLogger = predictionLogger.child({ operation: "createPredictionWithBalance", marketId: data.marketId, userId: data.userId, amount: data.amount }); opLogger.info({}, "Starting prediction creation with balance update"); if (!data.marketId || !data.userId || data.amount <= 0) { const error = new AppError({ message: "Invalid prediction data", context: "prediction-store", code: "PREDICTION_VALIDATION_ERROR", data: { hasMarketId: !!data.marketId, hasUserId: !!data.userId, amount: data.amount } }).log(); return { success: false, error: error.message }; } const market = await marketStore.getMarket(data.marketId); if (!market) { const error = new AppError({ message: `Market not found: ${data.marketId}`, context: "prediction-store", code: "MARKET_NOT_FOUND", data: { marketId: data.marketId } }).log(); return { success: false, error: error.message }; } const outcome = market.outcomes.find((o) => o.id === data.outcomeId); if (!outcome) { const error = new AppError({ message: `Outcome ${data.outcomeId} not found in market ${data.marketId}`, context: "prediction-store", code: "OUTCOME_NOT_FOUND", data: { marketId: data.marketId, outcomeId: data.outcomeId, availableOutcomes: market.outcomes.map((o) => o.id) } }).log(); return { success: false, error: error.message }; } if (market.resolvedOutcomeId !== void 0) { const error = new AppError({ message: `Market ${data.marketId} is already resolved`, context: "prediction-store", code: "MARKET_ALREADY_RESOLVED", data: { marketId: data.marketId, resolvedOutcomeId: market.resolvedOutcomeId } }).log(); return { success: false, error: error.message }; } if (new Date(market.endDate) < /* @__PURE__ */ new Date()) { const error = new AppError({ message: `Market ${data.marketId} has ended`, context: "prediction-store", code: "MARKET_ENDED", data: { marketId: data.marketId, endDate: market.endDate, currentDate: (/* @__PURE__ */ new Date()).toISOString() } }).log(); return { success: false, error: error.message }; } opLogger.debug({}, "Market validation completed, processing balance update"); try { const balanceResult = await userBalanceStore.updateBalanceForPrediction( data.userId, data.amount ); if (!balanceResult) { throw new AppError({ message: "Failed to update user balance", context: "prediction-store", code: "BALANCE_UPDATE_FAILED", data: { userId: data.userId, amount: data.amount } }); } opLogger.debug({}, "Balance updated successfully, creating prediction"); } catch (balanceError) { if (balanceError instanceof Error && balanceError.message.includes("Insufficient balance")) { const error = new AppError({ message: "Insufficient balance. Please add more funds to your account.", context: "prediction-store", code: "INSUFFICIENT_BALANCE", originalError: balanceError, data: { userId: data.userId, amount: data.amount } }).log(); return { success: false, error: error.message }; } throw balanceError; } const prediction = await this.createPrediction({ marketId: market.id, marketName: market.name, outcomeId: data.outcomeId, outcomeName: outcome.name, userId: data.userId, amount: data.amount }); opLogger.debug( { predictionId: prediction.id }, "Prediction created, updating user stats" ); await userStatsStore.updateStatsForNewPrediction(data.userId, prediction); opLogger.info( { predictionId: prediction.id }, "Prediction with balance update completed successfully" ); const marketData = { ...market }; return { success: true, prediction, market: marketData, outcomeName: outcome.name }; } catch (error) { if (error instanceof AppError) { error.log(); return { success: false, error: error.message }; } const appError = new AppError({ message: "Failed to create prediction", context: "prediction-store", code: "PREDICTION_CREATE_ERROR", originalError: error instanceof Error ? error : new Error(String(error)), data: { marketId: data.marketId, userId: data.userId, amount: data.amount } }).log(); return { success: false, error: appError.message }; } }, // Delete a prediction and its associated NFT receipt async deletePrediction(predictionId) { try { if (!predictionId) return false; const prediction = await this.getPrediction(predictionId); if (!prediction) return false; await deleteEntity("PREDICTION", predictionId); await removeFromSet("USER_PREDICTIONS", prediction.userId, predictionId); await removeFromSet("MARKET_PREDICTIONS", prediction.marketId, predictionId); if (prediction.nftReceipt?.id) { await deleteEntity("PREDICTION_NFT", prediction.nftReceipt.id); } return true; } catch (error) { console.error(`Error deleting prediction ${predictionId}:`, error); return false; } }, /** * Update a prediction with new data */ async updatePrediction(predictionId, data) { try { const prediction = await this.getPrediction(predictionId); if (!prediction) { return null; } const updatedPrediction = { ...prediction, ...data }; await storeEntity("PREDICTION", predictionId, updatedPrediction); return updatedPrediction; } catch (error) { console.error(`Error updating prediction ${predictionId}:`, error); return null; } }, /** * Validate if a prediction is eligible for redemption * * @param predictionId ID of the prediction * @param userId User attempting to redeem * @returns Validation result with prediction if successful */ async validateRedemptionEligibility(predictionId, userId) { try { const prediction = await this.getPrediction(predictionId); if (!prediction) { return { success: false, error: "Prediction not found" }; } const userIsAdmin = isAdmin(userId); if (prediction.userId !== userId && !userIsAdmin) { return { success: false, error: "Unauthorized: This prediction does not belong to you" }; } if (prediction.status === "redeemed") { return { success: false, error: "Prediction has already been redeemed" }; } if (prediction.status !== "won" && prediction.status !== "lost") { return { success: false, error: "Prediction is not eligible for redemption yet" }; } return { success: true, prediction, isAdmin: userIsAdmin }; } catch (error) { console.error(`Error validating redemption eligibility for prediction ${predictionId}:`, error); return { success: false, error: "Failed to validate prediction redemption eligibility" }; } }, /** * Redeem a prediction with balance update * This function handles the complete process of: * 1. Validating the prediction is eligible for redemption * 2. Updating the prediction status * 3. Updating the user's balance * * @param predictionId The ID of the prediction to redeem * @param userId The ID of the user trying to redeem * @returns Object with redemption result */ async redeemPredictionWithBalanceUpdate(predictionId, userId) { try { const validationResult = await this.validateRedemptionEligibility(predictionId, userId); if (!validationResult.success) { return { success: false, error: validationResult.error }; } const prediction = validationResult.prediction; const payout = prediction.status === "won" ? prediction.potentialPayout || 0 : 0; const updatedPrediction = await this.updatePrediction(predictionId, { status: "redeemed", redeemedAt: (/* @__PURE__ */ new Date()).toISOString() }); if (!updatedPrediction) { return { success: false, error: "Failed to update prediction" }; } if (payout > 0) { await userBalanceStore.updateBalanceForResolvedPrediction( userId, prediction.amount, payout ); } else { await userBalanceStore.updateBalanceForResolvedPrediction( userId, prediction.amount, 0 ); } return { success: true, prediction: updatedPrediction, payout }; } catch (error) { console.error(`Error redeeming prediction ${predictionId}:`, error); return { success: false, error: "Failed to redeem prediction" }; } }, /** * Redeem a prediction after market resolution * This is kept for backward compatibility but we recommend using * redeemPredictionWithBalanceUpdate for new code */ async redeemPrediction(predictionId) { try { const prediction = await this.getPrediction(predictionId); if (!prediction) { return { prediction: null, payout: 0 }; } if (prediction.status === "redeemed") { return { prediction, payout: 0 }; } if (prediction.status !== "won" && prediction.status !== "lost") { return { prediction, payout: 0 }; } const payout = prediction.status === "won" ? prediction.potentialPayout || 0 : 0; const updatedPrediction = await this.updatePrediction(predictionId, { status: "redeemed", redeemedAt: (/* @__PURE__ */ new Date()).toISOString() }); return { prediction: updatedPrediction, payout }; } catch (error) { console.error(`Error redeeming prediction ${predictionId}:`, error); return { prediction: null, payout: 0 }; } }, /** * Get all predictions for a specific market with a specific status */ async getMarketPredictionsByStatus(marketId, status) { try { const predictions = await this.getMarketPredictions(marketId); return predictions.filter((p) => p.status === status); } catch (error) { console.error(`Error getting market predictions by status for market ${marketId}:`, error); return []; } } }; export { predictionStore }; //# sourceMappingURL=chunk-I6ML34A4.js.map //# sourceMappingURL=chunk-I6ML34A4.js.map