diff --git a/api/api.go b/api/api.go index 38ec705b..d2bf2532 100644 --- a/api/api.go +++ b/api/api.go @@ -1273,6 +1273,15 @@ func (api *api) RequestMempoolApi(ctx context.Context, endpoint string) (interfa return nil, errors.New("failed to read response body") } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "endpoint": endpoint, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Mempool endpoint returned non-success code") + return nil, fmt.Errorf("mempool endpoint returned non-success code: %s", string(body)) + } + var jsonContent interface{} jsonErr := json.Unmarshal(body, &jsonContent) if jsonErr != nil { diff --git a/api/esplora.go b/api/esplora.go index 791aef4a..510e51c3 100644 --- a/api/esplora.go +++ b/api/esplora.go @@ -46,6 +46,15 @@ func (api *api) RequestEsploraApi(ctx context.Context, endpoint string) (interfa return nil, errors.New("failed to read response body") } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "endpoint": endpoint, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Esplora endpoint returned non-success code") + return nil, fmt.Errorf("esplora endpoint returned non-success code: %s", string(body)) + } + var jsonContent interface{} jsonErr := json.Unmarshal(body, &jsonContent) if jsonErr != nil { diff --git a/api/rebalance.go b/api/rebalance.go index 44b1c8b3..7adbf083 100644 --- a/api/rebalance.go +++ b/api/rebalance.go @@ -84,7 +84,7 @@ func (api *api) RebalanceChannel(ctx context.Context, rebalanceChannelRequest *R return nil, errors.New("failed to read response body") } - if res.StatusCode >= 300 { + if res.StatusCode != http.StatusOK { logger.Logger.WithFields(logrus.Fields{ "request": newRspCreateOrderRequest, "body": string(body), diff --git a/frontend/src/constants.ts b/frontend/src/constants.ts index 65e8a1f6..2f1cdaa1 100644 --- a/frontend/src/constants.ts +++ b/frontend/src/constants.ts @@ -25,3 +25,5 @@ export const DEFAULT_APP_BUDGET_RENEWAL = "monthly"; export const BITCOIN_DISPLAY_FORMAT_BIP177 = "bip177"; export const BITCOIN_DISPLAY_FORMAT_SATS = "sats"; + +export const RATES_API_URL = "https://getalby.com/api/rates"; diff --git a/frontend/src/hooks/useCurrencies.ts b/frontend/src/hooks/useCurrencies.ts new file mode 100644 index 00000000..abdcaad0 --- /dev/null +++ b/frontend/src/hooks/useCurrencies.ts @@ -0,0 +1,58 @@ +import React from "react"; +import useSWR from "swr"; + +import { RATES_API_URL } from "src/constants"; +import { handleRequestError } from "src/utils/handleRequestError"; + +const albyRatesFetcher = (url: string) => + fetch(url).then((res) => { + if (!res.ok) { + throw new Error(`Failed to fetch currencies: ${res.status}`); + } + return res.json() as Promise< + Record + >; + }); + +export function useCurrencies(includeSats = false) { + const { data: ratesData, isLoading } = useSWR< + Record + >(RATES_API_URL, albyRatesFetcher, { + onError: (error) => handleRequestError("Failed to fetch currencies", error), + }); + + const currencies = React.useMemo(() => { + if (!ratesData) { + return []; + } + + if (includeSats) { + return [ + ["SATS", "sats"], + ...Object.entries(ratesData) + .filter(([code]) => code !== "BTC") + .sort((a, b) => { + const priorityDiff = a[1].priority - b[1].priority; + return priorityDiff !== 0 ? priorityDiff : a[0].localeCompare(b[0]); + }) + .map(([code, details]): [string, string] => [ + code.toUpperCase(), + details.name, + ]), + ]; + } + + return Object.entries(ratesData) + .filter(([code]) => code !== "BTC") + .map(([code, details]): [string, string] => [ + code.toUpperCase(), + details.name, + ]) + .sort((a, b) => a[1].localeCompare(b[1])); + }, [ratesData, includeSats]); + + return { + currencies, + isLoading, + }; +} diff --git a/frontend/src/screens/internal-apps/ZapPlanner.tsx b/frontend/src/screens/internal-apps/ZapPlanner.tsx index 9058a691..3b6faebf 100644 --- a/frontend/src/screens/internal-apps/ZapPlanner.tsx +++ b/frontend/src/screens/internal-apps/ZapPlanner.tsx @@ -7,6 +7,7 @@ import { CardTitle, } from "src/components/ui/card"; import { useApps } from "src/hooks/useApps"; +import { useCurrencies } from "src/hooks/useCurrencies"; import { createApp } from "src/requests/createApp"; import { CreateAppRequest, UpdateAppRequest } from "src/types"; import { handleRequestError } from "src/utils/handleRequestError"; @@ -114,39 +115,13 @@ export function ZapPlanner() { const [frequencyValue, setFrequencyValue] = React.useState("1"); const [frequencyUnit, setFrequencyUnit] = React.useState("months"); const [currency, setCurrency] = React.useState("USD"); - const [currencies, setCurrencies] = React.useState([]); + const { currencies, isLoading: isCurrenciesLoading } = useCurrencies(true); const [convertedAmount, setConvertedAmount] = React.useState(""); const [satoshiAmount, setSatoshiAmount] = React.useState( undefined ); - React.useEffect(() => { - // fetch the fiat list and prepend sats/BTC - async function fetchCurrencies() { - try { - const res = await fetch("https://getalby.com/api/rates"); - const data: Record = - await res.json(); - const fiatCodes = Object.keys(data) - // drop "BTC" - ZapPlanner uses SATS for the bitcoin currency - .filter((code) => code !== "BTC") - .sort((a, b) => { - const priorityDiff = data[a].priority - data[b].priority; - if (priorityDiff !== 0) { - return priorityDiff; - } - return a.localeCompare(b); - }) - .map((c) => c.toUpperCase()); - setCurrencies(["SATS", ...fiatCodes]); - } catch (err) { - console.error("Failed to load currencies", err); - } - } - fetchCurrencies(); - }, []); - React.useEffect(() => { // reset form on close if (!open) { @@ -164,6 +139,10 @@ export function ZapPlanner() { }, [open]); React.useEffect(() => { + if (isCurrenciesLoading) { + return; + } + // If amount is empty, clear conversion output if (!amount) { setConvertedAmount(""); @@ -200,7 +179,7 @@ export function ZapPlanner() { }; convertCurrency(); - }, [amount, currency, open]); + }, [amount, currency, open, isCurrenciesLoading]); const appStoreApp = appStoreApps.find((app) => app.id === "zapplanner"); if (!appStoreApp) { @@ -428,14 +407,24 @@ export function ZapPlanner() { )} - - + - {currencies.map((code) => ( + {currencies.map(([code]) => ( - {code === "BTC" ? "BTC (sats)" : code} + {code} ))} diff --git a/frontend/src/screens/settings/Settings.tsx b/frontend/src/screens/settings/Settings.tsx index d74c0cb4..1301c76d 100644 --- a/frontend/src/screens/settings/Settings.tsx +++ b/frontend/src/screens/settings/Settings.tsx @@ -1,5 +1,4 @@ import { StarsIcon } from "lucide-react"; -import { useEffect, useState } from "react"; import { toast } from "sonner"; import Loading from "src/components/Loading"; import SettingsHeader from "src/components/SettingsHeader"; @@ -23,6 +22,7 @@ import { BITCOIN_DISPLAY_FORMAT_SATS, } from "src/constants"; import { useAlbyMe } from "src/hooks/useAlbyMe"; +import { useCurrencies } from "src/hooks/useCurrencies"; import { useInfo } from "src/hooks/useInfo"; import { cn } from "src/lib/utils"; import { handleRequestError } from "src/utils/handleRequestError"; @@ -31,33 +31,10 @@ import { request } from "src/utils/request"; function Settings() { const { data: albyMe } = useAlbyMe(); const { theme, darkMode, setTheme, setDarkMode } = useTheme(); - - const [fiatCurrencies, setFiatCurrencies] = useState<[string, string][]>([]); + const { currencies, isLoading: isCurrenciesLoading } = useCurrencies(); const { data: info, mutate: reloadInfo } = useInfo(); - useEffect(() => { - async function fetchCurrencies() { - try { - const response = await fetch(`https://getalby.com/api/rates`); - const data: Record = await response.json(); - - const mappedCurrencies: [string, string][] = Object.entries(data).map( - ([code, details]) => [code.toUpperCase(), details.name] - ); - - mappedCurrencies.sort((a, b) => a[1].localeCompare(b[1])); - - setFiatCurrencies(mappedCurrencies); - } catch (error) { - console.error(error); - handleRequestError("Failed to fetch currencies", error); - } - } - - fetchCurrencies(); - }, []); - async function updateSettings( payload: Record, successMessage: string, @@ -206,12 +183,22 @@ function Settings() {
- - + - {fiatCurrencies.map(([code, name]) => ( + {currencies.map(([code, name]) => ( {name} ({code}) diff --git a/lnclient/phoenixd/phoenixd.go b/lnclient/phoenixd/phoenixd.go index 0b8c211a..94815388 100644 --- a/lnclient/phoenixd/phoenixd.go +++ b/lnclient/phoenixd/phoenixd.go @@ -5,6 +5,8 @@ import ( b64 "encoding/base64" "encoding/json" "errors" + "fmt" + "io" "net/http" "net/url" "strconv" @@ -105,8 +107,20 @@ func (svc *PhoenixService) GetBalances(ctx context.Context, includeInactiveChann } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "body": string(body), + "status_code": resp.StatusCode, + }).Error("phoenixd get balance endpoint returned non-success code") + return nil, fmt.Errorf("phoenixd get balance endpoint returned non-success code: %d %s", resp.StatusCode, string(body)) + } + var balanceRes BalanceResponse - if err := json.NewDecoder(resp.Body).Decode(&balanceRes); err != nil { + if err := json.Unmarshal(body, &balanceRes); err != nil { return nil, err } @@ -145,8 +159,20 @@ func fetchNodeInfo(ctx context.Context, svc *PhoenixService) (info *lnclient.Nod } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "body": string(body), + "status_code": resp.StatusCode, + }).Error("phoenixd get info endpoint returned non-success code") + return nil, fmt.Errorf("phoenixd get info endpoint returned non-success code: %d %s", resp.StatusCode, string(body)) + } + var infoRes InfoResponse - if err := json.NewDecoder(resp.Body).Decode(&infoRes); err != nil { + if err := json.Unmarshal(body, &infoRes); err != nil { return nil, err } return &lnclient.NodeInfo{ @@ -199,8 +225,20 @@ func (svc *PhoenixService) MakeInvoice(ctx context.Context, amount int64, descri } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "body": string(body), + "status_code": resp.StatusCode, + }).Error("phoenixd create invoice endpoint returned non-success code") + return nil, fmt.Errorf("phoenixd create invoice endpoint returned non-success code: %d %s", resp.StatusCode, string(body)) + } + var invoiceRes MakeInvoiceResponse - if err := json.NewDecoder(resp.Body).Decode(&invoiceRes); err != nil { + if err := json.Unmarshal(body, &invoiceRes); err != nil { return nil, err } @@ -238,8 +276,20 @@ func (svc *PhoenixService) LookupInvoice(ctx context.Context, paymentHash string } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "body": string(body), + "status_code": resp.StatusCode, + }).Error("phoenixd incoming payments endpoint returned non-success code") + return nil, fmt.Errorf("phoenixd incoming payments endpoint returned non-success code: %d %s", resp.StatusCode, string(body)) + } + var invoiceRes InvoiceResponse - if err := json.NewDecoder(resp.Body).Decode(&invoiceRes); err != nil { + if err := json.Unmarshal(body, &invoiceRes); err != nil { return nil, err } @@ -271,8 +321,16 @@ func (svc *PhoenixService) SendPaymentSync(payReq string, amount *uint64) (*lncl } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /payinvoice returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var payRes PayResponse - if err := json.NewDecoder(resp.Body).Decode(&payRes); err != nil { + if err := json.Unmarshal(body, &payRes); err != nil { return nil, err } @@ -312,8 +370,16 @@ func (svc *PhoenixService) GetNodeConnectionInfo(ctx context.Context) (nodeConne } defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("phoenixd /getinfo returned non-success status: %d %s", resp.StatusCode, string(body)) + } + var infoRes InfoResponse - if err := json.NewDecoder(resp.Body).Decode(&infoRes); err != nil { + if err := json.Unmarshal(body, &infoRes); err != nil { return nil, err } return &lnclient.NodeConnectionInfo{ diff --git a/swaps/swaps_service.go b/swaps/swaps_service.go index e4351c4c..8321b2f0 100644 --- a/swaps/swaps_service.go +++ b/swaps/swaps_service.go @@ -1415,6 +1415,15 @@ func (svc *swapsService) doMempoolRequest(endpoint string, result interface{}) e return errors.New("failed to read response body") } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "endpoint": endpoint, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Swaps mempool API endpoint returned non-success code") + return fmt.Errorf("swaps mempool API endpoint returned non-success code: %s", string(body)) + } + jsonErr := json.Unmarshal(body, &result) if jsonErr != nil { logger.Logger.WithError(jsonErr).WithFields(logrus.Fields{ @@ -1560,6 +1569,15 @@ func (svc *swapsService) getNextUnusedAddressFromXpub() (string, error) { return nil, err } + if res.StatusCode != http.StatusOK { + logger.Logger.WithFields(logrus.Fields{ + "endpoint": endpoint, + "status_code": res.StatusCode, + "body": string(body), + }).Error("Swaps esplora endpoint returned non-success code") + return nil, fmt.Errorf("swaps esplora endpoint returned non-success code: %s", string(body)) + } + var jsonContent interface{} err = json.Unmarshal(body, &jsonContent) if err != nil {