api_url = rtrim($api_url, '/'); $this->api_key = $api_key; $this->logger = new Breez_Logger('yes' === get_option('woocommerce_breez_debug', 'no')); if (!$this->api_url || !$this->api_key) { $this->logger->log('API client initialized without credentials', 'error'); throw new Exception('API credentials are required'); } // Validate API URL format if (!filter_var($this->api_url, FILTER_VALIDATE_URL)) { $this->logger->log('Invalid API URL format: ' . $this->api_url, 'error'); throw new Exception('API URL must be a valid URL'); } $this->logger->log('API client initialized', 'debug', array( 'api_url' => $this->api_url, 'api_key_set' => !empty($this->api_key) )); } /** * Check API connectivity * * @return bool True if API is accessible, false otherwise */ public function check_health() { // In test mode, bypass the health check if ('yes' === get_option('woocommerce_breez_testmode', 'no')) { $this->logger->log('API health check bypassed in test mode', 'debug'); return true; } try { $this->logger->log('Starting API health check', 'debug'); $response = $this->request('GET', '/health'); $this->logger->log('API health check response: ' . json_encode($response), 'debug'); return isset($response['status']) && $response['status'] === 'ok'; } catch (Exception $e) { $this->logger->log('API health check failed: ' . $e->getMessage(), 'error'); $this->logger->log('Stack trace: ' . $e->getTraceAsString(), 'debug'); return false; } } /** * Generate a payment request * * @param int $amount Amount in satoshis * @param string $method Payment method (LIGHTNING, BITCOIN_ADDRESS, LIQUID_ADDRESS) * @param string $description Payment description * @return array|false Payment details on success, false on failure */ public function receive_payment($amount, $method = 'LIGHTNING', $description = '') { $data = array( 'amount' => $amount, 'method' => $method, 'description' => $description ); return $this->request('POST', '/receive_payment', $data); } /** * Check payment status using API endpoint * * @param string $invoice_id Invoice ID or payment identifier * @return array Payment status response */ public function check_payment_status($invoice_id) { try { $url = $this->api_url . "/check_payment_status/{$invoice_id}?source=woocommerce"; $response = wp_remote_get($url, array( 'headers' => array( 'Content-Type' => 'application/json', 'x-api-key' => $this->api_key ), 'timeout' => 30 )); if (is_wp_error($response)) { throw new Exception($response->get_error_message()); } $body = wp_remote_retrieve_body($response); $data = json_decode($body, true); if (!$data) { throw new Exception('Invalid response from API'); } // Log the raw API response for debugging $this->logger->log('Payment status check response', 'debug', array( 'invoice_id' => $invoice_id, 'response' => $data )); // If the payment is not found, return pending with full details if ($data['status'] === 'UNKNOWN') { return array( 'status' => 'PENDING', 'destination' => $invoice_id, 'sdk_status' => 'UNKNOWN', 'amount_sat' => 0, 'fees_sat' => 0, 'payment_type' => 'UNKNOWN', 'timestamp' => time(), 'payment_hash' => null, 'tx_id' => null, 'swap_id' => null, 'source' => 'woocommerce', 'error' => null, 'description' => null, 'metadata' => array(), 'exchange_rate' => null, 'fiat_amount' => null, 'fiat_currency' => null ); } // Extract payment details from response $payment_details = $data['payment_details'] ?? array(); // Return comprehensive payment status information return array( 'status' => $data['status'], 'sdk_status' => $payment_details['sdk_status'] ?? $data['status'], 'destination' => $payment_details['destination'] ?? $invoice_id, 'amount_sat' => $payment_details['amount_sat'] ?? 0, 'fees_sat' => $payment_details['fees_sat'] ?? 0, 'payment_type' => $payment_details['payment_type'] ?? 'UNKNOWN', 'timestamp' => $payment_details['timestamp'] ?? time(), 'payment_hash' => $payment_details['payment_hash'] ?? null, 'tx_id' => $payment_details['tx_id'] ?? null, 'swap_id' => $payment_details['swap_id'] ?? null, 'source' => $payment_details['source'] ?? 'woocommerce', 'error' => $payment_details['error'] ?? null, 'description' => $payment_details['description'] ?? null, 'metadata' => $payment_details['metadata'] ?? array(), 'exchange_rate' => $payment_details['exchange_rate'] ?? null, 'fiat_amount' => $payment_details['fiat_amount'] ?? null, 'fiat_currency' => $payment_details['fiat_currency'] ?? null ); } catch (Exception $e) { $this->logger->log('Payment status check error: ' . $e->getMessage(), 'error'); // Return pending status with error information return array( 'status' => 'PENDING', 'sdk_status' => 'UNKNOWN', 'destination' => $invoice_id, 'amount_sat' => 0, 'fees_sat' => 0, 'payment_type' => 'UNKNOWN', 'timestamp' => time(), 'payment_hash' => null, 'tx_id' => null, 'swap_id' => null, 'source' => 'woocommerce', 'error' => $e->getMessage(), 'description' => null, 'metadata' => array(), 'exchange_rate' => null, 'fiat_amount' => null, 'fiat_currency' => null ); } } /** * Map SDK payment states to WooCommerce payment states * * @param string $sdk_status The status from the SDK * @return string WooCommerce payment status */ private function map_payment_status($sdk_status) { switch ($sdk_status) { case 'SUCCEEDED': case 'WAITING_CONFIRMATION': // Consider payment complete when claim tx is broadcast return 'completed'; case 'PENDING': // Lockup transaction broadcast return 'pending'; case 'WAITING_FEE_ACCEPTANCE': // Needs fee approval return 'pending'; case 'FAILED': // Swap failed (expired or lockup tx failed) return 'failed'; case 'UNKNOWN': // Payment not found or error default: return 'pending'; } } /** * Get human-readable status description * * @param string $sdk_status The SDK status * @return string Human-readable description */ private function get_status_description($sdk_status) { switch ($sdk_status) { case 'SUCCEEDED': return __('Payment confirmed and completed.', 'breez-woocommerce'); case 'WAITING_CONFIRMATION': return __('Payment received and being confirmed.', 'breez-woocommerce'); case 'PENDING': return __('Payment initiated, waiting for completion.', 'breez-woocommerce'); case 'WAITING_FEE_ACCEPTANCE': return __('Waiting for fee approval.', 'breez-woocommerce'); case 'FAILED': return __('Payment failed or expired.', 'breez-woocommerce'); case 'UNKNOWN': default: return __('Payment status unknown.', 'breez-woocommerce'); } } /** * Register a webhook URL * * @param string $webhook_url Webhook URL * @return array|false Response on success, false on failure */ /** * Check if webhooks are supported by the API * * @return bool True if webhooks are supported */ public function check_webhook_support() { try { // Try to get API endpoints/capabilities $response = $this->request('GET', '/capabilities', array(), 1); // If capabilities endpoint exists, check webhook support if ($response && isset($response['features'])) { return in_array('webhooks', $response['features']); } // If capabilities endpoint doesn't exist, try webhook endpoint directly $this->request('GET', '/register_webhook', array(), 1); return true; } catch (Exception $e) { // If we get a 404, webhooks aren't supported if (strpos($e->getMessage(), '404') !== false) { $this->logger->log('Webhooks not supported by API', 'debug'); return false; } // For other errors, assume webhooks might be supported $this->logger->log('Webhook support check failed: ' . $e->getMessage(), 'warning'); return true; } } /** * Register a webhook URL * * @param string $webhook_url Webhook URL * @return bool True if registration successful * @throws Exception if registration fails */ public function register_webhook($webhook_url) { if (!$webhook_url) { throw new Exception('Webhook URL is required'); } $this->logger->log('Registering webhook', 'debug', array( 'url' => $webhook_url )); try { $data = array( 'webhook_url' => $webhook_url ); $response = $this->request('POST', '/register_webhook', $data, 1); if ($response && isset($response['success']) && $response['success']) { $this->logger->log('Webhook registration successful', 'info'); return true; } $this->logger->log('Webhook registration failed - invalid response', 'error'); return false; } catch (Exception $e) { // If we get a 404, webhooks aren't supported if (strpos($e->getMessage(), '404') !== false) { $this->logger->log('Webhook registration failed - endpoint not found', 'debug'); return false; } // Re-throw other errors throw $e; } } /** * Get payment by ID * * @param string $payment_hash Payment hash * @return array|false Payment details on success, false on failure */ public function get_payment($payment_hash) { return $this->request('GET', "/payment/{$payment_hash}"); } /** * List all payments * * @param array $params Optional query parameters * @return array|false List of payments on success, false on failure */ public function list_payments($params = array()) { $query_string = ''; if (!empty($params)) { $query_string = '?' . http_build_query($params); } return $this->request('GET', "/list_payments{$query_string}"); } /** * Make API request * * @param string $method HTTP method * @param string $endpoint API endpoint * @param array $data Request data * @param int $max_retries Maximum number of retries * @return array|false Response data on success, false on failure */ public function request($method, $endpoint, $data = array(), $max_retries = 2) { // Normalize the endpoint to ensure it starts with a slash $endpoint = ltrim($endpoint, '/'); // Full API URL $url = $this->api_url . '/' . $endpoint; $this->logger->log('Making API request', 'debug', array( 'method' => $method, 'url' => $url )); $args = array( 'method' => $method, 'timeout' => 30, 'headers' => array( 'Content-Type' => 'application/json', 'x-api-key' => $this->api_key, 'Accept' => 'application/json' ) ); if (!empty($data) && $method !== 'GET') { $args['body'] = json_encode($data); } elseif (!empty($data) && $method === 'GET') { // For GET requests, append query string $url = add_query_arg($data, $url); } $retries = 0; $response_data = false; while ($retries <= $max_retries) { $response = wp_remote_request($url, $args); if (is_wp_error($response)) { $error_message = $response->get_error_message(); $this->logger->log('API request error', 'error', array( 'message' => $error_message, 'attempt' => $retries + 1 )); $retries++; if ($retries <= $max_retries) { $this->logger->log('Retrying request', 'debug', array( 'attempt' => $retries )); // Exponential backoff sleep(pow(2, $retries - 1)); continue; } throw new Exception('API request failed: ' . $error_message); } $http_code = wp_remote_retrieve_response_code($response); $body = wp_remote_retrieve_body($response); $this->logger->log('API response received', 'debug', array( 'http_code' => $http_code, 'body_length' => strlen($body) )); if ($http_code >= 200 && $http_code < 300) { if (!empty($body)) { $response_data = json_decode($body, true); if (json_last_error() !== JSON_ERROR_NONE) { $this->logger->log('JSON decode error', 'error', array( 'error' => json_last_error_msg(), 'body_excerpt' => substr($body, 0, 100) . (strlen($body) > 100 ? '...' : '') )); // If we can't decode JSON, try to return the raw body $response_data = array( 'raw_response' => $body ); } } else { // Empty but successful response $response_data = array( 'success' => true ); } // Success - break out of retry loop break; } else { // Handle error $message = $body; if (!empty($body)) { $decoded = json_decode($body, true); if (is_array($decoded) && isset($decoded['message'])) { $message = $decoded['message']; } elseif (is_array($decoded) && isset($decoded['error'])) { $message = $decoded['error']; } } // 404 might be normal in some cases (checking if endpoint exists) $error_level = ($http_code == 404) ? 'debug' : 'error'; $this->logger->log('API error response', $error_level, array( 'http_code' => $http_code, 'message' => $message, 'endpoint' => $endpoint, 'attempt' => $retries + 1 )); if ($http_code == 404 || ($http_code >= 500 && $retries < $max_retries)) { $retries++; if ($retries <= $max_retries) { $this->logger->log('Retrying request', 'debug', array( 'attempt' => $retries )); // Exponential backoff sleep(pow(2, $retries - 1)); continue; } } throw new Exception("API error ($http_code): $message"); } } return $response_data; } /** * Create a new payment * * @param array $payment_data Payment data including amount, currency, description, etc. * @return array Payment details * @throws Exception if payment creation fails */ public function create_payment($payment_data) { $this->logger->log('Creating payment with data: ' . print_r($payment_data, true), 'debug'); try { // Prepare the API request data according to ReceivePaymentBody schema $api_data = array( 'amount' => $payment_data['amount_sat'], // Amount must be in satoshis 'method' => strtoupper($payment_data['payment_method']), // LIGHTNING or BITCOIN_ADDRESS 'description' => $payment_data['description'] ?? '', ); // Make the API request to create payment $response = $this->request('POST', '/receive_payment', $api_data); if (!$response || !isset($response['destination'])) { throw new Exception('Invalid API response: Missing payment destination'); } // Format the response to match what the gateway expects return array( 'id' => $response['destination'], // Use destination as ID 'invoice_id' => $response['destination'], 'payment_url' => $response['destination'], // For QR code generation 'payment_request' => $response['destination'], 'status' => 'PENDING', 'amount' => $payment_data['amount'], 'amount_sat' => $payment_data['amount_sat'], 'currency' => $payment_data['currency'], 'created_at' => time(), 'expires_at' => time() + ($payment_data['expires_in'] ?? 1800), 'fees_sat' => $response['fees_sat'] ?? 0 ); } catch (Exception $e) { $this->logger->log('Payment creation failed: ' . $e->getMessage(), 'error'); throw new Exception('Failed to create payment: ' . $e->getMessage()); } } /** * Convert fiat amount to satoshis * * @param float $amount Amount in fiat * @param string $currency Currency code * @return int Amount in satoshis * @throws Exception if conversion fails */ private function convert_to_sats($amount, $currency) { try { // Try to get the exchange rate from the API $response = $this->request('GET', '/exchange_rates/' . strtoupper($currency)); if (!$response || !isset($response['rate'])) { throw new Exception('Invalid exchange rate response'); } // Calculate satoshis $btc_amount = $amount / $response['rate']; return (int)($btc_amount * 100000000); // Convert BTC to sats } catch (Exception $e) { $this->logger->log('Currency conversion failed: ' . $e->getMessage(), 'error'); throw new Exception('Failed to convert currency: ' . $e->getMessage()); } } }