From a4981cb8cf0d8afe6f7e5d540fac0cc6b9c858d4 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Mon, 22 Apr 2024 16:00:39 -0500 Subject: [PATCH] get rid of unified receive only do one at a time, address or invoice move method chooser to receive amount screen fix flicker on send / swap pages amountless on-chain, fix receive warnings fix unnecessary "Failed to conduct wallet operation" errors close receive method chooser automatically --- e2e/fedimint.spec.ts | 6 +- e2e/roundtrip.spec.ts | 6 +- public/i18n/en.json | 14 +- src/components/AmountEditable.tsx | 1 - src/components/IntegratedQR.tsx | 10 - src/components/ReceiveWarnings.tsx | 37 ++- src/routes/Receive.tsx | 377 ++++++++++++++--------------- src/routes/Send.tsx | 26 +- src/routes/Swap.tsx | 101 ++++---- src/workers/walletWorker.ts | 20 ++ 10 files changed, 324 insertions(+), 274 deletions(-) diff --git a/e2e/fedimint.spec.ts b/e2e/fedimint.spec.ts index 6a56db6f..e51e47dd 100644 --- a/e2e/fedimint.spec.ts +++ b/e2e/fedimint.spec.ts @@ -77,10 +77,10 @@ test("fedmint join, receive, send", async ({ page }) => { const value = await qrCode.getAttribute("value"); - // The SVG's value property includes "bitcoin:t" - expect(value).toContain("bitcoin:t"); + // The SVG's value property includes "bitcoin:l" + expect(value).toContain("lightning:l"); - const lightningInvoice = value?.split("lightning=")[1]; + const lightningInvoice = value?.split("lightning:")[1]; // Post the lightning invoice to the server const _response = await fetch( diff --git a/e2e/roundtrip.spec.ts b/e2e/roundtrip.spec.ts index 7001f7eb..e922e755 100644 --- a/e2e/roundtrip.spec.ts +++ b/e2e/roundtrip.spec.ts @@ -51,10 +51,10 @@ test("rountrip receive and send", async ({ page }) => { const value = await qrCode.getAttribute("value"); - // The SVG's value property includes "bitcoin:t" - expect(value).toContain("bitcoin:t"); + // The SVG's value property includes "lightning:l" + expect(value).toContain("lightning:l"); - const lightningInvoice = value?.split("lightning=")[1]; + const lightningInvoice = value?.split("lightning:")[1]; // Post the lightning invoice to the server const _response = await fetch( diff --git a/public/i18n/en.json b/public/i18n/en.json index 101da1a7..bb0d2611 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -108,13 +108,10 @@ "receive_add_the_sender": "Add the sender for your records", "keep_mutiny_open": "Keep Mutiny open to complete the payment.", "choose_payment_format": "Choose payment format", - "unified_label": "Unified", - "unified_caption": "Combines a bitcoin address and a lightning invoice. Sender chooses payment method.", "lightning_label": "Lightning invoice", "lightning_caption": "Ideal for small transactions. Usually lower fees than on-chain.", "onchain_label": "Bitcoin address", "onchain_caption": "On-chain, just like Satoshi did it. Ideal for very large transactions.", - "unified_setup_fee": "A lightning setup fee of {{amount}} SATS will be charged if paid over lightning.", "lightning_setup_fee": "A lightning setup fee of {{amount}} SATS will be charged for this receive.", "amount": "Amount", "fee": "+ Fee", @@ -123,11 +120,11 @@ "channel_size": "Channel size", "channel_reserve": "- Channel reserve", "error_under_min_lightning": "Defaulting to On-chain. Amount is too small for your initial Lightning receive.", - "error_creating_unified": "Defaulting to On-chain. Something went wrong when creating the unified address", - "error_creating_address": "Something went wrong when creating the on-chain address", + "error_creating_unified": "Defaulting to On-chain. Something went wrong when creating the Lightning invoice.", + "error_creating_address": "Something went wrong when creating the on-chain address.", "amount_editable": { "receive_too_small": "Your first receive needs to be {{amount}} SATS or greater.", - "setup_fee_lightning": "A lightning setup fee will be charged if paid over lightning.", + "setup_fee_lightning": "A lightning setup fee will be charged.", "more_than_21m": "There are only 21 million bitcoin.", "set_amount": "Set amount", "max": "MAX", @@ -142,7 +139,6 @@ "integrated_qr": { "onchain": "On-chain", "lightning": "Lightning", - "unified": "Unified", "gift": "Lightning Gift" }, "remember_choice": "Remember my choice next time", @@ -150,7 +146,9 @@ "method_help": { "title": "Receive Method", "body": "Lightning receives will automatically go into your chosen federation. You can swap to self-custodial later if you want." - } + }, + "receive_strings_error": "Something went wrong generating an invoice or on-chain address.", + "error_under_min_onchain": "That's under the dust limit! On-chain transactions should be much bigger." }, "send": { "search": { diff --git a/src/components/AmountEditable.tsx b/src/components/AmountEditable.tsx index ce673570..9f84ac83 100644 --- a/src/components/AmountEditable.tsx +++ b/src/components/AmountEditable.tsx @@ -59,7 +59,6 @@ export const AmountEditable: ParentComponent<{ state.fiat, sw ).then((sats) => { - console.log("sats", sats); setLocalFiat(sats); }); } diff --git a/src/components/IntegratedQR.tsx b/src/components/IntegratedQR.tsx index ae8eed64..cab19668 100644 --- a/src/components/IntegratedQR.tsx +++ b/src/components/IntegratedQR.tsx @@ -39,16 +39,6 @@ function KindIndicator(props: { kind: ReceiveFlavor | "gift" | "lnAddress" }) { - - -

- {i18n.t("receive.integrated_qr.unified")} -

-
- - -
-
); diff --git a/src/components/ReceiveWarnings.tsx b/src/components/ReceiveWarnings.tsx index 15aa40a1..c7895afc 100644 --- a/src/components/ReceiveWarnings.tsx +++ b/src/components/ReceiveWarnings.tsx @@ -3,11 +3,13 @@ import { createResource, Match, Switch } from "solid-js"; import { InfoBox } from "~/components/InfoBox"; import { FeesModal } from "~/components/MoreInfoModal"; import { useI18n } from "~/i18n/context"; +import { ReceiveFlavor } from "~/routes"; import { useMegaStore } from "~/state/megaStore"; export function ReceiveWarnings(props: { amountSats: bigint; from_fedi_to_ln?: boolean; + flavor?: ReceiveFlavor; }) { const i18n = useI18n(); const [state, _actions, sw] = useMegaStore(); @@ -37,17 +39,19 @@ export function ReceiveWarnings(props: { if (state.federations?.length !== 0 && props.from_fedi_to_ln !== true) { return undefined; } - if ( - (state.balance?.lightning || 0n) === 0n && - !state.settings?.lsps_connection_string - ) { - return i18n.t("receive.amount_editable.receive_too_small", { - amount: "100,000" - }); - } + if (props.flavor === "lightning") { + if ( + (state.balance?.lightning || 0n) === 0n && + !state.settings?.lsps_connection_string + ) { + return i18n.t("receive.amount_editable.receive_too_small", { + amount: "100,000" + }); + } - if (props.amountSats > (inboundCapacity() || 0n)) { - return i18n.t("receive.amount_editable.setup_fee_lightning"); + if (props.amountSats > (inboundCapacity() || 0n)) { + return i18n.t("receive.amount_editable.setup_fee_lightning"); + } } return undefined; @@ -65,8 +69,21 @@ export function ReceiveWarnings(props: { } }; + const tooSmallWarning = () => { + if ( + props.flavor === "onchain" && + props.amountSats > 0n && + props.amountSats < 546n + ) { + return i18n.t("receive.error_under_min_onchain"); + } + }; + return ( + + {tooSmallWarning()} + {sillyAmountWarning()} diff --git a/src/routes/Receive.tsx b/src/routes/Receive.tsx index 38c35796..2116a2f4 100644 --- a/src/routes/Receive.tsx +++ b/src/routes/Receive.tsx @@ -1,9 +1,6 @@ -import { - MutinyBip21RawMaterials, - MutinyInvoice -} from "@mutinywallet/mutiny-wasm"; +import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"; import { useNavigate } from "@solidjs/router"; -import { ArrowLeftRight, CircleHelp, Users } from "lucide-solid"; +import { CircleHelp, Link, Users, Zap } from "lucide-solid"; import { createEffect, createMemo, @@ -23,7 +20,6 @@ import { BackButton, BackLink, Button, - Checkbox, DefaultMain, Fee, FeesModal, @@ -36,6 +32,7 @@ import { MutinyWalletGuard, NavBar, ReceiveWarnings, + SharpButton, showToast, SimpleDialog, SimpleInput, @@ -71,37 +68,74 @@ export type OnChainTx = { }; }; -export type ReceiveFlavor = "unified" | "lightning" | "onchain"; +export type ReceiveFlavor = "lightning" | "onchain"; type ReceiveState = "edit" | "show" | "paid"; type PaidState = "lightning_paid" | "onchain_paid"; function FeeWarning(props: { fee: bigint; flavor: ReceiveFlavor }) { const i18n = useI18n(); return ( - // TODO: probably won't always be fixed 2500? - 1000n}> - - - - {i18n.t("receive.unified_setup_fee", { - amount: props.fee.toLocaleString() - })} - - - - - - {i18n.t("receive.lightning_setup_fee", { - amount: props.fee.toLocaleString() - })} - - - - + 1000n && props.flavor === "lightning"}> + + {i18n.t("receive.lightning_setup_fee", { + amount: props.fee.toLocaleString() + })} + + ); } +function FlavorChooser(props: { + flavor: ReceiveFlavor; + setFlavor: (value: string) => void; +}) { + const [methodChooserOpen, setMethodChooserOpen] = createSignal(false); + const i18n = useI18n(); + + const RECEIVE_FLAVORS = [ + { + value: "lightning", + label: i18n.t("receive.lightning_label"), + caption: i18n.t("receive.lightning_caption") + }, + { + value: "onchain", + label: i18n.t("receive.onchain_label"), + caption: i18n.t("receive.onchain_caption") + } + ]; + return ( + <> + setMethodChooserOpen(true)}> + {props.flavor === "lightning" ? ( + + ) : ( + + )} + {props.flavor === "lightning" ? "Lightning" : "On-chain"} + + setMethodChooserOpen(open)} + > + { + props.setFlavor(flavor); + setMethodChooserOpen(false); + }} + choices={RECEIVE_FLAVORS} + accent="white" + vertical + delayOnChange + /> + + + ); +} + function ReceiveMethodHelp() { const i18n = useI18n(); const [open, setOpen] = createSignal(false); @@ -123,7 +157,7 @@ function ReceiveMethodHelp() { } export function Receive() { - const [state, actions, sw] = useMegaStore(); + const [state, _actions, sw] = useMegaStore(); const navigate = useNavigate(); const i18n = useI18n(); @@ -131,8 +165,16 @@ export function Receive() { const [whatForInput, setWhatForInput] = createSignal(""); const [receiveState, setReceiveState] = createSignal("edit"); - const [bip21Raw, setBip21Raw] = createSignal(); - const [unified, setUnified] = createSignal(""); + // We use these for displaying the QR + const [receiveStrings, setReceiveStrings] = createSignal<{ + lightning?: string; + onchain?: string; + }>(); + // We use these for checking the payment status + const [rawReceiveStrings, setRawReceiveStrings] = createSignal<{ + bolt11?: string; + address?: string; + }>(); const [lspFee, setLspFee] = createSignal(0n); @@ -140,10 +182,8 @@ export function Receive() { const [paymentTx, setPaymentTx] = createSignal(); const [paymentInvoice, setPaymentInvoice] = createSignal(); - // The flavor of the receive (defaults to unified) - const [flavor, setFlavor] = createSignal( - state.preferredInvoiceType - ); + // The flavor of the receive + const [flavor, setFlavor] = createSignal("lightning"); // loading state for the continue button const [loading, setLoading] = createSignal(false); @@ -154,47 +194,19 @@ export function Receive() { const [detailsKind, setDetailsKind] = createSignal(); const [detailsId, setDetailsId] = createSignal(""); - const RECEIVE_FLAVORS = [ - { - value: "unified", - label: i18n.t("receive.unified_label"), - caption: i18n.t("receive.unified_caption") - }, - { - value: "lightning", - label: i18n.t("receive.lightning_label"), - caption: i18n.t("receive.lightning_caption") - }, - { - value: "onchain", - label: i18n.t("receive.onchain_label"), - caption: i18n.t("receive.onchain_caption") - } - ]; - - const [rememberChoice, setRememberChoice] = createSignal(false); - - const receiveString = createMemo(() => { - if (unified() && receiveState() === "show") { - if (flavor() === "unified") { - return unified(); - } else if (flavor() === "lightning") { - return bip21Raw()?.invoice ?? ""; - } else if (flavor() === "onchain") { - return bip21Raw()?.address ?? ""; - } - } - }); - - function clearAll() { - setAmount(0n); + function clearAllButAmount() { setReceiveState("edit"); - setBip21Raw(undefined); - setUnified(""); + setReceiveStrings(undefined); setPaymentTx(undefined); setPaymentInvoice(undefined); setError(""); - setFlavor(state.preferredInvoiceType); + } + + function clearAll() { + clearAllButAmount(); + setAmount(0n); + setFlavor("lightning"); + setWhatForInput(""); } function openDetailsModal() { @@ -221,67 +233,43 @@ export function Receive() { setDetailsOpen(true); } - async function getUnifiedQr(amount: bigint) { - console.log("get unified amount", amount); - const bigAmount = BigInt(amount); - setLoading(true); - - // Both paths use tags so we'll do this once - let tags; - - try { - tags = whatForInput() ? [whatForInput().trim()] : []; - } catch (e) { - showToast(eify(e)); - console.error(e); - setLoading(false); - return; - } + const receiveTags = createMemo(() => { + return whatForInput() ? [whatForInput().trim()] : []; + }); - // Happy path - // First we try to get both an invoice and an address + async function getLightningReceiveString(amount: bigint) { try { - console.log("big amount", bigAmount); - const raw = await sw.create_bip21(bigAmount, tags); - // Save the raw info so we can watch the address and invoice - setBip21Raw(raw); + const inv = await sw.create_invoice(amount, receiveTags()); - console.log("raw", raw); + const bolt11 = inv?.bolt11; + setRawReceiveStrings({ bolt11 }); - const params = objectToSearchParams({ - amount: raw?.btc_amount, - lightning: raw?.invoice - }); - - setLoading(false); - return `bitcoin:${raw?.address}?${params}`; + return `lightning:${bolt11}`; } catch (e) { console.error(e); - if (e === "Satoshi amount is invalid") { - setError(i18n.t("receive.error_under_min_lightning")); - } else { - setError(i18n.t("receive.error_creating_unified")); - } } + } - // If we didn't return before this, that means create_bip21 failed - // So now we'll just try and get an address without the invoice + async function getOnchainReceiveString(amount?: bigint) { try { - const raw = await sw.get_new_address(tags); - - // Save the raw info so we can watch the address - setBip21Raw(raw); - - setFlavor("onchain"); - - // We won't meddle with a "unified" QR here - return raw?.address; + if (amount && amount < 546n) { + throw new Error(i18n.t("receive.error_under_min_onchain")); + } + const raw = await sw.get_new_address(receiveTags()); + const address = raw?.address; + + if (amount && amount > 0n) { + const btc_amount = await sw.convert_sats_to_btc(amount); + const params = objectToSearchParams({ + amount: btc_amount.toString() + }); + setRawReceiveStrings({ address }); + return `bitcoin:${address}?${params}`; + } else { + return `bitcoin:${address}`; + } } catch (e) { - // If THAT failed we're really screwed - showToast(eify(i18n.t("receive.error_creating_address"))); console.error(e); - } finally { - setLoading(false); } } @@ -292,25 +280,55 @@ export function Receive() { } async function getQr() { - if (amount()) { - const unifiedQr = await getUnifiedQr(amount()); + setLoading(true); + try { + if (flavor() === "lightning") { + const lightning = await getLightningReceiveString(amount()); + setReceiveStrings({ lightning }); + } + + if (flavor() === "onchain") { + const onchain = await getOnchainReceiveString(amount()); + setReceiveStrings({ onchain }); + } + + if (!receiveStrings()?.lightning && !receiveStrings()?.onchain) { + throw new Error(i18n.t("receive.receive_strings_error")); + } - setUnified(unifiedQr || ""); - setReceiveState("show"); + if (!error()) { + setReceiveState("show"); + } + } catch (e) { + console.error(e); + showToast(eify(e)); } + + setLoading(false); } - async function checkIfPaid( - bip21?: MutinyBip21RawMaterials - ): Promise { - if (bip21) { - console.debug("checking if paid..."); - const lightning = bip21.invoice; - const address = bip21.address; + const qrString = createMemo(() => { + if (receiveState() === "show") { + if (flavor() === "lightning") { + return receiveStrings()?.lightning; + } else if (flavor() === "onchain") { + return receiveStrings()?.onchain; + } + } + }); + + async function checkIfPaid(receiveStrings?: { + bolt11?: string; + address?: string; + }): Promise { + if (receiveStrings) { + const lightning = receiveStrings.bolt11; + const address = receiveStrings.address; try { // Lightning invoice might be blank if (lightning) { + console.log("checking invoice", lightning); const invoice = await sw.get_invoice(lightning); // If the invoice has a fees amount that's probably the LSP fee @@ -326,15 +344,16 @@ export function Receive() { } } - const tx = (await sw.check_address(address)) as - | OnChainTx - | undefined; + if (address) { + console.log("checking address", address); + const tx = await sw.check_address(address); - if (tx) { - setReceiveState("paid"); - setPaymentTx(tx); - await vibrateSuccess(); - return "onchain_paid"; + if (tx) { + setReceiveState("paid"); + setPaymentTx(tx); + await vibrateSuccess(); + return "onchain_paid"; + } } } catch (e) { console.error(e); @@ -342,33 +361,29 @@ export function Receive() { } } - function selectFlavor(flavor: string) { - setFlavor(flavor as ReceiveFlavor); - if (rememberChoice()) { - actions.setPreferredInvoiceType(flavor as ReceiveFlavor); - } - setMethodChooserOpen(false); - } - - const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid); + const [paidState, { refetch }] = createResource( + rawReceiveStrings, + checkIfPaid + ); createEffect(() => { const interval = setInterval(() => { if (receiveState() === "show") refetch(); }, 1000); // Poll every second + if (receiveState() !== "show") { + clearInterval(interval); + } onCleanup(() => { clearInterval(interval); }); }); - const [methodChooserOpen, setMethodChooserOpen] = createSignal(false); - return ( }> clearAll()} + onClick={() => clearAllButAmount()} title={i18n.t("receive.edit")} showOnDesktop /> @@ -383,17 +398,26 @@ export function Receive() { {i18n.t("receive.receive_bitcoin")} - +
- +
+ + +
@@ -417,7 +441,9 @@ export function Receive() { /> - setMethodChooserOpen(open)} - > - - - - { - setError(undefined); // Don't recompute if sending if (sending()) return; if (source() === "onchain" && maxOnchain() < amountSats()) { @@ -303,10 +302,13 @@ export function Send() { ); return; } + setError(undefined); }); // Rerun every time the amount changes if we're onchain - const feeEstimate = createAsync(async () => { + const [feeEstimate, { refetch }] = createResource(async () => { + // If it's under the dust limit don't bother + if (amountSats() < 546n) return undefined; if ( source() === "onchain" && amountSats() && @@ -327,12 +329,19 @@ export function Send() { console.log("estimate", estimate); return estimate; } catch (e) { - setError(eify(e).message); + // This is usually because the amount is too small or too large so we can ignore + console.error(e); } } return undefined; }); + createEffect(() => { + if (amountSats() && amountSats() > 0n) { + refetch(); + } + }); + const [parsingDestination, setParsingDestination] = createSignal(false); const [decodingLnUrl, setDecodingLnUrl] = createSignal(false); @@ -540,7 +549,7 @@ export function Send() { sentDetails.amount = amountSats(); sentDetails.destination = address(); sentDetails.txid = txid; - sentDetails.fee_estimate = feeEstimate() ?? 0; + sentDetails.fee_estimate = feeEstimate.latest ?? 0; } else if (payjoinEnabled()) { const txid = await sw.send_payjoin( originalScan()!, @@ -550,7 +559,7 @@ export function Send() { sentDetails.amount = amountSats(); sentDetails.destination = address(); sentDetails.txid = txid; - sentDetails.fee_estimate = feeEstimate() ?? 0; + sentDetails.fee_estimate = feeEstimate.latest ?? 0; } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const txid = await sw.send_to_address( @@ -561,7 +570,7 @@ export function Send() { sentDetails.amount = amountSats(); sentDetails.destination = address(); sentDetails.txid = txid; - sentDetails.fee_estimate = feeEstimate() ?? 0; + sentDetails.fee_estimate = feeEstimate.latest ?? 0; } } if (sentDetails.payment_hash || sentDetails.txid) { @@ -589,6 +598,7 @@ export function Send() { sending() || amountSats() == 0n || amountSats() === undefined || + (source() === "onchain" && amountSats() < 546n) || !!error() ); }); @@ -802,10 +812,10 @@ export function Send() { /> - + diff --git a/src/routes/Swap.tsx b/src/routes/Swap.tsx index cef28530..157543bd 100644 --- a/src/routes/Swap.tsx +++ b/src/routes/Swap.tsx @@ -2,6 +2,7 @@ import { createForm, required } from "@modular-forms/solid"; import { MutinyChannel } from "@mutinywallet/mutiny-wasm"; import { createAsync, useNavigate } from "@solidjs/router"; import { + createEffect, createMemo, createResource, createSignal, @@ -187,25 +188,28 @@ export function Swap() { ); }); - const amountWarning = createAsync(async () => { - if (amountSats() === 0n || !!channelOpenResult()) { - return undefined; - } + const [amountWarning, { refetch: refetchAmountWarning }] = createResource( + async () => { + if (amountSats() === 0n || !!channelOpenResult()) { + return undefined; + } - if (amountSats() < 100000n) { - return i18n.t("swap.channel_too_small", { amount: "100,000" }); - } + if (amountSats() < 100000n) { + return i18n.t("swap.channel_too_small", { amount: "100,000" }); + } - if ( - amountSats() > - (state.balance?.confirmed || 0n) + - (state.balance?.unconfirmed || 0n) - ) { - return i18n.t("swap.insufficient_funds"); - } + if ( + amountSats() > + (state.balance?.confirmed || 0n) + + (state.balance?.unconfirmed || 0n) || + !feeEstimate() + ) { + return i18n.t("swap.insufficient_funds"); + } - return undefined; - }); + return undefined; + } + ); function calculateMaxOnchain() { return ( @@ -222,31 +226,42 @@ export function Swap() { return amountSats() === calculateMaxOnchain(); }); - const feeEstimate = createAsync(async () => { - const max = maxOnchain(); - // If max we want to use the sweep fee estimator - if (amountSats() > 0n && amountSats() === max) { - try { - return await sw.estimate_sweep_channel_open_fee(); - } catch (e) { - console.error(e); - return undefined; + const [feeEstimate, { refetch: refetchFeeEstimate }] = createResource( + async () => { + // If it's under the dust limit don't bother + if (amountSats() < 546n) return undefined; + const max = maxOnchain(); + // If max we want to use the sweep fee estimator + if (amountSats() > 0n && amountSats() === max) { + try { + return await sw.estimate_sweep_channel_open_fee(); + } catch (e) { + console.error(e); + return undefined; + } } - } - if (amountSats() > 0n) { - try { - return await sw.estimate_tx_fee( - CHANNEL_FEE_ESTIMATE_ADDRESS, - amountSats(), - undefined - ); - } catch (e) { - console.error(e); - return undefined; + if (amountSats() > 0n) { + try { + return await sw.estimate_tx_fee( + CHANNEL_FEE_ESTIMATE_ADDRESS, + amountSats(), + undefined + ); + } catch (e) { + console.error(e); + return undefined; + } } + return undefined; + } + ); + + createEffect(() => { + if (amountSats()) { + refetchAmountWarning(); + refetchFeeEstimate(); } - return undefined; }); return ( @@ -419,18 +434,22 @@ export function Swap() { ]} /> - + 0n} + > - 0n}> + 0n} + > - {amountWarning()} + {amountWarning.latest} diff --git a/src/workers/walletWorker.ts b/src/workers/walletWorker.ts index 8c6ec0b2..8f5d54b5 100644 --- a/src/workers/walletWorker.ts +++ b/src/workers/walletWorker.ts @@ -505,6 +505,26 @@ export async function create_bip21( } as MutinyBip21RawMaterials; } +/** + * Creates a lightning invoice. The amount should be in satoshis. + * If no amount is provided, the invoice will be created with no amount. + * If no description is provided, the invoice will be created with no description. + * + * If the manager has more than one node it will create a phantom invoice. + * If there is only one node it will create an invoice just for that node. + * @param {bigint} amount + * @param {(string)[]} labels + * @returns {Promise} + */ +export async function create_invoice( + amount: bigint, + labels: string[] +): Promise { + const invoice = await wallet!.create_invoice(amount, labels); + if (!invoice) return undefined; + return destructureInvoice(invoice); +} + /** * Estimates the onchain fee for a transaction sweep our on-chain balance * to the given address.