142 lines
5.1 KiB
PHP
142 lines
5.1 KiB
PHP
|
|
<?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]]);
|
||
|
|
|