middleware/controllers/explorer.js (140 lines of code) (raw):
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
/**
* @typedef {Object} Transaction
* @property {string} cmd - The command executed in the transaction (e.g., "SET")
* @property {string} key - The key being operated on
* @property {string} value - The value associated with the key
*/
/**
* @typedef {Object} Block
* @property {number} id - The unique identifier of the block
* @property {string} number - The block number as a string
* @property {Transaction[]} transactions - Array of transactions contained in the block
* @property {number} size - The size of the block in bytes
* @property {string} createdAt - The creation timestamp in human-readable format (GMT)
* @property {number} createdAtEpoch - The creation timestamp as Unix epoch in microseconds
*/
const axios = require('axios');
const { getEnv } = require('../utils/envParser');
const logger = require('../utils/logger');
const { parseCreateTime, parseTimeToUnixEpoch} = require('../utils/time');
const { applyDeltaEncoding, decodeDeltaEncoding } = require('../utils/encoding');
const sqlite3 = require('sqlite3').verbose();
const path = require("path");
const { devNull } = require('os');
// Initialize SQLite database connection
const DB_PATH = path.join(__dirname, '../cache/transactions.db');
/**
* Fetches explorer data from the EXPLORER_BASE_URL and sends it as a response.
*
* @async
* @function getExplorerData
* @param {Object} req - The HTTP request object.
* @param {Object} res - The HTTP response object.
* @returns {Promise<void>} Sends the fetched data or an error response.
*/
async function getExplorerData(req, res) {
const baseUrl = `${getEnv("EXPLORER_BASE_URL")}/populatetable`;
const config = {
method: 'get',
maxBodyLength: Infinity,
url: baseUrl,
};
try {
const response = await axios.request(config);
return res.send(response.data);
} catch (error) {
logger.error('Error fetching explorer data:', error);
return res.status(500).send({
error: 'Failed to fetch explorer data',
details: error.message,
});
}
}
/**
* Fetches block data from the EXPLORER_BASE_URL and sends it as a response.
*
* @async
* @function getBlocks
* @param {Object} req - The HTTP request object.
* @param {Object} req.query - The query parameters.
* @param {number} req.query.start - The starting block number.
* @param {number} req.query.end - The ending block number.
* @param {Object} res - The HTTP response object.
* @returns {Promise<void>} Sends the fetched data or an error response.
*/
async function getBlocks(req, res) {
const start = parseInt(req.query.start, 10);
const end = parseInt(req.query.end, 10);
if (isNaN(start) || isNaN(end)) {
return res.status(400).send({
error: 'Pass valid start and end query params as part of the request',
});
}
const config = {
method: 'get',
maxBodyLength: Infinity,
url: `${getEnv("EXPLORER_BASE_URL")}/v1/blocks/${start}/${end}`,
};
try {
const response = await axios.request(config);
/** @type {Array<Block>} */
const data = response?.data
return res.send(data);
} catch (error) {
logger.error('Error fetching block data:', error);
return res.status(500).send({
error: 'Failed to fetch block data',
details: error.message,
});
}
}
// Using the function below for the graph, decoupled with pagination for the table
async function getAllEncodedBlocks(req, res) {
try {
logger.info(`Fetching ALL blocks from cache for full graph rendering`);
const cacheData = await getDataFromCache(null, null); // No start/end
if (!cacheData || cacheData.length === 0) {
logger.warn('No cache data available for full graph');
return res.status(404).send({ error: 'No cached block data available' });
}
const modifiedData = cacheData.map(record => ({
epoch: parseTimeToUnixEpoch(record.created_at),
volume: record.volume || 0,
}));
const encoded = applyDeltaEncoding(modifiedData);
return res.send(encoded);
} catch (error) {
logger.error('Error fetching full block data:', error);
return res.status(500).send({
error: 'Failed to fetch full block data',
details: error.message,
});
}
}
/**
* Retrieves data from the SQLite cache
*
* @param {number|null} start - Start block ID
* @param {number|null} end - End block ID
* @returns {Promise<Array>} - The cached block data
*/
function getDataFromCache(start, end) {
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
logger.error('Error connecting to SQLite database:', err);
} else {
logger.info('Connected to SQLite cache database');
}
});
return new Promise((resolve, reject) => {
let query;
let params = [];
if (typeof start === "number" && typeof end === "number" && !isNaN(start) && !isNaN(end)) {
// Used by table view
query = `
SELECT block_id, volume, created_at
FROM transactions
WHERE block_id BETWEEN ? AND ?
ORDER BY block_id ASC
`;
params = [start, end];
} else if (start === null && end === null) {
// Used by full chart query
query = `
SELECT block_id, volume, created_at
FROM transactions
ORDER BY block_id ASC
`;
} else {
// Fallback when parameters are missing or invalid
query = `
SELECT block_id, volume, created_at
FROM transactions
ORDER BY block_id ASC
LIMIT 100
`;
}
db.all(query, params, (err, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
});
});
}
/**
* Retrieves data from the API
*
* @param {number} start - Start block ID
* @param {number} end - End block ID
* @returns {Promise<Array>} - The formatted block data
*/
async function getDataFromApi(start, end) {
let baseUrl = `${getEnv("EXPLORER_BASE_URL")}/v1/blocks/1/100`; //never request the full data
if (!isNaN(start) && !isNaN(end)) {
baseUrl = `${getEnv("EXPLORER_BASE_URL")}/v1/blocks/${start}/${end}`;
}
const config = {
method: 'get',
maxBodyLength: Infinity,
url: baseUrl,
};
const response = await axios.request(config);
/** @type {Array<Block>} */
const data = response?.data;
return data.map((record) => {
return {
epoch: parseTimeToUnixEpoch(record?.createdAt),
volume: record?.transactions?.length || 0
};
});
}
module.exports = {
getExplorerData,
getBlocks,
getAllEncodedBlocks
};