This commit is contained in:
2026-06-08 02:48:24 -04:00
parent 95dfc02e82
commit bf3e72f3ce
4 changed files with 66 additions and 46 deletions

View File

@@ -2,8 +2,6 @@
"_note": "Copy to config.json. Do not commit config.json — it contains installation-specific values.", "_note": "Copy to config.json. Do not commit config.json — it contains installation-specific values.",
"g1_rpc_endpoint": "REPLACE — Duniter node RPC over Wireguard, e.g. http://10.0.0.105:9944", "g1_rpc_endpoint": "REPLACE — Duniter node RPC over Wireguard, e.g. http://10.0.0.105:9944",
"orchestrator_url": "REPLACE — orchestrator base URL, e.g. http://10.0.0.105:8700", "orchestrator_url": "REPLACE — orchestrator base URL, e.g. http://10.0.0.105:8700",
"operator_g1_pubkey": "REPLACE — operator Ğ1 public key (not private key — never store private keys)",
"operator_did": "REPLACE — operator did:web identifier, e.g. did:web:directory.diagnostics.kane-il.us",
"cache_file": "REPLACE — absolute path to balance cache JSON on the host", "cache_file": "REPLACE — absolute path to balance cache JSON on the host",
"cache_max_age_seconds": 3600, "cache_max_age_seconds": 3600,
"ots_calendar_url": "https://alice.btc.calendar.opentimestamps.org", "ots_calendar_url": "https://alice.btc.calendar.opentimestamps.org",

View File

@@ -122,22 +122,21 @@ function cry01_cache_is_stale() {
return (time() - filemtime($path)) > $max_age; return (time() - filemtime($path)) > $max_age;
} }
function cry01_refresh_balance_cache() { function cry01_refresh_balance_cache($g1_pubkey) {
// Refreshes the balance cache by querying the Duniter node. // Refreshes the balance cache for the given public key.
// Writes updated cache to disk. Called by the manage route. // The public key comes from the wallet session via the manage POST form — never from config.
$config = cry01_load_config(); // Returns true on success, false on failure.
$pubkey = $config['operator_g1_pubkey'] ?? ''; if (!$g1_pubkey) {
if (!$pubkey) { logger('cry01_chain: cry01_refresh_balance_cache called with empty pubkey');
logger('cry01_chain: operator_g1_pubkey not set in config');
return false; return false;
} }
$balance = cry01_get_balance($pubkey); $balance = cry01_get_balance($g1_pubkey);
if ($balance === null) return false; if ($balance === null) return false;
$cache = cry01_read_cache(); $cache = cry01_read_cache();
$cache['operator_balance'] = $balance; $cache['balance'] = $balance;
$cache['operator_g1_pubkey'] = $pubkey; $cache['g1_pubkey'] = $g1_pubkey;
$cache['refreshed_at'] = date('c'); $cache['refreshed_at'] = date('c');
return cry01_write_cache($cache); return cry01_write_cache($cache);
} }

View File

@@ -51,17 +51,18 @@ function cry01_render_landing($association_slug, $access) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function cry01_render_balance_display() { function cry01_render_balance_display() {
// Renders the operator Ğ1 balance from cache. Shows staleness if cache is old. // Renders the Ğ1 balance from cache. The cached key belongs to whoever last refreshed.
$cache = cry01_read_cache(); // Shows staleness if cache is old.
$balance = $cache['operator_balance'] ?? null; $cache = cry01_read_cache();
$pubkey = $cache['operator_g1_pubkey'] ?? ''; $balance = $cache['balance'] ?? null;
$pubkey = $cache['g1_pubkey'] ?? '';
$refreshed = $cache['refreshed_at'] ?? null; $refreshed = $cache['refreshed_at'] ?? null;
$out = '<div class="cry01-balance-display mb-4">'; $out = '<div class="cry01-balance-display mb-4">';
$out .= '<h5 class="cry01-section-label">Operator Ğ1 Balance</h5>'; $out .= '<h5 class="cry01-section-label">Ğ1 Balance</h5>';
if ($balance === null) { if ($balance === null) {
$out .= '<p class="text-muted fst-italic">Balance not yet loaded. Operator: use Manage to refresh.</p>'; $out .= '<p class="text-muted fst-italic">Balance not yet loaded. Unlock your Ğ1 wallet and use Manage to refresh.</p>';
} else { } else {
$out .= '<p class="cry01-balance-amount">' . cry01_h($balance) . '</p>'; $out .= '<p class="cry01-balance-amount">' . cry01_h($balance) . '</p>';
if ($pubkey) { if ($pubkey) {
@@ -88,7 +89,6 @@ function cry01_render_signal_board($association_slug, $access) {
} }
// TODO: load signals from orchestrator spool for this association. // TODO: load signals from orchestrator spool for this association.
// Placeholder until orchestrator query is implemented.
$out = '<div class="cry01-signal-board mb-4">'; $out = '<div class="cry01-signal-board mb-4">';
$out .= '<h5 class="cry01-section-label">Capacity Signals</h5>'; $out .= '<h5 class="cry01-section-label">Capacity Signals</h5>';
$out .= '<p class="text-muted fst-italic">No signals posted yet.</p>'; $out .= '<p class="text-muted fst-italic">No signals posted yet.</p>';
@@ -102,6 +102,7 @@ function cry01_render_signal_board($association_slug, $access) {
function cry01_render_signal_form($association_slug, $access) { function cry01_render_signal_form($association_slug, $access) {
// Renders the capacity signal registration form. // Renders the capacity signal registration form.
// The g1_pubkey field is populated by the g1wallet session event when available.
$config = cry01_load_config(); $config = cry01_load_config();
$categories = $config['signal_categories'] ?? []; $categories = $config['signal_categories'] ?? [];
$form_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/signal'; $form_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/signal';
@@ -110,14 +111,18 @@ function cry01_render_signal_form($association_slug, $access) {
$out .= '<h3>Register a Capacity Signal</h3>'; $out .= '<h3>Register a Capacity Signal</h3>';
$out .= '<p class="text-muted">Describe what you are offering or seeking, denominated in Ğ1.</p>'; $out .= '<p class="text-muted">Describe what you are offering or seeking, denominated in Ğ1.</p>';
// Wallet session note — replaced by g1wallet integration once built.
$out .= '<div class="alert alert-info small">Your Ğ1 public key will be populated automatically once the Ğ1 Wallet addon is installed. Until then, enter it manually below.</div>';
$out .= '<form method="post" action="' . $form_url . '" class="cry01-form" novalidate>'; $out .= '<form method="post" action="' . $form_url . '" class="cry01-form" novalidate>';
$out .= cry01_csrf_token(); $out .= cry01_csrf_token();
// Ğ1 public key // g1_pubkey — will be read-only and auto-populated by g1wallet session once available.
$out .= '<div class="mb-3">'; $out .= '<div class="mb-3">';
$out .= '<label class="form-label" for="g1_pubkey">Your Ğ1 Public Key <span class="text-danger">*</span></label>'; $out .= '<label class="form-label" for="g1_pubkey">Your Ğ1 Public Key <span class="text-danger">*</span></label>';
$out .= '<input type="text" class="form-control" id="g1_pubkey" name="g1_pubkey" required $out .= '<input type="text" class="form-control" id="g1_pubkey" name="g1_pubkey" required
placeholder="G1..." maxlength="64">'; placeholder="G1..." maxlength="64"
data-g1wallet-target="pubkey">';
$out .= '<div class="form-text">Your Ğ1 address. This is your payment address for this signal.</div>'; $out .= '<div class="form-text">Your Ğ1 address. This is your payment address for this signal.</div>';
$out .= '</div>'; $out .= '</div>';
@@ -200,11 +205,11 @@ function cry01_render_g1_candidates($association_slug) {
function cry01_render_manage($association_slug) { function cry01_render_manage($association_slug) {
// Renders the operator manage page: cache refresh and config status. // Renders the operator manage page: cache refresh and config status.
// The Ğ1 public key comes from the wallet session — not from config.
$cache = cry01_read_cache(); $cache = cry01_read_cache();
$refreshed = $cache['refreshed_at'] ?? null; $refreshed = $cache['refreshed_at'] ?? null;
$config = cry01_load_config(); $config = cry01_load_config();
$has_rpc = !empty($config['g1_rpc_endpoint']); $has_rpc = !empty($config['g1_rpc_endpoint']);
$has_key = !empty($config['operator_g1_pubkey']);
$manage_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/manage'; $manage_url = z_root() . '/cry01/' . cry01_h($association_slug) . '/manage';
@@ -215,29 +220,37 @@ function cry01_render_manage($association_slug) {
$out .= '<div class="mb-3">'; $out .= '<div class="mb-3">';
$out .= '<h5>Configuration</h5>'; $out .= '<h5>Configuration</h5>';
$out .= '<p>' . ($has_rpc ? '✓' : '✗') . ' Duniter RPC endpoint: '; $out .= '<p>' . ($has_rpc ? '✓' : '✗') . ' Duniter RPC endpoint: ';
$out .= $has_rpc ? '<span class="text-success">configured</span>' : '<span class="text-danger">missing</span>'; $out .= $has_rpc
$out .= '</p>'; ? '<span class="text-success">configured</span>'
$out .= '<p>' . ($has_key ? '✓' : '✗') . ' Operator Ğ1 public key: '; : '<span class="text-danger">missing</span>';
$out .= $has_key ? '<span class="text-success">configured</span>' : '<span class="text-danger">missing</span>';
$out .= '</p>'; $out .= '</p>';
$out .= '<p class="text-muted small">Your Ğ1 public key comes from your Ğ1 Wallet session — not from config. ';
$out .= 'Unlock your wallet at <a href="' . z_root() . '/g1wallet">Ğ1 Wallet</a>, then return here to refresh.</p>';
$out .= '</div>'; $out .= '</div>';
// Cache status // Cache status
$out .= '<div class="mb-3">'; $out .= '<div class="mb-3">';
$out .= '<h5>Balance Cache</h5>'; $out .= '<h5>Balance Cache</h5>';
if ($refreshed) { if ($refreshed) {
$stale = cry01_cache_is_stale() ? ' (stale)' : ' (current)'; $cached_key = $cache['g1_pubkey'] ?? '';
$stale = cry01_cache_is_stale() ? ' (stale)' : ' (current)';
$out .= '<p>Last refreshed: ' . cry01_h($refreshed) . $stale . '</p>'; $out .= '<p>Last refreshed: ' . cry01_h($refreshed) . $stale . '</p>';
if ($cached_key) {
$out .= '<p class="text-muted small">Cached key: ' . cry01_h(substr($cached_key, 0, 16) . '...') . '</p>';
}
} else { } else {
$out .= '<p class="text-muted">Cache has not been populated yet.</p>'; $out .= '<p class="text-muted">Cache has not been populated yet.</p>';
} }
$out .= '</div>'; $out .= '</div>';
// Refresh form // Refresh form — g1_pubkey comes from wallet session via hidden field.
// g1wallet.js populates data-g1wallet-target="pubkey" fields on unlock.
$out .= '<form method="post" action="' . $manage_url . '">'; $out .= '<form method="post" action="' . $manage_url . '">';
$out .= cry01_csrf_token(); $out .= cry01_csrf_token();
$out .= '<input type="hidden" name="action" value="refresh_cache">'; $out .= '<input type="hidden" name="action" value="refresh_cache">';
$out .= '<input type="hidden" name="g1_pubkey" id="manage_g1_pubkey" data-g1wallet-target="pubkey">';
$out .= '<button type="submit" class="btn btn-sm btn-outline-primary">Refresh Balance Cache</button>'; $out .= '<button type="submit" class="btn btn-sm btn-outline-primary">Refresh Balance Cache</button>';
$out .= '<span class="text-muted small ms-2">Requires active Ğ1 Wallet session.</span>';
$out .= '</form>'; $out .= '</form>';
$out .= '</div>'; $out .= '</div>';

View File

@@ -12,18 +12,18 @@
function cry01_handle_signal_post($association_slug, $access) { function cry01_handle_signal_post($association_slug, $access) {
// Validates and submits a capacity signal to the orchestrator spool receiver. // Validates and submits a capacity signal to the orchestrator spool receiver.
$g1_pubkey = trim($_POST['g1_pubkey'] ?? ''); $g1_pubkey = trim($_POST['g1_pubkey'] ?? '');
$direction = trim($_POST['direction'] ?? ''); $direction = trim($_POST['direction'] ?? '');
$category_id = trim($_POST['category_id'] ?? ''); $category_id = trim($_POST['category_id'] ?? '');
$capacity_description = trim($_POST['capacity_description'] ?? ''); $capacity_description = trim($_POST['capacity_description'] ?? '');
$g1_denomination = trim($_POST['g1_denomination'] ?? ''); $g1_denomination = trim($_POST['g1_denomination'] ?? '');
$duration_days = intval($_POST['duration_days'] ?? 0); $duration_days = intval($_POST['duration_days'] ?? 0);
// Validate required fields. // Validate required fields.
$errors = []; $errors = [];
if (!$g1_pubkey) $errors[] = 'Ğ1 public key is required.'; if (!$g1_pubkey) $errors[] = 'Ğ1 public key is required.';
if (!in_array($direction, ['offer', 'seek'])) $errors[] = 'Direction must be offer or seek.'; if (!in_array($direction, ['offer', 'seek'])) $errors[] = 'Direction must be offer or seek.';
if (!$category_id) $errors[] = 'Category is required.'; if (!$category_id) $errors[] = 'Category is required.';
if (!$capacity_description) $errors[] = 'Description is required.'; if (!$capacity_description) $errors[] = 'Description is required.';
if (!$g1_denomination || !is_numeric($g1_denomination) || floatval($g1_denomination) <= 0) { if (!$g1_denomination || !is_numeric($g1_denomination) || floatval($g1_denomination) <= 0) {
$errors[] = 'Ğ1 amount must be a positive number.'; $errors[] = 'Ğ1 amount must be a positive number.';
@@ -45,11 +45,11 @@ function cry01_handle_signal_post($association_slug, $access) {
// Build the spool envelope per contracts/signal-v1.json. // Build the spool envelope per contracts/signal-v1.json.
$envelope = [ $envelope = [
'_header' => [ '_header' => [
'addon' => 'cry01', 'addon' => 'cry01',
'association_slug' => $association_slug, 'association_slug' => $association_slug,
'participant_xchan' => get_observer_hash(), 'participant_xchan' => get_observer_hash(),
'submitted_at' => date('c'), 'submitted_at' => date('c'),
'node_token_hash' => hash('sha256', $token), 'node_token_hash' => hash('sha256', $token),
], ],
'_payload' => [ '_payload' => [
'g1_pubkey' => $g1_pubkey, 'g1_pubkey' => $g1_pubkey,
@@ -83,7 +83,7 @@ function cry01_handle_signal_post($association_slug, $access) {
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function cry01_handle_manage_post($association_slug) { function cry01_handle_manage_post($association_slug) {
// Handles operator manage actions: currently only cache refresh. // Handles operator manage actions.
if (!cry01_verify_csrf()) { if (!cry01_verify_csrf()) {
return cry01_render_error('Invalid form token. Please reload and try again.'); return cry01_render_error('Invalid form token. Please reload and try again.');
} }
@@ -91,10 +91,20 @@ function cry01_handle_manage_post($association_slug) {
$action = $_POST['action'] ?? ''; $action = $_POST['action'] ?? '';
if ($action === 'refresh_cache') { if ($action === 'refresh_cache') {
$ok = cry01_refresh_balance_cache(); // The public key comes from the wallet session via the hidden form field.
// It is never read from config — the operator is a participant like any other.
$g1_pubkey = trim($_POST['g1_pubkey'] ?? '');
if (!$g1_pubkey) {
return '<div class="cry01-content">'
. '<div class="alert alert-warning">No Ğ1 public key received. '
. 'Unlock your <a href="' . z_root() . '/g1wallet">Ğ1 Wallet</a> first, then try again.</div>'
. '</div>'
. cry01_render_manage($association_slug);
}
$ok = cry01_refresh_balance_cache($g1_pubkey);
$msg = $ok $msg = $ok
? '<div class="alert alert-success">Balance cache refreshed successfully.</div>' ? '<div class="alert alert-success">Balance cache refreshed successfully.</div>'
: '<div class="alert alert-warning">Cache refresh failed. Check that the Duniter node is reachable and the operator Ğ1 key is configured.</div>'; : '<div class="alert alert-warning">Cache refresh failed. Check that the Duniter node is reachable.</div>';
return '<div class="cry01-content">' . $msg . '</div>' . cry01_render_manage($association_slug); return '<div class="cry01-content">' . $msg . '</div>' . cry01_render_manage($association_slug);
} }
@@ -108,8 +118,8 @@ function cry01_handle_manage_post($association_slug) {
function cry01_post_to_orchestrator($path, $payload) { function cry01_post_to_orchestrator($path, $payload) {
// POSTs a JSON envelope to the orchestrator spool receiver. // POSTs a JSON envelope to the orchestrator spool receiver.
// Returns true on success (HTTP 201), false on any failure. // Returns true on success (HTTP 201), false on any failure.
$config = cry01_load_config(); $config = cry01_load_config();
$base = rtrim($config['receiver_url'] ?? '', '/'); $base = rtrim($config['receiver_url'] ?? '', '/');
if (!$base) { if (!$base) {
logger('cry01_spool: receiver_url not configured'); logger('cry01_spool: receiver_url not configured');
return false; return false;