Files
kane-diagnostics/hubzilla/addon/cry01/cry01_chain.php
2026-06-08 02:08:51 -04:00

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 : [];
}