diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1226b0e --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +MYSQL_HOST=127.0.0.1 +MYSQL_PORT=3306 +MYSQL_USER=peya +MYSQL_PASSWORD=change-me +MYSQL_DATABASE=peya_explorer + +PEYA_RPC_URL=http://127.0.0.1:17750 + +API_HOST=127.0.0.1 +API_PORT=4100 + +INDEXER_BATCH_SIZE=25 +INDEXER_POLL_INTERVAL_MS=5000 + +NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:4100 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..73b9325 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +node_modules +.next +dist +.env +.env.local +*.log +coverage +apps/web/.next +apps/web/out + diff --git a/README.md b/README.md index 9d319f4..10e6282 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,118 @@ -# peya-explorer +# Peya Explorer -Modern explorer for Peya mainnet diagnostics and blockchain insights \ No newline at end of file +Modern explorer stack for Peya with: + +- a fresh MySQL-first schema +- a dedicated RPC indexer +- a Fastify API +- a Next.js frontend + +Old `salstats` code is reference material only. The production data model here is intentionally rebuilt from scratch around current Peya RPC behavior. + +## Apps + +- `apps/indexer` - RPC ingestion and MySQL materialization +- `apps/api` - read API for the frontend and diagnostics tooling +- `apps/web` - public explorer UI +- `packages/shared` - shared types, labels, and formatting helpers + +## Data Model + +The explorer is intentionally **not** a port of old `salstats`. + +It stores: + +- `blocks` + - header data + - reward and miner burn values + - raw RPC JSON and blob + - merge-mining marker derived from `miner_tx.extra` +- `transactions` + - normalized tx type labels + - burnt amount + - source/destination asset types + - return-address fields when present +- `transaction_outputs` + - output amount + - asset type + - output target details +- `protocol_events` + - protocol outputs per block + - inferred `stake_tx_hash` + - computed yield amount / percent when a stake match is found +- `yield_metrics` + - `get_yield_info` materialized per block + - slippage, locked tally, and network health + +## Indexer Notes + +Primary RPC sources: + +- `get_block` +- `gettransactions` +- `get_yield_info` +- `/getheight` + +Current merge-mining detection: + +- a block is marked merge-mined when `miner_tx.extra` contains a merge-mining tag +- this is suitable for current Peya diagnostics because aux-mined blocks are built with that tag + +Current protocol/stake linkage: + +- protocol outputs are stored raw first +- then the indexer tries to match a protocol unlock with the closest `STAKE` tx at `block_height - 21601` +- yield is computed as `protocol unlock amount - original staked amount` + +That linkage is intentionally isolated in its own table logic so it can be tightened later if live RPC reveals a better authoritative signal. + +## Expected Server Layout + +- Apache stays public on `80/443` +- `web` runs on localhost +- `api` runs on localhost +- MySQL stores indexed chain data +- Redis is optional for caching and short-lived API responses + +## Environment + +Each app reads env from its own process environment. + +Common values: + +```bash +MYSQL_HOST=127.0.0.1 +MYSQL_PORT=3306 +MYSQL_USER=peya +MYSQL_PASSWORD=secret +MYSQL_DATABASE=peya_explorer + +PEYA_RPC_URL=http://127.0.0.1:17750 +API_PORT=4100 +NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:4100 +``` + +## Initial Commands + +```bash +npm install +npm run db:migrate +npm run index:once +npm run dev:api +npm run dev:web +``` + +## Current Status + +Implemented and building: + +- MySQL-first schema and migration +- dedicated RPC indexer +- Fastify read API +- Next.js frontend for dashboard, block list, and block detail + +Still pending before production rollout: + +- live validation against your localhost-exposed RPC +- tuning of stake/protocol linkage on real chain data +- search, charts, tx detail page polish, and Apache deployment snippets diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..1345228 --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,23 @@ +{ + "name": "@peya-explorer/api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "dev": "tsx watch src/index.ts" + }, + "dependencies": { + "@fastify/cors": "^10.0.1", + "@peya-explorer/shared": "0.1.0", + "dotenv": "^16.4.5", + "fastify": "^5.0.0", + "mysql2": "^3.11.3", + "zod": "^3.23.8" + }, + "devDependencies": { + "tsx": "^4.19.1", + "typescript": "^5.6.3" + } +} + diff --git a/apps/api/src/db.ts b/apps/api/src/db.ts new file mode 100644 index 0000000..86e9c9e --- /dev/null +++ b/apps/api/src/db.ts @@ -0,0 +1,14 @@ +import { createPool } from "mysql2/promise"; +import { env } from "./env.js"; + +export const pool = createPool({ + host: env.MYSQL_HOST, + port: env.MYSQL_PORT, + user: env.MYSQL_USER, + password: env.MYSQL_PASSWORD, + database: env.MYSQL_DATABASE, + waitForConnections: true, + connectionLimit: 10, + namedPlaceholders: true, + decimalNumbers: false +}); diff --git a/apps/api/src/env.ts b/apps/api/src/env.ts new file mode 100644 index 0000000..70c0755 --- /dev/null +++ b/apps/api/src/env.ts @@ -0,0 +1,15 @@ +import "dotenv/config"; +import { z } from "zod"; + +const envSchema = z.object({ + MYSQL_HOST: z.string().default("127.0.0.1"), + MYSQL_PORT: z.coerce.number().int().positive().default(3306), + MYSQL_USER: z.string().default("root"), + MYSQL_PASSWORD: z.string().default(""), + MYSQL_DATABASE: z.string().default("peya_explorer"), + API_HOST: z.string().default("127.0.0.1"), + API_PORT: z.coerce.number().int().positive().default(4100) +}); + +export const env = envSchema.parse(process.env); + diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..2ae259b --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,189 @@ +import Fastify from "fastify"; +import cors from "@fastify/cors"; +import type { RowDataPacket } from "mysql2/promise"; +import { shortHash } from "@peya-explorer/shared"; +import { pool } from "./db.js"; +import { env } from "./env.js"; + +type SummaryRow = RowDataPacket & { + chain_height: number | null; + top_hash: string | null; + avg_difficulty_24h: string | null; + merge_mined_last_100: number | null; + total_staked: string | null; + total_yield: string | null; + yield_per_stake: string | null; + network_health_percentage: number | null; +}; + +type BlockRow = RowDataPacket & { + height: number; + hash: string; + timestamp: number; + difficulty: string; + reward_atomic: string; + num_txs: number; + merge_mining_tag_present: number; + is_merge_mined: number | null; + slippage_total_this_block: string | null; + locked_coins_tally: string | null; + network_health_percentage: number | null; +}; + +const app = Fastify({ logger: true }); + +await app.register(cors, { + origin: true +}); + +app.get("/health", async () => { + const [rows] = await pool.query("SELECT 1 AS ok"); + return { ok: rows[0]?.ok === 1 }; +}); + +app.get("/api/summary", async () => { + const [rows] = await pool.query( + `SELECT + (SELECT MAX(height) FROM blocks) AS chain_height, + (SELECT hash FROM blocks ORDER BY height DESC LIMIT 1) AS top_hash, + (SELECT AVG(CAST(difficulty AS DECIMAL(39,0))) FROM blocks WHERE timestamp >= UNIX_TIMESTAMP() - 86400) AS avg_difficulty_24h, + (SELECT COUNT(*) FROM blocks WHERE height > GREATEST((SELECT COALESCE(MAX(height), 0) - 100 FROM blocks), 0) AND is_merge_mined = 1) AS merge_mined_last_100, + (SELECT locked_coins_tally FROM yield_metrics ORDER BY block_height DESC LIMIT 1) AS total_staked, + (SELECT SUM(slippage_total_this_block) FROM yield_metrics) AS total_yield, + (SELECT slippage_total_this_block FROM yield_metrics ORDER BY block_height DESC LIMIT 1) AS yield_per_stake, + (SELECT network_health_percentage FROM yield_metrics ORDER BY block_height DESC LIMIT 1) AS network_health_percentage` + ); + + const row = rows[0]; + return { + chainHeight: row.chain_height, + topHash: row.top_hash, + topHashShort: row.top_hash ? shortHash(row.top_hash) : null, + averageDifficulty24h: row.avg_difficulty_24h, + mergeMinedLast100: row.merge_mined_last_100 ?? 0, + totalStaked: row.total_staked ?? "0", + totalYield: row.total_yield ?? "0", + latestYieldBlockSlippage: row.yield_per_stake ?? "0", + networkHealthPercentage: row.network_health_percentage ?? 0 + }; +}); + +app.get("/api/blocks", async (request) => { + const query = request.query as { limit?: string; offset?: string }; + const limit = Math.min(Number(query.limit ?? 20), 100); + const offset = Math.max(Number(query.offset ?? 0), 0); + + const [rows] = await pool.query( + `SELECT + b.height, + b.hash, + b.timestamp, + b.difficulty, + b.reward_atomic, + b.num_txs, + b.merge_mining_tag_present, + b.is_merge_mined, + y.slippage_total_this_block, + y.locked_coins_tally, + y.network_health_percentage + FROM blocks b + LEFT JOIN yield_metrics y ON y.block_height = b.height + ORDER BY b.height DESC + LIMIT ? OFFSET ?`, + [limit, offset] + ); + + return { + items: rows.map((row) => ({ + height: row.height, + hash: row.hash, + timestamp: row.timestamp, + difficulty: row.difficulty, + rewardAtomic: row.reward_atomic, + txCount: row.num_txs, + mergeMiningTagPresent: Boolean(row.merge_mining_tag_present), + isMergeMined: row.is_merge_mined === null ? null : Boolean(row.is_merge_mined), + slippageTotalThisBlock: row.slippage_total_this_block ?? "0", + lockedCoinsTally: row.locked_coins_tally ?? "0", + networkHealthPercentage: row.network_health_percentage + })) + }; +}); + +app.get("/api/blocks/:height", async (request, reply) => { + const params = request.params as { height: string }; + const height = Number(params.height); + const [blockRows] = await pool.query( + `SELECT + b.*, + y.slippage_total_this_block, + y.locked_coins_this_block, + y.locked_coins_tally, + y.network_health_percentage + FROM blocks b + LEFT JOIN yield_metrics y ON y.block_height = b.height + WHERE b.height = ? + LIMIT 1`, + [height] + ); + + if (blockRows.length === 0) { + reply.code(404); + return { error: "block not found" }; + } + + const block = blockRows[0]; + const [transactions] = await pool.query( + `SELECT hash, tx_index, type_code, type_label, amount_burnt_atomic, source_asset_type, destination_asset_type + FROM transactions + WHERE block_height = ? + ORDER BY tx_index ASC`, + [height] + ); + + const [protocolEvents] = await pool.query( + `SELECT out_index, amount_atomic, asset_type, return_address, locked_at_height, stake_tx_hash, yield_atomic, yield_percent + FROM protocol_events + WHERE block_height = ? + ORDER BY out_index ASC`, + [height] + ); + + return { + block, + transactions, + protocolEvents + }; +}); + +app.get("/api/txs/:hash", async (request, reply) => { + const params = request.params as { hash: string }; + const [txRows] = await pool.query( + `SELECT * + FROM transactions + WHERE hash = ? + LIMIT 1`, + [params.hash] + ); + + if (txRows.length === 0) { + reply.code(404); + return { error: "transaction not found" }; + } + + const tx = txRows[0]; + const [outputs] = await pool.query( + `SELECT out_index, amount_atomic, asset_type, target_type, target_key, view_tag, unlock_time + FROM transaction_outputs + WHERE tx_hash = ? + ORDER BY out_index ASC`, + [params.hash] + ); + + return { tx, outputs }; +}); + +await app.listen({ + host: env.API_HOST, + port: env.API_PORT +}); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..af86f2f --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/apps/indexer/migrations/001_init.sql b/apps/indexer/migrations/001_init.sql new file mode 100644 index 0000000..41aaf8d --- /dev/null +++ b/apps/indexer/migrations/001_init.sql @@ -0,0 +1,103 @@ +CREATE TABLE IF NOT EXISTS explorer_meta ( + meta_key VARCHAR(128) NOT NULL PRIMARY KEY, + meta_value TEXT NOT NULL, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS yield_metrics ( + block_height BIGINT UNSIGNED NOT NULL PRIMARY KEY, + slippage_total_this_block DECIMAL(39,0) NOT NULL DEFAULT 0, + locked_coins_this_block DECIMAL(39,0) NOT NULL DEFAULT 0, + locked_coins_tally DECIMAL(39,0) NOT NULL DEFAULT 0, + network_health_percentage TINYINT UNSIGNED NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE IF NOT EXISTS blocks ( + height BIGINT UNSIGNED NOT NULL PRIMARY KEY, + hash CHAR(64) NOT NULL, + prev_hash CHAR(64) NOT NULL, + major_version INT UNSIGNED NOT NULL, + minor_version INT UNSIGNED NOT NULL, + timestamp BIGINT UNSIGNED NOT NULL, + nonce BIGINT UNSIGNED NOT NULL, + difficulty DECIMAL(39,0) NOT NULL DEFAULT 0, + cumulative_difficulty DECIMAL(39,0) NOT NULL DEFAULT 0, + reward_atomic DECIMAL(39,0) NOT NULL DEFAULT 0, + miner_burn_atomic DECIMAL(39,0) NOT NULL DEFAULT 0, + num_txs INT UNSIGNED NOT NULL DEFAULT 0, + block_size INT UNSIGNED NOT NULL DEFAULT 0, + block_weight INT UNSIGNED NOT NULL DEFAULT 0, + long_term_weight INT UNSIGNED NOT NULL DEFAULT 0, + orphaned TINYINT(1) NOT NULL DEFAULT 0, + pow_hash CHAR(64) NULL, + miner_tx_hash CHAR(64) NOT NULL, + protocol_tx_hash CHAR(64) NOT NULL, + merge_mining_tag_present TINYINT(1) NOT NULL DEFAULT 0, + is_merge_mined TINYINT(1) NULL, + parent_chain_hint VARCHAR(32) NULL, + raw_json JSON NOT NULL, + raw_blob MEDIUMTEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + UNIQUE KEY uq_blocks_hash (hash), + KEY idx_blocks_timestamp (timestamp DESC) +); + +CREATE TABLE IF NOT EXISTS transactions ( + hash CHAR(64) NOT NULL PRIMARY KEY, + block_height BIGINT UNSIGNED NULL, + tx_index INT UNSIGNED NOT NULL DEFAULT 0, + type_code TINYINT UNSIGNED NOT NULL DEFAULT 0, + type_label VARCHAR(32) NOT NULL, + version INT UNSIGNED NOT NULL DEFAULT 0, + unlock_time BIGINT UNSIGNED NOT NULL DEFAULT 0, + amount_burnt_atomic DECIMAL(39,0) NOT NULL DEFAULT 0, + source_asset_type VARCHAR(32) NULL, + destination_asset_type VARCHAR(32) NULL, + return_address TEXT NULL, + protocol_return_address TEXT NULL, + raw_json JSON NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + KEY idx_transactions_block_height (block_height), + CONSTRAINT fk_transactions_block_height FOREIGN KEY (block_height) REFERENCES blocks(height) ON DELETE SET NULL +); + +CREATE TABLE IF NOT EXISTS transaction_outputs ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + tx_hash CHAR(64) NOT NULL, + out_index INT UNSIGNED NOT NULL, + amount_atomic DECIMAL(39,0) NOT NULL DEFAULT 0, + asset_type VARCHAR(32) NULL, + target_type VARCHAR(32) NOT NULL, + target_key VARCHAR(128) NULL, + view_tag VARCHAR(64) NULL, + unlock_time BIGINT UNSIGNED NULL, + raw_target JSON NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_transaction_outputs (tx_hash, out_index), + KEY idx_transaction_outputs_asset_type (asset_type), + CONSTRAINT fk_transaction_outputs_tx_hash FOREIGN KEY (tx_hash) REFERENCES transactions(hash) ON DELETE CASCADE +); + +CREATE TABLE IF NOT EXISTS protocol_events ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + block_height BIGINT UNSIGNED NOT NULL, + protocol_tx_hash CHAR(64) NOT NULL, + out_index INT UNSIGNED NOT NULL, + amount_atomic DECIMAL(39,0) NOT NULL DEFAULT 0, + asset_type VARCHAR(32) NULL, + return_address VARCHAR(255) NULL, + locked_at_height BIGINT UNSIGNED NULL, + stake_tx_hash CHAR(64) NULL, + yield_atomic DECIMAL(39,0) NULL, + yield_percent DECIMAL(16,8) NULL, + raw_target JSON NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_protocol_events (protocol_tx_hash, out_index), + KEY idx_protocol_events_block_height (block_height), + KEY idx_protocol_events_stake_tx_hash (stake_tx_hash), + CONSTRAINT fk_protocol_events_block_height FOREIGN KEY (block_height) REFERENCES blocks(height) ON DELETE CASCADE +); + diff --git a/apps/indexer/package.json b/apps/indexer/package.json new file mode 100644 index 0000000..e90289e --- /dev/null +++ b/apps/indexer/package.json @@ -0,0 +1,23 @@ +{ + "name": "@peya-explorer/indexer", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "tsc -p tsconfig.json", + "db:migrate": "tsx src/migrate.ts", + "dev": "tsx watch src/index.ts", + "index:once": "tsx src/index.ts --once" + }, + "dependencies": { + "@peya-explorer/shared": "0.1.0", + "dotenv": "^16.4.5", + "mysql2": "^3.11.3", + "zod": "^3.23.8" + }, + "devDependencies": { + "tsx": "^4.19.1", + "typescript": "^5.6.3" + } +} + diff --git a/apps/indexer/src/db.ts b/apps/indexer/src/db.ts new file mode 100644 index 0000000..75752ea --- /dev/null +++ b/apps/indexer/src/db.ts @@ -0,0 +1,16 @@ +import { createPool as createMysqlPool } from "mysql2/promise"; +import { env } from "./env.js"; + +export function createPool() { + return createMysqlPool({ + host: env.MYSQL_HOST, + port: env.MYSQL_PORT, + user: env.MYSQL_USER, + password: env.MYSQL_PASSWORD, + database: env.MYSQL_DATABASE, + waitForConnections: true, + connectionLimit: 10, + namedPlaceholders: true, + decimalNumbers: false + }); +} diff --git a/apps/indexer/src/env.ts b/apps/indexer/src/env.ts new file mode 100644 index 0000000..f3bd466 --- /dev/null +++ b/apps/indexer/src/env.ts @@ -0,0 +1,16 @@ +import "dotenv/config"; +import { z } from "zod"; + +const envSchema = z.object({ + MYSQL_HOST: z.string().default("127.0.0.1"), + MYSQL_PORT: z.coerce.number().int().positive().default(3306), + MYSQL_USER: z.string().default("root"), + MYSQL_PASSWORD: z.string().default(""), + MYSQL_DATABASE: z.string().default("peya_explorer"), + PEYA_RPC_URL: z.string().url().default("http://127.0.0.1:17750"), + INDEXER_BATCH_SIZE: z.coerce.number().int().positive().default(25), + INDEXER_POLL_INTERVAL_MS: z.coerce.number().int().positive().default(5000) +}); + +export const env = envSchema.parse(process.env); + diff --git a/apps/indexer/src/index.ts b/apps/indexer/src/index.ts new file mode 100644 index 0000000..360235b --- /dev/null +++ b/apps/indexer/src/index.ts @@ -0,0 +1,385 @@ +import type { Pool, PoolConnection, RowDataPacket } from "mysql2/promise"; +import { env } from "./env.js"; +import { createPool } from "./db.js"; +import { inferStakeUnlockHeight, normalizeTransaction, parseRpcJson, hasMergeMiningTag, type RpcTransactionJson } from "./normalize.js"; +import { PeyaRpcClient } from "./rpc.js"; + +type MetaRow = RowDataPacket & { + meta_value: string; +}; + +type StakeLookupRow = RowDataPacket & { + hash: string; + amount_burnt_atomic: string; +}; + +const ONCE = process.argv.includes("--once"); + +async function getLastIndexedHeight(pool: Pool): Promise { + const [rows] = await pool.query("SELECT meta_value FROM explorer_meta WHERE meta_key = 'last_indexed_height' LIMIT 1"); + if (rows.length === 0) { + return -1; + } + return Number(rows[0].meta_value); +} + +async function setLastIndexedHeight(connection: PoolConnection, height: number) { + await connection.query( + `INSERT INTO explorer_meta (meta_key, meta_value) + VALUES ('last_indexed_height', ?) + ON DUPLICATE KEY UPDATE meta_value = VALUES(meta_value)`, + [String(height)] + ); +} + +async function syncYieldMetrics(pool: Pool, rpc: PeyaRpcClient, fromHeight: number, toHeight: number) { + if (toHeight < fromHeight) { + return; + } + + const yieldInfo = await rpc.getYieldInfo(fromHeight, toHeight); + if (yieldInfo.status !== "OK") { + throw new Error(`get_yield_info returned ${yieldInfo.status}`); + } + + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + for (const row of yieldInfo.yield_data) { + await connection.query( + `INSERT INTO yield_metrics ( + block_height, + slippage_total_this_block, + locked_coins_this_block, + locked_coins_tally, + network_health_percentage + ) VALUES (?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + slippage_total_this_block = VALUES(slippage_total_this_block), + locked_coins_this_block = VALUES(locked_coins_this_block), + locked_coins_tally = VALUES(locked_coins_tally), + network_health_percentage = VALUES(network_health_percentage)`, + [ + row.block_height, + String(row.slippage_total_this_block), + String(row.locked_coins_this_block), + String(row.locked_coins_tally), + row.network_health_percentage + ] + ); + } + await connection.commit(); + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} + +async function updateProtocolEventLinks(connection: PoolConnection, blockHeight: number) { + const [events] = await connection.query( + "SELECT id, block_height, amount_atomic, asset_type FROM protocol_events WHERE block_height = ?", + [blockHeight] + ); + + for (const event of events) { + const lockedAtHeight = inferStakeUnlockHeight(Number(event.block_height)); + if (!lockedAtHeight || event.asset_type !== "SAL") { + continue; + } + + const [stakes] = await connection.query( + `SELECT hash, amount_burnt_atomic + FROM transactions + WHERE block_height = ? AND type_code = 6 + ORDER BY ABS(CAST(amount_burnt_atomic AS SIGNED) - CAST(? AS SIGNED)) ASC + LIMIT 1`, + [lockedAtHeight, event.amount_atomic] + ); + + if (stakes.length === 0) { + continue; + } + + const originalStake = BigInt(stakes[0].amount_burnt_atomic); + const unlockedAmount = BigInt(String(event.amount_atomic)); + const yieldAtomic = unlockedAmount - originalStake; + const yieldPercent = originalStake > 0n ? Number((yieldAtomic * 10000n) / originalStake) / 100 : null; + + await connection.query( + `UPDATE protocol_events + SET locked_at_height = ?, stake_tx_hash = ?, yield_atomic = ?, yield_percent = ? + WHERE id = ?`, + [lockedAtHeight, stakes[0].hash, yieldAtomic.toString(), yieldPercent, event.id] + ); + } +} + +async function indexBlock(pool: Pool, rpc: PeyaRpcClient, height: number) { + const block = await rpc.getBlock(height); + if (block.status !== "OK") { + throw new Error(`get_block(${height}) returned ${block.status}`); + } + + const blockJson = parseRpcJson<{ + major_version: number; + minor_version: number; + timestamp: number; + prev_id: string; + nonce: number; + miner_tx: RpcTransactionJson; + protocol_tx: RpcTransactionJson; + tx_hashes: string[]; + }>(block.json); + + if (!blockJson) { + throw new Error(`Block ${height} did not return JSON`); + } + + const mergeMiningTagPresent = hasMergeMiningTag(blockJson.miner_tx.extra); + const txHashes = [block.miner_tx_hash, block.protocol_tx_hash, ...block.tx_hashes]; + const txResponse = await rpc.getTransactions(txHashes); + if (txResponse.status !== "OK") { + throw new Error(`gettransactions for block ${height} returned ${txResponse.status}`); + } + + const txJsonByHash = new Map(); + for (const tx of txResponse.txs) { + const parsed = parseRpcJson(tx.as_json); + if (parsed) { + txJsonByHash.set(tx.tx_hash, parsed); + } + } + + const connection = await pool.getConnection(); + try { + await connection.beginTransaction(); + await connection.query( + `INSERT INTO blocks ( + height, + hash, + prev_hash, + major_version, + minor_version, + timestamp, + nonce, + difficulty, + cumulative_difficulty, + reward_atomic, + miner_burn_atomic, + num_txs, + block_size, + block_weight, + long_term_weight, + orphaned, + pow_hash, + miner_tx_hash, + protocol_tx_hash, + merge_mining_tag_present, + is_merge_mined, + parent_chain_hint, + raw_json, + raw_blob + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + hash = VALUES(hash), + prev_hash = VALUES(prev_hash), + major_version = VALUES(major_version), + minor_version = VALUES(minor_version), + timestamp = VALUES(timestamp), + nonce = VALUES(nonce), + difficulty = VALUES(difficulty), + cumulative_difficulty = VALUES(cumulative_difficulty), + reward_atomic = VALUES(reward_atomic), + miner_burn_atomic = VALUES(miner_burn_atomic), + num_txs = VALUES(num_txs), + block_size = VALUES(block_size), + block_weight = VALUES(block_weight), + long_term_weight = VALUES(long_term_weight), + orphaned = VALUES(orphaned), + pow_hash = VALUES(pow_hash), + miner_tx_hash = VALUES(miner_tx_hash), + protocol_tx_hash = VALUES(protocol_tx_hash), + merge_mining_tag_present = VALUES(merge_mining_tag_present), + is_merge_mined = VALUES(is_merge_mined), + raw_json = VALUES(raw_json), + raw_blob = VALUES(raw_blob)`, + [ + block.block_header.height, + block.block_header.hash, + block.block_header.prev_hash, + blockJson.major_version, + blockJson.minor_version, + blockJson.timestamp, + blockJson.nonce, + String(block.block_header.difficulty), + String(block.block_header.cumulative_difficulty), + String(block.block_header.reward ?? 0), + String(blockJson.miner_tx.amount_burnt ?? 0), + block.block_header.num_txes, + block.block_header.block_size, + block.block_header.block_weight, + block.block_header.long_term_weight, + block.block_header.orphan_status ? 1 : 0, + block.block_header.pow_hash ?? null, + block.miner_tx_hash, + block.protocol_tx_hash, + mergeMiningTagPresent ? 1 : 0, + mergeMiningTagPresent ? 1 : 0, + mergeMiningTagPresent ? "detected-from-miner-tx-extra" : null, + block.json, + block.blob + ] + ); + + await connection.query("DELETE FROM transaction_outputs WHERE tx_hash IN (?)", [txHashes]); + await connection.query("DELETE FROM protocol_events WHERE protocol_tx_hash = ?", [block.protocol_tx_hash]); + + for (const [index, txHash] of txHashes.entries()) { + const txJson = txJsonByHash.get(txHash) ?? (index === 0 ? blockJson.miner_tx : index === 1 ? blockJson.protocol_tx : null); + if (!txJson) { + continue; + } + + const normalized = normalizeTransaction(txHash, height, index, txJson); + + await connection.query( + `INSERT INTO transactions ( + hash, + block_height, + tx_index, + type_code, + type_label, + version, + unlock_time, + amount_burnt_atomic, + source_asset_type, + destination_asset_type, + return_address, + protocol_return_address, + raw_json + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + block_height = VALUES(block_height), + tx_index = VALUES(tx_index), + type_code = VALUES(type_code), + type_label = VALUES(type_label), + version = VALUES(version), + unlock_time = VALUES(unlock_time), + amount_burnt_atomic = VALUES(amount_burnt_atomic), + source_asset_type = VALUES(source_asset_type), + destination_asset_type = VALUES(destination_asset_type), + return_address = VALUES(return_address), + protocol_return_address = VALUES(protocol_return_address), + raw_json = VALUES(raw_json)`, + [ + normalized.hash, + normalized.blockHeight, + normalized.txIndex, + normalized.typeCode, + normalized.typeLabel, + normalized.version, + normalized.unlockTime, + normalized.amountBurntAtomic, + normalized.sourceAssetType, + normalized.destinationAssetType, + normalized.returnAddress, + normalized.protocolReturnAddress, + JSON.stringify(normalized.rawJson) + ] + ); + + for (const output of normalized.outputs) { + await connection.query( + `INSERT INTO transaction_outputs ( + tx_hash, + out_index, + amount_atomic, + asset_type, + target_type, + target_key, + view_tag, + unlock_time, + raw_target + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, + [ + normalized.hash, + output.outIndex, + output.amountAtomic, + output.assetType, + output.targetType, + output.targetKey, + output.viewTag, + output.unlockTime, + JSON.stringify(output.rawTarget) + ] + ); + + if (normalized.typeCode === 2) { + await connection.query( + `INSERT INTO protocol_events ( + block_height, + protocol_tx_hash, + out_index, + amount_atomic, + asset_type, + return_address, + raw_target + ) VALUES (?, ?, ?, ?, ?, ?, ?)`, + [ + height, + normalized.hash, + output.outIndex, + output.amountAtomic, + output.assetType, + output.targetKey, + JSON.stringify(output.rawTarget) + ] + ); + } + } + } + + await updateProtocolEventLinks(connection, height); + await setLastIndexedHeight(connection, height); + await connection.commit(); + console.log(`Indexed block ${height}`); + } catch (error) { + await connection.rollback(); + throw error; + } finally { + connection.release(); + } +} + +async function syncLoop() { + const pool = createPool(); + const rpc = new PeyaRpcClient(env.PEYA_RPC_URL); + + while (true) { + const currentHeightResponse = await rpc.getHeight(); + const chainTop = currentHeightResponse.height - 1; + const lastIndexedHeight = await getLastIndexedHeight(pool); + const nextHeight = lastIndexedHeight + 1; + + if (nextHeight <= chainTop) { + const batchEnd = Math.min(chainTop, nextHeight + env.INDEXER_BATCH_SIZE - 1); + await syncYieldMetrics(pool, rpc, nextHeight, batchEnd); + for (let height = nextHeight; height <= batchEnd; height += 1) { + await indexBlock(pool, rpc, height); + } + } else if (ONCE) { + break; + } else { + await new Promise((resolve) => setTimeout(resolve, env.INDEXER_POLL_INTERVAL_MS)); + } + } + + await pool.end(); +} + +syncLoop().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/apps/indexer/src/migrate.ts b/apps/indexer/src/migrate.ts new file mode 100644 index 0000000..1b57846 --- /dev/null +++ b/apps/indexer/src/migrate.ts @@ -0,0 +1,21 @@ +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createPool } from "./db.js"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +async function main() { + const pool = createPool(); + const sqlPath = path.resolve(__dirname, "../migrations/001_init.sql"); + const sql = await readFile(sqlPath, "utf8"); + await pool.query(sql); + await pool.end(); + console.log(`Applied migration ${sqlPath}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); + diff --git a/apps/indexer/src/normalize.ts b/apps/indexer/src/normalize.ts new file mode 100644 index 0000000..f08e4ec --- /dev/null +++ b/apps/indexer/src/normalize.ts @@ -0,0 +1,164 @@ +import { txTypeLabel } from "@peya-explorer/shared"; + +export type RpcTransactionJson = { + version: number; + unlock_time: number; + inputs?: unknown[]; + outputs?: unknown[]; + extra?: number[]; + type?: number; + amount_burnt?: string | number; + source_asset_type?: string; + destination_asset_type?: string; + return_address?: string; + protocol_tx_data?: { + return_address?: string; + }; +}; + +type OutputTarget = + | { key?: { key?: string; asset_type?: string; unlock_time?: number } } + | { tagged_key?: { key?: string; asset_type?: string; unlock_time?: number; view_tag?: string } } + | { carrot_v1?: { key?: string; asset_type?: string; view_tag?: string } }; + +export type NormalizedOutput = { + outIndex: number; + amountAtomic: string; + assetType: string | null; + targetType: string; + targetKey: string | null; + viewTag: string | null; + unlockTime: number | null; + rawTarget: OutputTarget; +}; + +export function parseRpcJson(value: string | undefined): T | null { + if (!value) { + return null; + } + return JSON.parse(value) as T; +} + +export function normalizeOutputs(outputs: Array<{ amount?: string | number; target?: OutputTarget }> | undefined): NormalizedOutput[] { + return (outputs ?? []).map((output, index) => { + const target = output.target ?? {}; + if ("tagged_key" in target && target.tagged_key) { + return { + outIndex: index, + amountAtomic: String(output.amount ?? 0), + assetType: target.tagged_key.asset_type ?? null, + targetType: "tagged_key", + targetKey: target.tagged_key.key ?? null, + viewTag: target.tagged_key.view_tag ?? null, + unlockTime: target.tagged_key.unlock_time ?? null, + rawTarget: target + }; + } + + if ("key" in target && target.key) { + return { + outIndex: index, + amountAtomic: String(output.amount ?? 0), + assetType: target.key.asset_type ?? null, + targetType: "key", + targetKey: target.key.key ?? null, + viewTag: null, + unlockTime: target.key.unlock_time ?? null, + rawTarget: target + }; + } + + if ("carrot_v1" in target && target.carrot_v1) { + return { + outIndex: index, + amountAtomic: String(output.amount ?? 0), + assetType: target.carrot_v1.asset_type ?? null, + targetType: "carrot_v1", + targetKey: target.carrot_v1.key ?? null, + viewTag: target.carrot_v1.view_tag ?? null, + unlockTime: null, + rawTarget: target + }; + } + + return { + outIndex: index, + amountAtomic: String(output.amount ?? 0), + assetType: null, + targetType: "unknown", + targetKey: null, + viewTag: null, + unlockTime: null, + rawTarget: target + }; + }); +} + +export function hasMergeMiningTag(extra: number[] | undefined): boolean { + if (!extra || extra.length === 0) { + return false; + } + + let index = 0; + while (index < extra.length) { + const tag = extra[index]; + index += 1; + + if (tag === 0x00) { + while (index < extra.length && extra[index] === 0x00) { + index += 1; + } + continue; + } + + if (tag === 0x03) { + return true; + } + + if (tag === 0x01) { + index += 32; + continue; + } + + if (tag === 0x02) { + const length = extra[index] ?? 0; + index += 1 + length; + continue; + } + + if (tag === 0x04 || tag === 0x80 || tag === 0xde) { + return false; + } + + return false; + } + + return false; +} + +export function normalizeTransaction(txHash: string, blockHeight: number | null, txIndex: number, txJson: RpcTransactionJson) { + return { + hash: txHash, + blockHeight, + txIndex, + typeCode: txJson.type ?? 0, + typeLabel: txTypeLabel(txJson.type), + version: txJson.version ?? 0, + unlockTime: txJson.unlock_time ?? 0, + amountBurntAtomic: String(txJson.amount_burnt ?? 0), + sourceAssetType: txJson.source_asset_type ?? null, + destinationAssetType: txJson.destination_asset_type ?? null, + returnAddress: txJson.return_address ?? null, + protocolReturnAddress: txJson.protocol_tx_data?.return_address ?? null, + outputs: normalizeOutputs(txJson.outputs as Array<{ amount?: string | number; target?: OutputTarget }> | undefined), + rawJson: txJson + }; +} + +export function inferStakeUnlockHeight(protocolBlockHeight: number): number | null { + if (protocolBlockHeight <= 21601) { + return null; + } + return protocolBlockHeight - 21601; +} + diff --git a/apps/indexer/src/rpc.ts b/apps/indexer/src/rpc.ts new file mode 100644 index 0000000..622a148 --- /dev/null +++ b/apps/indexer/src/rpc.ts @@ -0,0 +1,139 @@ +const JSON_RPC_PATH = "/json_rpc"; + +type JsonRpcEnvelope = { + result?: T; + error?: { + code: number; + message: string; + }; +}; + +export type RpcBlockHeader = { + hash: string; + height: number; + prev_hash: string; + timestamp: number; + major_version: number; + minor_version: number; + nonce: number; + orphan_status: boolean; + reward: string | number; + block_size: number; + block_weight: number; + long_term_weight: number; + num_txes: number; + difficulty: string | number; + cumulative_difficulty: string | number; + pow_hash?: string; + miner_tx_hash: string; + protocol_tx_hash: string; +}; + +export type RpcGetBlockResult = { + blob: string; + json: string; + miner_tx_hash: string; + protocol_tx_hash: string; + tx_hashes: string[]; + block_header: RpcBlockHeader; + status: string; +}; + +export type RpcTransactionEntry = { + tx_hash: string; + as_json?: string; + as_hex: string; + in_pool: boolean; + block_height?: number; +}; + +export type RpcGetTransactionsResult = { + status: string; + txs: RpcTransactionEntry[]; + missed_tx: string[]; +}; + +export type RpcYieldInfoResult = { + status: string; + total_burnt: string | number; + total_staked: string | number; + total_yield: string | number; + yield_per_stake: string | number; + yield_data: Array<{ + block_height: number; + slippage_total_this_block: string | number; + locked_coins_this_block: string | number; + locked_coins_tally: string | number; + network_health_percentage: number; + }>; +}; + +export type RpcGetHeightResult = { + height: number; + status: string; +}; + +export class PeyaRpcClient { + constructor(private readonly baseUrl: string) {} + + private async jsonRpc(method: string, params: Record = {}): Promise { + const response = await fetch(`${this.baseUrl}${JSON_RPC_PATH}`, { + method: "POST", + headers: { + "content-type": "application/json" + }, + body: JSON.stringify({ + jsonrpc: "2.0", + id: "0", + method, + params + }) + }); + + if (!response.ok) { + throw new Error(`RPC ${method} failed with HTTP ${response.status}`); + } + + const payload = (await response.json()) as JsonRpcEnvelope; + if (payload.error) { + throw new Error(`RPC ${method} failed: ${payload.error.code} ${payload.error.message}`); + } + if (!payload.result) { + throw new Error(`RPC ${method} returned no result`); + } + return payload.result; + } + + async getHeight(): Promise { + const response = await fetch(`${this.baseUrl}/getheight`); + if (!response.ok) { + throw new Error(`GET /getheight failed with HTTP ${response.status}`); + } + return (await response.json()) as RpcGetHeightResult; + } + + async getBlock(height: number): Promise { + return this.jsonRpc("get_block", { height, fill_pow_hash: true }); + } + + async getTransactions(txHashes: string[]): Promise { + if (txHashes.length === 0) { + return { status: "OK", txs: [], missed_tx: [] }; + } + return this.jsonRpc("gettransactions", { + txs_hashes: txHashes, + decode_as_json: true, + prune: false, + split: false + }); + } + + async getYieldInfo(fromHeight: number, toHeight: number): Promise { + return this.jsonRpc("get_yield_info", { + include_raw_data: true, + from_height: fromHeight, + to_height: toHeight + }); + } +} + diff --git a/apps/indexer/tsconfig.json b/apps/indexer/tsconfig.json new file mode 100644 index 0000000..af86f2f --- /dev/null +++ b/apps/indexer/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/apps/web/app/blocks/[height]/page.tsx b/apps/web/app/blocks/[height]/page.tsx new file mode 100644 index 0000000..ee21c42 --- /dev/null +++ b/apps/web/app/blocks/[height]/page.tsx @@ -0,0 +1,137 @@ +import { formatAtomic, formatPercent, txTypeLabel } from "@peya-explorer/shared"; +import { getBlock } from "../../../lib/api"; + +export default async function BlockDetailPage({ params }: { params: Promise<{ height: string }> }) { + const { height } = await params; + const data = await getBlock(height); + const block = data.block; + + return ( +
+
+
+ Block detail +

Height {block.height}

+
+

{block.hash}

+
+ +
+
+
+
+ Consensus view +

Header & reward

+
+
+
+
+
Hash
+
{block.hash}
+
+
+
Prev hash
+
{block.prev_hash}
+
+
+
Difficulty
+
{block.difficulty}
+
+
+
Reward
+
{formatAtomic(block.reward_atomic)} PEY
+
+
+
Miner burn
+
{formatAtomic(block.miner_burn_atomic)} SAL
+
+
+
Merge-mined
+
{block.is_merge_mined === null ? "unknown" : block.is_merge_mined ? "yes" : "no"}
+
+
+
Locked coins tally
+
{formatAtomic(block.locked_coins_tally ?? "0")} SAL
+
+
+
Network health
+
{formatPercent(Number(block.network_health_percentage ?? 0))}
+
+
+
+ +
+
+
+ Transactions +

Block payload

+
+
+
+ {data.transactions.map( + (transaction: { + hash: string; + type_code: number; + amount_burnt_atomic: string; + source_asset_type: string | null; + destination_asset_type: string | null; + }) => ( +
+
+ {txTypeLabel(transaction.type_code)} + {transaction.hash} +
+
+ burnt {formatAtomic(transaction.amount_burnt_atomic)} + + {transaction.source_asset_type ?? "n/a"} → {transaction.destination_asset_type ?? "n/a"} + +
+
+ ) + )} +
+
+
+ +
+
+
+ Protocol events +

Stake unlocks and yield

+
+
+
+
+ Out + Asset + Amount + Stake tx + Yield +
+ {data.protocolEvents.map( + (event: { + out_index: number; + asset_type: string | null; + amount_atomic: string; + stake_tx_hash: string | null; + yield_atomic: string | null; + yield_percent: string | number | null; + }) => ( +
+ {event.out_index} + {event.asset_type ?? "n/a"} + {formatAtomic(event.amount_atomic)} + {event.stake_tx_hash ?? "unlinked"} + + {event.yield_atomic ? `${formatAtomic(event.yield_atomic)} SAL / ${formatPercent(Number(event.yield_percent))}` : "n/a"} + +
+ ) + )} +
+
+
+ ); +} + diff --git a/apps/web/app/blocks/page.tsx b/apps/web/app/blocks/page.tsx new file mode 100644 index 0000000..f1d9fca --- /dev/null +++ b/apps/web/app/blocks/page.tsx @@ -0,0 +1,54 @@ +import Link from "next/link"; +import { formatAtomic, shortHash } from "@peya-explorer/shared"; +import { getBlocks } from "../../lib/api"; + +export default async function BlocksPage() { + const data = await getBlocks(50); + + return ( +
+
+
+ Chain index +

Blocks

+
+

Freshly indexed blocks with yield and merge-mining diagnostics.

+
+ +
+
+
+ Height + Hash + Difficulty + Reward + Yield + MM +
+ {data.items.map( + (block: { + height: number; + hash: string; + difficulty: string; + rewardAtomic: string; + slippageTotalThisBlock: string; + isMergeMined: boolean | null; + }) => ( + + {block.height} + {shortHash(block.hash)} + {block.difficulty} + {formatAtomic(block.rewardAtomic)} PEY + {formatAtomic(block.slippageTotalThisBlock)} SAL + + {block.isMergeMined ? "merge-mined" : "solo/unknown"} + + + ) + )} +
+
+
+ ); +} + diff --git a/apps/web/app/globals.css b/apps/web/app/globals.css new file mode 100644 index 0000000..90a6526 --- /dev/null +++ b/apps/web/app/globals.css @@ -0,0 +1,324 @@ +:root { + --bg-main: #081118; + --bg-panel: rgba(12, 24, 36, 0.84); + --bg-panel-strong: rgba(15, 28, 43, 0.96); + --line: rgba(136, 255, 225, 0.12); + --text-main: #effaf8; + --text-soft: #9db7b2; + --text-dim: #59727a; + --accent-mint: #42f5c8; + --accent-cyan: #4cc8ff; + --accent-gold: #d4ff69; + --badge-mm: rgba(76, 200, 255, 0.14); + --badge-solo: rgba(212, 255, 105, 0.14); + --shadow: 0 24px 80px rgba(0, 0, 0, 0.35); + --font-display: "Space Grotesk", "Segoe UI", sans-serif; + --font-body: "Manrope", "Segoe UI", sans-serif; +} + +* { + box-sizing: border-box; +} + +html { + min-height: 100%; + background: + radial-gradient(circle at top left, rgba(76, 200, 255, 0.18), transparent 28%), + radial-gradient(circle at top right, rgba(66, 245, 200, 0.16), transparent 22%), + linear-gradient(160deg, #04090d, #09131c 55%, #060d14); +} + +body { + margin: 0; + color: var(--text-main); + font-family: var(--font-body); +} + +a { + color: inherit; + text-decoration: none; +} + +.shell { + width: min(1280px, calc(100% - 40px)); + margin: 0 auto; + padding: 36px 0 72px; +} + +.hero, +.subhero { + display: grid; + gap: 24px; + margin-bottom: 28px; +} + +.hero { + grid-template-columns: 1.2fr 0.8fr; + align-items: end; +} + +.hero h1, +.subhero h1 { + margin: 10px 0 12px; + font-family: var(--font-display); + font-size: clamp(2.6rem, 5vw, 4.5rem); + line-height: 0.96; + letter-spacing: -0.05em; +} + +.hero p, +.subhero p { + max-width: 780px; + color: var(--text-soft); + font-size: 1.05rem; + line-height: 1.7; +} + +.eyebrow { + display: inline-flex; + padding: 6px 10px; + border: 1px solid var(--line); + border-radius: 999px; + color: var(--accent-gold); + font-size: 0.74rem; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.hero-panel, +.kpi-grid, +.dashboard-grid, +.detail-grid { + display: grid; + gap: 18px; +} + +.hero-panel { + grid-template-columns: repeat(2, 1fr); +} + +.dashboard-grid, +.detail-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.glass-panel, +.metric-card { + background: var(--bg-panel); + border: 1px solid var(--line); + border-radius: 26px; + backdrop-filter: blur(18px); + box-shadow: var(--shadow); +} + +.glass-panel { + padding: 22px; +} + +.metric-card { + padding: 18px; +} + +.metric-card.highlight { + background: + linear-gradient(135deg, rgba(66, 245, 200, 0.2), rgba(76, 200, 255, 0.12)), + var(--bg-panel-strong); +} + +.metric-card span, +.kpi span, +.detail-list dt { + display: block; + color: var(--text-dim); + font-size: 0.78rem; + letter-spacing: 0.12em; + text-transform: uppercase; +} + +.metric-card strong, +.kpi strong { + display: block; + margin-top: 10px; + font-size: clamp(1.4rem, 2vw, 2.1rem); + font-family: var(--font-display); + letter-spacing: -0.04em; +} + +.panel-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 16px; + margin-bottom: 18px; +} + +.panel-head h2 { + margin: 8px 0 0; + font-family: var(--font-display); + letter-spacing: -0.04em; +} + +.panel-link { + color: var(--accent-cyan); + font-size: 0.92rem; +} + +.kpi-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.kpi { + padding: 18px; + border-radius: 20px; + background: rgba(255, 255, 255, 0.025); + border: 1px solid rgba(255, 255, 255, 0.06); +} + +.block-table { + display: grid; +} + +.table-head, +.table-row { + display: grid; + gap: 18px; + align-items: center; + padding: 14px 0; +} + +.table-head { + color: var(--text-dim); + font-size: 0.76rem; + letter-spacing: 0.14em; + text-transform: uppercase; +} + +.table-row { + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.table-row:hover { + background: rgba(255, 255, 255, 0.02); +} + +.badge { + display: inline-flex; + width: fit-content; + padding: 7px 10px; + border-radius: 999px; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.12em; +} + +.badge-mm { + background: var(--badge-mm); + color: var(--accent-cyan); +} + +.badge-solo { + background: var(--badge-solo); + color: var(--accent-gold); +} + +.detail-list { + display: grid; + gap: 16px; +} + +.detail-list div { + padding-bottom: 14px; + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.detail-list dd { + margin: 8px 0 0; + color: var(--text-main); + overflow-wrap: anywhere; +} + +.stack-list { + display: grid; + gap: 12px; +} + +.stack-row { + display: flex; + justify-content: space-between; + gap: 18px; + padding: 16px 18px; + border-radius: 18px; + background: rgba(255, 255, 255, 0.03); +} + +.stack-row strong { + display: block; + margin-bottom: 8px; +} + +.stack-row span { + display: block; + color: var(--text-soft); + overflow-wrap: anywhere; +} + +.stack-meta { + text-align: right; +} + +.table-head, +.table-row, +.protocol-head, +.protocol-row { + grid-template-columns: 100px minmax(0, 1.3fr) 1fr 1fr; +} + +.blocks-head, +.blocks-row { + grid-template-columns: 100px minmax(0, 1fr) 1fr 1fr 1fr 160px; +} + +.protocol-head, +.protocol-row { + grid-template-columns: 80px 110px 1fr 1.4fr 1fr; +} + +@media (max-width: 980px) { + .hero, + .dashboard-grid, + .detail-grid { + grid-template-columns: 1fr; + } + + .hero-panel, + .kpi-grid { + grid-template-columns: 1fr; + } +} + +@media (max-width: 720px) { + .shell { + width: min(100% - 20px, 100%); + padding-top: 20px; + } + + .table-head { + display: none; + } + + .table-row, + .blocks-row, + .protocol-row { + grid-template-columns: 1fr; + gap: 8px; + padding: 18px 0; + } + + .stack-row { + flex-direction: column; + } + + .stack-meta { + text-align: left; + } +} diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx new file mode 100644 index 0000000..aa37eb0 --- /dev/null +++ b/apps/web/app/layout.tsx @@ -0,0 +1,16 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "Peya Explorer", + description: "Modern diagnostics explorer for Peya mainnet and testnet operations." +}; + +export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) { + return ( + + {children} + + ); +} + diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx new file mode 100644 index 0000000..0fd23aa --- /dev/null +++ b/apps/web/app/page.tsx @@ -0,0 +1,100 @@ +import Link from "next/link"; +import { formatAtomic, shortHash } from "@peya-explorer/shared"; +import { getBlocks, getSummary } from "../lib/api"; + +export default async function HomePage() { + const [summary, blocks] = await Promise.all([getSummary(), getBlocks(12)]); + + return ( +
+
+
+ Peya Diagnostics Explorer +

Modern visibility into blocks, staking flow, and merge-mining behavior.

+

+ This stack is purpose-built for current Peya testing: clean MySQL indexing, protocol-aware block detail, + yield metrics, and explicit merge-mining markers. +

+
+
+
+ Chain height + {summary.chainHeight ?? "n/a"} +
+
+ Top hash + {summary.topHashShort ?? "n/a"} +
+
+ Merge-mined last 100 + {summary.mergeMinedLast100} +
+
+ Total staked + {formatAtomic(summary.totalStaked ?? "0")} SAL +
+
+
+ +
+
+
+
+ Realtime posture +

Network snapshot

+
+
+
+
+ 24h average difficulty + {summary.averageDifficulty24h ?? "n/a"} +
+
+ Network health + {summary.networkHealthPercentage}% +
+
+ Total yield + {formatAtomic(summary.totalYield ?? "0")} SAL +
+
+ Latest slippage + {formatAtomic(summary.latestYieldBlockSlippage ?? "0")} SAL +
+
+
+ +
+
+
+ Latest chain activity +

Recent blocks

+
+ + View all blocks + +
+
+
+ Height + Hash + Reward + MM +
+ {blocks.items.map((block: { height: number; hash: string; rewardAtomic: string; isMergeMined: boolean | null }) => ( + + {block.height} + {shortHash(block.hash)} + {formatAtomic(block.rewardAtomic)} PEY + + {block.isMergeMined ? "merge-mined" : "solo/unknown"} + + + ))} +
+
+
+
+ ); +} + diff --git a/apps/web/lib/api.ts b/apps/web/lib/api.ts new file mode 100644 index 0000000..68e698b --- /dev/null +++ b/apps/web/lib/api.ts @@ -0,0 +1,32 @@ +const API_BASE_URL = process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://127.0.0.1:4100"; + +export async function getSummary() { + const response = await fetch(`${API_BASE_URL}/api/summary`, { + cache: "no-store" + }); + if (!response.ok) { + throw new Error(`Failed to fetch summary: ${response.status}`); + } + return response.json(); +} + +export async function getBlocks(limit = 20) { + const response = await fetch(`${API_BASE_URL}/api/blocks?limit=${limit}`, { + cache: "no-store" + }); + if (!response.ok) { + throw new Error(`Failed to fetch blocks: ${response.status}`); + } + return response.json(); +} + +export async function getBlock(height: string) { + const response = await fetch(`${API_BASE_URL}/api/blocks/${height}`, { + cache: "no-store" + }); + if (!response.ok) { + throw new Error(`Failed to fetch block ${height}: ${response.status}`); + } + return response.json(); +} + diff --git a/apps/web/next-env.d.ts b/apps/web/next-env.d.ts new file mode 100644 index 0000000..830fb59 --- /dev/null +++ b/apps/web/next-env.d.ts @@ -0,0 +1,6 @@ +/// +/// +/// + +// NOTE: This file should not be edited +// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/web/next.config.mjs b/apps/web/next.config.mjs new file mode 100644 index 0000000..69e8dfc --- /dev/null +++ b/apps/web/next.config.mjs @@ -0,0 +1,6 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + typedRoutes: true +}; + +export default nextConfig; diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..98b5bdc --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,23 @@ +{ + "name": "@peya-explorer/web", + "version": "0.1.0", + "private": true, + "scripts": { + "build": "next build", + "dev": "next dev", + "start": "next start" + }, + "dependencies": { + "@peya-explorer/shared": "0.1.0", + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.6.3" + } +} + diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..87e3e77 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,24 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "allowJs": true, + "noEmit": true, + "isolatedModules": true + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d4ecb34 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2219 @@ +{ + "name": "peya-explorer", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "peya-explorer", + "workspaces": [ + "apps/*", + "packages/*" + ] + }, + "apps/api": { + "name": "@peya-explorer/api", + "version": "0.1.0", + "dependencies": { + "@fastify/cors": "^10.0.1", + "@peya-explorer/shared": "0.1.0", + "dotenv": "^16.4.5", + "fastify": "^5.0.0", + "mysql2": "^3.11.3", + "zod": "^3.23.8" + }, + "devDependencies": { + "tsx": "^4.19.1", + "typescript": "^5.6.3" + } + }, + "apps/indexer": { + "name": "@peya-explorer/indexer", + "version": "0.1.0", + "dependencies": { + "@peya-explorer/shared": "0.1.0", + "dotenv": "^16.4.5", + "mysql2": "^3.11.3", + "zod": "^3.23.8" + }, + "devDependencies": { + "tsx": "^4.19.1", + "typescript": "^5.6.3" + } + }, + "apps/web": { + "name": "@peya-explorer/web", + "version": "0.1.0", + "dependencies": { + "@peya-explorer/shared": "0.1.0", + "next": "^15.0.0", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "typescript": "^5.6.3" + } + }, + "apps/web/node_modules/@types/node": { + "version": "22.19.15", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "apps/web/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/cors": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-10.1.0.tgz", + "integrity": "sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "fastify-plugin": "^5.0.0", + "mnemonist": "0.40.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "optional": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", + "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "cpu": [ + "arm64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", + "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "cpu": [ + "x64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", + "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", + "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", + "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", + "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", + "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", + "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", + "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", + "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", + "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", + "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", + "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "cpu": [ + "arm" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", + "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "cpu": [ + "arm64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", + "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "cpu": [ + "ppc64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", + "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "cpu": [ + "riscv64" + ], + "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-riscv64": "1.2.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", + "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "cpu": [ + "s390x" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", + "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "cpu": [ + "x64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", + "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "cpu": [ + "arm64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", + "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "cpu": [ + "x64" + ], + "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.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", + "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "cpu": [ + "wasm32" + ], + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.7.0" + }, + "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.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", + "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "cpu": [ + "arm64" + ], + "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.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", + "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "cpu": [ + "ia32" + ], + "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.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", + "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@next/env": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.5.14.tgz", + "integrity": "sha512-aXeirLYuASxEgi4X4WhfXsShCFxWDfNn/8ZeC5YXAS2BB4A8FJi1kwwGL6nvMVboE7fZCzmJPNdMvVHc8JpaiA==" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.5.14.tgz", + "integrity": "sha512-Y9K6SPzobnZvrRDPO2s0grgzC+Egf0CqfbdvYmQVaztV890zicw8Z8+4Vqw8oPck8r1TjUHxVh8299Cg4TrxXg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.5.14.tgz", + "integrity": "sha512-aNnkSMjSFRTOmkd7qoNI2/rETQm/vKD6c/Ac9BZGa9CtoOzy3c2njgz7LvebQJ8iPxdeTuGnAjagyis8a9ifBw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.5.14.tgz", + "integrity": "sha512-tjlpia+yStPRS//6sdmlVwuO1Rioern4u2onafa5n+h2hCS9MAvMXqpVbSrjgiEOoCs0nJy7oPOmWgtRRNSM5Q==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.5.14.tgz", + "integrity": "sha512-8B8cngBaLadl5lbDRdxGCP1Lef8ipD6KlxS3v0ElDAGil6lafrAM3B258p1KJOglInCVFUjk751IXMr2ixeQOQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.5.14.tgz", + "integrity": "sha512-bAS6tIAg8u4Gn3Nz7fCPpSoKAexEt2d5vn1mzokcqdqyov6ZJ6gu6GdF9l8ORFrBuRHgv3go/RfzYz5BkZ6YSQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.5.14.tgz", + "integrity": "sha512-mMxv/FcrT7Gfaq4tsR22l17oKWXZmH/lVqcvjX0kfp5I0lKodHYLICKPoX1KRnnE+ci6oIUdriUhuA3rBCDiSw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.5.14.tgz", + "integrity": "sha512-OTmiBlYThppnvnsqx0rBqjDRemlmIeZ8/o4zI7veaXoeO1PVHoyj2lfTfXTiiGjCyRDhA10y4h6ZvZvBiynr2g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.14.tgz", + "integrity": "sha512-+W7eFf3RS7m4G6tppVTOSyP9Y6FsJXfOuKzav1qKniiFm3KFByQfPEcouHdjlZmysl4zJGuGLQ/M9XyVeyeNEg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@peya-explorer/api": { + "resolved": "apps/api", + "link": true + }, + "node_modules/@peya-explorer/indexer": { + "resolved": "apps/indexer", + "link": true + }, + "node_modules/@peya-explorer/shared": { + "resolved": "packages/shared", + "link": true + }, + "node_modules/@peya-explorer/web": { + "resolved": "apps/web", + "link": true + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==" + }, + "node_modules/@swc/helpers": { + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "peer": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==" + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001781", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001781.tgz", + "integrity": "sha512-RdwNCyMsNBftLjW6w01z8bKEvT6e/5tpPVEgtn22TiLGlstHOVecsX2KHFkD5e/vRnIE4EGzpuIODb3mtswtkw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stringify": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", + "integrity": "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastify": { + "version": "5.8.4", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.4.tgz", + "integrity": "sha512-sa42J1xylbBAYUWALSBoyXKPDUvM3OoNOibIefA+Oha57FryXKKCZarA1iDntOCWp3O35voZLuDg2mdODXtPzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastify-plugin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.1.0.tgz", + "integrity": "sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/find-my-way": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz", + "integrity": "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==" + }, + "node_modules/lru.min": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", + "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, + "node_modules/mnemonist": { + "version": "0.40.0", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.40.0.tgz", + "integrity": "sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==", + "dependencies": { + "obliterator": "^2.0.4" + } + }, + "node_modules/mysql2": { + "version": "3.20.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.20.0.tgz", + "integrity": "sha512-eCLUs7BNbgA6nf/MZXsaBO1SfGs0LtLVrJD3WeWq+jPLDWkSufTD+aGMwykfUVPdZnblaUK1a8G/P63cl9FkKg==", + "dependencies": { + "aws-ssl-profiles": "^1.1.2", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.7.2", + "long": "^5.3.2", + "lru.min": "^1.1.4", + "named-placeholders": "^1.1.6", + "sql-escaper": "^1.3.3" + }, + "engines": { + "node": ">= 8.0" + }, + "peerDependencies": { + "@types/node": ">= 8" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", + "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", + "dependencies": { + "lru.min": "^1.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "15.5.14", + "resolved": "https://registry.npmjs.org/next/-/next-15.5.14.tgz", + "integrity": "sha512-M6S+4JyRjmKic2Ssm7jHUPkE6YUJ6lv4507jprsSZLulubz0ihO2E+S4zmQK3JZ2ov81JrugukKU4Tz0ivgqqQ==", + "dependencies": { + "@next/env": "15.5.14", + "@swc/helpers": "0.5.15", + "caniuse-lite": "^1.0.30001579", + "postcss": "8.4.31", + "styled-jsx": "5.1.6" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "15.5.14", + "@next/swc-darwin-x64": "15.5.14", + "@next/swc-linux-arm64-gnu": "15.5.14", + "@next/swc-linux-arm64-musl": "15.5.14", + "@next/swc-linux-x64-gnu": "15.5.14", + "@next/swc-linux-x64-musl": "15.5.14", + "@next/swc-win32-arm64-msvc": "15.5.14", + "@next/swc-win32-x64-msvc": "15.5.14", + "sharp": "^0.34.3" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.51.1", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/obliterator": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.5.tgz", + "integrity": "sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==" + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/react": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.4" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==" + }, + "node_modules/safe-regex2": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.0.tgz", + "integrity": "sha512-pNHAuBW7TrcleFHsxBr5QMi/Iyp0ENjUKz7GCcX1UO7cMh+NmVK6HxQckNL1tJp1XAJVjG6B8OKIPqodqj9rtw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==" + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==" + }, + "node_modules/sharp": { + "version": "0.34.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", + "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@img/colour": "^1.0.0", + "detect-libc": "^2.1.2", + "semver": "^7.7.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.5", + "@img/sharp-darwin-x64": "0.34.5", + "@img/sharp-libvips-darwin-arm64": "1.2.4", + "@img/sharp-libvips-darwin-x64": "1.2.4", + "@img/sharp-libvips-linux-arm": "1.2.4", + "@img/sharp-libvips-linux-arm64": "1.2.4", + "@img/sharp-libvips-linux-ppc64": "1.2.4", + "@img/sharp-libvips-linux-riscv64": "1.2.4", + "@img/sharp-libvips-linux-s390x": "1.2.4", + "@img/sharp-libvips-linux-x64": "1.2.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", + "@img/sharp-libvips-linuxmusl-x64": "1.2.4", + "@img/sharp-linux-arm": "0.34.5", + "@img/sharp-linux-arm64": "0.34.5", + "@img/sharp-linux-ppc64": "0.34.5", + "@img/sharp-linux-riscv64": "0.34.5", + "@img/sharp-linux-s390x": "0.34.5", + "@img/sharp-linux-x64": "0.34.5", + "@img/sharp-linuxmusl-arm64": "0.34.5", + "@img/sharp-linuxmusl-x64": "0.34.5", + "@img/sharp-wasm32": "0.34.5", + "@img/sharp-win32-arm64": "0.34.5", + "@img/sharp-win32-ia32": "0.34.5", + "@img/sharp-win32-x64": "0.34.5" + } + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/sql-escaper": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz", + "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=2.0.0", + "node": ">=12.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "peer": true + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "packages/shared": { + "name": "@peya-explorer/shared", + "version": "0.1.0" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..53098ef --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "peya-explorer", + "private": true, + "workspaces": [ + "apps/*", + "packages/*" + ], + "scripts": { + "build": "npm run build --workspaces --if-present", + "dev:web": "npm run dev -w @peya-explorer/web", + "dev:api": "npm run dev -w @peya-explorer/api", + "dev:indexer": "npm run dev -w @peya-explorer/indexer", + "db:migrate": "npm run db:migrate -w @peya-explorer/indexer", + "index:once": "npm run index:once -w @peya-explorer/indexer" + } +} + diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..5fa1f0b --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,12 @@ +{ + "name": "@peya-explorer/shared", + "version": "0.1.0", + "private": true, + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "scripts": { + "build": "tsc -p tsconfig.json" + } +} + diff --git a/packages/shared/src/index.js b/packages/shared/src/index.js new file mode 100644 index 0000000..ab8726a --- /dev/null +++ b/packages/shared/src/index.js @@ -0,0 +1,42 @@ +export const PICO_PER_PEY = 100000000n; +export const TX_TYPE_LABELS = { + 0: "unset", + 1: "miner", + 2: "protocol", + 3: "burn", + 4: "convert", + 5: "transfer", + 6: "stake", + 7: "create-token", + 8: "audit", + 9: "return", + 10: "rollup" +}; +export function formatAtomic(value, decimals = 8) { + const atomic = typeof value === "bigint" ? value : BigInt(value); + const negative = atomic < 0n; + const abs = negative ? atomic * -1n : atomic; + const div = 10n ** BigInt(decimals); + const whole = abs / div; + const fraction = abs % div; + const fractionText = fraction.toString().padStart(decimals, "0").replace(/0+$/, ""); + return `${negative ? "-" : ""}${whole.toString()}${fractionText ? `.${fractionText}` : ""}`; +} +export function formatPercent(value) { + if (value === null || Number.isNaN(value)) { + return "n/a"; + } + return `${value.toFixed(2)}%`; +} +export function txTypeLabel(type) { + if (type === null || type === undefined) { + return "unknown"; + } + return TX_TYPE_LABELS[type] ?? `type-${type}`; +} +export function shortHash(hash, left = 8, right = 8) { + if (!hash || hash.length <= left + right) { + return hash; + } + return `${hash.slice(0, left)}...${hash.slice(-right)}`; +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..c14cc80 --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1,47 @@ +export const PICO_PER_PEY = 100000000n; + +export const TX_TYPE_LABELS: Record = { + 0: "unset", + 1: "miner", + 2: "protocol", + 3: "burn", + 4: "convert", + 5: "transfer", + 6: "stake", + 7: "create-token", + 8: "audit", + 9: "return", + 10: "rollup" +}; + +export function formatAtomic(value: bigint | number | string, decimals = 8): string { + const atomic = typeof value === "bigint" ? value : BigInt(value); + const negative = atomic < 0n; + const abs = negative ? atomic * -1n : atomic; + const div = 10n ** BigInt(decimals); + const whole = abs / div; + const fraction = abs % div; + const fractionText = fraction.toString().padStart(decimals, "0").replace(/0+$/, ""); + return `${negative ? "-" : ""}${whole.toString()}${fractionText ? `.${fractionText}` : ""}`; +} + +export function formatPercent(value: number | null): string { + if (value === null || Number.isNaN(value)) { + return "n/a"; + } + return `${value.toFixed(2)}%`; +} + +export function txTypeLabel(type: number | null | undefined): string { + if (type === null || type === undefined) { + return "unknown"; + } + return TX_TYPE_LABELS[type] ?? `type-${type}`; +} + +export function shortHash(hash: string, left = 8, right = 8): string { + if (!hash || hash.length <= left + right) { + return hash; + } + return `${hash.slice(0, left)}...${hash.slice(-right)}`; +} diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..14638f8 --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true + }, + "include": [ + "src/**/*.ts" + ] +} + diff --git a/tsconfig.base.json b/tsconfig.base.json new file mode 100644 index 0000000..b144764 --- /dev/null +++ b/tsconfig.base.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "baseUrl": ".", + "paths": { + "@peya-explorer/shared": [ + "packages/shared/src/index.ts" + ] + }, + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "allowSyntheticDefaultImports": true, + "lib": [ + "es2022", + "dom" + ] + } +}