aboutsummaryrefslogtreecommitdiff
path: root/website/api
diff options
context:
space:
mode:
authorMonzero Build System <builds@monzero.org>2026-08-15 21:14:26 +0100
committerMonzero Build System <builds@monzero.org>2026-08-15 21:14:26 +0100
commit28f1919249465d3230f45ed21686c5a836d56df0 (patch)
tree948fe5e7052c45c366c24a82b485ae9d1d38e39b /website/api
parent4f92268d7c16741cfb41e5bbe2aa46cc260a9ea5 (diff)
downloadmonzero-core-28f1919249465d3230f45ed21686c5a836d56df0.tar.gz
monzero-core-28f1919249465d3230f45ed21686c5a836d56df0.tar.xz
monzero-core-28f1919249465d3230f45ed21686c5a836d56df0.zip
Establish Monzero Phase 0 and inactive assets baselinemonzero-phase0-assets-prototype-20260815
Diffstat (limited to 'website/api')
-rw-r--r--website/api/miner-stats/index.php130
-rw-r--r--website/api/node-info/index.php58
2 files changed, 188 insertions, 0 deletions
diff --git a/website/api/miner-stats/index.php b/website/api/miner-stats/index.php
new file mode 100644
index 000000000..43ae75fbc
--- /dev/null
+++ b/website/api/miner-stats/index.php
@@ -0,0 +1,130 @@
+<?php
+declare(strict_types=1);
+
+header('Content-Type: application/json; charset=utf-8');
+header('Cache-Control: no-store');
+header('X-Content-Type-Options: nosniff');
+
+const ACTIVE_SECONDS = 180;
+const MAX_RECORD_AGE = 604800;
+
+$fileConfig = [];
+$documentRoot = $_SERVER['DOCUMENT_ROOT'] ?? '';
+if (is_string($documentRoot) && $documentRoot !== '') {
+ $configPath = dirname($documentRoot) . '/.monzero-miner-stats.php';
+ if (is_file($configPath)) {
+ $loadedConfig = require $configPath;
+ if (is_array($loadedConfig)) $fileConfig = $loadedConfig;
+ }
+}
+
+function fail(int $status, string $message): never
+{
+ http_response_code($status);
+ echo json_encode(['status' => $message]);
+ exit;
+}
+
+function configValue(string $name): string
+{
+ global $fileConfig;
+ if (isset($fileConfig[$name]) && is_string($fileConfig[$name])) {
+ return trim($fileConfig[$name]);
+ }
+ $value = getenv($name);
+ return is_string($value) ? trim($value) : '';
+}
+
+function dataPath(): string
+{
+ $configured = configValue('MONZERO_STATS_FILE');
+ return $configured !== '' ? $configured : sys_get_temp_dir() . '/monzero-miner-stats.json';
+}
+
+function readRecords($handle): array
+{
+ rewind($handle);
+ $contents = stream_get_contents($handle);
+ if (!is_string($contents) || trim($contents) === '') return [];
+ $decoded = json_decode($contents, true);
+ return is_array($decoded) ? $decoded : [];
+}
+
+$secret = configValue('MONZERO_STATS_SECRET');
+if (strlen($secret) < 32) fail(503, 'NOT_CONFIGURED');
+
+$path = dataPath();
+$directory = dirname($path);
+if (!is_dir($directory) || !is_writable($directory)) fail(503, 'STORAGE_UNAVAILABLE');
+
+$handle = @fopen($path, 'c+');
+if ($handle === false || !flock($handle, LOCK_EX)) fail(503, 'STORAGE_UNAVAILABLE');
+$records = readRecords($handle);
+$now = time();
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ $token = configValue('MONZERO_STATS_INGEST_TOKEN');
+ if (strlen($token) < 24) fail(503, 'INGEST_NOT_CONFIGURED');
+ $authorization = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
+ if (!hash_equals('Bearer ' . $token, $authorization)) fail(401, 'UNAUTHORIZED');
+
+ $raw = file_get_contents('php://input');
+ $input = json_decode(is_string($raw) ? $raw : '', true);
+ if (!is_array($input)) fail(400, 'INVALID_JSON');
+
+ $installationId = $input['installation_id'] ?? '';
+ $hashrate = filter_var($input['hashrate'] ?? null, FILTER_VALIDATE_FLOAT);
+ $blocks = filter_var($input['blocks_found'] ?? null, FILTER_VALIDATE_INT);
+ if (!is_string($installationId) || !preg_match('/^[a-f0-9-]{32,64}$/i', $installationId)) fail(400, 'INVALID_INSTALLATION_ID');
+ if ($hashrate === false || $hashrate < 0 || $hashrate > 1000000000000) fail(400, 'INVALID_HASHRATE');
+ if ($blocks === false || $blocks < 0 || $blocks > 1000000000) fail(400, 'INVALID_BLOCK_COUNT');
+
+ $privateId = hash_hmac('sha256', strtolower($installationId), $secret);
+ $records[$privateId] = [
+ 'hashrate' => round((float)$hashrate, 2),
+ 'blocks_found' => (int)$blocks,
+ 'updated_at' => $now,
+ ];
+}
+
+foreach ($records as $id => $record) {
+ if (!is_array($record) || ($record['updated_at'] ?? 0) < $now - MAX_RECORD_AGE) unset($records[$id]);
+}
+
+rewind($handle);
+ftruncate($handle, 0);
+fwrite($handle, json_encode($records, JSON_UNESCAPED_SLASHES));
+fflush($handle);
+flock($handle, LOCK_UN);
+fclose($handle);
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+ echo json_encode(['status' => 'OK']);
+ exit;
+}
+if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
+ header('Allow: GET, POST');
+ fail(405, 'METHOD_NOT_ALLOWED');
+}
+
+$miners = [];
+foreach ($records as $id => $record) {
+ $active = (int)$record['updated_at'] >= $now - ACTIVE_SECONDS;
+ if (!$active) continue;
+ $miners[] = [
+ 'name' => 'Miner ' . strtoupper(substr($id, 0, 8)),
+ 'hashrate' => (float)$record['hashrate'],
+ 'blocks_found' => (int)$record['blocks_found'],
+ 'active' => true,
+ ];
+}
+usort($miners, static fn(array $a, array $b): int => $b['hashrate'] <=> $a['hashrate']);
+
+echo json_encode([
+ 'status' => 'OK',
+ 'generated_at' => $now,
+ 'active_miners' => count($miners),
+ 'total_hashrate' => array_sum(array_column($miners, 'hashrate')),
+ 'total_blocks' => array_sum(array_column($miners, 'blocks_found')),
+ 'miners' => $miners,
+], JSON_UNESCAPED_SLASHES);
diff --git a/website/api/node-info/index.php b/website/api/node-info/index.php
new file mode 100644
index 000000000..05f1610a4
--- /dev/null
+++ b/website/api/node-info/index.php
@@ -0,0 +1,58 @@
+<?php
+declare(strict_types=1);
+
+header('Content-Type: application/json; charset=utf-8');
+header('Cache-Control: no-store');
+header('X-Content-Type-Options: nosniff');
+
+if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
+ http_response_code(405);
+ header('Allow: POST');
+ echo json_encode(['status' => 'METHOD_NOT_ALLOWED']);
+ exit;
+}
+
+$url = 'http://node.monzero.org:6175/get_info';
+$error = '';
+
+if (function_exists('curl_init')) {
+ $curl = curl_init($url);
+ curl_setopt_array($curl, [
+ CURLOPT_POST => true,
+ CURLOPT_POSTFIELDS => '{}',
+ CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_CONNECTTIMEOUT => 2,
+ CURLOPT_TIMEOUT => 5,
+ ]);
+ $response = curl_exec($curl);
+ $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
+ $error = curl_error($curl);
+ curl_close($curl);
+} else {
+ $context = stream_context_create(['http' => [
+ 'method' => 'POST',
+ 'header' => "Content-Type: application/json\r\nConnection: close\r\n",
+ 'content' => '{}',
+ 'timeout' => 5,
+ 'ignore_errors' => true,
+ ]]);
+ $response = @file_get_contents($url, false, $context);
+ $statusLine = $http_response_header[0] ?? '';
+ preg_match('/\s(\d{3})\s/', $statusLine, $match);
+ $status = isset($match[1]) ? (int)$match[1] : 0;
+ $error = 'HTTP ' . $status;
+}
+
+$decoded = is_string($response) ? json_decode($response, true) : null;
+if ($response === false || $status !== 200 || !is_array($decoded)) {
+ http_response_code(502);
+ echo json_encode(['status' => 'NODE_UNAVAILABLE']);
+ error_log('Monzero node status proxy failed: ' . $error);
+ exit;
+}
+
+$peerCount = (int)($decoded['incoming_connections_count'] ?? 0)
+ + (int)($decoded['outgoing_connections_count'] ?? 0);
+$decoded['connections_hidden'] = !empty($decoded['restricted']) && $peerCount === 0;
+echo json_encode($decoded, JSON_UNESCAPED_SLASHES);