191 lines
7.2 KiB
PHP
191 lines
7.2 KiB
PHP
<?php
|
|
|
|
/**
|
|
* cry01_chain.php — Ğ1 application layer on top of cry01_substrate.php.
|
|
*
|
|
* This file contains Ğ1-specific and cry01-specific logic: what we do with
|
|
* account data once decoded — formatting amounts in Ğ1, caching balances,
|
|
* refreshing the cache. Generic Substrate protocol mechanics (storage keys,
|
|
* SCALE decoding, SS58, RPC transport) live in cry01_substrate.php and are
|
|
* required by it.
|
|
*/
|
|
|
|
require_once 'addon/cry01/cry01_substrate.php';
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// ACCOUNT INFO / BALANCE READ
|
|
// ----------------------------------------------------------------------------
|
|
|
|
function cry01_get_account_info($account_id_bytes) {
|
|
// Returns the full decoded AccountInfo struct for the given 32-byte raw
|
|
// account ID (as returned by cry01_ss58_decode()):
|
|
// ['nonce', 'consumers', 'providers', 'sufficients',
|
|
// 'free', 'reserved', 'frozen', 'flags']
|
|
// All values are base-10 decimal strings (raw on-chain units, not
|
|
// formatted Ğ1 amounts).
|
|
//
|
|
// Returns null on RPC failure. Returns an all-zero struct (not null) if
|
|
// the account has never received funds — empty storage at this key is a
|
|
// valid "account does not exist yet" response, not an error.
|
|
$config = cry01_load_config();
|
|
$rpc = $config['g1_rpc_endpoint'] ?? '';
|
|
if (!$rpc || !$account_id_bytes) return null;
|
|
|
|
if (strlen($account_id_bytes) !== 32) {
|
|
logger('cry01_chain: cry01_get_account_info called with account_id of length ' . strlen($account_id_bytes) . ', expected 32');
|
|
return null;
|
|
}
|
|
|
|
$storage_key = cry01_storage_key('System', 'Account', $account_id_bytes);
|
|
$hex = cry01_rpc_state_get_storage($rpc, $storage_key);
|
|
if ($hex === null) return null;
|
|
|
|
if ($hex === '0x') {
|
|
// Account has never received funds. All fields zero.
|
|
return [
|
|
'nonce' => '0', 'consumers' => '0', 'providers' => '0', 'sufficients' => '0',
|
|
'free' => '0', 'reserved' => '0', 'frozen' => '0', 'flags' => '0',
|
|
];
|
|
}
|
|
|
|
$bytes = hex2bin(substr($hex, 0, 2) === '0x' ? substr($hex, 2) : $hex);
|
|
if ($bytes === false) {
|
|
logger('cry01_chain: cry01_get_account_info got non-hex storage value');
|
|
return null;
|
|
}
|
|
|
|
return cry01_decode_account_info($bytes);
|
|
}
|
|
|
|
function cry01_get_balance($account_id_bytes) {
|
|
// Returns the current Ğ1 balance for the given 32-byte raw account ID,
|
|
// formatted as a human-readable string (e.g. "1.00 Ğ1"). Thin wrapper
|
|
// around cry01_get_account_info() — extracts and formats 'free'.
|
|
// Returns null on failure.
|
|
$info = cry01_get_account_info($account_id_bytes);
|
|
if ($info === null) return null;
|
|
|
|
return cry01_format_g1_amount($info['free']);
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// Ğ1 AMOUNT FORMATTING
|
|
// ----------------------------------------------------------------------------
|
|
|
|
function cry01_format_g1_amount($decimal_string) {
|
|
// Formats a raw on-chain u128 amount (in the smallest unit) as a
|
|
// human-readable Ğ1 balance string.
|
|
//
|
|
// Duniter v2 uses centimes (hundredths of Ğ1) as the smallest unit,
|
|
// same as Duniter v1. Divide by 100 and format with 2 decimal places.
|
|
//
|
|
// $decimal_string is a base-10 string (may be arbitrarily long for u128).
|
|
$len = strlen($decimal_string);
|
|
|
|
if ($len <= 2) {
|
|
$decimal_string = str_pad($decimal_string, 3, '0', STR_PAD_LEFT);
|
|
$len = strlen($decimal_string);
|
|
}
|
|
|
|
$whole = substr($decimal_string, 0, $len - 2);
|
|
$cents = substr($decimal_string, $len - 2);
|
|
|
|
$whole = ltrim($whole, '0') ?: '0';
|
|
|
|
$whole_formatted = '';
|
|
$rev = strrev($whole);
|
|
for ($i = 0; $i < strlen($rev); $i++) {
|
|
if ($i > 0 && $i % 3 === 0) {
|
|
$whole_formatted .= ',';
|
|
}
|
|
$whole_formatted .= $rev[$i];
|
|
}
|
|
$whole_formatted = strrev($whole_formatted);
|
|
|
|
return $whole_formatted . '.' . $cents . ' Ğ1';
|
|
}
|
|
|
|
// ----------------------------------------------------------------------------
|
|
// 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($g1_address) {
|
|
// Refreshes the balance cache for the given Ğ1 address (SS58 string,
|
|
// e.g. "g1LvTpY..." — the format provided by g1wallet's
|
|
// data-g1wallet-target="pubkey" fields and entered by the operator).
|
|
// Decodes the address internally, consistent with
|
|
// cry01_handle_lookup_post(). Returns true on success, false on failure.
|
|
//
|
|
// Cache stores the account ID as hex, not raw binary — raw binary bytes
|
|
// are not valid UTF-8 and cause json_encode() to fail silently (returns
|
|
// false), which previously resulted in an empty (0-byte) cache file
|
|
// despite this function reporting success.
|
|
if (!$g1_address) {
|
|
logger('cry01_chain: cry01_refresh_balance_cache called with empty address');
|
|
return false;
|
|
}
|
|
|
|
$account_id = cry01_ss58_decode($g1_address);
|
|
if ($account_id === null) {
|
|
logger('cry01_chain: cry01_refresh_balance_cache could not decode address');
|
|
return false;
|
|
}
|
|
|
|
$balance = cry01_get_balance($account_id);
|
|
if ($balance === null) return false;
|
|
|
|
$cache = cry01_read_cache();
|
|
$cache['balance'] = $balance;
|
|
$cache['g1_pubkey'] = bin2hex($account_id);
|
|
$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 : [];
|
|
}
|