157 lines
5.7 KiB
PHP
157 lines
5.7 KiB
PHP
<?php
|
|
|
|
/**
|
|
* cry01_chain.php — On-chain read layer.
|
|
* Reads Ğ1 balances from the local Duniter node RPC over Wireguard.
|
|
* Reads and writes the local balance cache.
|
|
* This is the only file in the project that makes outbound network calls.
|
|
* If the node infrastructure changes, only this file changes.
|
|
*/
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// BALANCE READ
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function cry01_get_balance($g1_pubkey) {
|
|
// Returns the current Ğ1 balance for the given public key.
|
|
// Queries the local Duniter node RPC. Returns null on failure.
|
|
$config = cry01_load_config();
|
|
$rpc = $config['g1_rpc_endpoint'] ?? '';
|
|
if (!$rpc || !$g1_pubkey) return null;
|
|
|
|
$payload = json_encode([
|
|
'jsonrpc' => '2.0',
|
|
'id' => 1,
|
|
'method' => 'state_getStorage',
|
|
'params' => [cry01_balance_storage_key($g1_pubkey)],
|
|
]);
|
|
|
|
$result = cry01_rpc_post($rpc, $payload);
|
|
if ($result === null) return null;
|
|
|
|
return cry01_decode_balance($result);
|
|
}
|
|
|
|
function cry01_balance_storage_key($g1_pubkey) {
|
|
// Builds the Substrate storage key for the account balance of the given Ğ1 public key.
|
|
// The key format is defined by the Duniter v2 runtime (pallet_balances storage map).
|
|
// Placeholder — requires Substrate storage key encoding (xxHash + Blake2).
|
|
// TODO: implement full storage key derivation or use duniter-specific RPC method.
|
|
return '0x' . bin2hex($g1_pubkey);
|
|
}
|
|
|
|
function cry01_decode_balance($rpc_result) {
|
|
// Decodes the raw RPC result into a human-readable Ğ1 balance string.
|
|
// Duniter v2 balances are stored as u64 encoded in SCALE codec.
|
|
// Returns formatted balance string or null if decoding fails.
|
|
$hex = $rpc_result['result'] ?? null;
|
|
if (!$hex || $hex === '0x') return '0';
|
|
// Remove 0x prefix and decode little-endian u64.
|
|
$hex = ltrim($hex, '0x');
|
|
$bytes = array_reverse(str_split(str_pad($hex, 16, '0', STR_PAD_LEFT), 2));
|
|
$val = 0;
|
|
foreach ($bytes as $b) {
|
|
$val = ($val << 8) | hexdec($b);
|
|
}
|
|
// Duniter uses centimes (hundredths of Ğ1). Divide by 100.
|
|
return number_format($val / 100, 2) . ' Ğ1';
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// RPC TRANSPORT
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function cry01_rpc_post($endpoint, $payload) {
|
|
// POSTs a JSON-RPC request to the Duniter node. Returns decoded response or null.
|
|
$context = stream_context_create([
|
|
'http' => [
|
|
'method' => 'POST',
|
|
'header' => "Content-Type: application/json\r\n",
|
|
'content' => $payload,
|
|
'timeout' => 5,
|
|
],
|
|
]);
|
|
$raw = @file_get_contents($endpoint, false, $context);
|
|
if ($raw === false) {
|
|
logger('cry01_chain: RPC call failed to ' . $endpoint);
|
|
return null;
|
|
}
|
|
$data = json_decode($raw, true);
|
|
if (json_last_error() !== JSON_ERROR_NONE) {
|
|
logger('cry01_chain: RPC response is not valid JSON');
|
|
return null;
|
|
}
|
|
return $data;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CACHE
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function cry01_read_cache() {
|
|
// Returns the balance cache array. Returns empty array if cache does not exist or is unreadable.
|
|
$config = cry01_load_config();
|
|
$path = $config['cache_file'] ?? '';
|
|
if (!$path) return [];
|
|
$raw = @file_get_contents($path);
|
|
if ($raw === false) return [];
|
|
$data = json_decode($raw, true);
|
|
return (json_last_error() === JSON_ERROR_NONE) ? $data : [];
|
|
}
|
|
|
|
function cry01_write_cache($data) {
|
|
// Writes the balance cache to disk. Returns true on success, false on failure.
|
|
$config = cry01_load_config();
|
|
$path = $config['cache_file'] ?? '';
|
|
if (!$path) return false;
|
|
$tmp = $path . '.tmp';
|
|
$ok = @file_put_contents($tmp, json_encode($data, JSON_PRETTY_PRINT));
|
|
if ($ok === false) {
|
|
logger('cry01_chain: could not write cache to ' . $tmp);
|
|
return false;
|
|
}
|
|
return @rename($tmp, $path);
|
|
}
|
|
|
|
function cry01_cache_is_stale() {
|
|
// Returns true if the cache is older than cache_max_age_seconds or does not exist.
|
|
$config = cry01_load_config();
|
|
$path = $config['cache_file'] ?? '';
|
|
$max_age = intval($config['cache_max_age_seconds'] ?? 3600);
|
|
if (!$path || !file_exists($path)) return true;
|
|
return (time() - filemtime($path)) > $max_age;
|
|
}
|
|
|
|
function cry01_refresh_balance_cache() {
|
|
// Refreshes the balance cache by querying the Duniter node.
|
|
// Writes updated cache to disk. Called by the manage route.
|
|
$config = cry01_load_config();
|
|
$pubkey = $config['operator_g1_pubkey'] ?? '';
|
|
if (!$pubkey) {
|
|
logger('cry01_chain: operator_g1_pubkey not set in config');
|
|
return false;
|
|
}
|
|
$balance = cry01_get_balance($pubkey);
|
|
if ($balance === null) return false;
|
|
|
|
$cache = cry01_read_cache();
|
|
$cache['operator_balance'] = $balance;
|
|
$cache['operator_g1_pubkey'] = $pubkey;
|
|
$cache['refreshed_at'] = date('c');
|
|
return cry01_write_cache($cache);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// CONFIG LOADER
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function cry01_load_config() {
|
|
// Loads cry01 config.json from the addon directory. Returns empty array on failure.
|
|
static $cfg = null;
|
|
if ($cfg !== null) return $cfg;
|
|
$raw = @file_get_contents('addon/cry01/config.json');
|
|
if ($raw === false) return [];
|
|
$cfg = json_decode($raw, true);
|
|
return (json_last_error() === JSON_ERROR_NONE) ? $cfg : [];
|
|
}
|