aboutsummaryrefslogtreecommitdiff
path: root/explorer/api.php
blob: 109485dab62f60399dd781584e62bc8ad6f0863b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
<?php
declare(strict_types=1);

const NODE_RPC = 'http://node.monzero.org:6175';
const MAX_RANGE = 25;

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    respond(['error' => 'POST required'], 405);
}

$input = json_decode(file_get_contents('php://input'), true);
if (!is_array($input)) respond(['error' => 'Invalid JSON'], 400);
$action = $input['action'] ?? '';

try {
    switch ($action) {
        case 'info':
            respond(rpc('/get_info', []));

        case 'blocks':
            $info = rpc('/get_info', []);
            $height = max(1, (int)($info['height'] ?? 1));
            $limit = min(MAX_RANGE, max(1, (int)($input['limit'] ?? 15)));
            $end = max(0, $height - 1);
            $start = max(0, $end - $limit + 1);
            $range = jsonRpc('get_block_headers_range', [
                'start_height' => $start,
                'end_height' => $end,
                'fill_pow_hash' => false,
            ]);
            respond(['info' => $info, 'headers' => array_reverse($range['headers'] ?? [])]);

        case 'block':
            $params = ['fill_pow_hash' => true];
            if (isset($input['height']) && is_numeric($input['height'])) {
                $params['height'] = max(0, (int)$input['height']);
            } elseif (validHash($input['hash'] ?? '')) {
                $params['hash'] = strtolower($input['hash']);
            } else {
                respond(['error' => 'Valid height or block hash required'], 400);
            }
            respond(jsonRpc('get_block', $params));

        case 'transaction':
            $hash = strtolower((string)($input['hash'] ?? ''));
            if (!validHash($hash)) respond(['error' => 'Valid transaction hash required'], 400);
            $result = rpc('/get_transactions', [
                'txs_hashes' => [$hash],
                'decode_as_json' => true,
                'prune' => false,
                'split' => true,
            ]);
            if (empty($result['txs']) && empty($result['txs_as_json'])) respond(['error' => 'Transaction not found'], 404);
            respond($result);

        case 'pool':
            respond(rpc('/get_transaction_pool', []));

        default:
            respond(['error' => 'Unknown action'], 400);
    }
} catch (Throwable $error) {
    error_log('Monzero explorer API: ' . $error->getMessage());
    respond(['error' => 'Node request failed'], 502);
}

function validHash(mixed $value): bool {
    return is_string($value) && preg_match('/^[0-9a-fA-F]{64}$/', $value) === 1;
}

function jsonRpc(string $method, array $params): array {
    $response = rpc('/json_rpc', [
        'jsonrpc' => '2.0',
        'id' => '0',
        'method' => $method,
        'params' => $params,
    ]);
    if (isset($response['error'])) throw new RuntimeException($response['error']['message'] ?? 'RPC error');
    return $response['result'] ?? [];
}

function rpc(string $path, array $payload): array {
    // Daemon endpoints expect an empty JSON object, not an empty JSON array.
    $json = $payload === [] ? '{}' : json_encode($payload, JSON_THROW_ON_ERROR);
    if (function_exists('curl_init')) {
        $curl = curl_init(NODE_RPC . $path);
        curl_setopt_array($curl, [
            CURLOPT_POST => true,
            CURLOPT_POSTFIELDS => $json,
            CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_CONNECTTIMEOUT => 3,
            CURLOPT_TIMEOUT => 12,
        ]);
        $body = curl_exec($curl);
        $status = curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
        $message = curl_error($curl);
        curl_close($curl);
        if ($body === false || $status !== 200) throw new RuntimeException($message ?: 'HTTP ' . $status);
    } else {
        $context = stream_context_create(['http' => [
            'method' => 'POST',
            'header' => "Content-Type: application/json\r\nConnection: close\r\n",
            'content' => $json,
            'timeout' => 12,
            'ignore_errors' => true,
        ]]);
        $body = @file_get_contents(NODE_RPC . $path, false, $context);
        $statusLine = $http_response_header[0] ?? '';
        preg_match('/\s(\d{3})\s/', $statusLine, $match);
        $status = isset($match[1]) ? (int)$match[1] : 0;
        if ($body === false || $status !== 200) throw new RuntimeException('HTTP ' . $status);
    }
    $decoded = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
    return is_array($decoded) ? $decoded : [];
}

function respond(array $data, int $status = 200): never {
    http_response_code($status);
    echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_INVALID_UTF8_SUBSTITUTE);
    exit;
}