feat: bip177 (#1864)

* feat: bip177 option in settings

* fix: tests

* fix: use constants

* fix: bitcoin display format type

* fix: patch request

* fix: cleanup types

* fix: migrate to formatted bitcoin amount component

* fix: prevent banner from being shown again when updating settings

* fix: move stuff into api

* fix: component usage

* fix: onchain transaction table

* fix: amounts

* fix: format numbers

* fix: settings sections

* fix: copy

* chore: use existing constants

* chore: minor ui fixes and copy improvements for amount displays

---------

Co-authored-by: Roland Bewick <roland.bewick@gmail.com>
This commit is contained in:
René Aaron 2025-11-10 09:48:27 +01:00 committed by GitHub
parent ef10799d4b
commit 6294d519c2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 722 additions and 376 deletions

View file

@ -1200,6 +1200,7 @@ func (api *api) GetInfo(ctx context.Context) (*InfoResponse, error) {
autoUnlockPassword, _ := api.cfg.Get("AutoUnlockPassword", "")
info.SetupCompleted = api.cfg.SetupCompleted()
info.Currency = api.cfg.GetCurrency()
info.BitcoinDisplayFormat = api.cfg.GetBitcoinDisplayFormat()
info.StartupState = api.svc.GetStartupState()
if api.startupError != nil {
info.StartupError = api.startupError.Error()
@ -1264,6 +1265,38 @@ func (api *api) SetCurrency(currency string) error {
return nil
}
func (api *api) SetBitcoinDisplayFormat(format string) error {
if format != constants.BITCOIN_DISPLAY_FORMAT_SATS && format != constants.BITCOIN_DISPLAY_FORMAT_BIP177 {
return fmt.Errorf("bitcoin display format must be '%s' or '%s'", constants.BITCOIN_DISPLAY_FORMAT_SATS, constants.BITCOIN_DISPLAY_FORMAT_BIP177)
}
err := api.cfg.SetBitcoinDisplayFormat(format)
if err != nil {
logger.Logger.WithError(err).Error("Failed to update bitcoin display format")
return err
}
return nil
}
func (api *api) UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error {
if updateSettingsRequest.Currency != "" {
err := api.SetCurrency(updateSettingsRequest.Currency)
if err != nil {
return fmt.Errorf("failed to set currency: %w", err)
}
}
if updateSettingsRequest.BitcoinDisplayFormat != "" {
err := api.SetBitcoinDisplayFormat(updateSettingsRequest.BitcoinDisplayFormat)
if err != nil {
return fmt.Errorf("failed to set bitcoin display format: %w", err)
}
}
return nil
}
func (api *api) SetNodeAlias(nodeAlias string) error {
err := api.cfg.SetUpdate("NodeAlias", nodeAlias, "")
if err != nil {

View file

@ -65,6 +65,8 @@ type API interface {
GetWalletCapabilities(ctx context.Context) (*WalletCapabilitiesResponse, error)
Health(ctx context.Context) (*HealthResponse, error)
SetCurrency(currency string) error
SetBitcoinDisplayFormat(format string) error
UpdateSettings(updateSettingsRequest *UpdateSettingsRequest) error
LookupSwap(swapId string) (*LookupSwapResponse, error)
ListSwaps() (*ListSwapsResponse, error)
GetSwapInInfo() (*SwapInfoResponse, error)
@ -291,13 +293,15 @@ type InfoResponse struct {
AutoUnlockPasswordSupported bool `json:"autoUnlockPasswordSupported"`
AutoUnlockPasswordEnabled bool `json:"autoUnlockPasswordEnabled"`
Currency string `json:"currency"`
BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"`
Relays []InfoResponseRelay `json:"relays"`
NodeAlias string `json:"nodeAlias"`
MempoolUrl string `json:"mempoolUrl"`
}
type UpdateSettingsRequest struct {
Currency string `json:"currency"`
Currency string `json:"currency"`
BitcoinDisplayFormat string `json:"bitcoinDisplayFormat"`
}
type SetNodeAliasRequest struct {

View file

@ -9,6 +9,7 @@ import (
"path"
"strings"
"github.com/getAlby/hub/constants"
"github.com/getAlby/hub/db"
"github.com/getAlby/hub/logger"
"github.com/sirupsen/logrus"
@ -344,6 +345,7 @@ func randomHex(n int) (string, error) {
}
const defaultCurrency = "USD"
const defaultBitcoinDisplayFormat = constants.BITCOIN_DISPLAY_FORMAT_BIP177
func (cfg *config) GetCurrency() string {
currency, err := cfg.Get("Currency", "")
@ -368,3 +370,27 @@ func (cfg *config) SetCurrency(value string) error {
}
return nil
}
func (cfg *config) GetBitcoinDisplayFormat() string {
format, err := cfg.Get("BitcoinDisplayFormat", "")
if err != nil {
logger.Logger.WithError(err).Error("Failed to fetch bitcoin display format")
return defaultBitcoinDisplayFormat
}
if format == "" {
return defaultBitcoinDisplayFormat
}
return format
}
func (cfg *config) SetBitcoinDisplayFormat(value string) error {
if value != constants.BITCOIN_DISPLAY_FORMAT_SATS && value != constants.BITCOIN_DISPLAY_FORMAT_BIP177 {
return fmt.Errorf("bitcoin display format must be '%s' or '%s'", constants.BITCOIN_DISPLAY_FORMAT_SATS, constants.BITCOIN_DISPLAY_FORMAT_BIP177)
}
err := cfg.SetUpdate("BitcoinDisplayFormat", value, "")
if err != nil {
logger.Logger.WithError(err).Error("Failed to update bitcoin display format")
return err
}
return nil
}

View file

@ -88,4 +88,6 @@ type Config interface {
SetupCompleted() bool
GetCurrency() string
SetCurrency(value string) error
GetBitcoinDisplayFormat() string
SetBitcoinDisplayFormat(value string) error
}

View file

@ -77,3 +77,8 @@ const (
)
const SUBWALLET_APPSTORE_APP_ID = "uncle-jim"
const (
BITCOIN_DISPLAY_FORMAT_SATS = "sats"
BITCOIN_DISPLAY_FORMAT_BIP177 = "bip177"
)

View file

@ -1,4 +1,5 @@
import { AlertTriangleIcon } from "lucide-react";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
import { useBalances } from "src/hooks/useBalances";
import { useChannels } from "src/hooks/useChannels";
@ -35,7 +36,8 @@ export function AnchorReserveAlert({
including your anchor reserves may put your node at risk of unable to
reclaim funds in your channel after a force-closure. To prevent this,
set aside at least{" "}
{new Intl.NumberFormat().format(channels.length * 25000)} sats on-chain.
<FormattedBitcoinAmount amount={channels.length * 25000 * 1000} />{" "}
on-chain.
</AlertDescription>
</Alert>
);

View file

@ -1,4 +1,6 @@
import React from "react";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import { Input } from "src/components/ui/input";
import { Label } from "src/components/ui/label";
import { cn } from "src/lib/utils";
@ -20,7 +22,7 @@ function BudgetAmountSelect({
);
return (
<>
<div className="grid grid-cols-2 md:grid-cols-5 gap-2 text-xs mb-4">
<div className="grid grid-cols-2 md:grid-cols-3 gap-2 text-xs mb-4">
{Object.keys(budgetOptions)
.filter(
(budget) =>
@ -44,7 +46,20 @@ function BudgetAmountSelect({
: "border-muted"
)}
>
{`${budget} ${budgetOptions[budget] ? " sats" : ""}`}
{budgetOptions[budget] ? (
<>
<FormattedBitcoinAmount
amount={budgetOptions[budget] * 1000}
/>
<FormattedFiatAmount
className="text-xs"
showApprox
amount={budgetOptions[budget]}
/>
</>
) : (
budget
)}
</button>
);
})}

View file

@ -8,6 +8,7 @@ import React from "react";
import { toast } from "sonner";
import { SwapAlert } from "src/components/channels/SwapAlert";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { MempoolAlert } from "src/components/MempoolAlert";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
import { Button } from "src/components/ui/button";
@ -17,7 +18,6 @@ import { useBalances } from "src/hooks/useBalances";
import { useChannels } from "src/hooks/useChannels";
import { useInfo } from "src/hooks/useInfo";
import { copyToClipboard } from "src/lib/clipboard";
import { formatAmount } from "src/lib/utils";
import { Channel, CloseChannelResponse } from "src/types";
import { request } from "src/utils/request";
import {
@ -116,10 +116,13 @@ export function CloseChannelDialogContent({ alias, channel }: Props) {
<Alert className="mb-4">
<AlertCircleIcon className="h-4 w-4" />
<AlertDescription>
Closing this channel will move{" "}
{formatAmount(channel.localBalance)} sats in this channel to
your on-chain balance and reduce your receive limit by{" "}
{formatAmount(channel.remoteBalance)} sats.
<div>
Closing this channel will move{" "}
<FormattedBitcoinAmount amount={channel.localBalance} /> in
this channel to your on-chain balance and reduce your
receive limit by{" "}
<FormattedBitcoinAmount amount={channel.remoteBalance} />.
</div>
</AlertDescription>
</Alert>
<div>

View file

@ -0,0 +1,41 @@
import { useInfo } from "src/hooks/useInfo";
import { BitcoinDisplayFormat } from "src/types";
interface FormattedBitcoinAmountProps {
amount: number; // Amount in millisatoshis
className?: string;
showSymbol?: boolean; // Whether to show the symbol/unit
}
/**
* Formats a Bitcoin amount according to user settings
* @param amount - Amount in millisatoshis
* @param className - Optional CSS classes
* @param showSymbol - Whether to show the symbol/unit (default: true)
*/
export function FormattedBitcoinAmount({
amount,
className = "",
showSymbol = true,
}: FormattedBitcoinAmountProps) {
const { data: info } = useInfo();
// Convert from millisatoshis to satoshis
const sats = Math.floor(amount / 1000);
// Get display format from settings, default to BIP177
const displayFormat: BitcoinDisplayFormat =
info?.bitcoinDisplayFormat || "bip177";
const formattedNumber = new Intl.NumberFormat().format(sats);
if (!showSymbol) {
return <span className={className}>{formattedNumber}</span>;
}
if (displayFormat === "bip177") {
return <span className={className}>{formattedNumber}</span>;
} else {
return <span className={className}>{formattedNumber} sats</span>;
}
}

View file

@ -1,6 +1,7 @@
import { Invoice, getFiatValue } from "@getalby/lightning-tools";
import { CopyIcon, LightbulbIcon } from "lucide-react";
import React from "react";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { LightningIcon } from "src/components/icons/Lightning";
import Loading from "src/components/Loading";
import QRCode from "src/components/QRCode";
@ -40,7 +41,7 @@ export function PayLightningInvoice({ invoice }: PayLightningInvoiceProps) {
</div>
<div>
<p className="text-lg font-semibold">
{new Intl.NumberFormat().format(amount)} sats
<FormattedBitcoinAmount amount={amount * 1000} />
</p>
<p className="flex flex-col items-center justify-center">
{new Intl.NumberFormat("en-US", {

View file

@ -3,6 +3,7 @@ import React from "react";
import BudgetAmountSelect from "src/components/BudgetAmountSelect";
import BudgetRenewalSelect from "src/components/BudgetRenewalSelect";
import ExpirySelect from "src/components/ExpirySelect";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Scopes from "src/components/Scopes";
import { Badge } from "src/components/ui/badge";
import { Button } from "src/components/ui/button";
@ -169,12 +170,22 @@ const Permissions: React.FC<PermissionsProps> = ({
<span className="text-primary font-medium">
Budget Amount:
</span>{" "}
{permissions.maxAmount
? new Intl.NumberFormat().format(permissions.maxAmount)
: "∞"}
{" sats "}
{!isNewConnection &&
`(${new Intl.NumberFormat().format(budgetUsage || 0)} sats used)`}
{permissions.maxAmount ? (
<FormattedBitcoinAmount
amount={permissions.maxAmount * 1000}
/>
) : (
"∞"
)}{" "}
{!isNewConnection && (
<>
(
<FormattedBitcoinAmount
amount={(budgetUsage || 0) * 1000}
/>{" "}
used)
</>
)}
</p>
</div>
</div>

View file

@ -1,3 +1,4 @@
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { Boostagram } from "src/types";
function PodcastingInfo({ boost }: { boost: Boostagram }) {
@ -37,10 +38,7 @@ function PodcastingInfo({ boost }: { boost: Boostagram }) {
<div className="mt-6">
<p>Total amount</p>
<p className="text-muted-foreground break-all sensitive">
{new Intl.NumberFormat().format(
Math.floor(boost.valueMsatTotal / 1000)
)}{" "}
{Math.floor(boost.valueMsatTotal / 1000) == 1 ? "sat" : "sats"}
<FormattedBitcoinAmount amount={boost.valueMsatTotal} />
</p>
</div>
)}

View file

@ -2,6 +2,7 @@ import { AlertTriangleIcon, ExternalLinkIcon } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
import { LoadingButton } from "src/components/ui/custom/loading-button";
@ -135,7 +136,13 @@ export function RebalanceChannelDialogContent({
<p className="mt-2 text-xs text-muted-foreground">
Fee: 0.3%
{!!amount && (
<> ({Math.floor(parseInt(amount || "0") * 0.003)} sats)</>
<>
&nbsp;(
<FormattedBitcoinAmount
amount={Math.floor(parseInt(amount || "0") * 0.003 * 1000)}
/>
)
</>
)}{" "}
+ routing fees
</p>

View file

@ -1,4 +1,5 @@
import { AlertTriangleIcon } from "lucide-react";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
import { LinkButton } from "src/components/ui/custom/link-button";
import { useBalances } from "src/hooks/useBalances";
@ -41,8 +42,7 @@ export function SpendingAlert({
<p>
Your payment will likely fail because your maximum spendable balance
for the next payment is currently{" "}
{new Intl.NumberFormat().format(Math.floor(maxSpendable / 1000))}{" "}
sats.
<FormattedBitcoinAmount amount={maxSpendable} />.
</p>
<div className="flex gap-2 mt-2">
<LinkButton

View file

@ -15,6 +15,7 @@ import React from "react";
import { Link } from "react-router-dom";
import AppAvatar from "src/components/AppAvatar";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import { PaymentFailedAlert } from "src/components/PaymentFailedAlert";
import PodcastingInfo from "src/components/PodcastingInfo";
@ -194,14 +195,10 @@ function TransactionItem({ tx }: Props) {
)}
>
{type == "outgoing" ? "-" : "+"}
<span className="font-medium">
{new Intl.NumberFormat().format(
Math.floor(tx.amount / 1000)
)}
</span>
</p>
<p className="text-muted-foreground">
{Math.floor(tx.amount / 1000) == 1 ? "sat" : "sats"}
<FormattedBitcoinAmount
amount={tx.amount}
className="font-medium"
/>
</p>
</div>
<FormattedFiatAmount
@ -227,8 +224,7 @@ function TransactionItem({ tx }: Props) {
{typeStateIcon}
<div className="ml-4">
<p className="text-xl md:text-2xl font-semibold sensitive">
{new Intl.NumberFormat().format(Math.floor(tx.amount / 1000))}{" "}
{Math.floor(tx.amount / 1000) == 1 ? "sat" : "sats"}
<FormattedBitcoinAmount amount={tx.amount} />
</p>
<FormattedFiatAmount amount={Math.floor(tx.amount / 1000)} />
</div>
@ -278,10 +274,7 @@ function TransactionItem({ tx }: Props) {
<div className="mt-6">
<p>Fee</p>
<p className="text-muted-foreground">
{new Intl.NumberFormat().format(
Math.floor(tx.feesPaid / 1000)
)}{" "}
{Math.floor(tx.feesPaid / 1000) == 1 ? "sat" : "sats"}
<FormattedBitcoinAmount amount={tx.feesPaid} />
{tx.feesPaid > 0 && (
<>&nbsp;({((tx.feesPaid / tx.amount) * 100).toFixed(2)}%)</>
)}

View file

@ -1,6 +1,7 @@
import { InfoIcon } from "lucide-react";
import { ChannelDropdownMenu } from "src/components/channels/ChannelDropdownMenu";
import { ChannelWarning } from "src/components/channels/ChannelWarning";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { Badge } from "src/components/ui/badge.tsx";
import {
Card,
@ -153,7 +154,9 @@ function ChannelCard({
</Tooltip>
</TooltipProvider>
<p className="text-foreground">{formatAmount(capacity)} sats</p>
<p className="text-foreground">
<FormattedBitcoinAmount amount={capacity} />
</p>
</div>
<div className="flex justify-between items-center">
<TooltipProvider>
@ -188,7 +191,9 @@ function ChannelCard({
/{" "}
</>
)}
{formatAmount(channel.unspendablePunishmentReserve * 1000)} sats
<FormattedBitcoinAmount
amount={channel.unspendablePunishmentReserve * 1000}
/>
</p>
</div>
<div className="flex justify-between items-center">
@ -206,11 +211,13 @@ function ChannelCard({
className="h-6 absolute"
/>
<div className="flex flex-row w-full justify-between px-2 text-xs items-center h-6 mix-blend-exclusion text-white">
<span title={channel.localSpendableBalance / 1000 + " sats"}>
{formatAmount(channel.localSpendableBalance)} sats
<span>
<FormattedBitcoinAmount
amount={channel.localSpendableBalance}
/>
</span>
<span title={channel.remoteBalance / 1000 + " sats"}>
{formatAmount(channel.remoteBalance)} sats
<span>
<FormattedBitcoinAmount amount={channel.remoteBalance} />
</span>
</div>
</div>

View file

@ -1,5 +1,6 @@
import { InfoIcon } from "lucide-react";
import { ChannelWarning } from "src/components/channels/ChannelWarning";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading.tsx";
import { Badge } from "src/components/ui/badge.tsx";
import {
@ -24,7 +25,6 @@ import {
TooltipTrigger,
} from "src/components/ui/tooltip.tsx";
import { useNodeDetails } from "src/hooks/useNodeDetails";
import { formatAmount } from "src/lib/utils.ts";
import { Channel, LongUnconfirmedZeroConfChannel } from "src/types";
import { ChannelDropdownMenu } from "./ChannelDropdownMenu";
@ -196,22 +196,25 @@ function ChannelTableRow({
<Badge variant="warning">Offline</Badge>
)}
</TableCell>
<TableCell title={capacity / 1000 + " sats"}>
{formatAmount(capacity)} sats
<TableCell>
<FormattedBitcoinAmount amount={capacity} />
</TableCell>
<TableCell title={channel.unspendablePunishmentReserve + " sats"}>
{channel.localBalance < channel.unspendablePunishmentReserve * 1000 && (
<>
{formatAmount(
Math.min(
<FormattedBitcoinAmount
amount={Math.min(
channel.localBalance,
channel.unspendablePunishmentReserve * 1000
)
)}{" "}
)}
showSymbol={false}
/>{" "}
/{" "}
</>
)}
{formatAmount(channel.unspendablePunishmentReserve * 1000)} sats
<FormattedBitcoinAmount
amount={channel.unspendablePunishmentReserve * 1000}
/>
</TableCell>
<TableCell>
<div className="relative">
@ -220,11 +223,11 @@ function ChannelTableRow({
className="h-6 absolute"
/>
<div className="flex flex-row w-full justify-between px-2 text-xs items-center h-6 mix-blend-exclusion text-white">
<span title={channel.localSpendableBalance / 1000 + " sats"}>
{formatAmount(channel.localSpendableBalance)} sats
<span>
<FormattedBitcoinAmount amount={channel.localSpendableBalance} />
</span>
<span title={channel.remoteBalance / 1000 + " sats"}>
{formatAmount(channel.remoteBalance)} sats
<span>
<FormattedBitcoinAmount amount={channel.remoteBalance} />
</span>
</div>
</div>

View file

@ -1,5 +1,6 @@
import dayjs from "dayjs";
import { ArrowDownIcon, ArrowUpIcon } from "lucide-react";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import {
Card,
@ -96,12 +97,11 @@ export function OnchainTransactionsTable() {
>
{tx.type == "outgoing" ? "-" : "+"}
<span className="font-medium">
{new Intl.NumberFormat().format(tx.amountSat)}
<FormattedBitcoinAmount
amount={tx.amountSat * 1000}
/>
</span>
</p>
<p className="text-muted-foreground">
{tx.amountSat == 1 ? "sat" : "sats"}
</p>
</div>
<FormattedFiatAmount
className="text-xs"

View file

@ -1,10 +1,11 @@
import dayjs from "dayjs";
import { BrickWallIcon, CircleCheckIcon, PlusCircleIcon } from "lucide-react";
import { Link } from "react-router-dom";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { Button } from "src/components/ui/button";
import { Progress } from "src/components/ui/progress";
import { SUBWALLET_APPSTORE_APP_ID } from "src/constants";
import { formatAmount, getBudgetRenewalLabel } from "src/lib/utils";
import { getBudgetRenewalLabel } from "src/lib/utils";
import { App } from "src/types";
type AppCardConnectionInfoProps = {
@ -42,10 +43,7 @@ export function AppCardConnectionInfo({
<div className="flex flex-col items-end justify-end">
<p>Balance</p>
<p className="text-xl font-medium">
{new Intl.NumberFormat().format(
Math.floor(connection.balance / 1000)
)}{" "}
sats
<FormattedBitcoinAmount amount={connection.balance} />
</p>
</div>
</div>
@ -58,10 +56,11 @@ export function AppCardConnectionInfo({
{budgetRemainingText}
</p>
<p className="text-xl font-medium">
{new Intl.NumberFormat().format(
connection.maxAmount - connection.budgetUsage
)}{" "}
sats
<FormattedBitcoinAmount
amount={
(connection.maxAmount - connection.budgetUsage) * 1000
}
/>
</p>
</div>
</div>
@ -79,7 +78,9 @@ export function AppCardConnectionInfo({
<div>
{connection.maxAmount && (
<>
{formatAmount(connection.maxAmount * 1000)} sats
<FormattedBitcoinAmount
amount={connection.maxAmount * 1000}
/>
{connection.budgetRenewal !== "never" && (
<> / {getBudgetRenewalLabel(connection.budgetRenewal)}</>
)}
@ -96,7 +97,9 @@ export function AppCardConnectionInfo({
You've spent
</p>
<p className="text-xl font-medium">
{new Intl.NumberFormat().format(connection.budgetUsage)} sats
<FormattedBitcoinAmount
amount={connection.budgetUsage * 1000}
/>
</p>
</div>
</div>

View file

@ -5,6 +5,7 @@ import {
Trash2Icon,
} from "lucide-react";
import React from "react";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import { IsolatedAppDrawDownDialog } from "src/components/IsolatedAppDrawDownDialog";
import { IsolatedAppTopupDialog } from "src/components/IsolatedAppTopupDialog";
@ -26,7 +27,7 @@ import { useCreateLightningAddress } from "src/hooks/useCreateLightningAddress";
import { useDeleteLightningAddress } from "src/hooks/useDeleteLightningAddress";
import { useTransactions } from "src/hooks/useTransactions";
import { copyToClipboard } from "src/lib/clipboard";
import { cn, formatAmount, getBudgetRenewalLabel } from "src/lib/utils";
import { cn, getBudgetRenewalLabel } from "src/lib/utils";
import { App, Transaction } from "src/types";
export function AppUsage({ app }: { app: App }) {
@ -89,10 +90,7 @@ export function AppUsage({ app }: { app: App }) {
<div className="flex justify-between items-end">
<div>
<p className="font-medium text-2xl">
{new Intl.NumberFormat().format(
Math.floor(app.balance / 1000)
)}{" "}
sats
<FormattedBitcoinAmount amount={app.balance} />
</p>
<FormattedFiatAmount
amount={Math.floor(app.balance / 1000)}
@ -210,7 +208,7 @@ export function AppUsage({ app }: { app: App }) {
</CardHeader>
<CardContent>
<p className="font-medium text-2xl">
{new Intl.NumberFormat().format(totalSpent)} sats
<FormattedBitcoinAmount amount={totalSpent * 1000} />
</p>
<FormattedFiatAmount amount={totalSpent} />
</CardContent>
@ -221,7 +219,7 @@ export function AppUsage({ app }: { app: App }) {
</CardHeader>
<CardContent>
<p className="font-medium text-2xl">
{new Intl.NumberFormat().format(totalReceived)} sats
<FormattedBitcoinAmount amount={totalReceived * 1000} />
</p>
<FormattedFiatAmount amount={totalReceived} />
</CardContent>
@ -240,10 +238,9 @@ export function AppUsage({ app }: { app: App }) {
Left in budget
</p>
<p className="text-xl font-medium">
{new Intl.NumberFormat().format(
app.maxAmount - app.budgetUsage
)}{" "}
sats
<FormattedBitcoinAmount
amount={(app.maxAmount - app.budgetUsage) * 1000}
/>
</p>
<FormattedFiatAmount amount={app.maxAmount - app.budgetUsage} />
</div>
@ -252,7 +249,7 @@ export function AppUsage({ app }: { app: App }) {
Budget renewal
</p>
<p className="text-xl font-medium">
{formatAmount(app.maxAmount * 1000)} sats
<FormattedBitcoinAmount amount={app.maxAmount * 1000} />
{app.budgetRenewal !== "never" && (
<> / {getBudgetRenewalLabel(app.budgetRenewal)}</>
)}

View file

@ -1,3 +1,4 @@
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import {
Card,
@ -25,10 +26,7 @@ export function ForwardsWidget() {
<div>
<p className="text-muted-foreground text-xs">Fees Earned</p>
<p className="text-xl font-semibold">
{new Intl.NumberFormat().format(
Math.floor(forwards.totalFeeEarnedMsat / 1000)
)}{" "}
sats
<FormattedBitcoinAmount amount={forwards.totalFeeEarnedMsat} />
<FormattedFiatAmount
amount={Math.floor(forwards.totalFeeEarnedMsat / 1000)}
/>
@ -37,10 +35,9 @@ export function ForwardsWidget() {
<div>
<p className="text-muted-foreground text-xs">Total Routed</p>
<p className="text-xl font-semibold">
{new Intl.NumberFormat().format(
Math.floor(forwards.outboundAmountForwardedMsat / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={forwards.outboundAmountForwardedMsat}
/>
<FormattedFiatAmount
amount={Math.floor(forwards.outboundAmountForwardedMsat / 1000)}
/>

View file

@ -11,6 +11,7 @@ import dayjs from "dayjs";
import { ChevronUpIcon, ZapIcon } from "lucide-react";
import React from "react";
import { toast } from "sonner";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { Badge } from "src/components/ui/badge";
import { Button } from "src/components/ui/button";
@ -235,7 +236,9 @@ export function LightningMessageboardWidget() {
<div>
<Badge>
<ZapIcon />
{new Intl.NumberFormat().format(message.amount)}
<FormattedBitcoinAmount
amount={message.amount * 1000}
/>
</Badge>
</div>
</CardFooter>

View file

@ -1,5 +1,6 @@
import { Outlet } from "react-router-dom";
import AppHeader from "src/components/AppHeader";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { useBalances } from "src/hooks/useBalances";
import { useChannels } from "src/hooks/useChannels";
@ -24,10 +25,9 @@ export default function ReceiveLayout() {
<div className="md:flex md:items-center md:gap-4">
<span className="text-muted-foreground">Receive Limit:</span>
<div className="balance sensitive slashed-zero">
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalReceivable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalReceivable}
/>
</div>
</div>
)

View file

@ -9,10 +9,10 @@ export function useBanner() {
const { data: albyInfo } = useAlbyInfo();
const { data: albyMe } = useAlbyMe();
const [showBanner, setShowBanner] = React.useState(false);
const isDismissedRef = React.useRef(false);
React.useEffect(() => {
if (!info || !albyInfo) {
setShowBanner(false);
if (!info || !albyInfo || isDismissedRef.current) {
return;
}
@ -33,6 +33,7 @@ export function useBanner() {
}, [info, albyInfo, albyMe?.subscription.plan_code]);
const dismissBanner = () => {
isDismissedRef.current = true;
setShowBanner(false);
};

View file

@ -2,6 +2,7 @@ import React from "react";
import { toast } from "sonner";
import { useTransactions } from "src/hooks/useTransactions";
import { Transaction } from "src/types";
import { formatBitcoinAmount } from "src/utils/bitcoinFormatting";
export function useNotifyReceivedPayments() {
const { data: transactionsData } = useTransactions(undefined, true, 1);
@ -15,7 +16,7 @@ export function useNotifyReceivedPayments() {
if (latestTx !== prevTransaction) {
if (prevTransaction && latestTx.type === "incoming") {
toast("Payment received", {
description: `${new Intl.NumberFormat().format(Math.floor(latestTx.amount / 1000))} sats`,
description: formatBitcoinAmount(latestTx.amount),
});
}
setPrevTransaction(latestTx);

View file

@ -12,6 +12,7 @@ import {
import albyExtension from "src/assets/suggested-apps/alby-extension.png";
import albyGo from "src/assets/suggested-apps/alby-go.png";
import alby from "src/assets/suggested-apps/alby.png";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
interface Platform {
name: string;
@ -21,7 +22,7 @@ interface Platform {
interface ProductOpportunity {
title: string;
logo: string;
reward: string;
reward: number;
platforms: Platform[];
}
@ -29,7 +30,7 @@ const productOpportunities: ProductOpportunity[] = [
{
title: "Alby Go",
logo: albyGo,
reward: "1,000 sats",
reward: 1000,
platforms: [
{
name: "Google Play",
@ -44,7 +45,7 @@ const productOpportunities: ProductOpportunity[] = [
{
title: "Alby Extension",
logo: albyExtension,
reward: "1,000 sats",
reward: 1000,
platforms: [
{
name: "Chrome",
@ -59,7 +60,7 @@ const productOpportunities: ProductOpportunity[] = [
{
title: "Alby",
logo: alby,
reward: "2,000 sats",
reward: 2000,
platforms: [
{
name: "Trustpilot",
@ -74,6 +75,16 @@ export function AlbyReviews() {
<>
<AppHeader title="Earn Bitcoin" />
<p className="text-muted-foreground text-xs">
For a smooth experience consider a opening a channel of{" "}
<FormattedBitcoinAmount amount={200_000 * 1000} /> in size or more.{" "}
<ExternalLink
to="https://guides.getalby.com/user-guide/alby-hub/node"
className="underline"
>
Learn more
</ExternalLink>
</p>
<div className="space-y-8">
<Card>
<CardHeader>
@ -87,7 +98,7 @@ export function AlbyReviews() {
>
support@getalby.com
</ExternalLink>{" "}
to receive sats.
to receive your bitcoin.
</CardDescription>
</CardHeader>
<CardContent>
@ -116,7 +127,9 @@ export function AlbyReviews() {
))}
</div>
</div>
<div className="text-right font-medium">{product.reward}</div>
<div className="text-right font-medium">
<FormattedBitcoinAmount amount={product.reward * 1000} />
</div>
</div>
))}
</div>

View file

@ -38,6 +38,7 @@ import {
} from "src/constants";
import { createApp } from "src/requests/createApp";
import { CreateAppRequest, UpdateAppRequest } from "src/types";
import { formatBitcoinAmount } from "src/utils/bitcoinFormatting";
import { handleRequestError } from "src/utils/handleRequestError";
import { request } from "src/utils/request";
@ -61,7 +62,7 @@ function SupportAlby() {
if (+amount < 1000) {
toast.error("Amount too low", {
description: "Minimum payment is 1000 sats",
description: `Minimum payment is ${formatBitcoinAmount(1_000 * 1000)}`,
});
return;
}

View file

@ -3,6 +3,7 @@ import { Navigate, useLocation, useNavigate } from "react-router-dom";
import React from "react";
import { toast } from "sonner";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { appStoreApps } from "src/components/connections/SuggestedAppData";
import PasswordInput from "src/components/password/PasswordInput";
@ -530,8 +531,7 @@ function FinalizeConnection({
{app?.isolated && (
<li>
Optional: Top up sub-wallet balance (
{new Intl.NumberFormat().format(Math.floor(app.balance / 1000))}{" "}
sats){" "}
<FormattedBitcoinAmount amount={app.balance} />){" "}
<IsolatedAppTopupDialog appId={app.id}>
<Button size="sm" variant="secondary">
Top Up

View file

@ -24,6 +24,7 @@ import { LDKChannelWithoutPeerAlert } from "src/components/channels/LDKChannelWi
import { OnchainTransactionsTable } from "src/components/channels/OnchainTransactionsTable.tsx";
import EmptyState from "src/components/EmptyState.tsx";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert";
import ResponsiveButton from "src/components/ResponsiveButton";
@ -379,28 +380,28 @@ export default function Channels() {
Your spending balance is the funds on your side of
your channels, which you can use to make lightning
payments. Your total lightning balance is{" "}
{new Intl.NumberFormat().format(
channels
?.map((channel) =>
Math.floor(channel.localBalance / 1000)
)
.reduce((a, b) => a + b, 0) || 0
)}{" "}
sats which includes{" "}
{new Intl.NumberFormat().format(
Math.floor(
<FormattedBitcoinAmount
amount={
channels
?.map((channel) => channel.localBalance)
.reduce((a, b) => a + b, 0) || 0
}
/>{" "}
which includes{" "}
<FormattedBitcoinAmount
amount={
channels
?.map((channel) =>
Math.min(
Math.floor(channel.localBalance / 1000),
channel.unspendablePunishmentReserve
channel.localBalance,
channel.unspendablePunishmentReserve *
1000
)
)
.reduce((a, b) => a + b, 0) || 0
)
)}{" "}
sats reserved in your channels which cannot be
spent.
}
/>{" "}
reserved in your channels which cannot be spent.
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -417,10 +418,9 @@ export default function Channels() {
{balances && (
<>
<div className="text-xl font-medium balance sensitive mb-1">
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalSpendable}
/>
</div>
<FormattedFiatAmount
amount={balances.lightning.totalSpendable / 1000}
@ -453,12 +453,9 @@ export default function Channels() {
{balances && (
<>
<div className="text-xl font-medium balance sensitive mb-1">
{new Intl.NumberFormat().format(
Math.floor(
balances.lightning.totalReceivable / 1000
)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalReceivable}
/>
</div>
<FormattedFiatAmount
amount={balances.lightning.totalReceivable / 1000}
@ -510,10 +507,9 @@ export default function Channels() {
<>
<div className="mb-1">
<span className="text-xl font-medium balance sensitive mb-1 mr-1">
{new Intl.NumberFormat().format(
Math.floor(balances.onchain.spendable)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.onchain.spendable * 1000}
/>
</span>
{!!channels?.length &&
balances.onchain.reserved +
@ -528,8 +524,11 @@ export default function Channels() {
You have insufficient funds in reserve to
close channels or bump on-chain transactions
and currently rely on the counterparty. It is
recommended to deposit at least 25,000 sats to
your on-chain balance.
recommended to deposit at least{" "}
<FormattedBitcoinAmount
amount={25_000 * 1000}
/>{" "}
to your on-chain balance.
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -544,11 +543,14 @@ export default function Channels() {
balances.onchain.total && (
<p className="text-xs text-muted-foreground animate-pulse">
+
{new Intl.NumberFormat().format(
balances.onchain.total -
balances.onchain.spendable
)}{" "}
sats incoming
<FormattedBitcoinAmount
amount={
(balances.onchain.total -
balances.onchain.spendable) *
1000
}
/>{" "}
incoming
</p>
)}
</>
@ -565,10 +567,12 @@ export default function Channels() {
<AlertTitle>Pending Closed Channels</AlertTitle>
<AlertDescription className="block">
You have{" "}
{new Intl.NumberFormat().format(
balances.onchain.pendingBalancesFromChannelClosures
)}{" "}
sats pending from closed channels with
<FormattedBitcoinAmount
amount={
balances.onchain.pendingBalancesFromChannelClosures * 1000
}
/>{" "}
pending from closed channels with
{[
...balances.onchain.pendingBalancesDetails,
...balances.onchain.pendingSweepBalancesDetails,
@ -680,7 +684,8 @@ function PendingBalancesDetailsItem({
{nodeDetails?.alias || "Unknown"}
<ExternalLinkIcon className="ml-1 w-4 h-4 inline" />
</ExternalLink>{" "}
({new Intl.NumberFormat().format(details.amount)} sats)&nbsp;
(<FormattedBitcoinAmount amount={details.amount * 1000} />
)&nbsp;
<ExternalLink
to={`${info?.mempoolUrl}/tx/${details.fundingTxId}#flow=&vout=${details.fundingTxVout}`}
className="underline"

View file

@ -13,6 +13,7 @@ import { Link } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import QRCode from "src/components/QRCode";
import { Button } from "src/components/ui/button";
@ -282,13 +283,13 @@ function PayBitcoinChannelOrderTopup({ order }: { order: NewChannelOrder }) {
<p className="text-xs slashed-zero">
You currently have{" "}
<span className="font-semibold sensitive">
{new Intl.NumberFormat().format(balances.onchain.total)}
</span>{" "}
sats. We recommend depositing{" "}
<FormattedBitcoinAmount amount={balances.onchain.total * 1000} />
</span>
. We recommend depositing an additional amount of{" "}
<span className="font-semibold">
{new Intl.NumberFormat().format(recommendedAmount)}
<FormattedBitcoinAmount amount={recommendedAmount * 1000} />
</span>{" "}
sats to open this channel.
to open this channel.
</p>
<p className="text-xs text-muted-foreground">
This amount includes cost for the channel opening and potential
@ -361,7 +362,7 @@ function PayBitcoinChannelOrderTopup({ order }: { order: NewChannelOrder }) {
</CardHeader>
{unspentAmount > 0 && (
<CardContent className="slashed-zero">
{new Intl.NumberFormat().format(unspentAmount)} sats deposited
<FormattedBitcoinAmount amount={unspentAmount * 1000} /> deposited
</CardContent>
)}
</Card>
@ -651,10 +652,9 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
Spending Balance
</TableCell>
<TableCell className="text-right p-3">
{new Intl.NumberFormat().format(
lspOrderResponse.outgoingLiquidity
)}{" "}
sats
<FormattedBitcoinAmount
amount={lspOrderResponse.outgoingLiquidity * 1000}
/>
</TableCell>
</TableRow>
)}
@ -664,10 +664,9 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
Incoming Liquidity
</TableCell>
<TableCell className="text-right p-3">
{new Intl.NumberFormat().format(
lspOrderResponse.incomingLiquidity
)}{" "}
sats
<FormattedBitcoinAmount
amount={lspOrderResponse.incomingLiquidity * 1000}
/>
</TableCell>
</TableRow>
)}
@ -676,10 +675,9 @@ function PayLightningChannelOrder({ order }: { order: NewChannelOrder }) {
Amount to pay
</TableCell>
<TableCell className="font-semibold text-right p-3">
{new Intl.NumberFormat().format(
lspOrderResponse.invoiceAmount
)}{" "}
sats
<FormattedBitcoinAmount
amount={lspOrderResponse.invoiceAmount * 1000}
/>
</TableCell>
</TableRow>
</TableBody>

View file

@ -8,6 +8,7 @@ import { ChannelPublicPrivateAlert } from "src/components/channels/ChannelPublic
import { DuplicateChannelAlert } from "src/components/channels/DuplicateChannelAlert";
import { SwapAlert } from "src/components/channels/SwapAlert";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { MempoolAlert } from "src/components/MempoolAlert";
import { Button } from "src/components/ui/button";
@ -277,8 +278,9 @@ function NewChannelInternal({
{order.amount && +order.amount < 200_000 && (
<p className="text-muted-foreground text-xs">
For a smooth experience consider a opening a channel of 200k
sats in size or more.{" "}
For a smooth experience consider a opening a channel of{" "}
<FormattedBitcoinAmount amount={200_000 * 1000} /> in size or
more.{" "}
<ExternalLink
to="https://guides.getalby.com/user-guide/alby-hub/node"
className="underline"
@ -321,7 +323,9 @@ function NewChannelInternal({
{" "}
Estimated channel price:{" "}
<span className="font-semibold">
{new Intl.NumberFormat().format(estimatedChannelPrice)} sats
<FormattedBitcoinAmount
amount={estimatedChannelPrice * 1000}
/>
</span>
</span>
)}
@ -385,16 +389,14 @@ function NewChannelInternal({
{peer.name}
<span className="ml-4 text-xs text-muted-foreground slashed-zero">
Min.{" "}
{new Intl.NumberFormat().format(
peer.minimumChannelSize
)}{" "}
sats
<FormattedBitcoinAmount
amount={peer.minimumChannelSize * 1000}
/>
<span className="mr-5" />
Max.{" "}
{new Intl.NumberFormat().format(
peer.maximumChannelSize
)}{" "}
sats
<FormattedBitcoinAmount
amount={peer.maximumChannelSize * 1000}
/>
</span>
</div>
</div>

View file

@ -8,6 +8,7 @@ import { ChannelPublicPrivateAlert } from "src/components/channels/ChannelPublic
import { DuplicateChannelAlert } from "src/components/channels/DuplicateChannelAlert";
import { SwapAlert } from "src/components/channels/SwapAlert";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { MempoolAlert } from "src/components/MempoolAlert";
import { Alert, AlertDescription } from "src/components/ui/alert";
@ -265,8 +266,9 @@ function NewChannelInternal({
{order.amount && +order.amount < 200_000 && (
<p className="text-muted-foreground text-xs">
For a smooth experience consider a opening a channel of 200k
sats in size or more.{" "}
For a smooth experience consider a opening a channel of{" "}
<FormattedBitcoinAmount amount={200_000 * 1000} /> in size or
more.{" "}
<ExternalLink
to="https://guides.getalby.com/user-guide/alby-hub/node"
className="underline"
@ -287,7 +289,9 @@ function NewChannelInternal({
/>
<div className="text-muted-foreground text-sm sensitive slashed-zero">
Current on-chain balance:{" "}
{new Intl.NumberFormat().format(balances.onchain.spendable)} sats
<FormattedBitcoinAmount
amount={balances.onchain.spendable * 1000}
/>
</div>
<div className="grid grid-cols-3 gap-1.5 text-muted-foreground text-xs">
{presetAmounts.map((amount) => (
@ -350,10 +354,11 @@ function NewChannelInternal({
{peer.minimumChannelSize > 0 && (
<span className="ml-4 text-xs text-muted-foreground slashed-zero">
Min.{" "}
{new Intl.NumberFormat().format(
peer.minimumChannelSize
)}{" "}
sats
<FormattedBitcoinAmount
amount={
peer.minimumChannelSize * 1000
}
/>
</span>
)}
</div>
@ -465,10 +470,9 @@ function NewChannelInternal({
<div>
<div className="font-medium text-muted-foreground">Amount</div>
<div>
{new Intl.NumberFormat().format(
parseInt(order.amount || "0")
)}{" "}
sats
<FormattedBitcoinAmount
amount={parseInt(order.amount || "0") * 1000}
/>
</div>
</div>
<div>

View file

@ -4,6 +4,7 @@ import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { Button } from "src/components/ui/button";
import { Checkbox } from "src/components/ui/checkbox";
@ -108,7 +109,7 @@ export function AutoChannel() {
<p className="text-muted-foreground slashed-zero">
Please pay the lightning invoice below which will cover the costs of
opening your channel. You will receive a channel with{" "}
{new Intl.NumberFormat().format(channelSize)} sats of receiving
<FormattedBitcoinAmount amount={channelSize * 1000} /> of receiving
capacity.
</p>
<PayLightningInvoice invoice={invoice} />

View file

@ -9,6 +9,7 @@ import { Link, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { Button } from "src/components/ui/button";
import { Checkbox } from "src/components/ui/checkbox";
@ -122,7 +123,7 @@ export function FirstChannel() {
Incoming Liquidity
</TableCell>
<TableCell className="text-right p-3">
{new Intl.NumberFormat().format(channelSize)} sats
<FormattedBitcoinAmount amount={channelSize * 1000} />
</TableCell>
</TableRow>
{invoice && (
@ -131,10 +132,9 @@ export function FirstChannel() {
Amount to pay
</TableCell>
<TableCell className="font-semibold text-right p-3">
{new Intl.NumberFormat().format(
new Invoice({ pr: invoice }).satoshi
)}{" "}
sats
<FormattedBitcoinAmount
amount={new Invoice({ pr: invoice }).satoshi * 1000}
/>
</TableCell>
</TableRow>
)}
@ -230,8 +230,8 @@ export function FirstChannel() {
</div>
</TooltipTrigger>
<TooltipContent className="max-w-sm">
You will be able to receive up to this amount of
sats in this channel.
You will be able to receive up to this amount in
this channel.
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -239,10 +239,9 @@ export function FirstChannel() {
</TableCell>
<TableCell className="p-3 flex flex-col gap-2 items-end justify-center align-top">
<span>
{new Intl.NumberFormat().format(
lspChannelOffer.lspBalanceSat
)}{" "}
sats
<FormattedBitcoinAmount
amount={lspChannelOffer.lspBalanceSat * 1000}
/>
</span>
<FormattedFiatAmount
amount={lspChannelOffer.lspBalanceSat}

View file

@ -2,6 +2,7 @@ import { Invoice } from "@getalby/lightning-tools";
import React, { useEffect } from "react";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import {
@ -109,7 +110,9 @@ export function Bitrefill() {
<div className="font-medium">Amount</div>
<div className="flex flex-row gap-2 items-center">
<span className="font-medium slashed-zero">
{new Intl.NumberFormat().format(invoice?.satoshi || 0)} sats
<FormattedBitcoinAmount
amount={(invoice?.satoshi || 0) * 1000}
/>
</span>
<FormattedFiatAmount
className="text-muted-foreground"

View file

@ -9,6 +9,7 @@ import { useEffect, useState } from "react";
import { Link } from "react-router-dom";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import LottieLoading from "src/components/LottieLoading";
@ -155,7 +156,7 @@ function DepositPending({
{amount && (
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-semibold slashed-zero">
{new Intl.NumberFormat().format(amount)} sats
<FormattedBitcoinAmount amount={amount * 1000} />
</p>
<FormattedFiatAmount amount={amount} />
</div>
@ -189,7 +190,7 @@ function DepositSuccess({ amount, txId }: { amount: number; txId: string }) {
<CircleCheckIcon className="w-72 h-72 p-2" />
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-semibold slashed-zero">
{new Intl.NumberFormat().format(amount)} sats
<FormattedBitcoinAmount amount={amount * 1000} />
</p>
<FormattedFiatAmount amount={amount} />
</div>

View file

@ -54,23 +54,43 @@ function Settings() {
fetchCurrencies();
}, []);
async function updateCurrency(currency: string) {
async function updateSettings(
payload: Record<string, string>,
successMessage: string,
errorMessage: string
) {
try {
await request("/api/settings", {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ currency }),
body: JSON.stringify(payload),
});
await reloadInfo();
toast(`Currency set to ${currency}`);
toast(successMessage);
} catch (error) {
console.error(error);
handleRequestError("Failed to update currencies", error);
handleRequestError(errorMessage, error);
}
}
async function updateCurrency(currency: string) {
await updateSettings(
{ currency },
`Currency set to ${currency}`,
"Failed to update currencies"
);
}
async function updateBitcoinDisplayFormat(bitcoinDisplayFormat: string) {
await updateSettings(
{ bitcoinDisplayFormat },
"Bitcoin display format updated",
"Failed to update bitcoin display format"
);
}
if (!info) {
return <Loading />;
}
@ -84,81 +104,113 @@ function Settings() {
title="General"
description="General Alby Hub settings."
/>
<form className="w-full flex flex-col gap-4">
<div className="grid gap-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={theme}
onValueChange={(value) => {
setTheme(value as Theme);
toast("Theme updated.");
}}
>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
{Themes.map((theme) => {
const isPaidTheme = paidThemes.includes(theme);
const isDisabled = isPaidTheme && !hasPlan;
<form className="w-full flex flex-col gap-8">
{/* Theme & Appearance Section */}
<div className="space-y-4">
<h3 className="text-xl font-medium">Appearance</h3>
<div className="space-y-4">
<div className="grid gap-2">
<Label htmlFor="theme">Theme</Label>
<Select
value={theme}
onValueChange={(value) => {
setTheme(value as Theme);
toast("Theme updated.");
}}
>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Theme" />
</SelectTrigger>
<SelectContent>
{Themes.map((theme) => {
const isPaidTheme = paidThemes.includes(theme);
const isDisabled = isPaidTheme && !hasPlan;
return (
<SelectItem key={theme} value={theme} disabled={isDisabled}>
<div className="flex items-center justify-between gap-2 w-full">
<span
className={cn(
"capitalize",
isDisabled && "text-muted-foreground"
)}
return (
<SelectItem
key={theme}
value={theme}
disabled={isDisabled}
>
{theme}
</span>
{isPaidTheme && (
<Badge variant="outline">
<StarsIcon />
Pro
</Badge>
)}
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
<div className="flex items-center justify-between gap-2 w-full">
<span
className={cn(
"capitalize",
isDisabled && "text-muted-foreground"
)}
>
{theme}
</span>
{isPaidTheme && (
<Badge variant="outline">
<StarsIcon />
Pro
</Badge>
)}
</div>
</SelectItem>
);
})}
</SelectContent>
</Select>
</div>
<div className="grid gap-2">
<Label htmlFor="appearance">Appearance</Label>
<Select
value={darkMode}
onValueChange={(value) => {
setDarkMode(value as DarkMode);
toast("Appearance updated.");
}}
>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Appearance" />
</SelectTrigger>
<SelectContent>
<SelectItem value="system">System</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
<div className="grid gap-2">
<Label htmlFor="theme">Appearance</Label>
<Select
value={darkMode}
onValueChange={(value) => {
setDarkMode(value as DarkMode);
toast("Appearance updated.");
}}
>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Appearance" />
</SelectTrigger>
<SelectContent>
<SelectItem value="system">System</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-1.5">
<Label htmlFor="currency">Fiat Currency</Label>
<Select value={info?.currency} onValueChange={updateCurrency}>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Select a currency" />
</SelectTrigger>
<SelectContent>
{fiatCurrencies.map(([code, name]) => (
<SelectItem key={code} value={code}>
{name} ({code})
</SelectItem>
))}
</SelectContent>
</Select>
{/* Units & Currency Section */}
<div className="space-y-4">
<h3 className="text-xl font-medium">Units & Currency</h3>
<div className="space-y-4">
<div className="grid gap-1.5">
<Label htmlFor="bitcoinDisplayFormat">Display Unit</Label>
<Select
value={info?.bitcoinDisplayFormat || "bip177"}
onValueChange={updateBitcoinDisplayFormat}
>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Select a display format" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bip177"></SelectItem>
<SelectItem value="sats">sats</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid gap-1.5">
<Label htmlFor="currency">Fiat Currency</Label>
<Select value={info?.currency} onValueChange={updateCurrency}>
<SelectTrigger className="w-full md:w-60">
<SelectValue placeholder="Select a currency" />
</SelectTrigger>
<SelectContent>
{fiatCurrencies.map(([code, name]) => (
<SelectItem key={code} value={code}>
{name} ({code})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
</div>
</form>
</>

View file

@ -14,6 +14,7 @@ import React from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import { AppleIcon } from "src/components/icons/Apple";
import { PlayStoreIcon } from "src/components/icons/PlayStore";
import { ZapStoreIcon } from "src/components/icons/ZapStore";
@ -179,11 +180,7 @@ export function SubwalletCreated() {
<CardHeader>
<CardTitle>{name}</CardTitle>
<CardDescription>
Balance:{" "}
{new Intl.NumberFormat().format(
Math.floor(app.balance / 1000)
)}{" "}
sats
Balance: <FormattedBitcoinAmount amount={app.balance} />
</CardDescription>
</CardHeader>
<CardFooter className="flex flex-row justify-end">

View file

@ -13,6 +13,7 @@ import AppHeader from "src/components/AppHeader";
import AppCard from "src/components/connections/AppCard";
import { CustomPagination } from "src/components/CustomPagination";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import ResponsiveButton from "src/components/ResponsiveButton";
@ -152,10 +153,7 @@ export function SubwalletList() {
<CardContent className="grow">
<div className="mb-1">
<span className="text-2xl font-medium balance sensitive">
{new Intl.NumberFormat().format(
Math.floor(subwalletTotalAmount / 1000)
)}{" "}
sats
<FormattedBitcoinAmount amount={subwalletTotalAmount} />
</span>
</div>
<FormattedFiatAmount amount={subwalletTotalAmount / 1000} />

View file

@ -10,6 +10,7 @@ import { toast } from "sonner";
import { AnchorReserveAlert } from "src/components/AnchorReserveAlert";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import { MempoolAlert } from "src/components/MempoolAlert";
import { Alert, AlertDescription, AlertTitle } from "src/components/ui/alert";
@ -186,8 +187,9 @@ export default function WithdrawOnchainFunds() {
<div className="flex justify-between items-center">
<p className="text-sm text-muted-foreground sensitive slashed-zero">
Current onchain balance:{" "}
{new Intl.NumberFormat().format(balances.onchain.spendable)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.onchain.spendable * 1000}
/>
</p>
<div className="flex items-center gap-1">
<Checkbox
@ -344,7 +346,9 @@ export default function WithdrawOnchainFunds() {
{sendAll ? (
"entire on-chain balance"
) : (
<>{new Intl.NumberFormat().format(+amount)} sats</>
<>
<FormattedBitcoinAmount amount={+amount * 1000} />
</>
)}
</span>
</p>

View file

@ -11,6 +11,7 @@ import {
import { Link } from "react-router-dom";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert";
@ -103,10 +104,9 @@ function Wallet() {
<div className="flex flex-col xl:flex-row justify-between xl:items-start gap-3">
<div className="flex flex-col gap-1 p-6 xl:p-0 text-center xl:text-left">
<div className="text-5xl font-medium balance sensitive slashed-zero">
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalSpendable}
/>
</div>
<FormattedFiatAmount
className="text-xl"

View file

@ -10,6 +10,7 @@ import React from "react";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert";
@ -122,13 +123,10 @@ export default function ReceiveInvoice() {
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col items-center gap-6">
<QRCode value={transaction.invoice} className="w-full" />
<QRCode value={transaction.invoice} />
<div className="flex flex-col gap-1 items-center">
<p className="text-2xl font-medium slashed-zero">
{new Intl.NumberFormat().format(
Math.floor(transaction.amount / 1000)
)}{" "}
sats
<FormattedBitcoinAmount amount={transaction.amount} />
</p>
<FormattedFiatAmount
amount={Math.floor(transaction.amount / 1000)}
@ -158,10 +156,7 @@ export default function ReceiveInvoice() {
<img src={TickSVG} className="w-48" />
<div className="flex flex-col gap-1 items-center">
<p className="text-2xl font-medium slashed-zero">
{new Intl.NumberFormat().format(
Math.floor(transaction.amount / 1000)
)}{" "}
sats
<FormattedBitcoinAmount amount={transaction.amount} />
</p>
<FormattedFiatAmount
amount={Math.floor(transaction.amount / 1000)}

View file

@ -10,6 +10,7 @@ import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import LottieLoading from "src/components/LottieLoading";
@ -200,7 +201,7 @@ function DepositPending({
{amount && (
<div className="flex flex-col gap-1 items-center">
<p className="text-2xl font-medium slashed-zero">
{new Intl.NumberFormat().format(amount)} sats
<FormattedBitcoinAmount amount={amount * 1000} />
</p>
<FormattedFiatAmount amount={amount} className="text-xl" />
</div>
@ -232,7 +233,7 @@ function DepositSuccess({ amount, txId }: { amount: number; txId: string }) {
<img src={TickSVG} className="w-48" />
<div className="flex flex-col gap-1 items-center">
<p className="text-2xl font-medium slashed-zero">
{new Intl.NumberFormat().format(amount)} sats
<FormattedBitcoinAmount amount={amount * 1000} />
</p>
<FormattedFiatAmount amount={amount} className="text-xl" />
</div>
@ -342,10 +343,9 @@ function ReceiveToSpending() {
<div className="flex justify-between text-muted-foreground text-xs sensitive slashed-zero">
<div>
Receiving Capacity:{" "}
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalReceivable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalReceivable}
/>
</div>
<FormattedFiatAmount
className="text-xs"

View file

@ -7,6 +7,7 @@ import type { Invoice } from "@getalby/lightning-tools/bolt11";
import { ArrowLeftIcon } from "lucide-react";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import { PaymentFailedAlert } from "src/components/PaymentFailedAlert";
@ -102,7 +103,7 @@ export default function ConfirmPayment() {
<CardContent className="flex flex-col items-center gap-6 pt-2">
<div className="flex flex-col gap-1 items-center">
<p className="text-2xl font-medium slashed-zero">
{new Intl.NumberFormat().format(invoice.satoshi)} sats
<FormattedBitcoinAmount amount={invoice.satoshi * 1000} />
</p>
<FormattedFiatAmount
amount={invoice.satoshi}
@ -128,10 +129,9 @@ export default function ConfirmPayment() {
</LoadingButton>
<div className="flex items-center justify-between gap-2 text-muted-foreground text-xs sensitive slashed-zero">
Spending Balance:{" "}
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalSpendable}
/>
</div>
<LinkButton to="/wallet/send" variant="link" className="w-full">
<ArrowLeftIcon className="w-4 h-4 mr-2" />

View file

@ -5,6 +5,7 @@ import React from "react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import { PaymentFailedAlert } from "src/components/PaymentFailedAlert";
@ -146,10 +147,9 @@ export default function LnurlPay() {
<div className="flex justify-between text-xs text-muted-foreground sensitive slashed-zero">
<div>
Spending Balance:{" "}
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalSpendable}
/>
</div>
<FormattedFiatAmount
className="text-xs"

View file

@ -11,6 +11,7 @@ import { toast } from "sonner";
import { AnchorReserveAlert } from "src/components/AnchorReserveAlert";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import { MempoolAlert } from "src/components/MempoolAlert";
@ -203,7 +204,9 @@ function OnchainForm({
<div className="flex justify-between text-muted-foreground text-xs sensitive slashed-zero">
<div>
On-chain Balance:{" "}
{new Intl.NumberFormat().format(balances.onchain.spendable)} sats
<FormattedBitcoinAmount
amount={balances.onchain.spendable * 1000}
/>
</div>
<FormattedFiatAmount
className="text-xs"
@ -390,10 +393,9 @@ function SwapForm({
<div className="flex justify-between text-xs text-muted-foreground sensitive slashed-zero">
<div>
Spending Balance:{" "}
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalSpendable}
/>
</div>
<FormattedFiatAmount
className="text-xs"
@ -402,7 +404,8 @@ function SwapForm({
</div>
<div className="flex justify-between text-muted-foreground text-xs sensitive slashed-zero">
<div>
Minimum: {new Intl.NumberFormat().format(swapInfo.minAmount)} sats
Minimum:{" "}
<FormattedBitcoinAmount amount={swapInfo.minAmount * 1000} />
</div>
<FormattedFiatAmount
className="text-xs"

View file

@ -1,5 +1,6 @@
import { ArrowLeftIcon, ExternalLinkIcon, HandCoinsIcon } from "lucide-react";
import { useLocation } from "react-router-dom";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import {
Card,
@ -55,7 +56,7 @@ export default function OnchainSuccess() {
)}
<div className="flex flex-col gap-1 items-center">
<p className="text-2xl font-medium slashed-zero">
{new Intl.NumberFormat().format(amount)} sats
<FormattedBitcoinAmount amount={amount * 1000} />
</p>
<FormattedFiatAmount amount={amount} className="text-xl" />
</div>

View file

@ -8,6 +8,7 @@ import {
import { useEffect } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import { Button } from "src/components/ui/button";
import {
@ -58,7 +59,9 @@ export default function PaymentSuccess() {
<img src={TickSVG} className="w-48" />
<div className="flex flex-col gap-1 items-center">
<p className="text-2xl font-medium slashed-zero">
{new Intl.NumberFormat().format(invoice.satoshi || amount)} sats
<FormattedBitcoinAmount
amount={(invoice.satoshi || amount) * 1000}
/>
</p>
<FormattedFiatAmount
amount={invoice.satoshi || amount}

View file

@ -6,6 +6,7 @@ import { XIcon } from "lucide-react";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import { PaymentFailedAlert } from "src/components/PaymentFailedAlert";
@ -135,10 +136,9 @@ export default function ZeroAmount() {
<div className="flex justify-between text-xs text-muted-foreground sensitive slashed-zero">
<div>
Spending Balance:{" "}
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalSpendable}
/>
</div>
<FormattedFiatAmount
className="text-xs"

View file

@ -9,6 +9,7 @@ import { useState } from "react";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import ResponsiveLinkButton from "src/components/ResponsiveLinkButton";
import { Button } from "src/components/ui/button";
@ -152,7 +153,7 @@ function AutoSwapOutForm() {
required
/>
<p className="text-xs text-muted-foreground">
Minimum {new Intl.NumberFormat().format(swapInfo.minAmount)} sats
Minimum <FormattedBitcoinAmount amount={swapInfo.minAmount * 1000} />
</p>
</div>
<div className="flex flex-col gap-4">
@ -360,13 +361,15 @@ function ActiveSwapOutConfig({ swapConfig }: { swapConfig: AutoSwapConfig }) {
Spending Balance Threshold
</span>
<span className="shrink-0 text-muted-foreground text-right">
{new Intl.NumberFormat().format(swapConfig.balanceThreshold)} sats
<FormattedBitcoinAmount
amount={swapConfig.balanceThreshold * 1000}
/>
</span>
</div>
<div className="flex justify-between items-center gap-2">
<span className="font-medium truncate">Swap amount</span>
<span className="shrink-0 text-muted-foreground text-right">
{new Intl.NumberFormat().format(swapConfig.swapAmount)} sats
<FormattedBitcoinAmount amount={swapConfig.swapAmount * 1000} />
</span>
</div>
<div className="flex justify-between items-center gap-2">

View file

@ -11,6 +11,7 @@ import { useParams, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import LottieLoading from "src/components/LottieLoading";
@ -175,10 +176,9 @@ export default function SwapInStatus() {
<CircleCheckIcon className="w-60 h-60" />
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-bold slashed-zero text-center">
{new Intl.NumberFormat().format(
swap.receiveAmount as number
)}{" "}
sats
<FormattedBitcoinAmount
amount={(swap.receiveAmount as number) * 1000}
/>
</p>
<FormattedFiatAmount amount={swap.receiveAmount as number} />
</div>
@ -203,7 +203,7 @@ export default function SwapInStatus() {
<div className="flex flex-col gap-2 items-center">
<div className="flex items-center gap-2">
<p className="text-xl font-bold slashed-zero text-center">
{new Intl.NumberFormat().format(swap.sendAmount)} sats
<FormattedBitcoinAmount amount={swap.sendAmount * 1000} />
</p>
{!swap.lockupTxId && !isInternalSwap && (
<CopyIcon

View file

@ -7,6 +7,7 @@ import {
import { useParams } from "react-router-dom";
import AppHeader from "src/components/AppHeader";
import ExternalLink from "src/components/ExternalLink";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import FormattedFiatAmount from "src/components/FormattedFiatAmount";
import Loading from "src/components/Loading";
import LottieLoading from "src/components/LottieLoading";
@ -74,10 +75,9 @@ export default function SwapOutStatus() {
<CircleCheckIcon className="w-60 h-60" />
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-bold slashed-zero text-center">
{new Intl.NumberFormat().format(
swap.receiveAmount as number
)}{" "}
sats
<FormattedBitcoinAmount
amount={(swap.receiveAmount as number) * 1000}
/>
</p>
<FormattedFiatAmount amount={swap.receiveAmount as number} />
</div>
@ -97,7 +97,7 @@ export default function SwapOutStatus() {
)}
<div className="flex flex-col gap-2 items-center">
<p className="text-xl font-bold slashed-zero text-center">
{new Intl.NumberFormat().format(swap.sendAmount)} sats
<FormattedBitcoinAmount amount={swap.sendAmount * 1000} />
</p>
<div className="flex items-center">
<span className="text-sm text-muted-foreground">~</span>

View file

@ -8,6 +8,7 @@ import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { toast } from "sonner";
import AppHeader from "src/components/AppHeader";
import { FormattedBitcoinAmount } from "src/components/FormattedBitcoinAmount";
import Loading from "src/components/Loading";
import LowReceivingCapacityAlert from "src/components/LowReceivingCapacityAlert";
import ResponsiveLinkButton from "src/components/ResponsiveLinkButton";
@ -172,18 +173,16 @@ function SwapInForm() {
<div>
<p className="text-xs text-muted-foreground">
Receiving Capacity:{" "}
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalReceivable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalReceivable}
/>
</p>
{isInternalSwap && (
<p className="text-xs text-muted-foreground flex items-center justify-center gap-1">
Spendable On-Chain Balance:{" "}
{new Intl.NumberFormat().format(
spendableOnchainBalanceWithAnchorReserves
)}{" "}
sats
<FormattedBitcoinAmount
amount={spendableOnchainBalanceWithAnchorReserves * 1000}
/>
{!!channels?.length && (
<TooltipProvider>
<Tooltip>
@ -195,14 +194,13 @@ function SwapInForm() {
<TooltipContent>
To ensure you can close channels, you need to set
aside at least{" "}
{new Intl.NumberFormat().format(
channels.length * 25000
)}{" "}
sats on-chain. Your total on-chain balance is{" "}
{new Intl.NumberFormat().format(
balances.onchain.spendable
)}{" "}
sats
<FormattedBitcoinAmount
amount={channels.length * 25000 * 1000}
/>{" "}
on-chain. Your total on-chain balance is{" "}
<FormattedBitcoinAmount
amount={balances.onchain.spendable * 1000}
/>
</TooltipContent>
</Tooltip>
</TooltipProvider>
@ -343,14 +341,14 @@ function SwapOutForm() {
{balances && (
<p className="text-xs text-muted-foreground">
Balance:{" "}
{new Intl.NumberFormat().format(
Math.floor(balances.lightning.totalSpendable / 1000)
)}{" "}
sats
<FormattedBitcoinAmount
amount={balances.lightning.totalSpendable}
/>
</p>
)}
<p className="text-xs text-muted-foreground">
Minimum: {new Intl.NumberFormat().format(swapInfo.minAmount)} sats
Minimum:{" "}
<FormattedBitcoinAmount amount={swapInfo.minAmount * 1000} />
</p>
</div>
</div>

View file

@ -163,8 +163,11 @@ export interface InfoResponse {
currency: string;
nodeAlias: string;
mempoolUrl: string;
bitcoinDisplayFormat?: BitcoinDisplayFormat;
}
export type BitcoinDisplayFormat = "sats" | "bip177";
export type HealthAlarmKind =
| "alby_service"
| "node_not_ready"

View file

@ -0,0 +1,26 @@
import { BitcoinDisplayFormat } from "src/types";
/**
* Utility function to format Bitcoin amounts as a string
* @param amount - Amount in millisatoshis
* @param displayFormat - Display format
* @param showSymbol - Whether to show the symbol/unit
*/
export function formatBitcoinAmount(
amount: number,
displayFormat: BitcoinDisplayFormat = "bip177",
showSymbol: boolean = true
): string {
const sats = Math.floor(amount / 1000);
const formattedNumber = new Intl.NumberFormat().format(sats);
if (!showSymbol) {
return formattedNumber;
}
if (displayFormat === "bip177") {
return `${formattedNumber}`;
} else {
return `${formattedNumber} sats`;
}
}

View file

@ -386,16 +386,10 @@ func (httpSvc *HttpService) updateSettingsHandler(c echo.Context) error {
})
}
if updateSettingsRequest.Currency == "" {
return c.JSON(http.StatusBadRequest, ErrorResponse{
Message: "Currency value cannot be empty",
})
}
err := httpSvc.api.SetCurrency(updateSettingsRequest.Currency)
err := httpSvc.api.UpdateSettings(&updateSettingsRequest)
if err != nil {
return c.JSON(http.StatusInternalServerError, ErrorResponse{
Message: fmt.Sprintf("Failed to set currency: %s", err.Error()),
Message: fmt.Sprintf("Failed to update settings: %s", err.Error()),
})
}

View file

@ -722,3 +722,92 @@ func (_c *MockConfig_SetupCompleted_Call) RunAndReturn(run func() bool) *MockCon
_c.Call.Return(run)
return _c
}
// GetBitcoinDisplayFormat provides a mock function for the type MockConfig
func (_mock *MockConfig) GetBitcoinDisplayFormat() string {
ret := _mock.Called()
if len(ret) == 0 {
panic("no return value specified for GetBitcoinDisplayFormat")
}
var r0 string
if returnFunc, ok := ret.Get(0).(func() string); ok {
r0 = returnFunc()
} else {
r0 = ret.Get(0).(string)
}
return r0
}
// MockConfig_GetBitcoinDisplayFormat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetBitcoinDisplayFormat'
type MockConfig_GetBitcoinDisplayFormat_Call struct {
*mock.Call
}
// GetBitcoinDisplayFormat is a helper method to define mock.On call
func (_e *MockConfig_Expecter) GetBitcoinDisplayFormat() *MockConfig_GetBitcoinDisplayFormat_Call {
return &MockConfig_GetBitcoinDisplayFormat_Call{Call: _e.mock.On("GetBitcoinDisplayFormat")}
}
func (_c *MockConfig_GetBitcoinDisplayFormat_Call) Run(run func()) *MockConfig_GetBitcoinDisplayFormat_Call {
_c.Call.Run(func(args mock.Arguments) {
run()
})
return _c
}
func (_c *MockConfig_GetBitcoinDisplayFormat_Call) Return(s string) *MockConfig_GetBitcoinDisplayFormat_Call {
_c.Call.Return(s)
return _c
}
func (_c *MockConfig_GetBitcoinDisplayFormat_Call) RunAndReturn(run func() string) *MockConfig_GetBitcoinDisplayFormat_Call {
_c.Call.Return(run)
return _c
}
// SetBitcoinDisplayFormat provides a mock function for the type MockConfig
func (_mock *MockConfig) SetBitcoinDisplayFormat(value string) error {
ret := _mock.Called(value)
if len(ret) == 0 {
panic("no return value specified for SetBitcoinDisplayFormat")
}
var r0 error
if returnFunc, ok := ret.Get(0).(func(string) error); ok {
r0 = returnFunc(value)
} else {
r0 = ret.Error(0)
}
return r0
}
// MockConfig_SetBitcoinDisplayFormat_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SetBitcoinDisplayFormat'
type MockConfig_SetBitcoinDisplayFormat_Call struct {
*mock.Call
}
// SetBitcoinDisplayFormat is a helper method to define mock.On call
// - value
func (_e *MockConfig_Expecter) SetBitcoinDisplayFormat(value interface{}) *MockConfig_SetBitcoinDisplayFormat_Call {
return &MockConfig_SetBitcoinDisplayFormat_Call{Call: _e.mock.On("SetBitcoinDisplayFormat", value)}
}
func (_c *MockConfig_SetBitcoinDisplayFormat_Call) Run(run func(value string)) *MockConfig_SetBitcoinDisplayFormat_Call {
_c.Call.Run(func(args mock.Arguments) {
run(args[0].(string))
})
return _c
}
func (_c *MockConfig_SetBitcoinDisplayFormat_Call) Return(err error) *MockConfig_SetBitcoinDisplayFormat_Call {
_c.Call.Return(err)
return _c
}
func (_c *MockConfig_SetBitcoinDisplayFormat_Call) RunAndReturn(run func(value string) error) *MockConfig_SetBitcoinDisplayFormat_Call {
_c.Call.Return(run)
return _c
}

View file

@ -773,13 +773,13 @@ func (app *WailsApp) WailsRequestRouter(route string, method string, body string
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
err = app.api.SetCurrency(updateSettingsRequest.Currency)
err = app.api.UpdateSettings(updateSettingsRequest)
if err != nil {
logger.Logger.WithFields(logrus.Fields{
"route": route,
"method": method,
"body": body,
}).WithError(err).Error("Failed to set Currency")
}).WithError(err).Error("Failed to update settings")
return WailsRequestRouterResponse{Body: nil, Error: err.Error()}
}
return WailsRequestRouterResponse{Body: nil, Error: ""}