Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Lnurl withdrawal support #837

Closed
wants to merge 10 commits into from
Closed
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
5 changes: 4 additions & 1 deletion src/i18n/en/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,10 @@ export default {
gift: "Lightning Gift"
},
remember_choice: "Remember my choice next time",
what_for: "What's this for?"
what_for: "What's this for?",
lnurl_withdrawal_in_progress: "LNUrl withdrawal in progress",
lnurl_amount_message:
"Enter LNUrl withdrawal amount between min: {{min}} and max: {{max}}"
},
send: {
search: {
Expand Down
158 changes: 139 additions & 19 deletions src/routes/Receive.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ import {
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { LnUrlData, useMegaStore } from "~/state/megaStore";
import { eify, objectToSearchParams, vibrateSuccess } from "~/utils";

type OnChainTx = {
Expand Down Expand Up @@ -136,6 +136,14 @@ export function Receive() {
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
const [detailsId, setDetailsId] = createSignal<string>("");

const [lnUrlData, setLnUrlData] = createSignal<LnUrlData>();
const [fixedAmount, setFixedAmount] = createSignal(false);
const [lnUrlExecuted, setLnUrlExecuted] = createSignal(false);

if (state.lnUrlData) {
initLnUrlWithdrawal(state.lnUrlData);
}

const RECEIVE_FLAVORS = [
{
value: "unified",
Expand Down Expand Up @@ -177,6 +185,9 @@ export function Receive() {
setPaymentInvoice(undefined);
setError("");
setFlavor(state.preferredInvoiceType);
setLnUrlData(undefined);
setLnUrlExecuted(false);
setFixedAmount(false);
}

function openDetailsModal() {
Expand Down Expand Up @@ -272,7 +283,10 @@ export function Receive() {

async function onSubmit(e: Event) {
e.preventDefault();

const lnUrl = lnUrlData();
if (lnUrl) {
return await handleLnUrlWithdrawal(lnUrl);
}
await getQr();
}

Expand Down Expand Up @@ -328,6 +342,81 @@ export function Receive() {
}
}

function initLnUrlWithdrawal(lnUrlData: LnUrlData) {
console.log("handleLnUrlWithdrawal", lnUrlData.lnurl, lnUrlData.params);
actions.setScanResult(undefined);
actions.setLnUrlData(undefined);
setLnUrlData(lnUrlData);
setError("");
setFlavor("lightning");
const lnUrlParams = lnUrlData.params;

if (lnUrlParams.min === lnUrlParams.max) {
setAmount(mSatsToSats(lnUrlParams.max));
setFixedAmount(true);
} else {
setAmount(mSatsToSats(lnUrlParams.min));
setFixedAmount(false);
}

setReceiveState("edit");
}

async function handleLnUrlWithdrawal(lnUrlData: LnUrlData) {
const lnurl = lnUrlData.lnurl;
const lnUrlParams = lnUrlData.params;
const amt = amount();
const amtError = validateAmount(
amt,
mSatsToSats(lnUrlParams.min),
mSatsToSats(lnUrlParams.max)
);
if (amtError) {
showToast(new Error(amtError));
return;
}
setLoading(true);
setReceiveState("show");
try {
const success = await state.mutiny_wallet?.lnurl_withdraw(
lnurl,
amount()
);
if (!success) {
setError("lnurl_withdraw failed");
} else {
setReceiveState("paid");
}
} catch (e) {
console.error("lnurl_withdraw failed", e);
showToast(eify(e));
} finally {
setLnUrlExecuted(true);
setLoading(false);
}
}

function validateAmount(
amount: bigint,
min: bigint,
max: bigint
): string | undefined {
if (amount === 0n) return "amount is zero";
if (amount < min) return "amount smaller min";
if (amount > max) return "amount greater max";
}

function mSatsToSats(mSats: bigint) {
return mSats / 1000n;
}

function lnUrlAmountText(lnUrlData: LnUrlData) {
return i18n.t("receive.lnurl_amount_message", {
min: mSatsToSats(lnUrlData.params.min).toLocaleString(),
max: mSatsToSats(lnUrlData.params.max).toLocaleString()
});
}

function selectFlavor(flavor: string) {
setFlavor(flavor as ReceiveFlavor);
if (rememberChoice()) {
Expand All @@ -336,6 +425,18 @@ export function Receive() {
setMethodChooserOpen(false);
}

function satsReceived() {
if (receiveState() === "paid" && paidState() === "lightning_paid") {
return paymentInvoice()?.amount_sats;
}

if (lnUrlExecuted()) {
return amount();
}

return paymentTx()?.received;
}

const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid);

createEffect(() => {
Expand Down Expand Up @@ -369,13 +470,24 @@ export function Receive() {
{i18n.t("receive.receive_bitcoin")}
</LargeHeader>
<Switch>
<Match when={!unified() || receiveState() === "edit"}>
<Match
when={
(!unified() && !lnUrlData()) ||
receiveState() === "edit"
}
>
<div class="flex-1" />
<Show when={lnUrlData()}>
<InfoBox accent="white">
<p>{lnUrlAmountText(lnUrlData()!)}</p>
</InfoBox>
</Show>
<VStack>
<AmountEditable
initialAmountSats={amount() || "0"}
setAmountSats={setAmount}
onSubmit={getQr}
frozenAmount={fixedAmount()}
/>
<ReceiveWarnings
amountSats={amount() || "0"}
Expand Down Expand Up @@ -404,7 +516,12 @@ export function Receive() {
</Button>
</VStack>
</Match>
<Match when={unified() && receiveState() === "show"}>
<Match
when={
(unified() || lnUrlData()) &&
receiveState() === "show"
}
>
<FeeWarning fee={lspFee()} flavor={flavor()} />
<Show when={error()}>
<InfoBox accent="red">
Expand All @@ -416,6 +533,13 @@ export function Receive() {
amountSats={amount() ? amount().toString() : "0"}
kind={flavor()}
/>
<Show when={lnUrlData()}>
<p class="text-center text-neutral-400">
{i18n.t("receive.lnurl_withdrawal_in_progress")}{" "}
...
</p>
</Show>

<p class="text-center text-neutral-400">
{i18n.t("receive.keep_mutiny_open")}
</p>
Expand Down Expand Up @@ -451,7 +575,7 @@ export function Receive() {
</Match>
<Match when={receiveState() === "paid"}>
<SuccessModal
open={!!paidState()}
open={!!paidState() || lnUrlExecuted()}
setOpen={(open: boolean) => {
if (!open) clearAll();
}}
Expand All @@ -471,30 +595,21 @@ export function Receive() {
<MegaCheck />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{receiveState() === "paid" &&
paidState() === "lightning_paid"
(paidState() === "lightning_paid" ||
lnUrlData())
? i18n.t("receive.payment_received")
: i18n.t("receive.payment_initiated")}
</h1>
<div class="flex flex-col items-center gap-1">
<div class="text-xl">
<AmountSats
amountSats={
receiveState() === "paid" &&
paidState() === "lightning_paid"
? paymentInvoice()?.amount_sats
: paymentTx()?.received
}
amountSats={satsReceived()}
icon="plus"
/>
</div>
<div class="text-white/70">
<AmountFiat
amountSats={
receiveState() === "paid" &&
paidState() === "lightning_paid"
? paymentInvoice()?.amount_sats
: paymentTx()?.received
}
amountSats={satsReceived()}
denominationSize="sm"
/>
</div>
Expand All @@ -509,7 +624,12 @@ export function Receive() {
<Fee amountSats={lspFee()} />
</Show>
{/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first*/}
<Show when={receiveState() === "paid"}>
<Show
when={
receiveState() === "paid" &&
!lnUrlExecuted()
}
>
<p
class="cursor-pointer underline"
onClick={openDetailsModal}
Expand Down
38 changes: 34 additions & 4 deletions src/routes/Scanner.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Clipboard } from "@capacitor/clipboard";
import { Capacitor } from "@capacitor/core";
import { LnUrlParams } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import { createEffect, createSignal } from "solid-js";

import { Button, Scanner as Reader, showToast } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { eify } from "~/utils";

export function Scanner() {
const i18n = useI18n();
const [_state, actions] = useMegaStore();
const [state, actions] = useMegaStore();
const [scanResult, setScanResult] = createSignal<string>();
const navigate = useNavigate();

Expand Down Expand Up @@ -40,7 +42,19 @@ export function Scanner() {
}
}

// When we have a nice result we can head over to the send screen
function handleLnUrl(
lnUrl: string,
success: (result: LnUrlParams) => void
) {
state.mutiny_wallet
?.decode_lnurl(lnUrl)
.then((lnurlParams) => {
success(lnurlParams);
})
.catch((e) => showToast(eify(e)));
}

// When we have a nice result we can head over to the next screen
createEffect(() => {
if (scanResult()) {
actions.handleIncomingString(
Expand All @@ -49,8 +63,24 @@ export function Scanner() {
showToast(error);
},
(result) => {
actions.setScanResult(result);
navigate("/send");
if (result.lnurl && !result.is_lnurl_auth) {
const lnurl = result.lnurl;
handleLnUrl(result.lnurl, (lnurlParams) => {
actions.setScanResult(result);
actions.setLnUrlData({
lnurl: lnurl,
params: lnurlParams
});
if (lnurlParams.tag === "withdrawRequest") {
navigate("/receive");
} else {
navigate("/send");
}
});
} else {
actions.setScanResult(result);
navigate("/send");
}
}
);
}
Expand Down
Loading
Loading