updates
This commit is contained in:
@@ -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
|
||||
|
||||
388
cloud/automations/recent_ai_items.js
Normal file
388
cloud/automations/recent_ai_items.js
Normal 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
|
||||
};
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
@@ -850,5 +850,8 @@ async function setNewLocations() {
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
setNewLocations
|
||||
setNewLocations,
|
||||
shouldBeInWater,
|
||||
getRandomLandLocation,
|
||||
getRandomOceanLocation
|
||||
};
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
Please verify your email address
|
||||
@@ -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}`);
|
||||
}
|
||||
});
|
||||
@@ -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
512
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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!"
|
||||
37
server.js
37
server.js
@@ -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'}`);
|
||||
|
||||
@@ -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
11
start-server.sh
Executable 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"
|
||||
Reference in New Issue
Block a user