This commit is contained in:
2026-06-14 07:45:02 -04:00
parent e3128639df
commit b08450904d
3 changed files with 150 additions and 282 deletions

View File

@@ -2,151 +2,57 @@
/**
* g1wallet_spool.php — POST handlers for g1wallet.
* pubkey store: receives public key (SS58 g1... address) after unlock, stores in channel settings.
* broadcast relay: receives signed Duniter transaction, relays to orchestrator.
*
* The private key never reaches this file. Ever.
* Currently: address registration only.
* Future: transaction broadcast (when scn01 payment is wired).
*/
function g1wallet_handle_pubkey_post($access) {
// Stores the participant's Ğ1 public key (SS58 address) in their Hubzilla
// channel settings. Called once after each wallet unlock (server deduplicates
// by checking the existing value before writing).
// The public key is the only wallet-related thing the server ever stores.
function g1wallet_handle_address_post($access) {
$address = trim($_POST['g1_address'] ?? '');
$pubkey = trim($_POST['g1_pubkey'] ?? '');
if (!$pubkey) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Public key is required.']);
killme();
if (!$address) {
return g1wallet_render_landing_with_error($access, 'Ğ1 address is required.');
}
// Ğ1 SS58 addresses: base58-encoded, 36 bytes decoded, begin with "g1".
// Encoded length is 4647 characters. Reject anything outside that range.
$len = strlen($pubkey);
// Ğ1 SS58 addresses: begin with "g1", 4647 characters, base58 charset.
$len = strlen($address);
if ($len < 46 || $len > 48) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Invalid public key format.']);
killme();
return g1wallet_render_landing_with_error($access,
'Invalid address length (' . $len . ' characters). A Ğ1 address is 4647 characters long.');
}
// Must begin with "g1" (SS58 with Ğ1 network prefix).
if (strncmp($pubkey, 'g1', 2) !== 0) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Invalid public key prefix.']);
killme();
if (strncmp($address, 'g1', 2) !== 0) {
return g1wallet_render_landing_with_error($access,
'Invalid address: must begin with "g1".');
}
// Only base58 characters (Bitcoin alphabet, no 0/O/I/l).
if (!preg_match('/^[1-9A-HJ-NP-Za-km-z]+$/', $pubkey)) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Invalid public key characters.']);
killme();
if (!preg_match('/^[1-9A-HJ-NP-Za-km-z]+$/', $address)) {
return g1wallet_render_landing_with_error($access,
'Invalid address: contains characters not valid in a Ğ1 address.');
}
// Determine channel_id to write against.
// Operators use their own channel_id. Participants also use local_channel().
// We do not write other channels' keys — only the caller's own.
$channel_id = local_channel();
if (!$channel_id) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Not authenticated.']);
killme();
return g1wallet_render_landing_with_error($access, 'Not authenticated.');
}
// Deduplicate: if the stored key matches, return ok without writing.
$existing = get_pconfig($channel_id, 'g1wallet', 'g1_pubkey');
if ($existing === $pubkey) {
header('Content-Type: application/json');
echo json_encode(['status' => 'ok', 'note' => 'Key unchanged.']);
killme();
}
set_pconfig($channel_id, 'g1wallet', 'g1_address', $address);
// Store the public key.
set_pconfig($channel_id, 'g1wallet', 'g1_pubkey', $pubkey);
header('Content-Type: application/json');
echo json_encode(['status' => 'ok']);
killme();
// Render landing with success notice.
return g1wallet_render_landing_with_success($access,
'Ğ1 address saved. Your balance will appear below.');
}
function g1wallet_handle_broadcast_post() {
// Receives a signed Duniter transaction document (base64-encoded) from the browser.
// Validates the node token, relays to the orchestrator, returns the transaction hash.
//
// The browser signs the document with the participant's private key (WebCrypto).
// Only the signed bytes arrive here — never the private key.
// -----------------------------------------------------------------------------
// LANDING VARIANTS WITH NOTICE
// These wrap g1wallet_render_landing() with a prepended notice.
// -----------------------------------------------------------------------------
$signed_doc = trim($_POST['signed_doc'] ?? '');
$doc_type = trim($_POST['doc_type'] ?? ''); // e.g. 'transfer', 'certification'
if (!$signed_doc || !$doc_type) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'signed_doc and doc_type are required.']);
killme();
}
// Validate doc_type is a known type.
$allowed_types = ['transfer', 'certification'];
if (!in_array($doc_type, $allowed_types, true)) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Unknown doc_type.']);
killme();
}
// Load config for orchestrator endpoint.
$config = g1wallet_load_config();
$orchestrator_url = rtrim($config['orchestrator_url'] ?? '', '/');
$node_token = $config['node_token'] ?? '';
if (!$orchestrator_url || !$node_token) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Orchestrator not configured.']);
killme();
}
// Relay to orchestrator POST /g1wallet/broadcast.
$payload = json_encode([
'signed_doc' => $signed_doc,
'doc_type' => $doc_type,
]);
$ch = curl_init($orchestrator_url . '/g1wallet/broadcast');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Content-Type: application/json',
'X-Node-Token: ' . $node_token,
]);
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
$raw = curl_exec($ch);
$http = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$err = curl_error($ch);
curl_close($ch);
if ($err) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Orchestrator unreachable.']);
killme();
}
if ($http !== 200) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Orchestrator returned HTTP ' . intval($http) . '.']);
killme();
}
$result = json_decode($raw, true);
if (json_last_error() !== JSON_ERROR_NONE || !isset($result['status'])) {
header('Content-Type: application/json');
echo json_encode(['status' => 'error', 'message' => 'Invalid orchestrator response.']);
killme();
}
header('Content-Type: application/json');
echo json_encode($result);
killme();
function g1wallet_render_landing_with_error($access, $message) {
$notice = '<div class="alert alert-danger">' . g1wallet_h($message) . '</div>';
return $notice . g1wallet_render_landing($access);
}
function g1wallet_render_landing_with_success($access, $message) {
$notice = '<div class="alert alert-success">' . g1wallet_h($message) . '</div>';
return $notice . g1wallet_render_landing($access);
}