This commit is contained in:
Rytek Digital Inc
2025-07-13 10:32:45 -03:00
parent b21b2c1a00
commit a0d4769741
35 changed files with 1070 additions and 161765 deletions

BIN
.DS_Store vendored Normal file

Binary file not shown.

View File

@@ -1460,9 +1460,8 @@ Parse.Cloud.job('generateDailyNPC', async (request) => {
}
});
// Export the functions for testing
// Export the functions for testing (without duplicating the cloud job)
module.exports = {
generateDailyNPC: Parse.Cloud.job('generateDailyNPC'),
generateSocialPost,
generateNPCContent,
generateNPCImage

View File

@@ -0,0 +1,388 @@
const axios = require('axios');
const fs = require('fs').promises;
const path = require('path');
const sharp = require('sharp');
const chalk = require('chalk');
// Simple logger with colors and emojis
const logger = {
info: (msg) => console.log(chalk.blue(' INFO:'), msg),
error: (msg) => console.log(chalk.red('❌ ERROR:'), msg),
warn: (msg) => console.log(chalk.yellow('⚠️ WARN:'), msg),
progress: (msg) => console.log(chalk.green('🔄 PROGRESS:'), msg),
social: (msg) => console.log(chalk.magenta('🐦 SOCIAL:'), msg),
success: (msg) => console.log(chalk.green('✅ SUCCESS:'), msg),
image: (msg) => console.log(chalk.yellow('🖼️ IMAGE:'), msg),
webhook: (msg) => console.log(chalk.magenta('🔗 WEBHOOK:'), msg)
};
// Use the appropriate server URL based on environment
const getServerBaseUrl = () => {
const env = process.env.NODE_ENV || 'development';
return env === 'prod' ? process.env.serverURLProd : process.env.serverURL;
};
/**
* Recent AI-Generated Items Grid Generator
*
* This job creates a grid image of recent AI-generated IOSC items
* and posts it to social media via IFTTT.
*/
/**
* Get recent AI-generated items from the IOSC class
* @param {number} limit - Number of items to retrieve
* @returns {Promise<Array>} Array of recent AI-generated items
*/
async function getRecentAIGeneratedItems(limit = 12) {
logger.progress('Querying recent AI-generated items...');
try {
const IOSC = Parse.Object.extend("IOSC");
const query = new Parse.Query(IOSC);
// Filter for AI-generated items created in the last 7 days
query.equalTo("aiGenerated", true);
const sevenDaysAgo = new Date();
sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
query.greaterThan("createdAt", sevenDaysAgo);
// Include required fields
query.select("name", "description", "thumbnail_uri", "type", "links");
// Sort by creation date, newest first
query.descending("createdAt");
// Limit to specified number of items
query.limit(limit);
const results = await query.find({ useMasterKey: true });
logger.info(`Found ${results.length} recent AI-generated items`);
// Return items with required data
return results.map(item => ({
id: item.id,
name: item.get("name"),
description: item.get("description"),
thumbnail_uri: item.get("thumbnail_uri"),
type: item.get("type"),
links: item.get("links") || {}
}));
} catch (error) {
logger.error(`Error querying recent AI-generated items: ${error.message}`);
throw error;
}
}
/**
* Download an image from a URL
* @param {string} url - The URL of the image to download
* @returns {Promise<Buffer>} The image buffer
*/
async function downloadImage(url) {
try {
const response = await axios.get(url, { responseType: 'arraybuffer' });
return Buffer.from(response.data);
} catch (error) {
logger.error(`Error downloading image from ${url}: ${error.message}`);
throw error;
}
}
/**
* Creates a grid image of thumbnails
* @param {Array} items - Array of item objects with thumbnail_uri
* @returns {Promise<Buffer>} The grid image buffer
*/
async function createGridImage(items) {
logger.progress('Creating grid image...');
try {
// Calculate grid dimensions
const count = items.length;
const columns = count <= 3 ? count : Math.min(5, Math.ceil(Math.sqrt(count)));
const rows = Math.ceil(count / columns);
// Download all images
const imageBuffers = [];
for (const item of items) {
logger.progress(`Downloading image for ${item.name}...`);
const imageBuffer = await downloadImage(item.thumbnail_uri);
imageBuffers.push(imageBuffer);
}
// Process all images to consistent size - make them smaller to reduce final image size
const processedImages = await Promise.all(
imageBuffers.map(buffer =>
sharp(buffer)
.resize(256, 256, { fit: 'inside' }) // Reduced from 512 to 256
.toBuffer()
)
);
// Calculate grid dimensions
const tileSize = 256; // Reduced from 512 to 256
const gridWidth = columns * tileSize;
const gridHeight = rows * tileSize + 80; // Add extra height for title
// Create an empty canvas for the grid
const gridCanvas = sharp({
create: {
width: gridWidth,
height: gridHeight,
channels: 4,
background: { r: 20, g: 20, b: 40, alpha: 1 }
}
});
// Position each image in the grid
const compositeOperations = [];
for (let i = 0; i < processedImages.length; i++) {
const row = Math.floor(i / columns);
const col = i % columns;
const left = col * tileSize;
const top = row * tileSize;
compositeOperations.push({
input: processedImages[i],
left,
top
});
}
// Add a title to the grid
const svgTitle = `
<svg width="${gridWidth}" height="60">
<style>
.title { font: bold 32px sans-serif; fill: white; text-anchor: middle; }
</style>
<text x="${gridWidth/2}" y="35" class="title">Recent AI-Generated Discoveries</text>
</svg>
`;
// Add title to the composite operations
compositeOperations.push({
input: Buffer.from(svgTitle),
top: gridHeight - 60,
left: 0
});
// Create the grid image with higher compression for smaller file size
const gridImage = await gridCanvas
.composite(compositeOperations)
.jpeg({ quality: 70, progressive: true }) // Reduced quality to 70
.toBuffer();
logger.success('Grid image created successfully');
return gridImage;
} catch (error) {
logger.error(`Error creating grid image: ${error.message}`);
throw error;
}
}
/**
* Save the grid image to Parse in the ADS class
* @param {Buffer} imageBuffer - The grid image buffer
* @returns {Promise<Object>} The saved Parse object with image URL
*/
async function saveImageToADS(imageBuffer) {
try {
logger.progress('Saving grid image to ADS class...');
// Create a unique filename with timestamp
const filename = `ai_grid_${Date.now()}.jpg`;
// Create a Parse file with the image buffer
const parseFile = new Parse.File(filename, { base64: imageBuffer.toString('base64') });
// Save the file with master key
await parseFile.save({ useMasterKey: true });
// Create a new ADS object to store the image
const ADS = Parse.Object.extend("ADS");
const adsObject = new ADS();
// Set properties for the ADS object
adsObject.set("image", parseFile);
adsObject.set("type", "ai_grid");
adsObject.set("title", "Recent AI-Generated Items");
adsObject.set("description", "Grid of recent AI-generated items");
adsObject.set("active", true);
adsObject.set("isPublic", true);
// Set ACL
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
acl.setPublicWriteAccess(false);
adsObject.setACL(acl);
// Save the ADS object
const savedObject = await adsObject.save(null, { useMasterKey: true });
// Get the image URL
const imageUrl = parseFile.url();
logger.success(`Image saved to ADS class with ID: ${savedObject.id}`);
return {
objectId: savedObject.id,
imageUrl: imageUrl
};
} catch (error) {
logger.error(`Error saving image to ADS class: ${error.message}`);
throw error;
}
}
/**
* Post to social media via IFTTT using an image URL
* @param {string} socialPost - The social media post text
* @param {string} imageUrl - The URL of the image
* @returns {Promise<boolean>}
*/
async function postToIFTTT(socialPost, imageUrl) {
try {
logger.progress('Posting to IFTTT...');
// Wait a moment to ensure the image is accessible
await new Promise(resolve => setTimeout(resolve, 2000));
// Create webhook URL
const webhookUrl = 'https://maker.ifttt.com/trigger/New IOSC Item/with/key/dRrZJZ198HmU6ABVwlUvEb';
// Post to IFTTT
await axios.post(webhookUrl, {
value1: socialPost,
value2: imageUrl
}, {
headers: {
'Content-Type': 'application/json'
}
});
logger.social('IFTTT webhook triggered successfully');
return true;
} catch (error) {
logger.error(`Failed to post to IFTTT: ${error.message}`);
// Attempt a text-only fallback
try {
logger.progress('Attempting text-only fallback...');
// Post to IFTTT with just the text
await axios.post('https://maker.ifttt.com/trigger/New IOSC Item/with/key/dRrZJZ198HmU6ABVwlUvEb', {
value1: `${socialPost}\n\nImage failed to upload, but you can find the latest AI-generated items on the map!`,
value2: "" // No image
}, {
headers: {
'Content-Type': 'application/json'
}
});
logger.social('IFTTT webhook triggered with text-only fallback');
return true;
} catch (fallbackError) {
logger.error(`Failed to post fallback message: ${fallbackError.message}`);
throw error; // Throw the original error
}
}
}
/**
* Generate a social media post for the recent items - lore focused, no mention of AI
* @param {Array} items - The array of recent items
* @returns {Promise<string>} The generated social media post
*/
async function generateSocialPost(items) {
try {
// Extract item types for the post
const itemTypes = items.map(item => item.type || 'item');
// Count frequency of types
const typeCounts = {};
itemTypes.forEach(type => {
typeCounts[type] = (typeCounts[type] || 0) + 1;
});
// Create a lore-based message
const message = `🔮 New Discoveries on the Isle of Seven Cities! 🏝️
Ancient treasures have emerged from the mists of Dracattus. Explorers report findings of mysterious artifacts and encounters with local inhabitants.
⏰ Chart your course before they vanish again: https://dracattus.com/map #Dracattus #LimitedTime`;
// Verify character count and trim if needed
if (message.length <= 280) {
return message;
} else {
// Shorter fallback that fits in 280 chars
return `🔮 New Discoveries on the Isle of Seven Cities! 🏝️ Ancient treasures have emerged from the mists of Dracattus. Chart your course before they vanish again: https://dracattus.com/map #Dracattus #LimitedTime`;
}
} catch (error) {
logger.error(`Error generating social post: ${error.message}`);
return `🔮 New Discoveries on the Isle of Seven Cities! Ancient treasures await on Dracattus. Chart your course before they vanish: https://dracattus.com/map #Dracattus #LimitedTime`;
}
}
/**
* Main Cloud Job: Creates a grid of recent AI-generated items and posts to social media
*/
Parse.Cloud.job('generateRecentAIItemsGrid', async (request) => {
try {
logger.info('🚀 Starting Recent AI Items Grid generation...');
// Get the limit parameter or use default of 12
const limit = request.params && request.params.limit ? Number(request.params.limit) : 12;
// Get recent AI-generated items
const items = await getRecentAIGeneratedItems(limit);
if (items.length === 0) {
logger.warn('No recent AI-generated items found. Aborting job.');
return {
success: false,
message: 'No recent AI-generated items found'
};
}
// Create grid image
const gridImageBuffer = await createGridImage(items);
logger.image('🖼️ Grid image created');
// Save image to ADS class
const savedImage = await saveImageToADS(gridImageBuffer);
logger.success(`Image saved to ADS class with URL: ${savedImage.imageUrl}`);
// Generate social media post
const socialPost = await generateSocialPost(items);
logger.social('📝 Social media post generated');
// Post to social media
logger.webhook('📤 Posting to social media...');
await postToIFTTT(socialPost, savedImage.imageUrl);
logger.social('✅ Social media post complete');
logger.success(`✨ Recent AI Items Grid generation complete with ${items.length} items`);
return {
success: true,
itemCount: items.length,
adsObjectId: savedImage.objectId,
imageUrl: savedImage.imageUrl,
socialPost: socialPost
};
} catch (error) {
logger.error(`Recent AI Items Grid generation failed: ${error.message}`);
logger.error(`Stack trace: ${error.stack}`);
throw error;
}
});
// Export the functions for testing
module.exports = {
getRecentAIGeneratedItems,
createGridImage,
generateSocialPost,
saveImageToADS
};

View File

@@ -1,340 +0,0 @@
const { createCanvas, loadImage } = require('canvas');
const fs = require('fs').promises;
const path = require('path');
// Reuse logging utilities from breedAssets
const log = {
section: (title) => console.log(`\n🔸 ${'-'.repeat(20)} ${title} ${'-'.repeat(20)}`),
info: (msg) => console.log(' INFO:', msg),
success: (msg) => console.log('✅ SUCCESS:', msg),
warning: (msg) => console.log('⚠️ WARNING:', msg),
error: (msg) => console.error('❌ ERROR:', msg),
debug: (msg) => console.log('🔍 DEBUG:', msg),
trait: (msg) => console.log('🧬 TRAIT:', msg),
stats: (msg) => console.log('📊 STATS:', msg),
image: (msg) => console.log('🖼️ IMAGE:', msg)
};
// Reuse folder mapping from breedAssets
const FOLDER_NAMES = {
'background': 'Background',
'base': 'Base',
'wings': 'Wings',
'fur': 'Fur',
'lowerbody': 'Lower Body',
'tailbase': 'Tail Base',
'upperbody': 'Upper Body',
'belly': 'Belly',
'tailtip': 'Tail Tip',
'claws': 'Claws',
'ears': 'Ears',
'eyes': 'Eyes',
'facefeatures': 'Face Features',
'horns': 'Horns',
'mouthfeatures': 'Mouth Features',
};
// Reuse file name normalization from breedAssets
function normalizeFileName(value) {
if (!value) return value;
const replacements = {
'and': '&',
'with': 'w',
'spots': 'Spots',
'stripes': 'Stripes',
'spikes': 'Spikes',
'cave': 'Cave'
};
let fileName = `${value}.png`;
let spaced = value.replace(/([A-Z])/g, ' $1').trim();
let spacedFile = `${spaced}.png`;
let withAnd = value.replace(/[aA]nd/g, '&');
let andFile = `${withAnd}.png`;
return [fileName, spacedFile, andFile];
}
// Reuse layer file finder from breedAssets
async function findLayerFile(folderPath, traitValue) {
const variations = normalizeFileName(traitValue);
for (const fileName of variations) {
try {
const filePath = path.join(folderPath, fileName);
await fs.access(filePath);
return filePath;
} catch (e) {
continue;
}
}
throw new Error('ENOENT');
}
// Function to get random trait from a folder
async function getRandomTrait(folderPath) {
const files = await fs.readdir(folderPath);
const pngFiles = files.filter(file => file.endsWith('.png'));
const randomFile = pngFiles[Math.floor(Math.random() * pngFiles.length)];
return randomFile.replace('.png', '');
}
Parse.Cloud.define("createNewDrac", async (request) => {
const {name, type, primaryTraits, increaseMutationsChance, wallet, isMainnet: forcedMainnetFlag} = request.params
//// if name has text then + 1000000000000 mojos
//// if type is 'primal' value then + 2000000000000 mojos
//// primaryTraits is just key words for later - ignore for now
//// if increaseMutationsChance = true then + 2000000000000 mojos
try {
// Calculate requested mojos based on parameters
let mojos = 22222000000000; // Base amount
if (name && name.trim().length > 0) {
mojos += 1111000000000; // Add 1T mojos if name is provided
log.info(`Adding 1T mojos for custom name: ${name}`);
}
if (type === 'primal') {
mojos += 2000000000000; // Add 2T mojos for primal type
log.info(`Adding 2T mojos for primal type`);
}
if (increaseMutationsChance === true) {
mojos += 111000000000; // Add 2T mojos for increased mutations
log.info(`Adding 2T mojos for increased mutations chance`);
}
const requested_mojos = mojos.toString();
const targetWallet = wallet;
if (!targetWallet) {
throw new Error('wallet parameter is required');
}
log.section('Starting New Dracattus Creation Job');
console.log('\n💰 💰 💰 ------------------- WALLET ADDRESS ------------------- 💰 💰 💰');
console.log(`💼 TARGET WALLET: ${targetWallet}`);
console.log('💰 💰 💰 -------------------------------------------------------- 💰 💰 💰\n');
log.info(`Requested Mojos: ${requested_mojos}`);
const LAYERS_PATH = path.join(__dirname, '../../layers');
// Define the trait layers in order
const traitLayers = Object.keys(FOLDER_NAMES);
log.info('Layer order for rendering:');
traitLayers.forEach((trait, index) => {
log.debug(`${index + 1}. ${trait} -> ${FOLDER_NAMES[trait]}`);
});
// Generate random traits
const traits = {};
log.section('Generating Random Traits');
for (const trait of traitLayers) {
const folderPath = path.join(LAYERS_PATH, FOLDER_NAMES[trait]);
try {
traits[trait] = await getRandomTrait(folderPath);
log.trait(`Generated ${trait}: ${traits[trait]}`);
} catch (error) {
log.warning(`Could not generate trait for ${trait}: ${error.message}`);
}
}
// Generate random stats
const stats = {
attack: Math.floor(Math.random() * 100),
defence: Math.floor(Math.random() * 100),
agility: Math.floor(Math.random() * 100),
energy: Math.floor(Math.random() * 100),
magic: Math.floor(Math.random() * 100),
skill: Math.floor(Math.random() * 100)
};
log.section('Generated Stats');
Object.entries(stats).forEach(([stat, value]) => {
log.stats(`${stat}: ${value}`);
});
// Generate random location in Italy
const ITALY_BOUNDS = {
north: 47.1153931748,
south: 36.6902890859,
east: 18.4802470232,
west: 6.7499552751
};
const location = new Parse.GeoPoint(
ITALY_BOUNDS.south + (Math.random() * (ITALY_BOUNDS.north - ITALY_BOUNDS.south)),
ITALY_BOUNDS.west + (Math.random() * (ITALY_BOUNDS.east - ITALY_BOUNDS.west))
);
log.section('Image Generation');
// Create and compile the new image
const CANVAS_WIDTH = 883;
const CANVAS_HEIGHT = 1080;
const canvas = createCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
let successfulLayerDraws = 0;
let failedLayers = [];
// Load and draw each layer
for (const trait of traitLayers) {
const traitValue = traits[trait];
if (traitValue) {
const folderName = FOLDER_NAMES[trait];
const folderPath = path.join(LAYERS_PATH, folderName);
try {
const imagePath = await findLayerFile(folderPath, traitValue);
const img = await loadImage(imagePath);
ctx.drawImage(img, 0, 0, CANVAS_WIDTH, CANVAS_HEIGHT);
successfulLayerDraws++;
log.success(`Drew layer ${trait}`);
} catch (error) {
log.warning(`Failed to draw ${trait}: ${error.message}`);
failedLayers.push({ trait, value: traitValue });
}
}
}
log.section('Saving New Dracattus');
// Save the compiled image
const buffer = canvas.toBuffer('image/png');
const imageFile = new Parse.File('newDracattus.png', { base64: buffer.toString('base64') });
await imageFile.save({ useMasterKey: true });
// Get the latest edition number
const Dracattus = Parse.Object.extend("DRACATTUS");
const editionQuery = new Parse.Query(Dracattus);
editionQuery.equalTo('taxonomy', 'Dracattus');
editionQuery.descending('edition');
editionQuery.limit(1);
const lastDracattus = await editionQuery.first({ useMasterKey: true });
const newEdition = (lastDracattus ? lastDracattus.get('edition') : 0) + 1;
// Create and save new Dracattus record
const newDracattus = new Dracattus();
// Use custom name if provided, otherwise use default
const dracName = name && name.trim().length > 0 ?
name :
`Dracattus`;
newDracattus.set('name', dracName);
newDracattus.set('taxonomy', 'Dracattus');
newDracattus.set('edition', 2);
newDracattus.set('location', location);
newDracattus.set('thumbnail_uri', imageFile.url());
// Set type if provided
if (type) {
newDracattus.set('type', type);
}
// Set primary traits if provided
if (primaryTraits && Array.isArray(primaryTraits)) {
newDracattus.set('primaryTraits', primaryTraits);
}
// Set all traits
Object.entries(traits).forEach(([trait, value]) => {
newDracattus.set(trait, value);
});
// Set all stats
Object.entries(stats).forEach(([stat, value]) => {
newDracattus.set(stat, value);
});
// Set requested mojos
newDracattus.set('requested_mojos', parseInt(requested_mojos, 10));
// Set initial mint status
newDracattus.set('mint_status', 'pending');
newDracattus.set('target_wallet', targetWallet);
await newDracattus.save(null, { useMasterKey: true });
log.success(`Successfully created Dracattus: ${dracName}`);
// Mint the new Dracattus with provided parameters
log.section('Minting New Dracattus');
try {
// Get the mainnet configuration from Parse.Config
const config = await Parse.Config.get({ useMasterKey: true });
// Use the explicit isMainnet parameter if provided, otherwise fall back to config
const useMainnet = forcedMainnetFlag !== undefined ?
!!forcedMainnetFlag :
config.get('mintNewDracsOnMainnet') || false;
log.info(`Minting on ${useMainnet ? 'MAINNET' : 'TESTNET'} (${forcedMainnetFlag !== undefined ? 'flag provided' : 'using config default'})`);
const mintParams = {
objectId: newDracattus.id,
targetWallet,
requested_mojos,
isMainnet: useMainnet
};
const mintResult = await Parse.Cloud.run('mintAsset',
mintParams,
);
log.success(`Successfully minted Dracattus with NFT ID: ${mintResult.data.nft_id}`);
// Update the Dracattus object with mint data
newDracattus.set('nft_id', mintResult.data.nft_id);
newDracattus.set('data_hash', mintResult.hashes.dataHash);
newDracattus.set('metadata_hash', mintResult.hashes.metadataHash);
newDracattus.set('mint_status', 'success');
newDracattus.set('mint_timestamp', new Date());
newDracattus.set('collection_id', "a2564ae6-3893-4919-aa56-13e5c702460b");
newDracattus.set('collection_name',"Dracattus")
// Save additional mint data if available
if (mintResult.data.coin_id) {
newDracattus.set('coin_id', mintResult.data.coin_id);
}
if (mintResult.data.transaction_id) {
newDracattus.set('transaction_id', mintResult.data.transaction_id);
}
// Save the updated Dracattus
await newDracattus.save(null, { useMasterKey: true });
log.success('Updated Dracattus with mint data');
// Return both creation and mint results
return {
success: true,
message: 'Successfully created and minted new Dracattus',
dracattus: {
id: newDracattus.id,
name: newDracattus.get('name'),
traits,
stats,
location,
image_url: imageFile.url(),
nft_id: mintResult.data.nft_id,
data_hash: mintResult.hashes.dataHash,
metadata_hash: mintResult.hashes.metadataHash,
requested_mojos: parseInt(requested_mojos, 10)
},
mint: mintResult
};
} catch (mintError) {
// Update Dracattus with mint failure
newDracattus.set('mint_status', 'failed');
newDracattus.set('mint_error', mintError.message);
newDracattus.set('mint_timestamp', new Date());
await newDracattus.save(null, { useMasterKey: true });
log.error(`Failed to mint Dracattus: ${mintError.message}`);
throw new Error(`Minting failed: ${mintError.message}`);
}
} catch (error) {
log.error(`Job failed: ${error.message}`);
throw new Error(`Failed to create/mint Dracattus: ${error.message}`);
}
});

View File

@@ -1,19 +0,0 @@
Parse.Cloud.job("getDracattusSchema", async (request) => {
try {
const schema = await new Parse.Schema("DRACATTUS").get();
console.log('DRACATTUS Schema:', schema);
// Save to file
const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, 'dracattus-schema.json');
await fs.writeFile(filePath, JSON.stringify(schema, null, 2));
console.log(`Schema saved to: ${filePath}`);
return schema;
} catch (error) {
console.error('Error retrieving DRACATTUS schema:', error);
throw error;
}
});

View File

@@ -1,85 +0,0 @@
{
"className": "BATTLES",
"fields": {
"objectId": {
"type": "String"
},
"createdAt": {
"type": "Date"
},
"updatedAt": {
"type": "Date"
},
"ACL": {
"type": "ACL"
},
"battleSequence": {
"type": "Array"
},
"battleTimestamp": {
"type": "Date"
},
"winningFactors": {
"type": "Array"
},
"environmentalFactors": {
"type": "Object"
},
"winner": {
"type": "Pointer",
"targetClass": "DRACATTUS"
},
"drac2": {
"type": "Pointer",
"targetClass": "DRACATTUS"
},
"loserFinalHealth": {
"type": "Number"
},
"loser": {
"type": "Pointer",
"targetClass": "DRACATTUS"
},
"bloodStoneDetails": {
"type": "Object"
},
"drac1": {
"type": "Pointer",
"targetClass": "DRACATTUS"
},
"winnerFinalHealth": {
"type": "Number"
}
},
"classLevelPermissions": {
"find": {
"*": true
},
"count": {
"*": true
},
"get": {
"*": true
},
"create": {
"*": true
},
"update": {
"*": true
},
"delete": {
"*": true
},
"addField": {
"*": true
},
"protectedFields": {
"*": []
}
},
"indexes": {
"_id_": {
"_id": 1
}
}
}

View File

@@ -1,119 +0,0 @@
{
"className": "IOSC",
"fields": {
"objectId": {
"type": "String"
},
"createdAt": {
"type": "Date"
},
"updatedAt": {
"type": "Date"
},
"ACL": {
"type": "ACL"
},
"location": {
"type": "GeoPoint"
},
"iconSize": {
"type": "Array"
},
"name": {
"type": "String"
},
"type": {
"type": "String"
},
"discovered": {
"type": "Boolean"
},
"description": {
"type": "String"
},
"image": {
"type": "File"
},
"preview": {
"type": "File"
},
"thumbnail_uri": {
"type": "String",
"required": false
},
"coreItem": {
"type": "Boolean",
"required": false
},
"links": {
"type": "Object",
"required": false
},
"canMint": {
"type": "Boolean",
"required": false,
"defaultValue": false
},
"requested_mojos": {
"type": "String",
"required": false,
"defaultValue": ""
},
"requested_asset_id": {
"type": "String",
"required": false
},
"canQuest": {
"type": "Boolean",
"required": false
},
"quests": {
"type": "Pointer",
"targetClass": "QUESTS",
"required": false
},
"hasDialog": {
"type": "Boolean",
"required": false
},
"dialog": {
"type": "Pointer",
"targetClass": "DIALOG",
"required": false
}
},
"classLevelPermissions": {
"find": {
"*": true
},
"count": {
"*": true
},
"get": {
"*": true
},
"create": {
"*": true
},
"update": {
"*": true
},
"delete": {
"*": true
},
"addField": {
"*": true
},
"protectedFields": {
"*": []
}
},
"indexes": {
"_id_": {
"_id": 1
},
"location_2d": {
"location": "2d"
}
}
}

View File

@@ -1,140 +0,0 @@
{
"className": "QUESTS",
"fields": {
"objectId": {
"type": "String"
},
"createdAt": {
"type": "Date"
},
"updatedAt": {
"type": "Date"
},
"ACL": {
"type": "ACL"
},
"name": {
"type": "String",
"required": false,
"defaultValue": "unknown"
},
"description": {
"type": "String",
"required": false,
"defaultValue": "unknown"
},
"location": {
"type": "GeoPoint",
"required": false
},
"dialog": {
"type": "Object",
"required": false
},
"reward": {
"type": "String",
"required": false
},
"imageUrl": {
"type": "String"
},
"properties": {
"type": "Array"
},
"lore": {
"type": "String"
},
"origin": {
"type": "String"
},
"placeName": {
"type": "String"
},
"rarity": {
"type": "String"
},
"nftMinted": {
"type": "Boolean"
},
"biome": {
"type": "String"
},
"type": {
"type": "String"
},
"nftTokenId": {
"type": "String"
},
"nftContract": {
"type": "String"
},
"image": {
"type": "File"
},
"title": {
"type": "String"
},
"details": {
"type": "Object"
},
"summary": {
"type": "String"
},
"questChainId": {
"type": "String"
},
"segmentNumber": {
"type": "Number"
},
"isComplete": {
"type": "Boolean"
},
"totalSegments": {
"type": "Number"
},
"difficulty": {
"type": "String"
},
"isFinalSegment": {
"type": "Boolean"
},
"socialPost": {
"type": "String"
},
"previousSegment": {
"type": "Pointer",
"targetClass": "QUESTS"
}
},
"classLevelPermissions": {
"find": {
"*": true
},
"count": {
"*": true
},
"get": {
"*": true
},
"create": {
"*": true
},
"update": {
"*": true
},
"delete": {
"*": true
},
"addField": {
"*": true
},
"protectedFields": {
"*": []
}
},
"indexes": {
"_id_": {
"_id": 1
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,316 +0,0 @@
{
"className": "DRACATTUS",
"fields": {
"objectId": {
"type": "String"
},
"createdAt": {
"type": "Date"
},
"updatedAt": {
"type": "Date"
},
"ACL": {
"type": "ACL"
},
"attributes": {
"type": "Array"
},
"encoded_id": {
"type": "String"
},
"thumbnail_uri": {
"type": "String"
},
"nft_id": {
"type": "String"
},
"description": {
"type": "String"
},
"name": {
"type": "String"
},
"edition_total": {
"type": "Number"
},
"collection_id": {
"type": "String"
},
"collection_sensitive_content": {
"type": "Boolean"
},
"collection_blocked_content": {
"type": "Boolean"
},
"creator_address_encoded_id": {
"type": "String"
},
"timestamp": {
"type": "String"
},
"minted_at": {
"type": "String"
},
"owner_address_encoded_id": {
"type": "String"
},
"creator_id": {
"type": "String"
},
"updated_at": {
"type": "String"
},
"data_type": {
"type": "Number"
},
"creator_avatar_uri": {
"type": "String"
},
"creator_name": {
"type": "String"
},
"creator_verification_state": {
"type": "Number"
},
"collection_name": {
"type": "String"
},
"sensitive_content": {
"type": "Boolean"
},
"edition_number": {
"type": "Number"
},
"creator_encoded_id": {
"type": "String"
},
"chain": {
"type": "String"
},
"base": {
"type": "String"
},
"fur": {
"type": "String"
},
"wings": {
"type": "String"
},
"belly": {
"type": "String"
},
"background": {
"type": "String"
},
"ears": {
"type": "String"
},
"claws": {
"type": "String"
},
"eyes": {
"type": "String"
},
"horns": {
"type": "String"
},
"apathy": {
"type": "Number"
},
"attack": {
"type": "Number"
},
"agility": {
"type": "Number"
},
"defence": {
"type": "Number"
},
"energy": {
"type": "Number"
},
"taxonomy": {
"type": "String"
},
"series": {
"type": "Number"
},
"edition": {
"type": "Number"
},
"skill": {
"type": "Number"
},
"compatibility": {
"type": "Number"
},
"token_id": {
"type": "String"
},
"price": {
"type": "Number"
},
"token_code": {
"type": "String"
},
"owner_avatar_uri": {
"type": "String"
},
"owner_encoded_id": {
"type": "String"
},
"owner_id": {
"type": "String"
},
"owner_verification_state": {
"type": "String"
},
"owner_name": {
"type": "String"
},
"origin": {
"type": "String"
},
"material": {
"type": "String"
},
"magic": {
"type": "Number"
},
"qty": {
"type": "Number"
},
"type": {
"type": "String"
},
"location": {
"type": "GeoPoint"
},
"royaltySplitData": {
"type": "Object"
},
"parent1": {
"type": "Pointer",
"targetClass": "DRACATTUS"
},
"parent2": {
"type": "Pointer",
"targetClass": "DRACATTUS"
},
"image": {
"type": "File",
"required": false,
"defaultValue": {
"__type": "File",
"name": "3b2317fd8a9e0dfacbdfad0e96b9e613_default-marker.png",
"url": "https://apps.koba42.com/api/files/Koba42/3b2317fd8a9e0dfacbdfad0e96b9e613_default-marker.png"
}
},
"battleHistory": {
"type": "Array"
},
"victories": {
"type": "Number"
},
"defeats": {
"type": "Number"
},
"health": {
"type": "Number"
},
"bearing": {
"type": "Number"
},
"lowerbody": {
"type": "String"
},
"upperbody": {
"type": "String"
},
"tailtip": {
"type": "String"
},
"tailbase": {
"type": "String"
},
"facefeatures": {
"type": "String"
},
"mouthfeatures": {
"type": "String"
},
"last_rewards_update": {
"type": "Date"
},
"mint_timestamp": {
"type": "Date"
},
"data_hash": {
"type": "String"
},
"metadata_hash": {
"type": "String"
},
"requested_mojos": {
"type": "Number"
},
"target_wallet": {
"type": "String"
},
"mint_status": {
"type": "String"
},
"mint_error": {
"type": "String"
}
},
"classLevelPermissions": {
"find": {
"*": true,
"requiresAuthentication": true,
"role:Admin": true
},
"count": {
"*": true,
"requiresAuthentication": true,
"role:Admin": true
},
"get": {
"*": true,
"requiresAuthentication": true,
"role:Admin": true
},
"create": {
"*": true,
"role:Admin": true,
"requiresAuthentication": true
},
"update": {
"*": true,
"role:Admin": true,
"requiresAuthentication": true
},
"delete": {
"*": true,
"role:Admin": true,
"requiresAuthentication": true
},
"addField": {
"role:Admin": true,
"*": true,
"requiresAuthentication": true
},
"protectedFields": {
"*": []
}
},
"indexes": {
"_id_": {
"_id": 1
},
"location_2d": {
"location": "2d"
}
}
}

View File

@@ -1,219 +0,0 @@
const fs = require('fs');
const fsPromises = require('fs').promises;
const path = require('path');
const PDFDocument = require('pdfkit');
const Chart = require('chart.js/auto');
const { createCanvas } = require('canvas');
Parse.Cloud.job("generateDracattusPDFReport", async (request) => {
const { message } = request;
message('Starting PDF report generation...');
try {
// Read the JSON report
const reportPath = path.join(__dirname, 'dracattus-rarity-report.json');
message(`Reading JSON report from: ${reportPath}`);
if (!fs.existsSync(reportPath)) {
throw new Error('Rarity report JSON file not found. Please run generateDracattusReport first.');
}
const reportData = JSON.parse(await fsPromises.readFile(reportPath, 'utf-8'));
message('Successfully loaded JSON report data');
// Create reports directory if it doesn't exist
const reportsDir = path.join(__dirname, 'reports');
await fsPromises.mkdir(reportsDir, { recursive: true });
// Create PDF document
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const pdfPath = path.join(reportsDir, `dracattus-rarity-report-${timestamp}.pdf`);
return new Promise((resolve, reject) => {
// Create the PDF document
const doc = new PDFDocument({
size: 'A4',
layout: 'landscape',
margin: 50
});
// Create write stream
const stream = fs.createWriteStream(pdfPath);
// Pipe PDF to write stream
doc.pipe(stream);
// Error handling
stream.on('error', reject);
doc.on('error', reject);
// Title page
doc.rect(0, 0, doc.page.width, doc.page.height)
.fill('rgba(17, 17, 23, 0.95)');
try {
const dragonPath = path.join(__dirname, '../../../../public/assets/images/stoic-dragon-cat.png');
if (fs.existsSync(dragonPath)) {
doc.image(dragonPath, {
fit: [400, 400],
align: 'center',
valign: 'center'
});
}
} catch (error) {
message('Warning: Could not load dragon image');
}
doc.font('Helvetica-Bold')
.fontSize(64)
.fillColor('white')
.text('Rarity Analysis', {
align: 'center',
y: doc.page.height - 200
})
.fontSize(24)
.text('Dracattus NFT Collection', {
align: 'center'
})
.fontSize(16)
.text(new Date().toLocaleDateString(), {
align: 'center'
});
// Generate charts for each trait
for (const [trait, values] of Object.entries(reportData.traitRarity)) {
if (trait === 'taxonomy') continue;
// Data validation and processing
const processedData = Object.entries(values)
.filter(([name, value]) => {
// Filter out invalid or null values
return name && value !== undefined && value !== null && !isNaN(value);
})
.map(([name, value]) => ({
name,
value: parseFloat(value),
percentage: (parseFloat(value) * 100).toFixed(2)
}))
.sort((a, b) => b.value - a.value); // Sort by value descending
// Validate we have data to display
if (processedData.length === 0) {
message(`Warning: No valid data for trait ${trait}`);
continue;
}
message(`Processing ${trait} with ${processedData.length} valid entries`);
doc.addPage();
// Dark background
doc.rect(0, 0, doc.page.width, doc.page.height)
.fill('rgba(17, 17, 23, 0.95)');
const canvas = createCanvas(800, 500);
const ctx = canvas.getContext('2d');
// Take top 15 items for display
const displayData = processedData.slice(0, 15);
// Create chart with validated data
new Chart(ctx, {
type: 'bar',
data: {
labels: displayData.map(item => item.name),
datasets: [{
data: displayData.map(item => parseFloat(item.percentage)),
backgroundColor: 'rgba(48, 209, 255, 0.95)',
borderColor: 'rgba(255, 255, 255, 0.1)',
borderWidth: 1,
borderRadius: 8
}]
},
options: {
indexAxis: 'y',
plugins: {
legend: { display: false },
title: {
display: true,
text: [
`${trait.charAt(0).toUpperCase() + trait.slice(1)} Distribution`,
`(Top ${displayData.length} of ${processedData.length} total)`
],
color: 'white',
font: { size: 24 }
},
tooltip: {
callbacks: {
label: function(context) {
return `Rarity: ${context.raw}%`;
}
}
}
},
scales: {
x: {
grid: { color: 'rgba(255, 255, 255, 0.1)' },
ticks: {
color: 'white',
callback: function(value) {
return value.toFixed(2) + '%';
}
},
title: {
display: true,
text: 'Rarity Percentage',
color: 'white',
font: { size: 14 }
}
},
y: {
grid: { display: false },
ticks: {
color: 'white',
font: { size: 12 }
}
}
},
layout: {
padding: {
top: 30,
right: 30,
bottom: 30,
left: 30
}
}
}
});
// Add chart to PDF with debug info
doc.image(canvas.toBuffer(), {
fit: [700, 500],
align: 'center',
valign: 'center'
});
// Add data summary at bottom of page
doc.fontSize(10)
.fillColor('rgba(255, 255, 255, 0.7)')
.text(`Total variants: ${processedData.length} | Displayed: ${displayData.length} | Data range: ${displayData[0].percentage}% - ${displayData[displayData.length-1].percentage}%`, {
align: 'center',
bottom: doc.page.height - 30
});
}
// Finalize PDF
doc.end();
// Handle completion
stream.on('finish', () => {
message(`PDF report saved to: ${pdfPath}`);
resolve({ success: true, pdfPath });
});
});
} catch (error) {
message(`Error generating PDF report: ${error.message}`);
throw error;
}
});

View File

@@ -1,19 +0,0 @@
Parse.Cloud.job("getBattlesSchema", async (request) => {
try {
const schema = await new Parse.Schema("BATTLES").get();
console.log('BATTLES Schema:', schema);
// Save to file
const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, 'BATTLES-schema.json');
await fs.writeFile(filePath, JSON.stringify(schema, null, 2));
console.log(`Schema saved to: ${filePath}`);
return schema;
} catch (error) {
console.error('Error retrieving BATTLES schema:', error);
throw error;
}
});

View File

@@ -1,145 +0,0 @@
const fs = require('fs').promises;
const path = require('path');
Parse.Cloud.define("generateDracattusReport", async (request) => {
try {
const Dracattus = Parse.Object.extend("DRACATTUS");
const allResults = [];
const batchSize = 1000;
let skip = 0;
let hasMore = true;
while (hasMore) {
const query = new Parse.Query(Dracattus);
query.limit(batchSize);
query.skip(skip);
// Only include valid taxonomy types
query.containedIn('taxonomy', [
'Dracattus',
'Primal Dracattus',
'Diamond Dracattus',
'Blood Moon Stone'
]);
const batch = await query.find({ useMasterKey: true });
console.log(`Fetched batch of ${batch.length} records starting at offset ${skip}`);
allResults.push(...batch);
if (batch.length < batchSize) {
hasMore = false;
} else {
skip += batchSize;
}
}
console.log(`Found total of ${allResults.length} Dracattus records to process`);
// Initialize data structures to store trait counts and rarity scores
const traitCounts = {
base: {},
fur: {},
wings: {},
belly: {},
background: {},
ears: {},
claws: {},
eyes: {},
horns: {},
taxonomy: {},
attack: {},
defence: {},
agility: {},
energy: {},
magic: {},
skill: {},
compatibility: {},
};
// Process each Dracattus NFT to count trait occurrences
allResults.forEach(dracattus => {
for (const trait of Object.keys(traitCounts)) {
const traitValue = dracattus.get(trait);
if (traitValue !== undefined) {
if (!traitCounts[trait][traitValue]) {
traitCounts[trait][traitValue] = 0;
}
traitCounts[trait][traitValue]++;
}
}
});
// Calculate rarity scores for each trait value
const traitRarityScores = {};
for (const trait of Object.keys(traitCounts)) {
traitRarityScores[trait] = {};
for (const [value, count] of Object.entries(traitCounts[trait])) {
// For taxonomy, we want to calculate rarity within each type
if (trait === 'taxonomy') {
const totalOfType = count;
traitRarityScores[trait][value] = 1 / totalOfType;
} else {
traitRarityScores[trait][value] = 1 / count;
}
}
}
// Calculate total rarity score for each NFT
const nftRarityScores = [];
allResults.forEach(dracattus => {
let totalRarityScore = 0;
const traits = {};
const taxonomy = dracattus.get('taxonomy');
for (const trait of Object.keys(traitRarityScores)) {
const traitValue = dracattus.get(trait);
if (traitValue !== undefined) {
const rarityScore = traitRarityScores[trait][traitValue];
totalRarityScore += rarityScore;
traits[trait] = {
value: traitValue,
rarityScore: rarityScore,
};
}
}
// Only include NFTs with valid taxonomy types
if (['Dracattus', 'Primal Dracattus', 'Diamond Dracattus', 'Blood Moon Stone'].includes(taxonomy)) {
nftRarityScores.push({
nftId: dracattus.get('nft_id'),
name: dracattus.get('name'),
taxonomy: taxonomy,
totalRarityScore: totalRarityScore,
traits: traits,
});
}
});
// Sort NFTs by total rarity score (descending order)
nftRarityScores.sort((a, b) => b.totalRarityScore - a.totalRarityScore);
// Generate the report with taxonomy breakdowns
const report = {
taxonomyBreakdown: {
'Dracattus': nftRarityScores.filter(nft => nft.taxonomy === 'Dracattus').length,
'Primal Dracattus': nftRarityScores.filter(nft => nft.taxonomy === 'Primal Dracattus').length,
'Diamond Dracattus': nftRarityScores.filter(nft => nft.taxonomy === 'Diamond Dracattus').length,
'Blood Moon Stone': nftRarityScores.filter(nft => nft.taxonomy === 'Blood Moon Stone').length,
},
traitRarity: traitRarityScores,
nftRarityScores: nftRarityScores,
totalNFTs: nftRarityScores.length,
};
// Save the report to a file
const filePath = path.join(__dirname, 'dracattus-rarity-report.json');
await fs.writeFile(filePath, JSON.stringify(report, null, 2));
console.log(`Rarity report saved to: ${filePath}`);
return report;
} catch (error) {
console.error('Error generating Dracattus rarity report:', error);
throw error;
}
});

View File

@@ -1,19 +0,0 @@
Parse.Cloud.job("getDracattusSchema", async (request) => {
try {
const schema = await new Parse.Schema("DRACATTUS").get();
console.log('DRACATTUS Schema:', schema);
// Save to file
const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, 'dracattus-schema.json');
await fs.writeFile(filePath, JSON.stringify(schema, null, 2));
console.log(`Schema saved to: ${filePath}`);
return schema;
} catch (error) {
console.error('Error retrieving DRACATTUS schema:', error);
throw error;
}
});

View File

@@ -1,61 +0,0 @@
Parse.Cloud.job("getDracattusSchema", async (request) => {
try {
const schema = await new Parse.Schema("DRACATTUS").get();
console.log('DRACATTUS Schema:', schema);
// Save to file
const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, 'dracattus-schema.json');
await fs.writeFile(filePath, JSON.stringify(schema, null, 2));
console.log(`Schema saved to: ${filePath}`);
return schema;
} catch (error) {
console.error('Error retrieving DRACATTUS schema:', error);
throw error;
}
});
Parse.Cloud.job("getIOSCSchema", async (request) => {
try {
const schema = await new Parse.Schema("IOSC").get();
console.log('IOSC Schema:', schema);
// Save to file
const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, 'IOSC-schema.json');
await fs.writeFile(filePath, JSON.stringify(schema, null, 2));
console.log(`Schema saved to: ${filePath}`);
return schema;
} catch (error) {
console.error('Error retrieving IOSC schema:', error);
throw error;
}
});
Parse.Cloud.job("getQuestsSchema", async (request) => {
try {
const schema = await new Parse.Schema("QUESTS").get();
console.log('QUESTS Schema:', schema);
// Save to file
const fs = require('fs').promises;
const path = require('path');
const filePath = path.join(__dirname, 'Quests-schema.json');
await fs.writeFile(filePath, JSON.stringify(schema, null, 2));
console.log(`Schema saved to: ${filePath}`);
return schema;
} catch (error) {
console.error('Error retrieving IOSC schema:', error);
throw error;
}
});

View File

@@ -850,5 +850,8 @@ async function setNewLocations() {
}
module.exports = {
setNewLocations
setNewLocations,
shouldBeInWater,
getRandomLandLocation,
getRandomOceanLocation
};

View File

@@ -1,38 +0,0 @@
Parse.Cloud.define('getNftByCollection', async (request) => {
const { collection } = request.params;
try {
// Create a query for the specified collection
const query = new Parse.Query(collection);
// Get the total count of records
const count = await query.count();
if (count === 0) {
return { error: 'No records found in collection' };
}
// Generate a random skip value
const skip = Math.floor(Math.random() * count);
// Skip to random position and limit to 1 record
query.skip(skip).limit(1);
// Execute the query
const result = await query.first();
// Immediately set wasViewed to true and save
if (result) {
result.set('wasViewed', true);
await result.save(null, { useMasterKey: true });
}
return result;
} catch (error) {
console.error('Error fetching random record:', error);
return {
success: false,
error: error.message
};
}
});

View File

@@ -1,312 +0,0 @@
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
const FormData = require('form-data');
const WALLET_ADDRESS = 'xch18c9cptr20v4svjzckk98tnfhw32e9ywxpl8dnvtxk5keufyet5msm0q66d';
const SCAN_INTERVAL = 115000; // 115 seconds in milliseconds
// Function to fetch transactions from Spacescan API
async function fetchTransactions(address) {
try {
const response = await fetch(`https://api.spacescan.io/address/xch-transaction/${address}`);
const data = await response.json();
return data;
} catch (error) {
console.error('Error fetching transactions:', error);
return null;
}
}
// Function to decode hex memo to string
function decodeHexMemo(hexString) {
try {
// Remove '0x' prefix if present
const hex = hexString.startsWith('0x') ? hexString.slice(2) : hexString;
// Convert hex to buffer and then to string
return Buffer.from(hex, 'hex').toString('utf8');
} catch (error) {
console.error('Error decoding memo:', error);
return null;
}
}
// Function to upload image to IPFS using Infura
async function uploadToIPFS(imageUrl) {
try {
// Fetch the image from the URL
const imageResponse = await fetch(imageUrl);
const imageBuffer = await imageResponse.buffer();
// Create form data
const formData = new FormData();
formData.append('file', imageBuffer, {
filename: `nft_${Date.now()}.png`,
contentType: 'image/png'
});
// Upload to Infura IPFS
const response = await fetch('https://ipfs.infura.io:5001/api/v0/add', {
method: 'POST',
headers: {
'Authorization': `Basic ${Buffer.from(process.env.INFURA_PROJECT_ID + ':' + process.env.INFURA_PROJECT_SECRET).toString('base64')}`
},
body: formData
});
if (!response.ok) {
throw new Error(`IPFS upload failed: ${response.statusText}`);
}
const data = await response.json();
return `ipfs://${data.Hash}`;
} catch (error) {
console.error('Error uploading to IPFS:', error);
console.log('Falling back to stub IPFS data');
// Return a placeholder IPFS URL with timestamp to ensure uniqueness
return `ipfs://QmPlaceholderHash/${Date.now()}`;
}
}
// Function to get mint price from Parse Config
async function getMintPrice() {
try {
const config = await Parse.Config.get({ useMasterKey: true });
return config.get('MINT_MACHINE_MINT_PRICE') || 0.1;
} catch (error) {
console.error('Error getting mint price:', error);
return 0;
}
}
// Function to save transaction to Parse database
async function saveTransaction(transaction, type) {
try {
const MintMachineTransactions = Parse.Object.extend('MintMachine_Transactions');
const transactionRecord = new MintMachineTransactions();
// Get first memo and decode it
let memoString = null;
if (transaction.memo && Array.isArray(transaction.memo) && transaction.memo.length > 0) {
const firstMemo = transaction.memo[0];
if (firstMemo && typeof firstMemo === 'string') {
memoString = decodeHexMemo(firstMemo);
console.log(`Decoded memo: ${memoString}`);
}
}
// Find the queue object and set it as relation
if (memoString) {
try {
const queueQuery = new Parse.Query('MintMachine_Queue');
const queueObject = await queueQuery.get(memoString);
if (queueObject) {
console.log(`Found queue object: ${queueObject.id}`);
// Verify transaction amount matches mint price
const mintPrice = await getMintPrice();
const transactionAmount = parseFloat(transaction.amount_xch);
if (transactionAmount < mintPrice) {
console.warn(`Transaction amount ${transactionAmount} XCH is less than required mint price ${mintPrice} XCH`);
return false;
}
const relation = transactionRecord.relation('queued_object');
relation.add(queueObject);
// Get the NFT image and upload to IPFS
const nftImage = queueObject.get('nft_image');
if (nftImage && nftImage._url) {
console.log(`Found NFT image URL: ${nftImage._url}`);
const ipfsHash = await uploadToIPFS(nftImage._url);
if (ipfsHash) {
console.log(`[DEV] Generated IPFS URL: ${ipfsHash}`);
// Update queue object with IPFS hash, address, and status
queueObject.set({
ipfs_file: ipfsHash,
address: type === 'send' ? transaction.to : transaction.from,
status: 'processing'
});
await queueObject.save(null, { useMasterKey: true });
console.log(`Updated queue object with IPFS hash, address: ${type === 'send' ? transaction.to : transaction.from}, and status: processing`);
// Import and call the minting process
const { dropMint } = require('../pipeline/queue_airdrop');
try {
console.log(`Starting mint process for queue object: ${queueObject.id}`);
await dropMint(queueObject);
} catch (error) {
console.warn(`Failed to start mint process for queue object ${queueObject.id}:`, error);
// Continue processing even if minting fails
}
}
} else {
console.log('No NFT image found in queue object');
}
} else {
console.log(`No queue object found for memo: ${memoString} - continuing with transaction record only`);
}
} catch (error) {
console.warn(`Error processing queue object for memo ${memoString}:`, error);
// Continue with transaction record even if queue processing fails
}
}
// Save the transaction record regardless of queue object status
transactionRecord.set({
coin_id: transaction.coin_id,
timestamp: new Date(transaction.time),
height: transaction.height,
amount_xch: transaction.amount_xch,
amount_mojo: transaction.amount_mojo,
address: type === 'send' ? transaction.to : transaction.from,
memo: transaction.memo || [], // Keep original hex memos
memo_string: memoString, // Save decoded first memo
transaction_type: type,
createdAt: new Date(),
updatedAt: new Date()
});
await transactionRecord.save(null, { useMasterKey: true });
console.log(`Saved ${type} transaction: ${transaction.coin_id}`);
return true;
} catch (error) {
console.warn(`Error saving ${type} transaction:`, error);
// Return false to indicate failure but don't throw
return false;
}
}
// Main scanner function
async function scanWallet(address) {
try {
console.log(`Scanning wallet: ${address} at ${new Date().toISOString()}`);
const data = await fetchTransactions(address);
if (!data || data.status !== 'success') {
console.error('Failed to fetch transactions');
return;
}
// Process send transactions
if (data.send_transactions && data.send_transactions.transactions) {
for (const tx of data.send_transactions.transactions) {
// Check if transaction already exists
const query = new Parse.Query('MintMachine_Transactions');
query.equalTo('coin_id', tx.coin_id);
const existing = await query.first({ useMasterKey: true });
if (!existing) {
await saveTransaction(tx, 'send');
}
}
}
// Process received transactions
if (data.received_transactions && data.received_transactions.transactions) {
for (const tx of data.received_transactions.transactions) {
// Check if transaction already exists
const query = new Parse.Query('MintMachine_Transactions');
query.equalTo('coin_id', tx.coin_id);
const existing = await query.first({ useMasterKey: true });
if (!existing) {
await saveTransaction(tx, 'received');
}
}
}
} catch (error) {
console.error('Error in scanWallet:', error);
}
}
// Function to start the scanner
async function startScanner() {
try {
// Store the scanner interval ID in Parse Config
await Parse.Config.save({
scannerAddress: WALLET_ADDRESS,
scannerActive: true,
lastScanTime: new Date().toISOString()
}, { useMasterKey: true });
// Perform immediate scan
console.log('Performing initial scan...');
await scanWallet(WALLET_ADDRESS);
// Start the scanner interval
const intervalId = setInterval(async () => {
try {
await scanWallet(WALLET_ADDRESS);
// Update last scan time
await Parse.Config.save({
lastScanTime: new Date().toISOString()
}, { useMasterKey: true });
} catch (error) {
console.error('Scanner error:', error);
}
}, SCAN_INTERVAL);
// Store the interval ID
global.scannerIntervalId = intervalId;
return {
status: 'Scanner started',
address: WALLET_ADDRESS,
interval: '115 seconds',
initialScan: 'completed'
};
} catch (error) {
console.error('Error starting scanner:', error);
throw error;
}
}
// Cloud function to start the scanner
Parse.Cloud.define('startWalletScanner', async () => {
return await startScanner();
});
// Cloud function to stop the scanner
Parse.Cloud.define('stopWalletScanner', async () => {
if (global.scannerIntervalId) {
clearInterval(global.scannerIntervalId);
global.scannerIntervalId = null;
// Update Parse Config
await Parse.Config.save({
scannerActive: false
}, { useMasterKey: true });
return { status: 'Scanner stopped' };
}
return { status: 'No scanner running' };
});
// Cloud function to get scanner status
Parse.Cloud.define('getScannerStatus', async () => {
const config = await Parse.Config.get({ useMasterKey: true });
return {
active: config.get('scannerActive') || false,
address: config.get('scannerAddress') || WALLET_ADDRESS,
lastScanTime: config.get('lastScanTime') || null,
interval: '115 seconds'
};
});
// Cloud function to get config values
Parse.Cloud.define('getConfig', async () => {
const config = await Parse.Config.get({ useMasterKey: true });
return {
MINT_MACHINE_MINT_PRICE: config.get('MINT_MACHINE_MINT_PRICE') || 0,
scannerActive: config.get('scannerActive') || false,
scannerAddress: config.get('scannerAddress') || WALLET_ADDRESS,
lastScanTime: config.get('lastScanTime') || null
};
});
// Start the scanner when the cloud function is loaded
startScanner().then(() => {
console.log('Wallet scanner started successfully');
}).catch(error => {
console.error('Failed to start wallet scanner:', error);
});

View File

@@ -1,112 +0,0 @@
const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args));
// Helper function for formatted logging
function logWithTimestamp(emoji, message, data = null) {
const timestamp = new Date().toISOString();
const logMessage = `[${timestamp}] ${emoji} ${message}`;
if (data) {
console.log(logMessage, '\n', JSON.stringify(data, null, 2));
} else {
console.log(logMessage);
}
}
// Real MintGarden API call
async function mintGardenMintAPI(payload) {
const apiKey = process.env.MINTGARDEN_API_KEY;
const endpoint = process.env.MINTGARDEN_API_URL || 'https://api.testnet.mintgarden.io/mint/dynamic';
if (!apiKey) throw new Error('Missing MintGarden API key in .env');
if (!payload.profile_id) throw new Error('Missing profile_id in payload');
if (!payload.royalty_address) throw new Error('Missing royalty_address in payload');
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`MintGarden API error: ${response.status} ${errorText}`);
}
console.log(`*************************`)
console.log(response)
console.log(`*************************`)
return response.json();
}
// Function to handle the minting process
async function dropMint(queueItem) {
try {
logWithTimestamp('🚀', `Starting mint process for queue item: ${queueItem.id}`);
// Get the required fields
const ipfsFile = queueItem.get('ipfs_file');
const address = queueItem.get('address');
// Prepare the mint request payload
const mintPayload = {
profile_id: process.env.MINTGARDEN_PROFILE_ID,
metadata: {
data_uris: [ipfsFile],
// Add any additional metadata fields as needed
},
royalty_address: process.env.MINTGARDEN_ROYALTY_ADDRESS,
royalty_percentage: Number(process.env.MINTGARDEN_ROYALTY_PERCENTAGE) || 5,
target_address: address,
};
logWithTimestamp('📡', 'Sending request to MintGarden API');
// Call the real MintGarden API
const result = await mintGardenMintAPI(mintPayload);
logWithTimestamp('✅', `Mint successful for queue item: ${queueItem.id}`, result);
// Update the queue item with mint status
queueItem.set({
status: 'minted',
mint_timestamp: new Date(),
mint_response: result,
nft_id: result.nft_id
});
await queueItem.save(null, { useMasterKey: true });
logWithTimestamp('💾', 'Updated queue item with mint status', {
status: 'minted',
nft_id: result.nft_id,
timestamp: new Date().toISOString()
});
return result;
} catch (error) {
logWithTimestamp('❌', `Error in dropMint for queue item ${queueItem.id}:`, {
error: error.message,
stack: error.stack
});
// Update the queue item with error status
queueItem.set({
status: 'error',
error_message: error.message,
error_timestamp: new Date()
});
await queueItem.save(null, { useMasterKey: true });
logWithTimestamp('💾', 'Updated queue item with error status', {
status: 'error',
error: error.message,
timestamp: new Date().toISOString()
});
throw error;
}
}
// Export the dropMint function
module.exports = {
dropMint
};

View File

@@ -0,0 +1 @@
Please verify your email address

View File

@@ -4,12 +4,12 @@ const path = require('path');
// Import location utilities
const { shouldBeInWater, getRandomLandLocation, getRandomOceanLocation } = require('../../dracattus/api/utils/set-new-locations.js');
// Shared constants
const COLLECTION_ID = process.env.DRACATTUS_COLLECTION_ID;
// Shared constants - UPDATED FOR BETTER RATE LIMITING
const COLLECTION_ID = "col10xwvdgyhfr06nfssnve9vcp8g03dk65lvp5jj7eqs24ncvc0085sng6kn3";
const BASE_URL = 'https://api.mintgarden.io';
const DEFAULT_PAGE_SIZE = 100;
const MAX_RETRIES = 5;
const INITIAL_BACKOFF = 5000; // 5 seconds
const DEFAULT_PAGE_SIZE = 50; // Reduced from 100 to 50
const MAX_RETRIES = 3; // Reduced from 5 to 3
const INITIAL_BACKOFF = 10000; // Increased from 5000 to 10000 (10 seconds)
// Utility functions
const checkAdminAccess = async (user) => {
@@ -44,8 +44,16 @@ const makeApiRequest = async (endpoint, params = {}, attempt = 1) => {
return response.data;
} catch (error) {
if (attempt < MAX_RETRIES) {
const backoffTime = INITIAL_BACKOFF * Math.pow(2, attempt - 1);
console.log(`Request failed, waiting ${backoffTime/1000} seconds before retry ${attempt + 1}/${MAX_RETRIES}`);
let backoffTime = INITIAL_BACKOFF * Math.pow(2, attempt - 1);
// Special handling for rate limit errors (429)
if (error.response?.status === 429) {
backoffTime = 60000; // Wait 60 seconds for rate limit errors
console.log(`Rate limit hit (429), waiting ${backoffTime/1000} seconds before retry ${attempt + 1}/${MAX_RETRIES}`);
} else {
console.log(`Request failed, waiting ${backoffTime/1000} seconds before retry ${attempt + 1}/${MAX_RETRIES}`);
}
await new Promise(resolve => setTimeout(resolve, backoffTime));
return makeApiRequest(endpoint, params, attempt + 1);
}
@@ -93,14 +101,30 @@ const saveNFTToDatabase = async (nftData, metadata) => {
chicattusObject.set('attributes', metadata.attributes);
for (const attribute of metadata.attributes) {
const { trait_type, value } = attribute;
chicattusObject.set(trait_type.toLowerCase(), value);
const fieldName = trait_type.toLowerCase();
// Fields that should remain as strings even if they contain numeric values
const stringFields = ['magic', 'apathy'];
// Convert string numbers to actual numbers for numeric fields, except for specific string fields
let convertedValue = value;
if (typeof value === 'string' && !isNaN(value) && !isNaN(parseFloat(value)) && !stringFields.includes(fieldName)) {
// Check if it's an integer or float
if (value.includes('.')) {
convertedValue = parseFloat(value);
} else {
convertedValue = parseInt(value, 10);
}
}
chicattusObject.set(fieldName, convertedValue);
}
}
// Extract edition number
const name = nftData.name;
const name = nftData.name || nftData.id || 'Unknown NFT';
const regex = /#(\d+)/;
const match = name.match(regex);
const match = name ? name.match(regex) : null;
if (match) {
chicattusObject.set('edition', parseInt(match[1]));
}
@@ -127,7 +151,8 @@ const saveNFTToDatabase = async (nftData, metadata) => {
};
// Cloud Jobs and Functions
Parse.Cloud.job('populateCollectionFromMintGardenAPI', async (request) => {
Parse.Cloud.job('populateDracattusCollectionFromMintGardenAPI', async (request) => {
console.log('Populating collection from MintGarden API');
let page = '';
let processedCount = 0;
const { message } = request;
@@ -175,8 +200,8 @@ Parse.Cloud.job('populateCollectionFromMintGardenAPI', async (request) => {
continue;
}
// Add delay between NFTs
await new Promise(resolve => setTimeout(resolve, 1000));
// Add delay between NFTs - INCREASED FOR RATE LIMITING
await new Promise(resolve => setTimeout(resolve, 3000));
// Fetch metadata with retries
const metadata = await makeApiRequest(`/nfts/${encodedId}/metadata`);
@@ -202,8 +227,8 @@ Parse.Cloud.job('populateCollectionFromMintGardenAPI', async (request) => {
break;
}
// Add delay between pages
await new Promise(resolve => setTimeout(resolve, 2000));
// Add delay between pages - INCREASED FOR RATE LIMITING
await new Promise(resolve => setTimeout(resolve, 5000));
} catch (error) {
console.error(`Error processing page ${page}:`, error);
@@ -262,3 +287,88 @@ Parse.Cloud.define('getDracattusOffersOnMintGarden', async (request) => {
throw error;
}
});
Parse.Cloud.define('importSingleDracattusNFT', async (request) => {
const { encoded_id } = request.params;
if (!encoded_id) {
throw new Error('encoded_id is required');
}
try {
console.log(`Importing single NFT with encoded_id: ${encoded_id}`);
// Check if NFT already exists in database
const ChicattusClass = Parse.Object.extend('DRACATTUS');
const existingQuery = new Parse.Query(ChicattusClass);
existingQuery.equalTo('encoded_id', encoded_id);
const existingObject = await existingQuery.first();
if (existingObject) {
console.log(`NFT ${encoded_id} already exists in database`);
return {
success: true,
message: 'NFT already exists',
nft: existingObject.toJSON()
};
}
// Search for the NFT in the collection to get the same data structure
// as the working collection sync function
let nftData = null;
let page = '';
let found = false;
console.log(`Searching for NFT ${encoded_id} in collection...`);
while (!found) {
const data = await makeApiRequest(`/collections/${COLLECTION_ID}/nfts`, {
require_owner: false,
require_price: false,
size: DEFAULT_PAGE_SIZE,
page
});
const { items } = data;
if (!items?.length) {
break;
}
// Look for our specific NFT in this page
nftData = items.find(item => item.encoded_id === encoded_id);
if (nftData) {
found = true;
console.log('Found NFT in collection:', JSON.stringify(nftData, null, 2));
break;
}
page = data.next;
if (!page) {
break;
}
}
if (!nftData) {
throw new Error('NFT not found in collection');
}
// Fetch metadata
const metadata = await makeApiRequest(`/nfts/${encoded_id}/metadata`);
console.log('Raw metadata from MintGarden:', JSON.stringify(metadata, null, 2));
// Save to database using existing function
const savedNFT = await saveNFTToDatabase(nftData, metadata);
console.log(`Successfully imported NFT ${encoded_id}`);
return {
success: true,
message: 'NFT imported successfully',
nft: savedNFT.toJSON()
};
} catch (error) {
console.error(`Error importing NFT ${encoded_id}:`, error);
throw new Error(`Failed to import NFT: ${error.message}`);
}
});

View File

@@ -1,194 +1,9 @@
const axios = require('axios');
Parse.Cloud.job('populateCollectionFromMintGardenAPI', async (request) => {
const collectionId = "col10xwvdgyhfr06nfssnve9vcp8g03dk65lvp5jj7eqs24ncvc0085sng6kn3";
const pageSize = 100;
let page = '';
let allRecords = [];
try {
while (true) {
const apiUrl = `https://api.mintgarden.io/collections/${collectionId}/nfts?require_owner=false&require_price=false&size=${pageSize}&page=${page}`;
// Make a GET request to the API endpoint using Axios
const response = await axios.get(apiUrl);
const { items, ...responseData } = response.data;
console.log(responseData);
if (items.length === 0) {
// Break the loop if there are no more items
break;
}
const ChicattusClass = Parse.Object.extend('DRACATTUS');
for (let i = 0; i < items.length; i++) {
const item = items[i];
const encodedId = item.encoded_id;
// Query the database to check if the encoded_id already exists
const query = new Parse.Query(ChicattusClass);
query.equalTo('encoded_id', encodedId);
const existingObject = await query.first();
if (existingObject) {
console.log(`Skipping duplicate record with encoded_id: ${encodedId}`);
continue;
}
const chicattusObject = new ChicattusClass();
for (const prop in item) {
if (prop !== 'id') {
chicattusObject.set(prop, item[prop]);
}
if (prop === 'id') {
chicattusObject.set("nft_id", item[prop]);
}
}
try {
// Retrieve NFT details from the metadata URL
const metadataUrl = `https://api.mintgarden.io/nfts/${encodedId}/metadata`;
const metadataResponse = await axios.get(metadataUrl);
const attributes = metadataResponse.data.attributes;
console.log(attributes)
// Set the attributes field
chicattusObject.set('attributes', attributes);
// Spread the attributes across the record
for (const attribute of attributes) {
const { trait_type, value } = attribute;
chicattusObject.set(trait_type.toLowerCase(), value);
}
// Extract the edition number from the 'name' property
const name = item.name;
const regex = /#(\d+)/;
const match = name.match(regex);
if (match) {
const edition = parseInt(match[1]);
chicattusObject.set('edition', edition);
}
chicattusObject.set('chain', 'chia');
} catch (error) {
console.error('Error fetching NFT details:', error);
}
try {
// Save the object
// ACL stuff
const acl = new Parse.ACL();
acl.setPublicReadAccess(true);
chicattusObject.setACL(acl);
const savedObject = await chicattusObject.save();
console.log('Object saved:', savedObject.toJSON());
} catch (error) {
console.error('Error saving object:', error);
}
}
// Update the page for the next iteration
page = response.data.next;
if (!page) {
// Break the loop if there is no next page
break;
}
}
return;
} catch (error) {
console.error('Error fetching data:', error);
}
});
// populateCollectionFromMintGardenAPI function removed - duplicate exists in drcattus-scraper.js
Parse.Cloud.job('getMintGardenStats', async (request) => {
const user = request.user;
if (!user) {
throw 'Must be signed in to call this Cloud Function.';
}
const query = new Parse.Query(Parse.Role);
query.equalTo('name', 'administrator');
query.equalTo('users', user);
const adminRole = await query.first();
if (!adminRole) {
throw 'Not authorized to call this function.';
}
const apiUrl = 'https://api.mintgarden.io/collections/col10xwvdgyhfr06nfssnve9vcp8g03dk65lvp5jj7eqs24ncvc0085sng6kn3';
try {
// Make a GET request to the API endpoint using Axios
const response = await axios.get(apiUrl);
const statsData = response.data;
const StatsClass = Parse.Object.extend('STATS');
const statsObject = new StatsClass();
// Set the properties for the stats object
statsObject.set('type', 'mintgarden');
statsObject.set('data', statsData);
try {
// Save the stats object
const savedObject = await statsObject.save();
console.log('Stats object saved:', savedObject.toJSON());
} catch (error) {
console.error('Error saving stats object:', error);
}
return;
} catch (error) {
console.error('Error fetching stats data:', error);
}
});
// getMintGardenStats function removed - duplicate exists in drcattus-scraper.js
Parse.Cloud.define('getDracattusOffersOnMintGarden', async (request) => {
const user = request.user;
if (!user) {
throw 'Must be signed in to call this Cloud Function.';
}
const query = new Parse.Query(Parse.Role);
query.equalTo('name', 'administrator');
query.equalTo('users', user);
const adminRole = await query.first();
if (!adminRole) {
throw 'Not authorized to call this function.';
}
let collection = "col10xwvdgyhfr06nfssnve9vcp8g03dk65lvp5jj7eqs24ncvc0085sng6kn3";
let page = request.params.page; // Initialize the cursor to null
let size = request.params.size; // Initialize the cursor to null
try {
// Construct the API URL with the nextCursor (if available)
const apiUrl = `https://api.mintgarden.io/collections/${collection}/nfts/by_offers?size=${size}&page=${page || '>'}`;
// Make a GET request to the API endpoint using Axios
const response = await axios.get(apiUrl);
const offerData = response.data;
console.log('Offer Data:', offerData);
// Update the nextCursor from the response
nextCursor = offerData.next;
return offerData;
} catch (error) {
console.error('Error fetching stats data:', error);
}
});
// getDracattusOffersOnMintGarden function removed - duplicate exists in drcattus-scraper.js

512
package-lock.json generated
View File

@@ -22,11 +22,13 @@
"node-fetch": "^3.3.2",
"nodemailer": "^6.9.4",
"openai": "^4.86.1",
"parse-auditor": "^0.0.2",
"parse-dashboard": "^5.1.0",
"parse-jobs-scheduler": "github:jaeggerr/parse-jobs-scheduler",
"parse-server": "^6.2.0",
"parse-server-api-mail-adapter": "^2.2.0",
"pdfkit": "^0.16.0",
"sharp": "^0.34.3",
"web3": "^4.1.1"
},
"devDependencies": {
@@ -560,6 +562,16 @@
"kuler": "^2.0.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.4.tgz",
"integrity": "sha512-hHyapA4A3gPaDCNfiqyZUStTMqIkKRshqPIuDOXv1hcBnD4U3l8cP0T1HMCfGRxQ6V64TGCcoswChANyOAwbQg==",
"license": "MIT",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@envelop/core": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@envelop/core/-/core-2.6.0.tgz",
@@ -1374,6 +1386,424 @@
"license": "BSD-3-Clause",
"peer": true
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
"integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
"integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
"integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
"integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
"integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
"cpu": [
"arm"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
"integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
"integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
"cpu": [
"ppc64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
"integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
"cpu": [
"s390x"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
"integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
"integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
"cpu": [
"arm64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
"integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
"cpu": [
"x64"
],
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
"integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
"cpu": [
"arm"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
"integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
"integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
"cpu": [
"ppc64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
"integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
"cpu": [
"s390x"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
"integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
"integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
"integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
"cpu": [
"x64"
],
"license": "Apache-2.0",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
"integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
"cpu": [
"wasm32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.4.4"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
"integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
"cpu": [
"arm64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
"integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
"cpu": [
"ia32"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
"integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
"cpu": [
"x64"
],
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
@@ -4363,9 +4793,9 @@
}
},
"node_modules/detect-libc": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==",
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"license": "Apache-2.0",
"engines": {
"node": ">=8"
@@ -8488,6 +8918,15 @@
"crypto-js": "4.1.1"
}
},
"node_modules/parse-auditor": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/parse-auditor/-/parse-auditor-0.0.2.tgz",
"integrity": "sha512-Z6VKZTOrMKjYHe5VVKtguY/kS5ZzQGa0A39GFop9UDdzgRkqKHnjENONVkd6zyIHoOmB3NiyqSivebGjDDLNFg==",
"license": "BSD-3-Clause",
"engines": {
"node": "*"
}
},
"node_modules/parse-dashboard": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/parse-dashboard/-/parse-dashboard-5.4.0.tgz",
@@ -11015,6 +11454,73 @@
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
"license": "ISC"
},
"node_modules/sharp": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
"integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-libvips-darwin-arm64": "1.2.0",
"@img/sharp-libvips-darwin-x64": "1.2.0",
"@img/sharp-libvips-linux-arm": "1.2.0",
"@img/sharp-libvips-linux-arm64": "1.2.0",
"@img/sharp-libvips-linux-ppc64": "1.2.0",
"@img/sharp-libvips-linux-s390x": "1.2.0",
"@img/sharp-libvips-linux-x64": "1.2.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
"@img/sharp-libvips-linuxmusl-x64": "1.2.0",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-ppc64": "0.34.3",
"@img/sharp-linux-s390x": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-linuxmusl-arm64": "0.34.3",
"@img/sharp-linuxmusl-x64": "0.34.3",
"@img/sharp-wasm32": "0.34.3",
"@img/sharp-win32-arm64": "0.34.3",
"@img/sharp-win32-ia32": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
}
},
"node_modules/sharp/node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/sharp/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",

View File

@@ -26,11 +26,13 @@
"node-fetch": "^3.3.2",
"nodemailer": "^6.9.4",
"openai": "^4.86.1",
"parse-auditor": "^0.0.2",
"parse-dashboard": "^5.1.0",
"parse-jobs-scheduler": "github:jaeggerr/parse-jobs-scheduler",
"parse-server": "^6.2.0",
"parse-server-api-mail-adapter": "^2.2.0",
"pdfkit": "^0.16.0",
"sharp": "^0.34.3",
"web3": "^4.1.1"
},
"devDependencies": {

View File

@@ -3,10 +3,10 @@
# Create data directories for each replica
mkdir -p ./data/db1 ./data/db2 ./data/db3
# Start MongoDB instances
mongod --replSet rs0 --port 27017 --dbpath ./data/db1 --bind_ip localhost &
mongod --replSet rs0 --port 27018 --dbpath ./data/db2 --bind_ip localhost &
mongod --replSet rs0 --port 27019 --dbpath ./data/db3 --bind_ip localhost &
# Start MongoDB instances on different ports to avoid conflict with other MongoDB instances
mongod --replSet rs0 --port 27020 --dbpath ./data/db1 --bind_ip localhost &
mongod --replSet rs0 --port 27021 --dbpath ./data/db2 --bind_ip localhost &
mongod --replSet rs0 --port 27022 --dbpath ./data/db3 --bind_ip localhost &
# Wait for MongoDB instances to start
sleep 5
@@ -16,12 +16,12 @@ mongosh --eval '
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "localhost:27017" },
{ _id: 1, host: "localhost:27018" },
{ _id: 2, host: "localhost:27019" }
{ _id: 0, host: "localhost:27020" },
{ _id: 1, host: "localhost:27021" },
{ _id: 2, host: "localhost:27022" }
]
})'
echo "MongoDB replica set initialized. Waiting for primary election..."
echo "MongoDB replica set initialized on ports 27020-27022. Waiting for primary election..."
sleep 5
echo "MongoDB replica set is ready!"

View File

@@ -43,7 +43,9 @@ const dashboard = new ParseDashboard({
],
useEncryptedPasswords: false,
trustProxy: 1
}, { allowInsecureHTTP: process.env.NODE_ENV === 'dev' });
}, {
allowInsecureHTTP: process.env.NODE_ENV === 'dev'
});
// Main server initialization
(async () => {
@@ -58,7 +60,11 @@ const dashboard = new ParseDashboard({
// CORS configuration
const corsOptions = {
origin: [
'*',
'http://localhost:4040',
'http://localhost:3000',
'https://dracattus.com',
'https://api.dracattus.com',
'*'
],
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: [
@@ -78,7 +84,8 @@ const dashboard = new ParseDashboard({
],
exposedHeaders: ['X-Parse-Session-Token'],
credentials: true,
optionsSuccessStatus: 200
optionsSuccessStatus: 200,
preflightContinue: false
};
// Apply CORS middleware
@@ -240,27 +247,11 @@ const dashboard = new ParseDashboard({
console.log('Parse Auditor configured to track changes to important classes');
// Configure scheduled jobs
const scheduleOptions = {
// Run daily rewards job every day at midnight (0 0 * * *)
dailyRewards: {
schedule: '0 0 * * *',
description: 'Process daily rewards for active users',
retryOnError: true,
timeout: 30 * 60 * 1000, // 30 minute timeout
useMasterKey: true
}
};
try {
await api.scheduleJobs(scheduleOptions);
console.log('Scheduled jobs configured successfully');
} catch (scheduleError) {
console.error('Error setting up scheduled jobs:', scheduleError);
}
// Note: Scheduled jobs are handled by the cloud/scheduler module
console.log('Job scheduling is handled by the cloud/scheduler module');
// Mount Parse API AFTER the metadata endpoint
app.use('/v1', api.app);
app.use('/api', api.app);
// Mount Parse Dashboard
app.use('/dashboard', dashboard);
@@ -278,7 +269,7 @@ const dashboard = new ParseDashboard({
httpServer.listen(process.env.httpPORT, () => {
console.log(`\n=== Server Configuration ===`);
console.log(`🚀 Parse Server running at: ${process.env.NODE_ENV === 'dev' ? process.env.serverURL : process.env.serverURLProd}`);
console.log(`📊 Parse Dashboard: ${process.env.NODE_ENV === 'dev' ? process.env.serverURL.replace('/v1', '/dashboard') : process.env.serverURLProd.replace('/v1', '/dashboard')}`);
console.log(`📊 Parse Dashboard: ${process.env.NODE_ENV === 'dev' ? process.env.serverURL.replace('/api', '/dashboard') : process.env.serverURLProd.replace('/api', '/dashboard')}`);
console.log(`🌍 Environment: ${process.env.NODE_ENV}`);
console.log(`🔌 HTTP Port: ${process.env.httpPORT}`);
console.log(`🌐 Production Mode: ${process.env.NODE_ENV === 'prod'}`);

View File

@@ -13,12 +13,12 @@ mongosh --eval '
rs.initiate({
_id: "rs0",
members: [
{ _id: 0, host: "localhost:27017" },
{ _id: 1, host: "localhost:27018" },
{ _id: 2, host: "localhost:27019" }
{ _id: 0, host: "localhost:27020" },
{ _id: 1, host: "localhost:27021" },
{ _id: 2, host: "localhost:27022" }
]
})'
echo "Replica set initialized. Waiting for primary election..."
echo "Replica set initialized on ports 27020-27022. Waiting for primary election..."
sleep 5
mongosh --eval 'rs.status()'

11
start-server.sh Executable file
View File

@@ -0,0 +1,11 @@
#!/bin/bash
# Set up logs directory
mkdir -p logs
# Start the server and log output
node server.js > logs/server.log 2>&1 &
# Print the PID
echo "Server started with PID $!"
echo "View logs with: tail -f logs/server.log"