Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,7 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
const decodedInvoice = pendingData?.decodedInvoice
const isLightningPayment = pendingData?.network === 'lightning'
const isRgbLightningPayment =
isLightningPayment &&
decodedInvoice?.asset_amount &&
decodedInvoice?.asset_id
isLightningPayment && Boolean(decodedInvoice?.asset_id)
const hasRegularBtcAmount =
isLightningPayment && decodedInvoice?.amt_msat && !decodedInvoice?.asset_id

Expand Down Expand Up @@ -306,24 +304,28 @@ const ConfirmationModal: React.FC<ConfirmationModalProps> = ({
)}

{/* Asset Amount (if RGB Lightning) */}
{isLightningPayment &&
decodedInvoice?.asset_amount &&
decodedInvoice?.asset_id && (
<div className="flex justify-between py-2">
<span className="text-content-secondary text-sm">
{t('withdrawModal.confirmation.labels.assetAmount')}
</span>
<span className="text-white text-sm font-medium">
{getAssetAmount(
decodedInvoice.asset_amount,
decodedInvoice.asset_id
)}{' '}
{availableAssets.find(
(a: AssetOption) => a.value === decodedInvoice?.asset_id
)?.label || t('withdrawModal.main.labels.unknownAsset')}
</span>
</div>
)}
{isRgbLightningPayment && decodedInvoice?.asset_id && (
<div className="flex justify-between py-2">
<span className="text-content-secondary text-sm">
{t('withdrawModal.confirmation.labels.assetAmount')}
</span>
<span className="text-white text-sm font-medium">
{decodedInvoice.asset_amount
? `${getAssetAmount(decodedInvoice.asset_amount, decodedInvoice.asset_id)} ${
availableAssets.find(
(a: AssetOption) =>
a.value === decodedInvoice?.asset_id
)?.label || t('withdrawModal.main.labels.unknownAsset')
}`
: `${formatNumberWithCommas(String(pendingData?.amount ?? '0'))} ${
availableAssets.find(
(a: AssetOption) =>
a.value === decodedInvoice?.asset_id
)?.label || t('withdrawModal.main.labels.unknownAsset')
}`}
</span>
</div>
)}

{/* Fee (for BTC only) */}
{pendingData?.asset_id === BTC_ASSET_ID &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ const LightningInvoiceDetails: React.FC<LightningInvoiceDetailsProps> = ({

if (!decodedInvoice) return null

const hasAsset = decodedInvoice.asset_id && decodedInvoice.asset_amount
const hasAsset = Boolean(decodedInvoice.asset_id)
const hasAssetAmount = Boolean(decodedInvoice.asset_amount)

const assetInfo = hasAsset
? assets.data?.nia.find(
Expand All @@ -27,7 +28,7 @@ const LightningInvoiceDetails: React.FC<LightningInvoiceDetailsProps> = ({
assetInfo?.ticker || t('withdrawModal.main.labels.unknownAsset')

const formattedAssetAmount =
hasAsset && decodedInvoice.asset_amount
hasAssetAmount && decodedInvoice.asset_amount
? formatAssetAmountWithPrecision(
decodedInvoice.asset_amount,
ticker,
Expand Down Expand Up @@ -70,8 +71,8 @@ const LightningInvoiceDetails: React.FC<LightningInvoiceDetailsProps> = ({
(decodedInvoice.amt_msat ?? 0) > maxLightningCapacity

const isAssetBalanceExceeded =
hasAsset && assetInfo
? decodedInvoice.asset_amount! >
hasAssetAmount && assetInfo
? (decodedInvoice.asset_amount ?? 0) >
(assetInfo.balance.offchain_outbound || 0)
: false

Expand Down Expand Up @@ -100,7 +101,13 @@ const LightningInvoiceDetails: React.FC<LightningInvoiceDetailsProps> = ({
{t('withdrawModal.details.lightning.assetAmountLabel')}
</span>
<span className="text-white font-bold">
{formattedAssetAmount} {ticker}
{hasAssetAmount ? (
<>
{formattedAssetAmount} {ticker}
</>
) : (
t('withdrawModal.details.lightning.assetAmountNotSpecified')
)}
</span>
</div>
{assetInfo && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -576,7 +576,9 @@ const WithdrawForm: React.FC<WithdrawFormProps> = ({
addressType === 'lightning-address' ||
(addressType === 'lightning' &&
decodedInvoice &&
(!decodedInvoice.amt_msat || decodedInvoice.amt_msat === 0))) && (
(!decodedInvoice.amt_msat ||
decodedInvoice.amt_msat === 0 ||
(decodedInvoice.asset_id && !decodedInvoice.asset_amount)))) && (
<>
{/* Asset Selector - Only for rgb invoices without asset_id specified or lightning-address */}
{((addressType === 'rgb' && !decodedRgbInvoice?.asset_id) ||
Expand Down Expand Up @@ -724,7 +726,10 @@ const WithdrawForm: React.FC<WithdrawFormProps> = ({
addressType === 'rgb' ||
(addressType === 'lightning' &&
decodedInvoice &&
(!decodedInvoice.amt_msat || decodedInvoice.amt_msat === 0))) && (
(!decodedInvoice.amt_msat ||
decodedInvoice.amt_msat === 0 ||
(decodedInvoice.asset_id &&
!decodedInvoice.asset_amount)))) && (
<div className="space-y-1">
<Controller
control={control}
Expand Down
85 changes: 76 additions & 9 deletions src/components/Layout/Modal/Withdraw/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -448,16 +448,20 @@ export const WithdrawModalContent: React.FC = () => {
setValue('network', 'lightning')

// Validate invoice amount against balance and capacity
if (decoded.asset_id && decoded.asset_amount) {
// Invoice specifies an RGB asset
if (decoded.asset_id) {
// Invoice specifies an RGB asset (asset_amount may be null for open-amount invoices)
setValue('asset_id', decoded.asset_id) // Set asset_id for potential display/validation
// Reset amount so the user enters it for open-amount asset invoices
if (!decoded.asset_amount) {
setValue('amount', '')
}
await fetchAssetBalance(decoded.asset_id, 'offchain_outbound')

// Get max local asset amount for this asset from channels
const maxAssetAmount = maxAssetCapacities[decoded.asset_id] || 0

// Check asset amount against channel capacity
if (decoded.asset_amount > maxAssetAmount) {
// Check asset amount against channel capacity (only when invoice fixes the amount)
if (decoded.asset_amount && decoded.asset_amount > maxAssetAmount) {
setValidationMessage({
message: t('withdrawModal.main.errors.invoiceAssetCapacity', {
asset: decoded.asset_id.substring(0, 8),
Expand All @@ -483,6 +487,22 @@ export const WithdrawModalContent: React.FC = () => {
})
}
}

// Inform the user when they need to enter the asset amount themselves
if (!decoded.asset_amount) {
const assetInfo = (assets.data?.nia || []).find(
(a: any) => a.asset_id === decoded.asset_id
)
const ticker =
assetInfo?.ticker ||
t('withdrawModal.main.labels.assetFallback')
setValidationMessage({
message: t('withdrawModal.main.info.assetAmountRequired', {
ticker,
}),
type: 'info',
})
}
}
// If this is a regular BTC invoice with an amount
else if ((decoded.amt_msat || 0) > 0) {
Expand Down Expand Up @@ -724,8 +744,16 @@ export const WithdrawModalContent: React.FC = () => {

// For lightning payments, need to use a limit based on invoice or capacity
if (addressType === 'lightning') {
// RGB asset payment over Lightning: min is one base unit of the asset
if (decodedInvoice?.asset_id && assetId !== BTC_ASSET_ID) {
const assetInfo = (assets.data?.nia || []).find(
(a: any) => a.asset_id === assetId
)
const precision = assetInfo?.precision ?? 0
return precision > 0 ? 1 / Math.pow(10, precision) : 1
}
if (decodedInvoice?.amt_msat) {
// If invoice has an amount, use that as the min
// BTC Lightning invoice with amount: use that as the min
return msatToSat(decodedInvoice.amt_msat)
}
// Return a reasonable min for lightning (1 sat)
Expand All @@ -734,7 +762,13 @@ export const WithdrawModalContent: React.FC = () => {

// For other assets or address types, use 1 as minimum
return 1
}, [addressType, assetId, decodedInvoice, maxLightningCapacity]) // Add missing dependency
}, [
addressType,
assetId,
decodedInvoice,
maxLightningCapacity,
assets.data?.nia,
]) // Add missing dependency

const getMinAmountMessage = useCallback(() => {
if (assetId === BTC_ASSET_ID) {
Expand Down Expand Up @@ -858,9 +892,24 @@ export const WithdrawModalContent: React.FC = () => {
formattedData.decodedInvoice = decodedInvoice

if (decodedInvoice.asset_id && decodedInvoice.asset_amount) {
// RGB Lightning Invoice with asset
// RGB Lightning Invoice with fixed asset amount
formattedData.asset_id = decodedInvoice.asset_id
formattedData.amount = decodedInvoice.asset_amount
} else if (decodedInvoice.asset_id && !decodedInvoice.asset_amount) {
// RGB Lightning Invoice without a fixed asset amount —
// user provides the asset amount via the form (display units)
formattedData.asset_id = decodedInvoice.asset_id
const cleanAmount = String(formattedData.amount ?? '').replace(
/,/g,
''
)
const amountValue = Number(cleanAmount)
if (!cleanAmount || isNaN(amountValue) || amountValue <= 0) {
toast.error(t('withdrawModal.main.errors.zeroAmountRequired'), {
autoClose: 5000,
})
return
}
} else if ((decodedInvoice?.amt_msat || 0) > 0) {
// Standard BTC Lightning invoice - convert using helpers
const amountSats = msatToSat(decodedInvoice.amt_msat || 0)
Expand Down Expand Up @@ -952,13 +1001,31 @@ export const WithdrawModalContent: React.FC = () => {
invoice: pendingData.address,
}

// If zero-amount invoice, add the amount from user input
// RGB Lightning invoice without a fixed asset amount: pass
// asset_id + asset_amount (in raw base units) to the node.
if (
pendingData.decodedInvoice?.asset_id &&
!pendingData.decodedInvoice.asset_amount
) {
const assetInfo = (assets.data?.nia || []).find(
(a: any) => a.asset_id === pendingData.decodedInvoice!.asset_id
)
const ticker =
assetInfo?.ticker || t('withdrawModal.main.labels.unknownAsset')
const rawAssetAmount = parseAssetAmountWithPrecision(
String(pendingData.amount ?? ''),
ticker,
bitcoinUnit,
assets.data?.nia
)
paymentParams.asset_id = pendingData.decodedInvoice.asset_id
paymentParams.asset_amount = Math.floor(rawAssetAmount)
} else if (
pendingData.decodedInvoice &&
(!pendingData.decodedInvoice.amt_msat ||
pendingData.decodedInvoice.amt_msat === 0)
) {
// Convert user-entered amount to msat
// Zero-amount BTC invoice: convert user-entered amount to msat
const userAmount = Number(pendingData.amount)
if (bitcoinUnit === 'SAT') {
paymentParams.amt_msat = userAmount * 1000
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -1846,14 +1846,16 @@
"noRgbAssets": "Warnung: Keine RGB-Assets zum Senden verfügbar."
},
"info": {
"zeroAmountInvoice": "Dies ist eine Nullbetrags-Rechnung. Bitte geben Sie unten den Betrag ein, den Sie zahlen möchten."
"zeroAmountInvoice": "Dies ist eine Nullbetrags-Rechnung. Bitte geben Sie unten den Betrag ein, den Sie zahlen möchten.",
"assetAmountRequired": "Diese Rechnung gibt den {{ticker}}-Betrag nicht an. Bitte geben Sie ein, wie viel Sie senden möchten."
}
},
"details": {
"lightning": {
"title": "Lightning-Rechnungsdetails",
"assetLabel": "Asset:",
"assetAmountLabel": "Asset-Betrag:",
"assetAmountNotSpecified": "Nicht in der Rechnung angegeben",
"lightningBalanceLabel": "Ihr Lightning-Guthaben:",
"btcAmountLabel": "BTC-Betrag:",
"amountLabel": "Betrag:",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2459,14 +2459,16 @@
"noRgbAssets": "Warning: No RGB assets available to send."
},
"info": {
"zeroAmountInvoice": "This is a zero-amount invoice. Please enter the amount you want to pay below."
"zeroAmountInvoice": "This is a zero-amount invoice. Please enter the amount you want to pay below.",
"assetAmountRequired": "This invoice does not specify the {{ticker}} amount. Please enter how much you want to send."
}
},
"details": {
"lightning": {
"title": "Lightning Invoice Details",
"assetLabel": "Asset:",
"assetAmountLabel": "Asset Amount:",
"assetAmountNotSpecified": "Not specified by invoice",
"lightningBalanceLabel": "Your Lightning Balance:",
"btcAmountLabel": "BTC Amount:",
"amountLabel": "Amount:",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -2507,14 +2507,16 @@
"noRgbAssets": "Advertencia: No hay assets RGB disponibles para enviar."
},
"info": {
"zeroAmountInvoice": "Esta es una factura de importe cero. Introduce abajo la cantidad que deseas pagar."
"zeroAmountInvoice": "Esta es una factura de importe cero. Introduce abajo la cantidad que deseas pagar.",
"assetAmountRequired": "Esta factura no especifica la cantidad de {{ticker}}. Introduce cuánto deseas enviar."
}
},
"details": {
"lightning": {
"title": "Detalles de la Factura Lightning",
"assetLabel": "Activo:",
"assetAmountLabel": "Cantidad del Asset:",
"assetAmountNotSpecified": "No especificado por la factura",
"lightningBalanceLabel": "Tu Saldo Lightning:",
"btcAmountLabel": "Cantidad BTC:",
"amountLabel": "Cantidad:",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -1852,14 +1852,16 @@
"noRgbAssets": "Avertissement : Aucun actif RGB disponible à envoyer."
},
"info": {
"zeroAmountInvoice": "Il s'agit d'une facture à montant zéro. Veuillez saisir ci-dessous le montant que vous souhaitez payer."
"zeroAmountInvoice": "Il s'agit d'une facture à montant zéro. Veuillez saisir ci-dessous le montant que vous souhaitez payer.",
"assetAmountRequired": "Cette facture ne précise pas le montant en {{ticker}}. Veuillez saisir le montant que vous souhaitez envoyer."
}
},
"details": {
"lightning": {
"title": "Détails de la facture Lightning",
"assetLabel": "Actif :",
"assetAmountLabel": "Montant de l'actif :",
"assetAmountNotSpecified": "Non précisé par la facture",
"lightningBalanceLabel": "Votre solde Lightning :",
"btcAmountLabel": "Montant BTC :",
"amountLabel": "Montant :",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -2501,14 +2501,16 @@
"noRgbAssets": "Avviso: Nessun asset RGB disponibile da inviare."
},
"info": {
"zeroAmountInvoice": "Questa è una fattura a importo zero. Inserisci qui sotto l'importo che vuoi pagare."
"zeroAmountInvoice": "Questa è una fattura a importo zero. Inserisci qui sotto l'importo che vuoi pagare.",
"assetAmountRequired": "Questa fattura non specifica l'importo in {{ticker}}. Inserisci quanto vuoi inviare."
}
},
"details": {
"lightning": {
"title": "Dettagli Fattura Lightning",
"assetLabel": "Attivo:",
"assetAmountLabel": "Importo Asset:",
"assetAmountNotSpecified": "Non specificato dalla fattura",
"lightningBalanceLabel": "Tuo Saldo Lightning:",
"btcAmountLabel": "Importo BTC:",
"amountLabel": "Importo:",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -1852,14 +1852,16 @@
"noRgbAssets": "警告: 送信可能なRGBアセットがありません。"
},
"info": {
"zeroAmountInvoice": "これはゼロ金額インボイスです。下に支払いたい金額を入力してください。"
"zeroAmountInvoice": "これはゼロ金額インボイスです。下に支払いたい金額を入力してください。",
"assetAmountRequired": "このインボイスには{{ticker}}の金額が指定されていません。送信する金額を入力してください。"
}
},
"details": {
"lightning": {
"title": "Lightningインボイス詳細",
"assetLabel": "アセット:",
"assetAmountLabel": "アセット量:",
"assetAmountNotSpecified": "インボイスに未指定",
"lightningBalanceLabel": "あなたのLightning残高:",
"btcAmountLabel": "BTC量:",
"amountLabel": "金額:",
Expand Down
4 changes: 3 additions & 1 deletion src/i18n/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -2498,14 +2498,16 @@
"noRgbAssets": "警告:没有可用于发送的 RGB 资产。"
},
"info": {
"zeroAmountInvoice": "这是一张零金额发票。请在下方输入你要支付的金额。"
"zeroAmountInvoice": "这是一张零金额发票。请在下方输入你要支付的金额。",
"assetAmountRequired": "该发票未指定 {{ticker}} 金额。请输入您要发送的数量。"
}
},
"details": {
"lightning": {
"title": "Lightning 发票详情",
"assetLabel": "资产:",
"assetAmountLabel": "资产金额:",
"assetAmountNotSpecified": "发票未指定",
"lightningBalanceLabel": "您的 Lightning 余额:",
"btcAmountLabel": "BTC 金额:",
"amountLabel": "金额:",
Expand Down
Loading