import "dotenv/config"; import express from "express"; import http from "http"; import { Server } from "socket.io"; import axios from "axios"; import exceptionHandler from "./exceptionHandler"; import path from "path"; import initDB from "./database/initdb"; import sequelize from "./database/sequelize"; import { log, config, LTHN_ASSET_ID, parseComment, parseTrackingKey, decodeString, FUSD_ASSET_ID } from "./utils/utils"; import { blockInfo, lastBlock, setLastBlock, state, setState, setBlockInfo, PriceData } from "./utils/states"; import { emitSocketInfo, getBlocksDetails, getMainBlockDetails, getTxPoolDetails, getVisibilityInfo } from "./utils/methods"; import AltBlock from "./schemes/AltBlock"; import Transaction from "./schemes/Transaction"; import OutInfo, { IOutInfo } from "./schemes/OutInfo"; import Block, { IBlock } from "./schemes/Block"; import Alias from "./schemes/Alias"; import Chart, { IChart } from "./schemes/Chart"; import { get_all_pool_tx_list, get_alt_blocks_details, get_blocks_details, get_info, get_out_info, get_pool_txs_details, get_tx_details } from "./utils/letheand"; import { fn, literal, Op, Sequelize } from "sequelize"; import Pool from "./schemes/Pool"; import Asset, { IAsset } from "./schemes/Asset"; import { ITransaction } from "./schemes/Transaction"; import BigNumber from "bignumber.js"; import next from "next"; import { rateLimit } from 'express-rate-limit'; import bodyParser from 'body-parser'; import { syncHistoricalPrice, syncLatestPrice } from "./services/ltheanPrice.service"; import LtheanPrice from "./schemes/LtheanPrice"; import cron from "node-cron"; import syncService from "./services/sync.service.ts"; // @ts-ignore const __dirname = import.meta.dirname; const dev = process.env.NODE_ENV !== 'production'; const nextApp = next({ dev }); const handle = nextApp.getRequestHandler(); const app = express(); const server = http.createServer(app); export const io = new Server(server, { transports: ['websocket', 'polling'] }); const requestsLimiter = rateLimit({ windowMs: 5 * 1000, limit: 1, standardHeaders: 'draft-7', legacyHeaders: false, }); let dbInited = false; (async () => { await initDB(); await sequelize.authenticate(); await sequelize.sync(); dbInited = true; })(); async function waitForDb() { while (!dbInited) { await new Promise(resolve => setTimeout(resolve, 100)); } } (async () => { await waitForDb(); await syncLatestPrice(); syncHistoricalPrice(); // async io.engine.on('initial_headers', (headers, req) => { headers['Access-Control-Allow-Origin'] = config.frontend_api }) io.engine.on('headers', (headers, req) => { headers['Access-Control-Allow-Origin'] = config.frontend_api }); await nextApp.prepare(); app.use(express.static('dist')); app.use(function (req, res, next) { const allowedOrigin = config.frontend_api || process.env.FRONTEND_API || ''; res.header('Access-Control-Allow-Origin', allowedOrigin) res.header( 'Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept' ) next() }) app.use(express.static(path.resolve(__dirname, "../build/"))); app.use(bodyParser.json()); app.use([ "/api/find_outs_in_recent_blocks" ], requestsLimiter); // app.use("*", (req, res, next) => { // console.log(`Request path: ${req.path}`); // next(); // }); app.get('/api/find_outs_in_recent_blocks', exceptionHandler(async (req, res) => { const address = req.query.address; const viewkey = req.query.viewkey; const limit = req.query.limit || 5; if (!address) { return res.status(400).json({ error: 'Address is required' }); } if (!viewkey) { return res.status(400).json({ error: 'Viewkey is required' }); } const response = await axios({ method: 'get', url: config.api, data: { method: 'find_outs_in_recent_blocks', params: { "address": address, "viewkey": viewkey, "blocks_limit": limit } } }) res.json(response.data) })); app.get( '/api/get_info/:flags', exceptionHandler(async (req, res) => { let flags = req.params.flags const response = await axios({ method: 'get', url: config.api, data: { method: 'getinfo', params: { flags: parseInt(flags) } } }) res.json(response.data) }) ); app.get('/api/get_asset_supply', exceptionHandler(async (req, res) => { const response = await axios({ method: 'get', url: config.api, data: { method: 'get_asset_info', params: { asset_id: req.query.asset_id || "" } }, }); res.json(response?.data?.result?.asset_descriptor?.current_supply || "Error fetching supply"); })); app.get( '/api/get_total_coins', exceptionHandler(async (req, res) => { const response = await axios({ method: 'get', url: config.api, data: { method: 'getinfo', params: { flags: parseInt("4294967295") } } }) let str = response.data.result.total_coins let result: number | undefined; let totalCoins = Number(str) if (typeof totalCoins === 'number') { result = parseInt(totalCoins.toString()) / 1000000000000 } let r2 = result?.toFixed(2) res.send(r2) }) ); app.get( '/api/get_main_block_details/:id', exceptionHandler(async (req, res) => { try { let id = req.params.id.toLowerCase() if (id) { const result = await getMainBlockDetails(id); return res.json(result || "Block not found"); } res.status(400).json({ error: 'Invalid parameters' }); } catch (error) { console.log(error); res.status(500).json({ error: error.message }); } }) ); app.get('/api/ping', exceptionHandler(async (req, res) => { res.send('pong') })); app.get( '/api/get_assets/:offset/:count', exceptionHandler(async (req, res) => { const offset = parseInt(req.params.offset, 10); const count = parseInt(req.params.count, 10); const searchText = req.query.search || ''; if (!searchText) { // No searchText, fetch all assets with pagination const assets = await Asset.findAll({ order: [['id', 'ASC']], limit: count, offset: offset }); return res.send(assets); } // If there is searchText, count matching records const searchCondition = { [Op.or]: [ { ticker: { [Op.iLike]: `%${searchText}%` } }, { full_name: { [Op.iLike]: `%${searchText}%` } } ] }; const firstSearchRowCount = await Asset.count({ where: searchCondition }); if (firstSearchRowCount > 0) { // Fetch records that match the searchText const assets = await Asset.findAll({ where: searchCondition, order: [['id', 'ASC']], limit: count, offset: offset }); return res.send(assets); } else { // If no matching records found, fetch by asset_id const assets = await Asset.findAll({ where: { asset_id: searchText }, limit: count, offset: offset }); return res.send(assets); } }) ); app.get( "/api/get_historical_lthn_price", exceptionHandler(async (req, res) => { const target_timestamp = req.query.timestamp ? parseInt(req?.query?.timestamp as string) : null; if (!target_timestamp) { return res.json({ success: false, data: 'invalid timestamp' }) } const closestPrice = await LtheanPrice.findOne({ order: [[Sequelize.literal('ABS(timestamp - $targetTs)'), 'ASC']], bind: { targetTs: target_timestamp }, raw: true, }); if (!closestPrice) { return res.json({ success: false, data: 'price not found (unexpected error)' }); } res.json({ success: true, data: { timestamp: closestPrice.timestamp, price: closestPrice.price, src: closestPrice.src, raw: closestPrice.raw } }); }) ); const getWhitelistedAssets = async (offset, count, searchText) => { // Step 1: Fetch assets from external API const response = await axios({ method: 'get', url: config.assets_whitelist_url || 'https://api.lethean.io/assets_whitelist_testnet.json' }); if (!response.data.assets) { throw new Error('Assets whitelist response not correct'); } // Step 2: Add the native Lethean asset to the beginning of the array const allAssets = response.data.assets; allAssets.unshift({ asset_id: LTHN_ASSET_ID, logo: "", price_url: "", ticker: "LTHN", full_name: "Lethean (Native)", total_max_supply: "0", current_supply: "0", decimal_point: 0, meta_info: "", price: 0 }); // Step 3: Filter the assets based on searchText const searchTextLower = searchText?.toLowerCase(); const filteredAssets = allAssets.filter(asset => { return searchText ? ( asset.ticker?.toLowerCase()?.includes(searchTextLower) || asset.full_name?.toLowerCase()?.includes(searchTextLower) ) : true; }); // Step 4: Handle no search result in filtered assets if (filteredAssets.length > 0) { return filteredAssets.slice(offset, offset + count); } else { // Step 5: If no match found, try to fetch assets from the local database (using Sequelize) const dbAssets = await Asset.findAll({ where: { asset_id: searchText }, limit: count, offset: offset }); return dbAssets.map(asset => asset.toJSON()); } }; app.get( '/api/get_whitelisted_assets/:offset/:count', exceptionHandler(async (req, res) => { const offset = parseInt(req.params.offset, 10); const count = parseInt(req.params.count, 10); const searchText = req.query.search || ''; const assets = await getWhitelistedAssets(offset, count, searchText); res.send(assets); }) ); app.get( '/api/get_aliases/:offset/:count/:search/:filter', exceptionHandler(async (req, res, next) => { const { offset, count, search, filter } = req.params; const limit = Math.min(parseInt(count), config.maxDaemonRequestCount); const parsedOffset = parseInt(offset); const searchTerm = search.toLowerCase(); const premiumOnly = filter === 'premium'; const whereClause: any = { enabled: true, }; if (searchTerm !== 'all') { whereClause[Op.or] = [ Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('alias')), { [Op.like]: `%${searchTerm.toLowerCase()}%` }), Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('address')), { [Op.like]: `%${searchTerm.toLowerCase()}%` }), Sequelize.where(Sequelize.fn('LOWER', Sequelize.col('comment')), { [Op.like]: `%${searchTerm.toLowerCase()}%` }), ]; } if (premiumOnly) { whereClause.alias = Sequelize.where(Sequelize.fn("CHAR_LENGTH", Sequelize.col("alias")), { [Op.lte]: 5 }); } try { const aliasesRow = await Alias.findAll({ where: whereClause, order: [['block', 'DESC']], limit, offset: parsedOffset, }); const aliasesAddresses = aliasesRow.map((aliasRow) => aliasRow.address); let registeredAddresses: string[] = []; try { const addressesHasMatrixConnectionResp = await axios({ method: "post", url: config.matrix_api_url + "/get-addresses", data: { addresses: aliasesAddresses }, transformResponse: [(data) => JSON.parse(data)], }); const addresses = addressesHasMatrixConnectionResp.data.addresses; registeredAddresses = addresses.filter((address) => address.registered === true).map(({ address }) => address); } catch (e) { console.error(e) } const aliases = aliasesRow.map((aliasRow) => { const hasMatrixConnection = registeredAddresses.includes(aliasRow.address); aliasRow.dataValues.hasMatrixConnection = hasMatrixConnection; return aliasRow }) res.json(aliases.length > 0 ? aliases : []); } catch (error) { next(error); } }) ); async function fetchWhitelistedAssets() { const response = await axios({ method: 'get', url: config.assets_whitelist_url || 'https://api.lethean.io/assets_whitelist_testnet.json' }); if (!response.data.assets) { throw new Error('Assets whitelist response not correct'); } const allAssets = response.data.assets; return allAssets; } app.get( '/api/get_assets_count', exceptionHandler(async (req, res) => { const whitelistedAssets = await fetchWhitelistedAssets(); const whitelistedAssetsAmount = whitelistedAssets.length + 1; const assetsAmount = await Asset.count(); return res.json({ assetsAmount, whitelistedAssetsAmount }); }), ); app.get( '/api/get_aliases_count', exceptionHandler(async (req, res) => { const aliasesAmount = await Alias.count({ where: { enabled: true } }); const premiumAliasesAmount = await Alias.count({ where: { [Op.and]: [ sequelize.where( sequelize.fn( 'LENGTH', sequelize.col('alias') ), { [Op.lte]: 5 }, ), { enabled: true } ] } }); return res.json({ aliasesAmount, premiumAliasesAmount }); }), ); interface AggregatedData { at: number; sum_trc?: number; bcs?: number; trc?: number; totalSize?: number; totalTrans?: number; totalD120?: number; totalH100?: number; totalH400?: number; maxDiff?: number; minDiff?: number; sumDiff?: number; count?: number; d?: number; } app.get('/api/get_chart/:chart/:offset', async (req, res) => { const { chart, offset } = req.params; const offsetDate = parseInt(offset, 10) / 1000; if (!chart) { return res.status(400).json({ error: 'Invalid parameters' }); } if (chart === 'AvgBlockSize') { console.log('loading AvgBlockSize'); const result = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 3600)'), 'at', ], [fn('avg', literal('"block_cumulative_size"::REAL')), 'bcs'], ], group: ['at'], order: [[literal('"at"'), 'ASC']], where: { actual_timestamp: { [Op.gt]: offsetDate, }, }, raw: true, }); res.send(result); } else if (chart === 'AvgTransPerBlock') { const result = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 3600)'), 'at', ], [fn('avg', literal('"tr_count"::REAL')), 'trc'], ], group: ['at'], order: [[literal('"at"'), 'ASC']], where: { actual_timestamp: { [Op.gt]: offsetDate, }, }, raw: true, }); res.send(result); } else if (chart === 'hashRate') { console.time('hashRate'); const result = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 3600)'), 'at', ], [fn('avg', literal('"difficulty120"::REAL')), 'd120'], [fn('avg', literal('"hashrate100"::REAL')), 'h100'], [fn('avg', literal('"hashrate400"::REAL')), 'h400'], ], group: ['at'], where: { type: '1', actual_timestamp: { [Op.gt]: offsetDate, }, }, order: [[literal('"at"'), 'ASC']], raw: true, }); console.timeEnd('hashRate'); res.send(result); } else if (chart === 'pos-difficulty') { // Aggregated data at 3600-second intervals const result = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 3600)'), 'at', ], [ literal( `CASE WHEN (MAX("difficulty"::NUMERIC) - AVG("difficulty"::NUMERIC)) > (AVG("difficulty"::NUMERIC) - MIN("difficulty"::NUMERIC)) THEN MAX("difficulty"::NUMERIC) ELSE MIN("difficulty"::NUMERIC) END` ), 'd', ], ], group: ['at'], order: [[literal('"at"'), 'ASC']], where: { type: '0', actual_timestamp: { [Op.gt]: offsetDate, }, }, raw: true, }); // Detailed data at 3600-second intervals using AVG const result1 = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 3600)'), 'at', ], [fn('avg', literal('"difficulty"::NUMERIC')), 'd'], ], group: ['at'], order: [[literal('"at"'), 'ASC']], where: { type: '0', actual_timestamp: { [Op.gt]: offsetDate, }, }, raw: true, }); console.log('pos-difficulty', result1.length, result.length); res.send({ aggregated: result, detailed: result1, }); } else if (chart === 'pow-difficulty') { // Aggregated data at 3600-second intervals const result = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 3600)'), 'at', ], [ literal( `CASE WHEN (MAX("difficulty"::NUMERIC) - AVG("difficulty"::NUMERIC)) > (AVG("difficulty"::NUMERIC) - MIN("difficulty"::NUMERIC)) THEN MAX("difficulty"::NUMERIC) ELSE MIN("difficulty"::NUMERIC) END` ), 'd', ], ], group: ['at'], order: [[literal('"at"'), 'ASC']], where: { type: '1', actual_timestamp: { [Op.gt]: offsetDate, }, }, raw: true, }); // Detailed data at 3600-second intervals using AVG const result1 = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 3600)'), 'at', ], [fn('avg', literal('"difficulty"::REAL')), 'd'], ], group: ['at'], order: [[literal('"at"'), 'ASC']], where: { type: '1', actual_timestamp: { [Op.gt]: offsetDate, }, }, raw: true, }); res.send({ aggregated: result, detailed: result1, }); } else if (chart === 'ConfirmTransactPerDay') { const offsetDateStartOfDay = new Date(offsetDate); offsetDateStartOfDay.setHours(0, 0, 0, 0); // Group by day (86400-second intervals) const result = await Chart.findAll({ attributes: [ [ literal('"actual_timestamp" - ("actual_timestamp" % 86400)'), 'at', ], [fn('sum', literal('"tr_count"::REAL')), 'sum_trc'], ], group: ['at'], order: [[literal('"at"'), 'ASC']], where: { actual_timestamp: { [Op.gt]: +offsetDateStartOfDay / 1000, }, }, raw: true, }); res.send(result); } else { res.status(400).json({ error: 'Invalid chart type' }); } }); app.get( '/api/get_tx_details/:tx_hash', exceptionHandler(async (req, res) => { try { const tx_hash = req.params.tx_hash.toLowerCase(); if (tx_hash) { // Fetching transaction details with associated block information using Sequelize const transaction = await Transaction.findOne({ where: { tx_id: tx_hash, keeper_block: { [Op.ne]: null } }, }); const transactionBlock = await Block.findOne({ where: { height: transaction?.keeper_block }, }).catch(() => null); if (transaction && transactionBlock) { const response = { ...transaction.toJSON(), block_hash: transactionBlock?.tx_id, block_timestamp: transactionBlock?.timestamp, last_block: lastBlock.height, }; res.json(response); } else { const response = await get_tx_details(tx_hash); const data = response.data; if (data?.result?.tx_info) { if (data.result.tx_info.ins && typeof data.result.tx_info.ins === 'object') { data.result.tx_info.ins = JSON.stringify(data.result.tx_info.ins); } if (data.result.tx_info.outs && typeof data.result.tx_info.outs === 'object') { data.result.tx_info.outs = JSON.stringify(data.result.tx_info.outs); } const blockInfo = await Block.findOne({ where: { height: data.result?.tx_info?.keeper_block?.toString() }, }); res.json({ ...data.result.tx_info, ...(blockInfo?.toJSON() || {}), last_block: lastBlock.height, }); } else { res.status(500).json({ message: `/get_tx_details/:tx_hash ${JSON.stringify(req.params)}`, }); } } } else { res.status(500).json({ message: `/get_tx_details/:tx_hash ${JSON.stringify(req.params)}`, }); } } catch (error) { console.log(error); res.status(500).json({ error: error.message }); } }) ); app.get( "/api/get_tx_by_keyimage/:id", exceptionHandler(async (req, res, next) => { const id = req.params.id.toLowerCase(); try { // Find transactions where 'ins' column contains the keyimage const transactions = await Transaction.findAll({ where: { ins: { [Op.like]: `%"kimage_or_ms_id":"${id}"%` } } }); // Iterate through each transaction to find the exact match within the 'ins' field for (const tx of transactions) { try { const ins = JSON.parse(tx.ins); if (Array.isArray(ins) && ins.find(e => e.kimage_or_ms_id === id)) { return res.json({ result: "FOUND", data: tx.id }); } } catch (error) { // Skip the transaction if there's an error parsing the 'ins' field } } return res.json({ result: "NOT FOUND" }); } catch (error) { next(error); } }) ); app.get( '/api/search_by_id/:id', exceptionHandler(async (req, res, next) => { const id = req.params.id.toLowerCase(); if (!id) { return res.json({ result: 'NOT FOUND' }); } try { // Search in the 'blocks' table let blockResult = await Block.findOne({ where: { tx_id: id } }); if (blockResult) { return res.json({ result: 'block' }); } // Search in the 'alt_blocks' table let altBlockResult = await AltBlock.findOne({ where: { hash: id } }); if (altBlockResult) { return res.json({ result: 'alt_block' }); } // Search in the 'transactions' table let transactionResult = await Transaction.findOne({ where: { tx_id: id } }); if (transactionResult) { return res.json({ result: 'tx' }); } // Attempt to get transaction details from an external service try { let response = await get_tx_details(id); if (response.data.result) { return res.json({ result: 'tx' }); } } catch (error) { // Ignore the error and continue searching in the database } // Search in the 'aliases' table let aliasResult = await Alias.findOne({ where: { enabled: true, [Op.or]: [ { alias: { [Op.like]: `%${id}%` } }, { address: { [Op.like]: `%${id}%` } }, { comment: { [Op.like]: `%${id}%` } } ] }, order: [['block', 'DESC']], }); if (aliasResult) { return res.json({ result: 'alias' }); } // If nothing is found return res.json({ result: 'NOT FOUND' }); } catch (error) { console.log(error); next(error); } }) ); app.get( '/api/get_out_info/:amount/:i', exceptionHandler(async (req, res) => { const { amount, i } = req.params; const index = parseInt(i, 10); if (amount && !isNaN(index)) { const outInfo = await OutInfo.findOne({ where: { amount: amount, i: index, }, }); if (!outInfo) { const response = await get_out_info(amount, index); res.json({ tx_id: response.data.result.tx_id }); } else { res.json(outInfo.toJSON()); } } else { res.status(500).json({ message: `/get_out_info/:amount/:i ${req.params}`, }); } }) ); app.get( '/api/get_info', exceptionHandler((_, res) => { blockInfo.lastBlock = lastBlock.height res.json(blockInfo); }) ); app.get( '/api/get_blocks_details/:start/:count', exceptionHandler(async (req, res) => { try { const start = parseInt(req.params.start, 10); const count = parseInt(req.params.count, 10); if (!isNaN(start) && start >= 0 && count) { const result = await getBlocksDetails({ start, count }); res.json(result); } else { res.status(400).json({ error: 'Invalid parameters' }); } } catch (error) { console.log(error); res.status(500).json({ error: error.message }); } }) ); app.get('/api/get_asset_details/:asset_id', exceptionHandler(async (req, res) => { const { asset_id } = req.params; // Check if the asset exists in the database using Sequelize const dbAsset = await Asset.findOne({ where: { asset_id } }); if (!dbAsset) { try { // Fetch the assets whitelist from the external API const response = await axios.get(config.assets_whitelist_url || 'https://api.lethean.io/assets_whitelist_testnet.json'); if (!response.data.assets) { throw new Error('Assets whitelist response not correct'); } const allAssets = response.data.assets; // Add Lethean (Native) asset as the first item in the list allAssets.unshift({ asset_id: LTHN_ASSET_ID, logo: "", price_url: "", ticker: "LTHN", full_name: "Lethean (Native)", total_max_supply: "0", current_supply: "0", decimal_point: 0, meta_info: "", price: 0 }); // Find the asset in the whitelist const whitelistedAsset = allAssets.find(e => e.asset_id === asset_id); if (whitelistedAsset) { return res.json({ success: true, asset: whitelistedAsset }); } else { return res.json({ success: false, data: "Asset not found" }); } } catch (error) { return res.status(500).json({ success: false, error: error.message }); } } else { // If asset is found in the database, return it return res.json({ success: true, asset: dbAsset }); } })); app.get( '/api/get_visibility_info', exceptionHandler(async (_, res) => { const result = await getVisibilityInfo(); res.send(result) }) ); app.get( '/api/get_tx_pool_details/:count', exceptionHandler(async (req, res, next) => { let count = parseInt(req.params.count, 10); if (count !== undefined) { res.json(await getTxPoolDetails(count)) } else { res.status(400).json({ error: 'Invalid parameters' }); } }) ); app.get( '/api/get_alt_blocks_details/:offset/:count', exceptionHandler(async (req, res, next) => { let offset = parseInt(req.params.offset) let count = parseInt(req.params.count) if (count > config.maxDaemonRequestCount) { count = config.maxDaemonRequestCount } const result = await AltBlock.findAll({ order: [['height', 'DESC']], limit: count, offset: offset }); return result.length > 0 ? result.map(e => e.toJSON()) : []; }) ); app.get( '/api/get_alt_block_details/:id', exceptionHandler(async (req, res, next) => { const id = req.params.id.toLowerCase(); if (id) { const altBlock = await AltBlock.findOne({ where: { hash: id } }); if (altBlock) { res.json(altBlock.toJSON()); } else { res.json([]); } } else { res.status(500).json({ message: `/get_out_info/:amount/:i ${req.params}` }); } }) ); app.get( '/api/get_alt_blocks_details/:offset/:count', exceptionHandler(async (req, res) => { let offset = req.params.offset let count = req.params.count const response = await axios({ method: 'get', url: config.api, data: { method: 'get_alt_blocks_details', params: { offset: parseInt(offset), count: parseInt(count) } } }) res.json(response.data) }) ) app.get( '/api/get_alt_block_details/:id', exceptionHandler(async (req, res) => { let id = req.params.id const response = await axios({ method: 'get', url: config.api, data: { method: 'get_alt_block_details', params: { id: id } } }) res.json(response.data) }) ) app.get( '/api/get_all_pool_tx_list', exceptionHandler(async (req, res) => { const response = await axios({ method: 'get', url: config.api, data: { method: 'get_all_pool_tx_list' } }) res.json(response.data) }) ) app.get( '/api/get_pool_txs_details', exceptionHandler(async (req, res) => { const response = await axios({ method: 'get', url: config.api, data: { method: 'get_pool_txs_details' } }) res.json(response.data) }) ) app.get( '/api/get_pool_txs_brief_details', exceptionHandler(async (req, res) => { const response = await axios({ method: 'get', url: config.api, data: { method: 'get_pool_txs_brief_details' } }) res.json(response.data) }) ) app.get('/api/price', exceptionHandler(async (req, res) => { function calcFiatPrice(pricePerAssetUSD: number | undefined, ratesPerUSD: { [key: string]: number | undefined; }) { const fiatPrices: { [key: string]: number } = {}; const entries = Object.entries(ratesPerUSD); for (const entry of entries) { const [fiatName, pricePerUsd] = entry; fiatPrices[fiatName] = ((pricePerAssetUSD || 0) * (pricePerUsd || 0)); } const entitiesToExclude = [ "lastUpdated", "btc", "eth", "ltc", "bch", "bnb", "eos", "xrp", "xlm", "link", "dot", "yfi", ] for (const entity of entitiesToExclude) { delete fiatPrices[entity]; } return fiatPrices; } interface ResponsePriceData { name: string; lthn_price?: number; usd: number | undefined; usd_24h_change: number | undefined; fiat_prices: { [key: string]: number; }; } interface ResponseData { success: boolean; data: { [key: string]: ResponsePriceData; }; } if (req.query.asset_id) { if (req.query.asset_id === LTHN_ASSET_ID) { if (!state.priceData?.lethean?.price) { return res.send({ success: false, data: 'Price not found' }); } return res.send({ success: true, data: { name: 'Lethean', usd: state.priceData?.lethean?.price, lthn_price: 1, usd_24h_change: state.priceData?.lethean?.usd_24h_change, fiat_prices: calcFiatPrice(state.priceData?.lethean?.price, state?.fiat_rates), } }); } if (req.query.asset_id === FUSD_ASSET_ID) { if (!state.priceData?.fusd?.price) { return res.send({ success: false, data: 'Price not found' }); } return res.send({ success: true, data: { name: 'FUSD', usd: state.priceData?.fusd?.price, lthn_price: (state.priceData?.fusd?.price / (state.priceData?.lethean?.price || 1)), usd_24h_change: state.priceData?.fusd?.usd_24h_change, fiat_prices: calcFiatPrice(state.priceData?.fusd?.price, state?.fiat_rates), } }); } const assetData = await Asset.findOne({ where: { asset_id: req.query.asset_id } }); if (!assetData) { return res.json({ success: false, data: "Asset not found" }); } const assetsPricesResponse = await axios({ method: 'post', url: config.trade_api_url + '/dex/get-assets-price-rates', data: { assetsIds: [assetData.asset_id] }, }); const usdPrice = ((assetsPricesResponse?.data?.priceRates?.[0]?.rate || 0) * (state.priceData?.lethean?.price || 0)) return res.json({ success: true, data: { name: assetData.full_name, usd: usdPrice, lthn_price: (usdPrice / (state.priceData?.lethean?.price || 1)), usd_24h_change: null, fiat_prices: calcFiatPrice(usdPrice, state?.fiat_rates), } }); // Assuming that you handle further processing of the `assetData` here... } const responseData: ResponseData = { success: true, data: { lethean: { name: "Lethean", usd: state.priceData?.lethean?.price, lthn_price: 1, usd_24h_change: state.priceData?.lethean?.usd_24h_change, fiat_prices: calcFiatPrice(state.priceData?.lethean?.price, state?.fiat_rates), } } }; switch (req.query.asset) { case "ethereum": if (state.priceData?.ethereum?.price === undefined) { responseData.data = {}; responseData.success = false; } else { responseData.data = { ethereum: { usd: state.priceData?.ethereum?.price, lthn_price: state.priceData?.ethereum?.price / (state.priceData?.lethean?.price || 1), usd_24h_change: state.priceData?.ethereum?.usd_24h_change, name: "Ethereum", fiat_prices: calcFiatPrice(state.priceData?.ethereum?.price, state?.fiat_rates), } }; } break; default: if (state.priceData?.lethean?.price === undefined) { responseData.data = {}; responseData.success = false; } break; } return res.json(responseData); })); app.get('/api/get_asset_details/:asset_id', exceptionHandler(async (req, res) => { const { asset_id } = req.params; const dbAsset = await Asset.findOne({ where: { asset_id } }); if (!dbAsset) { // Fetch the external asset list const response = await axios({ method: 'get', url: config.assets_whitelist_url || 'https://api.lethean.io/assets_whitelist_testnet.json' }); if (!response.data.assets) { throw new Error('Assets whitelist response not correct'); } // Add Lethean to the beginning of the list const allAssets = response.data.assets; allAssets.unshift({ asset_id: LTHN_ASSET_ID, logo: "", price_url: "", ticker: "LTHN", full_name: "Lethean (Native)", total_max_supply: "0", current_supply: "0", decimal_point: 0, meta_info: "", price: 0 }); const whitelistedAsset = allAssets.find(e => e.asset_id === asset_id); if (whitelistedAsset) { return res.json({ success: true, asset: whitelistedAsset }); } else { return res.json({ success: false, data: "Asset not found" }); } } else { return res.json({ success: true, asset: dbAsset }); } }) ); app.post('/api/get_assets_price_rates', exceptionHandler(async (req, res) => { const { assetsIds } = req.body; if (!assetsIds) { return res.json({ success: false, data: "Asset id not provided" }); } const assetsPricesResponse = await axios({ method: 'post', url: config.trade_api_url + '/dex/get-assets-price-rates', data: { assetsIds }, }); const assetsPrices = assetsPricesResponse.data; if (assetsPricesResponse?.data?.success) { return res.json({ success: true, priceRates: assetsPrices.priceRates }); } else { return res.json({ success: false, data: "Assets not found" }) } })) app.get('/api/explorer_status', exceptionHandler(async (req, res) => { res.json({ success: true, data: { explorer_status: state.explorer_status, } }); })); app.get('/api/get_matrix_addresses', exceptionHandler(async (req, res) => { const { page, items } = req.query; if (!page || !items) { return res.status(200).send({ success: false, data: "no page or items provided" }) } const matrixAddressesResponse = await fetch(`${config.matrix_api_url}/get-registered-addresses/?page=${page}&items=${items}`) .then(res => res.json()) const { addresses } = matrixAddressesResponse; if (matrixAddressesResponse?.success && addresses) { return res.status(200).send({ success: true, addresses }) } else { return res.status(200).send({ success: false, }) } })) io.on('connection', (socket) => { console.log('new socket connected'); const onSocketInfo = () => emitSocketInfo(socket); const onSocketPool = async () => { io.emit('get_transaction_pool_info', JSON.stringify(await getTxPoolDetails(0))); }; socket.on('get-socket-info', onSocketInfo); socket.on('get-socket-pool', onSocketPool); socket.on('disconnect', () => { console.log('Socket disconnected'); socket.off('get-socket-info', onSocketInfo); socket.off('get-socket-pool', onSocketPool); }); }) app.all('*', (req, res) => { return handle(req, res); }); // config.server_port server.listen(config.server_port, () => { // @ts-ignore log(`Server listening on port ${server?.address()?.port}`) }) // App logic const syncTransactions = async () => { if (state.block_array.length > 0) { let blockInserts: IBlock[] = []; let transactionInserts: ITransaction[] = []; let chartInserts: IChart[] = []; let outInfoInserts: IOutInfo[] = []; for (const bl of state.block_array) { try { if (bl.tr_count === undefined) bl.tr_count = bl.transactions_details.length; if (bl.tr_out === undefined) bl.tr_out = []; let localTr: any; while (!!(localTr = bl.transactions_details.splice(0, 1)[0])) { let response = await get_tx_details(localTr.id); let tx_info = response.data.result.tx_info; // for (let item of tx_info.extra) { // if (item.type === 'alias_info') { // let arr = item.short_view.split('-->'); // let aliasName = arr[0]; // let aliasAddress = arr[1]; // let aliasComment = parseComment(item.datails_view || item.details_view); // let aliasTrackingKey = parseTrackingKey(item.datails_view || item.details_view); // let aliasBlock = bl.height; // let aliasTransaction = localTr.id; // await Alias.update( // { enabled: 0 }, // { where: { alias: aliasName } } // ); // try { // await Alias.upsert({ // alias: decodeString(aliasName), // address: aliasAddress, // comment: decodeString(aliasComment), // tracking_key: decodeString(aliasTrackingKey), // block: aliasBlock, // transact: aliasTransaction, // enabled: 1, // }); // } catch (error) { // log(`SyncTransactions() Insert into aliases ERROR: ${error}`); // } // } // } for (let item of tx_info.ins) { if (item.global_indexes) { bl.tr_out.push({ amount: item.amount, i: item.global_indexes[0], }); } } transactionInserts.push({ keeper_block: tx_info.keeper_block, tx_id: tx_info.id, amount: tx_info.amount.toString(), blob_size: tx_info.blob_size, extra: decodeString(JSON.stringify(tx_info.extra)), fee: tx_info.fee, ins: decodeString(JSON.stringify(tx_info.ins)), outs: decodeString(JSON.stringify(tx_info.outs)), pub_key: tx_info.pub_key, timestamp: tx_info.timestamp, attachments: decodeString( JSON.stringify(!!tx_info.attachments ? tx_info.attachments : {}) ), }); } } catch (error) { log(`SyncTransactions() Inserting aliases ERROR: ${error}`); } chartInserts.push({ height: bl.height, actual_timestamp: bl.actual_timestamp, block_cumulative_size: bl.block_cumulative_size, cumulative_diff_precise: bl.cumulative_diff_precise, difficulty: bl.difficulty, tr_count: bl.tr_count ? bl.tr_count : 0, type: bl.type, }); if (bl.tr_out && bl.tr_out.length > 0) { for (let localOut of bl.tr_out) { let localOutAmount = new BigNumber(localOut.amount).toNumber(); let response = await get_out_info(localOutAmount, localOut.i); outInfoInserts.push({ amount: localOut.amount?.toString(), i: localOut.i, tx_id: response.data.result.tx_id, block: bl.height, }); } await sequelize.transaction(async (transaction) => { try { if (outInfoInserts.length > 0) { await OutInfo.bulkCreate(outInfoInserts, { ignoreDuplicates: true, transaction, }); } } catch (error) { log(`SyncTransactions() Insert Into out_info ERROR: ${error}`); } }); } blockInserts.push({ height: bl.height, actual_timestamp: bl.actual_timestamp, base_reward: bl.base_reward, blob: bl.blob, block_cumulative_size: bl.block_cumulative_size, block_tself_size: bl.block_tself_size, cumulative_diff_adjusted: bl.cumulative_diff_adjusted, cumulative_diff_precise: bl.cumulative_diff_precise, difficulty: bl.difficulty, effective_fee_median: bl.effective_fee_median, tx_id: bl.id, is_orphan: bl.is_orphan, penalty: bl.penalty, prev_id: bl.prev_id, summary_reward: bl.summary_reward, this_block_fee_median: bl.this_block_fee_median, timestamp: bl.timestamp, total_fee: bl.total_fee, total_txs_size: bl.total_txs_size, tr_count: bl.tr_count ? bl.tr_count : 0, type: bl.type, miner_text_info: decodeString(bl.miner_text_info), already_generated_coins: bl.already_generated_coins, object_in_json: decodeString(bl.object_in_json), pow_seed: bl.pow_seed, }); } await sequelize.transaction(async (transaction) => { try { if (blockInserts.length > 0) { await Block.bulkCreate(blockInserts, { transaction, ignoreDuplicates: true }); } if (transactionInserts.length > 0) { await Transaction.bulkCreate(transactionInserts, { ignoreDuplicates: true, transaction, }); } if (chartInserts.length > 0) { await Chart.bulkCreate(chartInserts, { transaction, ignoreDuplicates: true }); } // const elementOne = state.block_array[0]; const newLastBlock = state.block_array.pop(); setLastBlock({ height: newLastBlock.height, tx_id: newLastBlock.id, }); log(`BLOCKS: db = ${lastBlock.height}/ server = ${blockInfo.height}`); // await sequelize.query( // `CALL update_statistics(${Math.min(elementOne.height, lastBlock.height)})`, // { transaction } // ); setState({ ...state, block_array: [], }) } catch (error) { console.log(error); log(`SyncTransactions() Transaction Commit ERROR: ${error}`); throw error; } }); } }; const syncBlocks = async () => { console.log('Sync block called'); try { // await syncPrevBlocksTnxKeepers(lastBlock.height); let count = (blockInfo?.height || 0) - lastBlock.height + 1; if (count > 100) { setState({ ...state, explorer_status: 'syncing' }); count = 100; } if (count < 0) { count = 1; } // Get block details from the external service let response = await get_blocks_details(lastBlock.height + 1, count); let localBlocks = response.data.result && response.data.result.blocks ? response.data.result.blocks : []; if (localBlocks.length && lastBlock.tx_id === localBlocks[0].prev_id) { state.block_array = localBlocks; await syncTransactions(); if (lastBlock.height >= (blockInfo?.height || 0) - 1) { state.now_blocks_sync = false; // config.websocket.enabled_during_sync = true; await emitSocketInfo(); } else { await pause(state.serverTimeout); await syncBlocks(); } } else { const deleteHeightThreshold = lastBlock.height - 100; // Delete blocks with height greater than the threshold await Block.destroy({ where: { height: { [Op.gt]: deleteHeightThreshold, }, }, }); // Find the block with the maximum height after deletion const result = await Block.findOne({ order: [['height', 'DESC']], }); if (result) { setLastBlock(result.dataValues); } else { setLastBlock({ height: -1, tx_id: '0000000000000000000000000000000000000000000000000000000000000000' }); } await pause(state.serverTimeout); await syncBlocks(); } } catch (error) { log(`SyncBlocks() get_blocks_details ERROR: ${error.message}`); state.now_blocks_sync = false; } }; const syncPrevBlocksTnxKeepers = async (syncedHeight: number) => { async function syncTnxKeepers(blocks: any[]) { for (const block of blocks) { const txs = block.transactions_details; for (const tx of txs) { let response = await get_tx_details(tx.id); const tx_info = response.data.result.tx_info; await Transaction.update({ keeper_block: tx_info.keeper_block, }, { where: { tx_id: tx_info.id } }); } } } const count = 10; let response = await get_blocks_details(syncedHeight - count, count); let localBlocks = response.data?.result?.blocks || []; if (localBlocks.length) { await syncTnxKeepers(localBlocks); } } const syncAltBlocks = async () => { try { setState({ ...state, statusSyncAltBlocks: true }) // Start a transaction const transaction = await sequelize.transaction(); try { // Delete all records from the alt_blocks table within the transaction await AltBlock.destroy({ where: {}, transaction }); // Fetch the alt block details from the external service let response = await get_alt_blocks_details(0, state.countAltBlocksServer); // Iterate through the blocks and insert them into the alt_blocks table for (let block of response.data.result.blocks) { await AltBlock.create({ height: block.height, timestamp: block.timestamp, actual_timestamp: block.actual_timestamp, size: block.block_cumulative_size, hash: block.id, type: block.type, difficulty: block.difficulty, cumulative_diff_adjusted: block.cumulative_diff_adjusted, cumulative_diff_precise: block.cumulative_diff_precise, is_orphan: block.is_orphan, base_reward: block.base_reward, total_fee: block.total_fee, penalty: block.penalty, summary_reward: block.summary_reward, block_cumulative_size: block.block_cumulative_size, this_block_fee_median: block.this_block_fee_median, effective_fee_median: block.effective_fee_median, total_txs_size: block.total_txs_size, transactions_details: JSON.stringify(block.transactions_details), miner_txt_info: block.miner_text_info .replace('\u0000', '') .replace("'", "''"), pow_seed: '', // Adjust as needed }, { transaction }); } // Commit the transaction await transaction.commit(); // Get the updated count of alt blocks in the database setState({ ...state, countAltBlocksDB: await AltBlock.count() }) } catch (error) { // Rollback the transaction in case of an error await transaction.rollback(); throw error; } } catch (error) { log(`SyncAltBlocks() ERROR: ${error.message}`); } finally { setState({ ...state, statusSyncAltBlocks: false }) } }; const syncPool = async () => { try { // statusSyncPool = true; // countTrPoolServer = blockInfo.tx_pool_size; setState({ ...state, statusSyncPool: true, countTrPoolServer: blockInfo.tx_pool_size }); if (state.countTrPoolServer === 0) { // Clear the pool if there are no transactions on the server await Pool.destroy({ where: {} }); setState({ ...state, statusSyncPool: false }) io.emit('get_transaction_pool_info', JSON.stringify([])); } else { let response = await get_all_pool_tx_list(); if (response.data.result.ids) { setState({ ...state, pools_array: response?.data?.result?.ids || [] }) try { // Delete pool entries not in the current pool list from the server await Pool.destroy({ where: { tx_id: { [Op.notIn]: state.pools_array } } }); } catch (error) { log(`Delete From Pool ERROR: ${error.message}`); } try { // Fetch all transaction ids currently in the pool const existingTransactions = await Pool.findAll({ attributes: ['id'] }); // Find new transactions that are not already in the pool const existingIds = existingTransactions.map(tx => tx.tx_id); const new_ids = state.pools_array.filter(id => !existingIds.includes(id)); if (new_ids.length) { try { // Fetch details of the new transactions let response = await get_pool_txs_details(new_ids); if (response.data.result && response.data.result.txs) { const existingTransactions = await Pool.findAll({ attributes: ['tx_id'] }); const txInserts = response.data.result.txs.map(tx => ({ blob_size: tx.blob_size, fee: tx.fee, tx_id: tx.id, timestamp: tx.timestamp * 1e3, })).filter(tx => !existingTransactions.map(e => e.tx_id).includes(tx.tx_id)); // Insert the new transactions into the pool if (txInserts.length > 0) { await sequelize.transaction(async (transaction) => { await Pool.bulkCreate(txInserts, { transaction, ignoreDuplicates: true }); }); } } io.emit('get_transaction_pool_info', JSON.stringify(await getTxPoolDetails(0))); } catch (error) { log(`Error fetching new pool transactions: ${error.message}`); } } setState({ ...state, statusSyncPool: false }); } catch (error) { log(`Select id from pool ERROR: ${error.message}`); } } else { setState({ ...state, statusSyncPool: false }); } } } catch (error) { log(`SyncPool() ERROR: ${error.message}`); await Pool.destroy({ where: {} }); setState({ ...state, statusSyncPool: false }); } }; const getInfoTimer = async () => { console.log('Called git info timer'); // chech explorer status const infoResponse = await get_info().then(r => r.data).catch(_ => null); if (!infoResponse || !infoResponse?.result?.height) { setState({ ...state, explorer_status: "offline" }) } else { setState({ ...state, explorer_status: "online" }); } if (!state.now_delete_offers) { try { const response = await get_info(); const databaseHeight = await Block.max('height') || 0; console.log('databaseHeight', databaseHeight); console.log('blockchain height', response.data?.result?.height); setBlockInfo({ ...response.data.result, database_height: databaseHeight }); const txs = await Transaction.findAll({ where: { keeper_block: { [Op.gte]: 2555000 }, fee: { [Op.ne]: "0" } }, attributes: ['fee'], raw: true }); let lthnBurnedBig = new BigNumber(0); txs.forEach(tx => { lthnBurnedBig = lthnBurnedBig.plus(new BigNumber(tx.fee)); }); console.log('LTHN BURNED: ', lthnBurnedBig.toString()); const lthnBurned = lthnBurnedBig.div(new BigNumber(10).pow(12)).toNumber(); setState({ ...state, countAliasesServer: response.data.result.alias_count, countAltBlocksServer: response.data.result.alt_blocks_count, countTrPoolServer: response.data.result.tx_pool_size, lthnBurned, }); if (!state.statusSyncPool) { // Fetch the count of transactions in the pool using Sequelize const poolTransactionCount = await Pool.count(); if (poolTransactionCount !== state.countTrPoolServer) { log( `need to update pool transactions, db=${poolTransactionCount} server=${state.countTrPoolServer}` ); await syncPool(); } } if (!state.statusSyncAltBlocks) { if (state.countAltBlocksServer !== state.countAltBlocksDB) { log( `need to update alt-blocks, db=${state.countAltBlocksDB} server=${state.countAltBlocksServer}` ); await syncAltBlocks(); } } if (lastBlock.height !== (blockInfo.height || 0) - 1 && !state.now_blocks_sync) { log( `need to update blocks, db=${lastBlock.height} server=${blockInfo.height}` ); // Fetch the count of aliases using Sequelize const aliasCountDB = await Alias.count({ where: { enabled: true } }); if (aliasCountDB !== state.countAliasesServer) { log( `need to update aliases, db=${aliasCountDB} server=${state.countAliasesServer}` ); } setState({ ...state, now_blocks_sync: true }) await syncBlocks(); await emitSocketInfo(); } // Pause for 10 seconds await pause(10000); return getInfoTimer(); } catch (error) { log(`getInfoTimer() ERROR: ${error.message}`); blockInfo.daemon_network_state = 0; // Pause for 5 minutes on error await pause(300000); return getInfoTimer(); } } else { // If now_delete_offers is true, pause for 10 seconds before retrying await pause(10000); return getInfoTimer(); } }; const pause = (ms: number) => { return new Promise((resolve) => setTimeout(resolve, ms)); }; const start = async () => { try { // Delete all records from the alt_blocks table await AltBlock.destroy({ where: {}, truncate: true }); // Get the block with the maximum height const lastBlockResult = await Block.findOne({ order: [['height', 'DESC']], }); if (lastBlockResult) { setLastBlock({ height: lastBlockResult.height, tx_id: lastBlockResult.tx_id }); } // Get the count of aliases const aliasCountResult = await Alias.count({ where: { enabled: true } }); setState({ ...state, countAliasesDB: aliasCountResult }); // Get the count of alt_blocks const altBlockCountResult = await AltBlock.count(); setState({ ...state, countAltBlocksDB: altBlockCountResult }) // Call the getInfoTimer function getInfoTimer(); } catch (error) { log(`Start ERROR: ${error.message}`); } }; start(); })(); cron.schedule("0 */4 * * *", async () => { console.log("[CRON] Syncing latest Lethean price..."); await syncLatestPrice(); }, { timezone: "UTC" }); (async () => { await waitForDb(); await syncService.startSyncDaemon(); if (process.env.RESYNC_ASSETS === "true") { console.log('Resyncing assets'); await Asset.destroy({ where: {} }); } async function fetchPriceFromMexc(symbol: string): Promise { if (!config.mexc_api_url || config.mexc_api_url.length < 5) return null; try { const [avgPriceResponse, tickerResponse] = await Promise.allSettled([ axios({ method: 'get', url: `${config.mexc_api_url}/api/v3/avgPrice?symbol=${symbol}USDT`, }), axios({ method: 'get', url: `${config.mexc_api_url}/api/v3/ticker/24hr?symbol=${symbol}USDT`, }), ]); const result: PriceData = { lastUpdated: new Date().toISOString(), }; if (avgPriceResponse.status === 'fulfilled' && avgPriceResponse.value?.data?.price) { result.price = parseFloat(avgPriceResponse.value.data.price); } if (tickerResponse.status === 'fulfilled' && tickerResponse.value?.data?.priceChange) { result.usd_24h_change = parseFloat(tickerResponse.value.data.priceChange); } return result; } catch (error) { console.error(`Error fetching ${symbol} price from MEXC:`, error); return null; } } // Fetch Lethean price from MEXC async function fetchLtheanPrice() { return fetchPriceFromMexc('LTHN'); } // Fetch Ethereum price from MEXC async function fetchEthereumPrice() { return fetchPriceFromMexc('ETH'); } async function fetchFUSDPrice() { return fetchPriceFromMexc('FUSD'); } // Fetch fiat rates from CoinGecko async function fetchFiatRates() { try { const { data: supportedCurrencies } = await axios({ method: 'get', url: `https://api.coingecko.com/api/v3/simple/supported_vs_currencies`, }); // Validate the response is an array if (!Array.isArray(supportedCurrencies)) { console.error( 'CoinGecko supported currencies response is not an array:', supportedCurrencies, ); return null; } const { data: ratesData } = await axios({ method: 'get', url: `https://api.coingecko.com/api/v3/simple/price?ids=usd&vs_currencies=${supportedCurrencies.join( ',', )}`, }); if (ratesData?.usd) { return { ...ratesData.usd, usd: 1, // USD to USD is always 1 lastUpdated: new Date().toISOString(), }; } return null; } catch (error) { console.error('Error fetching fiat rates from CoinGecko:', error); return null; } } while (true) { try { // Fetch assets from external API async function fetchAssets(offset, count) { try { const response = await axios({ method: 'get', url: config.api, data: { method: 'get_assets_list', params: { count: count, offset: offset, } } }); return response?.data?.result?.assets || []; } catch { return []; } } const letheanPrice = await fetchLtheanPrice(); const ethPrice = await fetchEthereumPrice(); const fusdPrice = await fetchFUSDPrice(); const pricesToIncert = { lethean: letheanPrice || null, ethereum: ethPrice || null, fusd: fusdPrice || null, }; const validPrices = Object.entries(pricesToIncert) .map(e => { if (!e[1]) { return null; } return { [e[0]]: e[1] }; }) .filter(e => e !== null); const validPricesObject = Object.assign({}, ...validPrices); setState({ ...state, priceData: { ...state.priceData, ...validPricesObject }, }); // Fetch fiat rates every 1h (3600 seconds) if ( Date.now() - (state?.fiat_rates?.lastUpdated ? new Date(state.fiat_rates.lastUpdated).getTime() : 0) > 3600 * 1000 ) { const fiatRates = await fetchFiatRates(); if (fiatRates) { setState({ ...state, fiat_rates: fiatRates, }); } } // Fetch all assets const assets: IAsset[] = []; let iterator = 0; const amountPerIteration = 100; while (true) { const newAssets = (await fetchAssets(iterator, amountPerIteration)) .filter(e => /^[A-Za-z0-9]{1,14}$/.test(e.ticker) && /^[A-Za-z0-9.,:!?\-() ]{0,400}$/.test(e?.full_name)); if (!newAssets.length) break; assets.push(...newAssets); iterator += amountPerIteration; } console.log('Got assets list', assets.length); // Fetch existing assets from the database const assetsRows = await Asset.findAll(); // Update or delete existing assets for (const assetRow of assetsRows) { const foundAsset = assets.find(e => e.asset_id === assetRow.asset_id); if (!foundAsset) { // Delete asset if not found in the external data await Asset.destroy({ where: { asset_id: assetRow.asset_id } }); } else { // Update existing asset const { asset_id, logo, price_url, ticker, full_name, total_max_supply, current_supply, decimal_point, meta_info, price } = foundAsset; await Asset.update({ logo: logo || "", price_url: price_url || "", ticker: ticker || "", full_name: full_name || "", total_max_supply: total_max_supply?.toString() || "0", current_supply: current_supply?.toString() || "0", decimal_point: decimal_point || 0, meta_info: meta_info || "", price: price }, { where: { asset_id } }); } } const addedAssetsIds = new Set(); // Insert new assets for (const asset of assets) { const foundAsset = assetsRows.find(e => e.asset_id === asset.asset_id); if (!foundAsset && !addedAssetsIds.has(asset.asset_id) && asset.asset_id) { const { asset_id, logo, price_url, ticker, full_name, total_max_supply, current_supply, decimal_point, meta_info, price = 0 } = asset; await Asset.create({ asset_id, logo: logo || "", price_url: price_url || "", ticker, full_name, total_max_supply: total_max_supply?.toString(), current_supply: current_supply?.toString(), decimal_point, meta_info: meta_info || "", price }); addedAssetsIds.add(asset_id); } } } catch (error) { console.log('ASSETS PARSING ERROR'); console.log('Error: ', error); } // Wait for 60 seconds before the next iteration await new Promise(res => setTimeout(res, 10 * 1e3)); } })();