mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-02-01 04:24:32 +01:00
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
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -59,7 +59,6 @@ export const AmountEditable: ParentComponent<{
|
||||
state.fiat,
|
||||
sw
|
||||
).then((sats) => {
|
||||
console.log("sats", sats);
|
||||
setLocalFiat(sats);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -39,16 +39,6 @@ function KindIndicator(props: { kind: ReceiveFlavor | "gift" | "lnAddress" }) {
|
||||
</h3>
|
||||
<Zap class="h-4 w-4" />
|
||||
</Match>
|
||||
|
||||
<Match when={props.kind === "unified"}>
|
||||
<h3 class="font-semibold">
|
||||
{i18n.t("receive.integrated_qr.unified")}
|
||||
</h3>
|
||||
<div class="flex gap-1">
|
||||
<Zap class="h-4 w-4" />
|
||||
<Link class="h-4 w-4" />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<Switch>
|
||||
<Match when={tooSmallWarning()}>
|
||||
<InfoBox accent="red">{tooSmallWarning()}</InfoBox>
|
||||
</Match>
|
||||
<Match when={sillyAmountWarning()}>
|
||||
<InfoBox accent="red">{sillyAmountWarning()}</InfoBox>
|
||||
</Match>
|
||||
|
||||
@@ -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?
|
||||
<Show when={props.fee > 1000n}>
|
||||
<Switch>
|
||||
<Match when={props.flavor === "unified"}>
|
||||
<InfoBox accent="blue">
|
||||
{i18n.t("receive.unified_setup_fee", {
|
||||
amount: props.fee.toLocaleString()
|
||||
})}
|
||||
<FeesModal />
|
||||
</InfoBox>
|
||||
</Match>
|
||||
<Match when={props.flavor === "lightning"}>
|
||||
<InfoBox accent="blue">
|
||||
{i18n.t("receive.lightning_setup_fee", {
|
||||
amount: props.fee.toLocaleString()
|
||||
})}
|
||||
<FeesModal />
|
||||
</InfoBox>
|
||||
</Match>
|
||||
</Switch>
|
||||
<Show when={props.fee > 1000n && props.flavor === "lightning"}>
|
||||
<InfoBox accent="blue">
|
||||
{i18n.t("receive.lightning_setup_fee", {
|
||||
amount: props.fee.toLocaleString()
|
||||
})}
|
||||
<FeesModal />
|
||||
</InfoBox>
|
||||
</Show>
|
||||
);
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<SharpButton onClick={() => setMethodChooserOpen(true)}>
|
||||
{props.flavor === "lightning" ? (
|
||||
<Zap class="h-4 w-4" />
|
||||
) : (
|
||||
<Link class="h-4 w-4" />
|
||||
)}
|
||||
{props.flavor === "lightning" ? "Lightning" : "On-chain"}
|
||||
</SharpButton>
|
||||
<SimpleDialog
|
||||
title={i18n.t("receive.choose_payment_format")}
|
||||
open={methodChooserOpen()}
|
||||
setOpen={(open) => setMethodChooserOpen(open)}
|
||||
>
|
||||
<StyledRadioGroup
|
||||
initialValue={props.flavor}
|
||||
onValueChange={(flavor) => {
|
||||
props.setFlavor(flavor);
|
||||
setMethodChooserOpen(false);
|
||||
}}
|
||||
choices={RECEIVE_FLAVORS}
|
||||
accent="white"
|
||||
vertical
|
||||
delayOnChange
|
||||
/>
|
||||
</SimpleDialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
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<ReceiveState>("edit");
|
||||
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
|
||||
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<OnChainTx>();
|
||||
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
|
||||
|
||||
// The flavor of the receive (defaults to unified)
|
||||
const [flavor, setFlavor] = createSignal<ReceiveFlavor>(
|
||||
state.preferredInvoiceType
|
||||
);
|
||||
// The flavor of the receive
|
||||
const [flavor, setFlavor] = createSignal<ReceiveFlavor>("lightning");
|
||||
|
||||
// loading state for the continue button
|
||||
const [loading, setLoading] = createSignal(false);
|
||||
@@ -154,47 +194,19 @@ export function Receive() {
|
||||
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
|
||||
const [detailsId, setDetailsId] = createSignal<string>("");
|
||||
|
||||
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;
|
||||
const receiveTags = createMemo(() => {
|
||||
return whatForInput() ? [whatForInput().trim()] : [];
|
||||
});
|
||||
|
||||
async function getLightningReceiveString(amount: bigint) {
|
||||
try {
|
||||
tags = whatForInput() ? [whatForInput().trim()] : [];
|
||||
const inv = await sw.create_invoice(amount, receiveTags());
|
||||
|
||||
const bolt11 = inv?.bolt11;
|
||||
setRawReceiveStrings({ bolt11 });
|
||||
|
||||
return `lightning:${bolt11}`;
|
||||
} catch (e) {
|
||||
showToast(eify(e));
|
||||
console.error(e);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Happy path
|
||||
// First we try to get both an invoice and an address
|
||||
async function getOnchainReceiveString(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);
|
||||
|
||||
console.log("raw", raw);
|
||||
|
||||
const params = objectToSearchParams({
|
||||
amount: raw?.btc_amount,
|
||||
lightning: raw?.invoice
|
||||
});
|
||||
|
||||
setLoading(false);
|
||||
return `bitcoin:${raw?.address}?${params}`;
|
||||
} 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 (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 we didn't return before this, that means create_bip21 failed
|
||||
// So now we'll just try and get an address without the invoice
|
||||
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 > 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 });
|
||||
}
|
||||
|
||||
setUnified(unifiedQr || "");
|
||||
setReceiveState("show");
|
||||
if (flavor() === "onchain") {
|
||||
const onchain = await getOnchainReceiveString(amount());
|
||||
setReceiveStrings({ onchain });
|
||||
}
|
||||
|
||||
if (!receiveStrings()?.lightning && !receiveStrings()?.onchain) {
|
||||
throw new Error(i18n.t("receive.receive_strings_error"));
|
||||
}
|
||||
|
||||
if (!error()) {
|
||||
setReceiveState("show");
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(eify(e));
|
||||
}
|
||||
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
async function checkIfPaid(
|
||||
bip21?: MutinyBip21RawMaterials
|
||||
): Promise<PaidState | undefined> {
|
||||
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<PaidState | undefined> {
|
||||
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 (
|
||||
<MutinyWalletGuard>
|
||||
<DefaultMain>
|
||||
<Show when={receiveState() === "show"} fallback={<BackLink />}>
|
||||
<BackButton
|
||||
onClick={() => clearAll()}
|
||||
onClick={() => clearAllButAmount()}
|
||||
title={i18n.t("receive.edit")}
|
||||
showOnDesktop
|
||||
/>
|
||||
@@ -383,17 +398,26 @@ export function Receive() {
|
||||
{i18n.t("receive.receive_bitcoin")}
|
||||
</LargeHeader>
|
||||
<Switch>
|
||||
<Match when={!unified() || receiveState() === "edit"}>
|
||||
<Match
|
||||
when={!receiveStrings() || receiveState() === "edit"}
|
||||
>
|
||||
<div class="flex-1" />
|
||||
<VStack>
|
||||
<AmountEditable
|
||||
initialAmountSats={amount() || "0"}
|
||||
setAmountSats={setAmount}
|
||||
onSubmit={getQr}
|
||||
/>
|
||||
<div class="mx-auto flex w-full max-w-[400px] flex-col items-center">
|
||||
<AmountEditable
|
||||
initialAmountSats={amount() || "0"}
|
||||
setAmountSats={setAmount}
|
||||
onSubmit={getQr}
|
||||
/>
|
||||
<FlavorChooser
|
||||
flavor={flavor()}
|
||||
setFlavor={setFlavor}
|
||||
/>
|
||||
</div>
|
||||
<ReceiveWarnings
|
||||
amountSats={amount() || 0n}
|
||||
from_fedi_to_ln={false}
|
||||
flavor={flavor()}
|
||||
/>
|
||||
</VStack>
|
||||
<div class="flex-1" />
|
||||
@@ -417,7 +441,9 @@ export function Receive() {
|
||||
/>
|
||||
</form>
|
||||
<Button
|
||||
disabled={!amount()}
|
||||
disabled={
|
||||
!amount() && !(flavor() === "onchain")
|
||||
}
|
||||
intent="green"
|
||||
onClick={onSubmit}
|
||||
loading={loading()}
|
||||
@@ -426,7 +452,7 @@ export function Receive() {
|
||||
</Button>
|
||||
</VStack>
|
||||
</Match>
|
||||
<Match when={unified() && receiveState() === "show"}>
|
||||
<Match when={receiveStrings() && receiveState() === "show"}>
|
||||
<FeeWarning fee={lspFee()} flavor={flavor()} />
|
||||
<Show when={error()}>
|
||||
<InfoBox accent="red">
|
||||
@@ -434,42 +460,13 @@ export function Receive() {
|
||||
</InfoBox>
|
||||
</Show>
|
||||
<IntegratedQr
|
||||
value={receiveString() ?? ""}
|
||||
value={qrString() ?? ""}
|
||||
amountSats={amount() ? amount().toString() : "0"}
|
||||
kind={flavor()}
|
||||
/>
|
||||
<p class="text-center text-m-grey-350">
|
||||
{i18n.t("receive.keep_mutiny_open")}
|
||||
</p>
|
||||
{/* Only show method chooser when we have an invoice */}
|
||||
<Show when={bip21Raw()?.invoice}>
|
||||
<button
|
||||
class="mx-auto flex items-center gap-2 pb-8 font-bold text-m-grey-400"
|
||||
onClick={() => setMethodChooserOpen(true)}
|
||||
>
|
||||
<span>{i18n.t("receive.choose_format")}</span>
|
||||
<ArrowLeftRight class="h-4 w-4" />
|
||||
</button>
|
||||
<SimpleDialog
|
||||
title={i18n.t("receive.choose_payment_format")}
|
||||
open={methodChooserOpen()}
|
||||
setOpen={(open) => setMethodChooserOpen(open)}
|
||||
>
|
||||
<StyledRadioGroup
|
||||
initialValue={flavor()}
|
||||
onValueChange={selectFlavor}
|
||||
choices={RECEIVE_FLAVORS}
|
||||
accent="white"
|
||||
vertical
|
||||
delayOnChange
|
||||
/>
|
||||
<Checkbox
|
||||
label={i18n.t("receive.remember_choice")}
|
||||
checked={rememberChoice()}
|
||||
onChange={setRememberChoice}
|
||||
/>
|
||||
</SimpleDialog>
|
||||
</Show>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid"}>
|
||||
<SuccessModal
|
||||
|
||||
@@ -276,7 +276,6 @@ export function Send() {
|
||||
|
||||
// Rerun every time the source or amount changes to check for amount errors
|
||||
createEffect(() => {
|
||||
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() {
|
||||
/>
|
||||
</Show>
|
||||
<Suspense>
|
||||
<Show when={feeEstimate()}>
|
||||
<Show when={feeEstimate.latest}>
|
||||
<FeeDisplay
|
||||
amountSats={amountSats().toString()}
|
||||
fee={feeEstimate()!.toString()}
|
||||
fee={feeEstimate.latest!.toString()}
|
||||
maxAmountSats={maxAmountSats()}
|
||||
/>
|
||||
</Show>
|
||||
|
||||
@@ -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()) {
|
||||
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() >
|
||||
(state.balance?.confirmed || 0n) +
|
||||
(state.balance?.unconfirmed || 0n) ||
|
||||
!feeEstimate()
|
||||
) {
|
||||
return i18n.t("swap.insufficient_funds");
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
|
||||
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() {
|
||||
]}
|
||||
/>
|
||||
<Suspense>
|
||||
<Show when={feeEstimate()}>
|
||||
<Show
|
||||
when={feeEstimate.latest && amountSats() > 0n}
|
||||
>
|
||||
<FeeDisplay
|
||||
amountSats={amountSats().toString()}
|
||||
fee={feeEstimate()!.toString()}
|
||||
fee={feeEstimate.latest!.toString()}
|
||||
maxAmountSats={maxOnchain()}
|
||||
/>
|
||||
</Show>
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<Show when={amountWarning() && amountSats() > 0n}>
|
||||
<Show
|
||||
when={amountWarning.latest && amountSats() > 0n}
|
||||
>
|
||||
<InfoBox accent={"red"}>
|
||||
{amountWarning()}
|
||||
{amountWarning.latest}
|
||||
</InfoBox>
|
||||
</Show>
|
||||
</Suspense>
|
||||
|
||||
@@ -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<MutinyInvoice>}
|
||||
*/
|
||||
export async function create_invoice(
|
||||
amount: bigint,
|
||||
labels: string[]
|
||||
): Promise<MutinyInvoice | undefined> {
|
||||
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.
|
||||
|
||||
Reference in New Issue
Block a user