diff --git a/README.md b/README.md index 7e440b5..8de019c 100644 --- a/README.md +++ b/README.md @@ -184,6 +184,10 @@ Transaction broadcasting accepts: The API never creates or signs a transaction for the caller. It only forwards the complete signed transaction through `Client::submitTransaction()`. +When the node rejects a transaction, the API returns its bounded, valid UTF-8 +verification reason with HTTP `422`. Malformed, binary, and oversized upstream +errors remain generic. This allows wallets to distinguish problems such as an +insufficient balance, an unregistered participant, or a fee below the minimum. Wallet registration accepts: diff --git a/src/Http/ExceptionResponder.php b/src/Http/ExceptionResponder.php index 7a92ed0..a1e20cf 100644 --- a/src/Http/ExceptionResponder.php +++ b/src/Http/ExceptionResponder.php @@ -22,7 +22,7 @@ final class ExceptionResponder SafeLogger::exception($error, RequestContext::requestId()); if ($error instanceof UpstreamRejectedException) { - JsonResponse::error('The Contractless node rejected the request.', 422); + JsonResponse::error($error->getMessage(), 422); } if ($error instanceof TransportException) { header('Retry-After: 5'); diff --git a/src/Rpc/RpcReplyDecoder.php b/src/Rpc/RpcReplyDecoder.php index bcb86e6..47c31a8 100644 --- a/src/Rpc/RpcReplyDecoder.php +++ b/src/Rpc/RpcReplyDecoder.php @@ -567,7 +567,7 @@ final class RpcReplyDecoder /** @return array{accepted: bool, already_in_mempool: bool, message: string} */ public static function broadcast(string $reply): array { - self::notNodeError($reply); + self::notNodeError($reply, true); $message = trim($reply); if ($message === 'successful_broadcast: true') { return [ @@ -586,11 +586,23 @@ final class RpcReplyDecoder throw new RuntimeException('The node returned an invalid broadcast response.'); } - public static function notNodeError(string $reply): void + public static function notNodeError(string $reply, bool $includeReason = false): void { if (preg_match('/^error:/i', ltrim($reply)) === 1) { + $message = 'The Contractless node rejected the request.'; + if ($includeReason) { + $reason = trim((string) preg_replace('/^error:\s*/i', '', ltrim($reply), 1)); + if ( + $reason !== '' + && strlen($reason) <= 500 + && preg_match('//u', $reason) === 1 + && preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $reason) !== 1 + ) { + $message = $reason; + } + } throw new UpstreamRejectedException( - 'The Contractless node rejected the request.', + $message, ); } }