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