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]]);