Contractless-Web3-Wallets/tests/dual-signature-store.php

142 lines
5.1 KiB
PHP
Raw Permalink Normal View History

<?php
declare(strict_types=1);
header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');
function reply(int $status, array $payload): never
{
http_response_code($status);
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
reply(405, ['success' => false, 'error' => 'POST is required.']);
}
$length = (int) ($_SERVER['CONTENT_LENGTH'] ?? 0);
if ($length > 2_000_000) {
reply(413, ['success' => false, 'error' => 'The request body is too large.']);
}
try {
$request = json_decode(file_get_contents('php://input'), true, 64, JSON_THROW_ON_ERROR);
} catch (JsonException) {
reply(400, ['success' => false, 'error' => 'The request body must be valid JSON.']);
}
if (!is_array($request)) {
reply(400, ['success' => false, 'error' => 'The request body must be a JSON object.']);
}
$action = (string) ($request['action'] ?? '');
$txid = strtolower(trim((string) ($request['txid'] ?? '')));
if (!preg_match('/^[a-f0-9]{64}$/', $txid)) {
reply(400, ['success' => false, 'error' => 'A valid 64-character transaction hash is required.']);
}
$storageDirectory = __DIR__ . '/storage/dual-signatures';
if (!is_dir($storageDirectory) && !mkdir($storageDirectory, 0770, true) && !is_dir($storageDirectory)) {
reply(500, ['success' => false, 'error' => 'The JSON storage directory could not be created.']);
}
$fileName = $txid . '.json';
$path = $storageDirectory . '/' . $fileName;
if ($action === 'load') {
if (!is_file($path)) {
reply(404, ['success' => false, 'error' => 'No saved transaction exists for that hash.']);
}
$handle = fopen($path, 'rb');
if ($handle === false || !flock($handle, LOCK_SH)) {
if (is_resource($handle)) fclose($handle);
reply(500, ['success' => false, 'error' => 'The saved transaction could not be opened.']);
}
$record = json_decode(stream_get_contents($handle) ?: '', true);
flock($handle, LOCK_UN);
fclose($handle);
if (!is_array($record)) {
reply(500, ['success' => false, 'error' => 'The saved transaction JSON is invalid.']);
}
reply(200, ['success' => true, 'data' => ['file' => $fileName, 'record' => $record]]);
}
if ($action !== 'save_signature') {
reply(400, ['success' => false, 'error' => 'The storage action is not supported.']);
}
$kind = strtolower(trim((string) ($request['kind'] ?? '')));
$slot = (int) ($request['signer_slot'] ?? 0);
$transaction = $request['transaction'] ?? null;
$signature = $request['signature'] ?? null;
if (!in_array($kind, ['swap', 'loan'], true)) {
reply(400, ['success' => false, 'error' => 'The transaction kind must be swap or loan.']);
}
if ($slot !== 1 && $slot !== 2) {
reply(400, ['success' => false, 'error' => 'The signer slot must be 1 or 2.']);
}
if (!is_array($transaction) || !is_array($signature)) {
reply(400, ['success' => false, 'error' => 'The transaction and signature result are required.']);
}
if (strtolower((string) ($signature['txid'] ?? '')) !== $txid) {
reply(400, ['success' => false, 'error' => 'The signature result does not match the transaction hash.']);
}
if ((int) ($signature['signerSlot'] ?? 0) !== $slot) {
reply(400, ['success' => false, 'error' => 'The signature result does not match the signer slot.']);
}
$handle = fopen($path, 'c+');
if ($handle === false || !flock($handle, LOCK_EX)) {
if (is_resource($handle)) fclose($handle);
reply(500, ['success' => false, 'error' => 'The transaction JSON could not be locked.']);
}
$existingJson = stream_get_contents($handle) ?: '';
$record = $existingJson === '' ? null : json_decode($existingJson, true);
if ($record !== null && !is_array($record)) {
flock($handle, LOCK_UN);
fclose($handle);
reply(500, ['success' => false, 'error' => 'The existing transaction JSON is invalid.']);
}
if ($record !== null && (
($record['kind'] ?? null) !== $kind
|| json_encode($record['transaction'] ?? null) !== json_encode($transaction)
)) {
flock($handle, LOCK_UN);
fclose($handle);
reply(409, ['success' => false, 'error' => 'That hash is already associated with different transaction data.']);
}
$record ??= [
'txid' => $txid,
'kind' => $kind,
'transaction' => $transaction,
'signer1' => null,
'signer2' => null,
'created_at' => gmdate(DATE_ATOM),
];
$record['signer' . $slot] = $signature;
$record['updated_at'] = gmdate(DATE_ATOM);
$encoded = json_encode($record, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
if ($encoded === false) {
flock($handle, LOCK_UN);
fclose($handle);
reply(500, ['success' => false, 'error' => 'The transaction record could not be encoded.']);
}
rewind($handle);
ftruncate($handle, 0);
if (fwrite($handle, $encoded . PHP_EOL) === false) {
flock($handle, LOCK_UN);
fclose($handle);
reply(500, ['success' => false, 'error' => 'The transaction record could not be saved.']);
}
fflush($handle);
flock($handle, LOCK_UN);
fclose($handle);
reply(200, ['success' => true, 'data' => ['file' => $fileName, 'record' => $record]]);