Spaces:
Running
Running
| const express = require('express'); | |
| const { Canvas, loadImage } = require('skia-canvas'); | |
| const axios = require('axios'); // For Translation | |
| const app = express(); | |
| const multer = require('multer'); // เทเถบเทเถฝเท เถเถฑเทเถฑ | |
| const { Sticker, StickerTypes } = require('wa-sticker-formatter'); | |
| const upload = multer({ storage: multer.memoryStorage() }); | |
| app.use(express.json({ limit: '50mb' })); | |
| app.get('/', (req, res) => res.send("๐ Cipher-MD ULTRA-SHARP ENGINE")); | |
| // ============================================================ | |
| // ๐จ 1. WELCOME IMAGE GENERATOR (Exact Custom Design) | |
| // ============================================================ | |
| app.post('/welcome', upload.single('image'), async (req, res) => { | |
| try { | |
| // เทเทเถงเท เถเถเถเท เถฑเทเถญเทเถฑเถธเท เถเถปเถปเท เถเถเถเท | |
| if (!req.file) return res.status(400).send("No Profile Pic Uploaded"); | |
| // เถถเทเถงเทเถเทเถฑเท เถเถฑ เถฉเทเถงเท (gName = title เทเทเถฏเทเทเถง เถเถฑเทเถฑเทเท) | |
| const { title, count, userName } = req.body; | |
| const gName = title || "Group Name"; // เถฑเถธ เทเทเทเท เถฑเถธเท default เถเถเถเท | |
| const size = 1024; | |
| const canvas = new Canvas(size, size); | |
| const ctx = canvas.getContext('2d'); | |
| const watermark = "SYNTIOX VISUAL ENGINE"; | |
| // Style 1 เทเท 3 เทเทเถญเถปเถบเท เถดเทเทเทเถ เทเถ เท เถเถปเถฑเทเถฑเท | |
| const styleId = Math.random() > 0.5 ? 1 : 3; | |
| // 1. Image Load (Buffer เถเถเทเถฑเท เถเทเถฝเทเถฑเทเถธ เถเถฑเทเถฑเทเท) | |
| const img = await loadImage(req.file.buffer); | |
| // 2. Background Blur | |
| ctx.filter = 'blur(50px) brightness(0.5)'; | |
| ctx.drawImage(img, -50, -50, size + 100, size + 100); | |
| ctx.filter = 'none'; | |
| // 3. Styles Logic (1 & 3) | |
| if (styleId === 1) { | |
| ctx.strokeStyle = 'rgba(0, 242, 255, 0.1)'; | |
| for (let i = 0; i < size; i += 60) { | |
| ctx.beginPath(); ctx.moveTo(i, 0); ctx.lineTo(i, size); ctx.stroke(); | |
| ctx.beginPath(); ctx.moveTo(0, i); ctx.lineTo(size, i); ctx.stroke(); | |
| } | |
| } else { | |
| ctx.strokeStyle = 'rgba(255, 255, 255, 0.2)'; | |
| ctx.lineWidth = 1; | |
| for (let i = 0; i < 20; i++) { | |
| ctx.beginPath(); ctx.moveTo(Math.random() * size, Math.random() * size); | |
| ctx.lineTo(Math.random() * size, Math.random() * size); ctx.stroke(); | |
| } | |
| } | |
| // 4. Highlight Beam (เถเถนเท เถเทเถฉเท เถเถเท เถญเทเถถเทเถถ เถ เถฝเทเถญเท เถเทเถฝเทเถฝ) | |
| ctx.save(); | |
| const beamGrad = ctx.createLinearGradient(0, 0, size, size); | |
| beamGrad.addColorStop(0, 'rgba(0, 242, 255, 0)'); | |
| beamGrad.addColorStop(0.5, 'rgba(0, 242, 255, 0.15)'); | |
| beamGrad.addColorStop(1, 'rgba(0, 242, 255, 0)'); | |
| ctx.fillStyle = beamGrad; ctx.rotate(-15 * Math.PI / 180); | |
| ctx.fillRect(-size, size / 3, size * 2, 150); ctx.restore(); | |
| // 5. โ AVATAR CENTERED (เถดเทเถงเท เถเถ เทเถปเท เถธเทเถฏเถง) | |
| const centerX = size / 2; | |
| const centerY = size / 2; | |
| const radius = 250; | |
| ctx.save(); | |
| ctx.strokeStyle = '#00f2ff'; ctx.lineWidth = 12; ctx.shadowBlur = 40; ctx.shadowColor = '#00f2ff'; | |
| ctx.beginPath(); ctx.arc(centerX, centerY, radius + 15, 0, Math.PI * 2); ctx.stroke(); ctx.restore(); | |
| ctx.save(); | |
| ctx.beginPath(); ctx.arc(centerX, centerY, radius, 0, Math.PI * 2); ctx.clip(); | |
| ctx.drawImage(img, centerX - radius, centerY - radius, radius * 2, radius * 2); ctx.restore(); | |
| // 6. โ TEXT LAYOUT | |
| ctx.textAlign = 'center'; | |
| // "W E L C O M E" Label | |
| ctx.font = 'bold 60px sans-serif'; | |
| ctx.strokeStyle = 'rgba(0, 242, 255, 0.8)'; | |
| ctx.lineWidth = 3; | |
| ctx.strokeText("W E L C O M E", centerX, centerY + radius + 120); | |
| // Group Name & Count | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.7)'; | |
| ctx.font = '35px sans-serif'; | |
| // Emoji Safe Regex เถเถเถญเท เถฏเทเถฝเท เถญเทเถบเทเถฑเทเถฑเท เถเถปเถเทเทเทเทเถง | |
| let safeTitle = gName.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, ''); | |
| safeTitle = await translateSinhalaToEnglish(safeTitle); | |
| ctx.fillText(`${safeTitle} โข MEMBER #${count}`, centerX, centerY + radius + 190); | |
| // User Name (if provided) | |
| if (userName) { | |
| let safeUserName = userName.replace(/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g, ''); | |
| safeUserName = await translateSinhalaToEnglish(safeUserName); | |
| ctx.fillStyle = '#00f2ff'; | |
| ctx.font = 'bold 45px sans-serif'; | |
| ctx.fillText(safeUserName, centerX, centerY + radius + 250); | |
| } | |
| // Watermark | |
| ctx.globalAlpha = 0.3; | |
| ctx.font = 'bold 25px sans-serif'; | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.fillText(watermark, centerX, size - 30); | |
| // เถบเทเทเทเถธ | |
| const buffer = await canvas.toBuffer('png'); | |
| res.set('Content-Type', 'image/png'); | |
| res.send(buffer); | |
| } catch (e) { | |
| console.error(e); | |
| res.status(500).send("Welcome Gen Error: " + e.message); | |
| } | |
| }); | |
| // ============================================================ | |
| // ๐ผ๏ธ 2. WATERMARK (Only for .img gen) | |
| // ============================================================ | |
| app.post('/watermark', upload.single('image'), async (req, res) => { | |
| try { | |
| if (!req.file) return res.status(400).send("No image"); | |
| const { watermarkText } = req.body; | |
| const finalWatermark = watermarkText || 'CIPHER-MD GEN ENGINE'; | |
| const img = await loadImage(req.file.buffer); | |
| const canvas = new Canvas(img.width, img.height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.drawImage(img, 0, 0); | |
| // Add Watermark | |
| const fontSize = Math.floor(img.width * 0.02); | |
| ctx.font = `bold ${fontSize}px sans-serif`; | |
| ctx.textAlign = 'right'; | |
| ctx.shadowColor = "rgba(0, 0, 0, 0.5)"; | |
| ctx.shadowBlur = 5; ctx.shadowOffsetX = 3; ctx.shadowOffsetY = 3; | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.3)'; | |
| ctx.fillText(finalWatermark, img.width - 20, img.height - 20); | |
| res.set('Content-Type', 'image/png'); | |
| res.send(await canvas.toBuffer('png')); | |
| } catch (e) { res.status(500).send("WM Error"); } | |
| }); | |
| // ============================================================ | |
| // ๐ ๏ธ HELPER: SINHALA TO ENGLISH TRANSLATION & TRANSLITERATION | |
| // ============================================================ | |
| async function translateSinhalaToEnglish(text) { | |
| if (!text) return text; | |
| // Check if it has Sinhala characters | |
| if (!/[\u0D80-\u0DFF]/.test(text)) return text; | |
| try { | |
| const url = `https://translate.googleapis.com/translate_a/single?client=gtx&sl=si&tl=en&dt=t&q=${encodeURIComponent(text)}`; | |
| const res = await axios.get(url); | |
| let translated = res.data[0][0][0]; | |
| // Capitalize words | |
| return translated.replace(/\b\w/g, l => l.toUpperCase()); | |
| } catch (e) { | |
| console.error("Translation API error:", e.message); | |
| return transliterateSinhala(text); | |
| } | |
| } | |
| const transliterateSinhala = (text) => { | |
| if (!text) return text; | |
| text = text.replace(/\u200D/g, ''); // Remove ZWJ | |
| const vowels = { 'เถ ': 'a', 'เถ': 'aa', 'เถ': 'ae', 'เถ': 'aae', 'เถ': 'i', 'เถ': 'ii', 'เถ': 'u', 'เถ': 'uu', 'เถ': 'e', 'เถ': 'ee', 'เถ': 'ai', 'เถ': 'o', 'เถ': 'oo', 'เถ': 'au' }; | |
| const consonants = { 'เถ': 'k', 'เถ': 'kh', 'เถ': 'g', 'เถ': 'gh', 'เถ': 'ng', 'เถ': 'ng', 'เถ ': 'ch', 'เถก': 'chh', 'เถข': 'j', 'เถฃ': 'jh', 'เถค': 'ny', 'เถฅ': 'gn', 'เถฆ': 'ndj', 'เถง': 't', 'เถจ': 'th', 'เถฉ': 'd', 'เถช': 'dh', 'เถซ': 'n', 'เถฌ': 'nd', 'เถญ': 't', 'เถฎ': 'th', 'เถฏ': 'd', 'เถฐ': 'dh', 'เถฑ': 'n', 'เถณ': 'nd', 'เถด': 'p', 'เถต': 'ph', 'เถถ': 'b', 'เถท': 'bh', 'เถธ': 'm', 'เถน': 'mb', 'เถบ': 'y', 'เถป': 'r', 'เถฝ': 'l', 'เท': 'v', 'เท': 'sh', 'เท': 'sh', 'เท': 's', 'เท': 'h', 'เท ': 'l', 'เท': 'f' }; | |
| const modifiers = { 'เท': 'a', 'เท': 'ae', 'เท': 'aae', 'เท': 'i', 'เท': 'ii', 'เท': 'u', 'เท': 'uu', 'เท': 'ru', 'เท': 'e', 'เท': 'ee', 'เท': 'ai', 'เท': 'o', 'เท': 'oo', 'เท': 'au', 'เท': '' }; | |
| const specials = { 'เถ': 'n', 'เถ': 'h', 'เทฒ': 'ruu' }; | |
| let res = ""; | |
| for (let i = 0; i < text.length; i++) { | |
| let c = text[i]; | |
| if (vowels[c]) { | |
| res += vowels[c]; | |
| } else if (specials[c]) { | |
| res += specials[c]; | |
| } else if (consonants[c]) { | |
| res += consonants[c]; | |
| if (i + 1 < text.length && modifiers[text[i + 1]] !== undefined) { | |
| res += modifiers[text[i + 1]]; | |
| i++; | |
| } else { | |
| res += "a"; | |
| } | |
| } else { | |
| res += c; | |
| } | |
| } | |
| // Capitalize first letter of words to make it look nicer | |
| return res.replace(/\b\w/g, l => l.toUpperCase()); | |
| }; | |
| // ============================================================ | |
| // ๐ ๏ธ HELPER: ROUNDED RECTANGLE | |
| // ============================================================ | |
| function roundedRect(ctx, x, y, width, height, radius) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + radius, y); | |
| ctx.lineTo(x + width - radius, y); | |
| ctx.quadraticCurveTo(x + width, y, x + width, y + radius); | |
| ctx.lineTo(x + width, y + height - radius); | |
| ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); | |
| ctx.lineTo(x + radius, y + height); | |
| ctx.quadraticCurveTo(x, y + height, x, y + height - radius); | |
| ctx.lineTo(x, y + radius); | |
| ctx.quadraticCurveTo(x, y, x + radius, y); | |
| ctx.closePath(); | |
| } | |
| // ============================================================ | |
| // ๐ ENGINE 1: ULTRA-SHARP LINE CHART (Gold, P2P, CSE) | |
| // ============================================================ | |
| async function drawLineChart(title, subTitle, history, colorTheme) { | |
| const width = 2400; | |
| const height = 1500; | |
| const canvas = new Canvas(width, height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.scale(2, 2); | |
| // 1. Background (Solid Dark) | |
| ctx.fillStyle = '#131722'; | |
| ctx.fillRect(0, 0, width, height); | |
| // ๐ Precise Layout Calculation | |
| // เถบเถง เถเถฉ (Bottom Padding) เทเทเถฉเท เถเท เท เถ เถเทเถปเท เถเทเทเทเถฑเทเถฑเท เถฑเทเถญเท เทเทเถฑเทเถฑ | |
| const chartX = 20; | |
| const chartY = 120; // Title area height | |
| const chartW = 1050; | |
| const chartH = 530; // Chart height reduced slightly to keep bottom clear | |
| // Data Min/Max | |
| const values = history.map(h => (h.price || h.rate || h.value)); | |
| const minVal = Math.min(...values) * 0.9995; | |
| const maxVal = Math.max(...values) * 1.0005; | |
| const range = maxVal - minVal; | |
| const getY = (val) => chartY + chartH - ((val - minVal) / range) * chartH; | |
| const getX = (i) => chartX + (i / (values.length - 1)) * chartW; | |
| // 2. GRID LINES (Very Subtle) | |
| ctx.lineWidth = 1; | |
| ctx.strokeStyle = '#2a2e39'; | |
| // Horizontal Grids | |
| const rows = 6; | |
| for (let i = 0; i <= rows; i++) { | |
| const y = chartY + (chartH / rows) * i; | |
| ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX + chartW, y); ctx.stroke(); | |
| // Price Label (Sharp Font) | |
| const price = maxVal - (range / rows) * i; | |
| ctx.fillStyle = '#787b86'; | |
| ctx.font = '13px Consolas, monospace'; // Monospace for alignment | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(price.toLocaleString(undefined, { minimumFractionDigits: 2 }), chartX + chartW + 12, y + 8); | |
| } | |
| // Vertical Grids | |
| const cols = 8; | |
| for (let i = 0; i <= cols; i++) { | |
| const x = chartX + (chartW / cols) * i; | |
| ctx.beginPath(); ctx.moveTo(x, chartY); ctx.lineTo(x, chartY + chartH + 60); ctx.stroke(); | |
| } | |
| // 3. THE CHART AREA (Clean Gradient) | |
| ctx.save(); | |
| ctx.beginPath(); | |
| ctx.moveTo(getX(0), getY(values[0])); | |
| for (let i = 0; i < values.length; i++) ctx.lineTo(getX(i), getY(values[i])); | |
| // Close path to bottom of chart area | |
| ctx.lineTo(getX(values.length - 1), chartY + chartH + 60); | |
| ctx.lineTo(getX(0), chartY + chartH + 60); | |
| ctx.closePath(); | |
| // Sharp Gradient (No blurring at bottom) | |
| const gradient = ctx.createLinearGradient(0, chartY, 0, chartY + chartH + 60); | |
| gradient.addColorStop(0, `${colorTheme}55`); // Top: Semi-transparent | |
| gradient.addColorStop(1, `${colorTheme}00`); // Bottom: Completely transparent | |
| ctx.fillStyle = gradient; | |
| ctx.fill(); | |
| ctx.restore(); | |
| // 4. THE MAIN LINE (Thin & Sharp) | |
| ctx.beginPath(); | |
| ctx.lineWidth = 3; // Thinner line for "Pro" look (was 5) | |
| ctx.strokeStyle = colorTheme; | |
| ctx.lineJoin = 'round'; | |
| // Removed shadowBlur here to avoid "smudge" look | |
| for (let i = 0; i < values.length; i++) { | |
| if (i === 0) ctx.moveTo(getX(i), getY(values[i])); | |
| else ctx.lineTo(getX(i), getY(values[i])); | |
| } | |
| ctx.stroke(); | |
| // 5. CURRENT PRICE BADGE (Right Axis) | |
| const lastVal = values[values.length - 1]; | |
| const lastY = getY(lastVal); | |
| // Dotted line to axis | |
| ctx.setLineDash([2, 2]); | |
| ctx.lineWidth = 1; | |
| ctx.beginPath(); ctx.moveTo(getX(values.length - 1), lastY); ctx.lineTo(chartX + chartW, lastY); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // Badge | |
| ctx.fillStyle = colorTheme; | |
| roundedRect(ctx, chartX + chartW + 5, lastY - 11, 115, 22, 3); | |
| ctx.fill(); | |
| ctx.fillStyle = '#ffffff'; | |
| ctx.font = 'bold 13px Consolas'; | |
| ctx.textAlign = 'left'; | |
| ctx.fillText(lastVal.toLocaleString(undefined, { minimumFractionDigits: 2 }), chartX + chartW + 10, lastY + 5); | |
| // Pulsing Dot (Sharp) | |
| const lastX = getX(values.length - 1); | |
| ctx.beginPath(); ctx.arc(lastX, lastY, 5, 0, Math.PI * 2); ctx.fillStyle = '#ffffff'; ctx.fill(); | |
| ctx.beginPath(); ctx.arc(lastX, lastY, 12, 0, Math.PI * 2); ctx.strokeStyle = `${colorTheme}66`; ctx.lineWidth = 2; ctx.stroke(); | |
| // 6. HEADER (Aligned) | |
| ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left'; | |
| ctx.fillText(title, 30, 60); | |
| ctx.fillStyle = colorTheme; ctx.font = 'bold 32px sans-serif'; | |
| ctx.fillText(subTitle, 30, 100); | |
| // 7. WATERMARK (Safe Distance) | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right'; | |
| ctx.fillText('โก Cipher-MD Analytics (4K)', (width / 2) - 20, (height / 2) - 20); | |
| return canvas.toBuffer('png'); | |
| } | |
| // ============================================================ | |
| // ๐ฏ๏ธ ENGINE 2: CANDLESTICK CHART (Fixed Sharpness) | |
| // ============================================================ | |
| app.post('/chart/candle', async (req, res) => { | |
| try { | |
| const { candles, symbol, change, currentPrice } = req.body; | |
| const width = 2400; | |
| const height = 1500; | |
| const canvas = new Canvas(width, height); | |
| const ctx = canvas.getContext('2d'); | |
| ctx.scale(2, 2); | |
| ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, width, height); | |
| const chartX = 20; | |
| const chartY = 60; | |
| const chartW = 1050; | |
| const chartH = 530; | |
| const minPrice = Math.min(...candles.map(c => c.low)); | |
| const maxPrice = Math.max(...candles.map(c => c.high)); | |
| const range = maxPrice - minPrice; | |
| const maxVol = Math.max(...candles.map(c => c.volume)); | |
| const getY = (p) => chartY + chartH - ((p - minPrice) / range) * chartH; | |
| // Grid | |
| ctx.lineWidth = 1; ctx.strokeStyle = '#2a2e39'; | |
| const rows = 8; | |
| for (let i = 0; i <= rows; i++) { | |
| const y = chartY + (chartH / rows) * i; | |
| ctx.beginPath(); ctx.moveTo(chartX, y); ctx.lineTo(chartX + chartW, y); ctx.stroke(); | |
| const p = maxPrice - (range / rows) * i; | |
| ctx.fillStyle = '#787b86'; ctx.textAlign = 'left'; ctx.font = '13px Consolas'; | |
| ctx.fillText(p.toFixed(2), chartX + chartW + 10, y + 4); | |
| } | |
| const candleW = (chartW / candles.length) * 0.7; | |
| const spacing = chartW / candles.length; | |
| candles.forEach((c, i) => { | |
| const x = chartX + (i * spacing) + (spacing / 2); | |
| const isUp = c.close >= c.open; | |
| const color = isUp ? '#089981' : '#f23645'; | |
| // Volume (Sharp, low opacity) | |
| const volH = (c.volume / maxVol) * 120; | |
| ctx.fillStyle = isUp ? 'rgba(8, 153, 129, 0.2)' : 'rgba(242, 54, 69, 0.2)'; | |
| ctx.fillRect(x - candleW / 2, chartY + chartH - volH + 150, candleW, volH); | |
| // Wick (Thin line) | |
| ctx.strokeStyle = color; ctx.lineWidth = 1.5; | |
| ctx.beginPath(); ctx.moveTo(x, getY(c.high)); ctx.lineTo(x, getY(c.low)); ctx.stroke(); | |
| // Body | |
| ctx.fillStyle = color; | |
| const yOpen = getY(c.open); | |
| const yClose = getY(c.close); | |
| ctx.fillRect(x - candleW / 2, Math.min(yOpen, yClose), candleW, Math.max(Math.abs(yOpen - yClose), 1)); | |
| }); | |
| // Price Badge | |
| const lastC = candles[candles.length - 1]; | |
| const lastY = getY(lastC.close); | |
| const lastColor = lastC.close >= lastC.open ? '#089981' : '#f23645'; | |
| ctx.strokeStyle = '#ffffff'; ctx.setLineDash([2, 2]); ctx.lineWidth = 1; | |
| ctx.beginPath(); ctx.moveTo(0, lastY); ctx.lineTo(chartX + chartW, lastY); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| ctx.fillStyle = lastColor; | |
| roundedRect(ctx, chartX + chartW + 5, lastY - 12, 115, 24, 3); ctx.fill(); | |
| ctx.fillStyle = '#ffffff'; ctx.font = 'bold 13px Consolas'; | |
| ctx.fillText(parseFloat(currentPrice).toFixed(2), chartX + chartW + 10, lastY + 5); | |
| // Header | |
| ctx.fillStyle = '#ffffff'; ctx.font = 'bold 42px sans-serif'; ctx.textAlign = 'left'; | |
| ctx.fillText(`${symbol} / USDT`, 30, 60); | |
| ctx.fillStyle = parseFloat(change) >= 0 ? '#089981' : '#f23645'; ctx.font = 'bold 32px sans-serif'; | |
| ctx.fillText(`${change}%`, 30, 100); | |
| ctx.fillStyle = 'rgba(255, 255, 255, 0.1)'; ctx.font = '18px sans-serif'; ctx.textAlign = 'right'; | |
| ctx.fillText('โก Cipher-MD Analytics', (width / 2) - 20, (height / 2) - 20); | |
| res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png')); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| // ============================================================ | |
| // ๐ ENGINE 3: NFT & ARB (Retained Fixed Version) | |
| // ============================================================ | |
| app.post('/chart/nft', async (req, res) => { | |
| try { | |
| const { data } = req.body; | |
| const canvas = new Canvas(800, 400); const ctx = canvas.getContext('2d'); | |
| ctx.fillStyle = '#0a0b0d'; roundedRect(ctx, 0, 0, 800, 400, 25); ctx.fill(); | |
| if (data.image_url) { | |
| try { | |
| const img = await loadImage(data.image_url); | |
| ctx.save(); ctx.beginPath(); roundedRect(ctx, 40, 40, 200, 200, 20); ctx.clip(); | |
| ctx.drawImage(img, 40, 40, 200, 200); ctx.restore(); | |
| } catch (e) { } | |
| } | |
| ctx.fillStyle = '#ffffff'; ctx.font = 'bold 36px sans-serif'; | |
| ctx.fillText(data.name.substring(0, 18), 270, 70); | |
| ctx.fillStyle = '#8a939b'; ctx.font = '24px sans-serif'; | |
| ctx.fillText(`Chain: ${data.chain.toUpperCase()}`, 270, 110); | |
| ctx.fillStyle = '#2081e2'; ctx.font = 'bold 55px sans-serif'; | |
| ctx.fillText(`${data.floor_price} ${data.floor_price_symbol}`, 270, 190); | |
| ctx.fillStyle = '#ffffff'; ctx.font = '18px sans-serif'; | |
| ctx.fillText('FLOOR PRICE', 270, 220); | |
| ctx.fillStyle = '#161b22'; roundedRect(ctx, 270, 260, 480, 80, 15); ctx.fill(); | |
| ctx.fillStyle = '#ffffff'; ctx.font = 'bold 20px sans-serif'; | |
| ctx.textAlign = 'left'; ctx.fillText(`Vol: ${Math.round(data.total_volume)}`, 290, 308); | |
| ctx.textAlign = 'right'; ctx.fillText(`Owners: ${data.owner_count}`, 730, 308); | |
| ctx.textAlign = 'right'; ctx.fillStyle = 'rgba(255,255,255,0.2)'; | |
| ctx.fillText('Cipher-MD NFT', 750, 380); | |
| res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png')); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| app.post('/chart/arb', async (req, res) => { | |
| try { | |
| const { coin, exData } = req.body; | |
| const canvas = new Canvas(900, 600); const ctx = canvas.getContext('2d'); | |
| ctx.fillStyle = '#131722'; ctx.fillRect(0, 0, 900, 600); | |
| ctx.fillStyle = '#ffffff'; ctx.font = 'bold 40px sans-serif'; | |
| ctx.fillText(`${coin} Arbitrage Monitor`, 40, 60); | |
| let y = 120; | |
| const avg = exData.reduce((a, b) => a + b.price, 0) / exData.length; | |
| exData.forEach(ex => { | |
| const diff = (((ex.price - avg) / avg) * 100); | |
| const color = diff >= 0 ? '#089981' : '#f23645'; | |
| ctx.fillStyle = '#1e222d'; roundedRect(ctx, 40, y, 820, 70, 10); ctx.fill(); | |
| ctx.fillStyle = '#b2b5be'; ctx.textAlign = 'left'; ctx.font = 'bold 24px sans-serif'; | |
| ctx.fillText(ex.name, 70, y + 45); | |
| ctx.fillStyle = '#ffffff'; ctx.textAlign = 'right'; | |
| ctx.fillText(`$${ex.price.toLocaleString()}`, 600, y + 45); | |
| ctx.fillStyle = color; | |
| ctx.fillText(`${diff > 0 ? '+' : ''}${diff.toFixed(2)}%`, 830, y + 45); | |
| y += 85; | |
| }); | |
| res.set('Content-Type', 'image/png'); res.send(await canvas.toBuffer('png')); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| // ============================================================ | |
| // โก ROUTERS | |
| // ============================================================ | |
| app.post('/chart/p2p', async (req, res) => { | |
| try { | |
| const { history, currentRate } = req.body; | |
| const buf = await drawLineChart("USDT / LKR (P2P)", `Rs. ${currentRate}`, history, '#2962ff'); | |
| res.set('Content-Type', 'image/png'); res.send(buf); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| app.post('/chart/forex', async (req, res) => { | |
| try { | |
| const { history, currentRate } = req.body; | |
| const buf = await drawLineChart("USD / LKR (Official)", `Rs. ${currentRate}`, history, '#089981'); | |
| res.set('Content-Type', 'image/png'); res.send(buf); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| app.post('/chart/gold', async (req, res) => { | |
| try { | |
| const { history, currentRate } = req.body; | |
| const buf = await drawLineChart("Gold (24K)", `Rs. ${currentRate}`, history, '#f2a900'); | |
| res.set('Content-Type', 'image/png'); res.send(buf); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| app.post('/chart/cse', async (req, res) => { | |
| try { | |
| const { history, currentRate } = req.body; | |
| const buf = await drawLineChart("CSE ASPI", `${currentRate}`, history, '#a300cc'); | |
| res.set('Content-Type', 'image/png'); res.send(buf); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| app.post('/chart/fear', async (req, res) => { | |
| try { | |
| const { history } = req.body; | |
| const last = history[history.length - 1].value; | |
| const color = last > 50 ? '#089981' : '#f23645'; | |
| const buf = await drawLineChart("Fear & Greed", `${last}/100`, history, color); | |
| res.set('Content-Type', 'image/png'); res.send(buf); | |
| } catch (e) { res.status(500).send(e.message); } | |
| }); | |
| const port = 7861; | |
| app.listen(port, () => console.log(`๐ ULTRA-SHARP ENGINE running on port ${port}`)); |