mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-20 15:54:22 +01:00
support lnurlpay
This commit is contained in:
@@ -24,7 +24,7 @@ export function StyledRadioGroup(props: { value: string, choices: Choices, onVal
|
|||||||
</RadioGroup.ItemControl>
|
</RadioGroup.ItemControl>
|
||||||
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
<RadioGroup.ItemLabel class="ui-checked:text-white text-neutral-400">
|
||||||
<div class="block">
|
<div class="block">
|
||||||
<div class={`text-${props.small ? "base" : "lg"} font-semibold`}>{choice.label}</div>
|
<div classList={{ "text-base": props.small, "text-lg": !props.small }} class={`font-semibold max-sm:text-sm`}>{choice.label}</div>
|
||||||
<Show when={!props.small}>
|
<Show when={!props.small}>
|
||||||
<div class="text-sm font-light">{choice.caption}</div>
|
<div class="text-sm font-light">{choice.caption}</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export type ParsedParams = {
|
|||||||
network?: string;
|
network?: string;
|
||||||
memo?: string;
|
memo?: string;
|
||||||
node_pubkey?: string;
|
node_pubkey?: string;
|
||||||
|
lnurl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toParsedParams(str: string, ourNetwork: string): Result<ParsedParams> {
|
export function toParsedParams(str: string, ourNetwork: string): Result<ParsedParams> {
|
||||||
@@ -25,33 +26,23 @@ export function toParsedParams(str: string, ourNetwork: string): Result<ParsedPa
|
|||||||
return { ok: false, error: new Error("Invalid payment request") }
|
return { ok: false, error: new Error("Invalid payment request") }
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("params:", params.node_pubkey)
|
// If WAILA doesn't return a network we should default to our own
|
||||||
console.log("params network:", params.network)
|
// If the networks is testnet and we're on signet we should use signet
|
||||||
console.log("our network:", ourNetwork)
|
const network = !params.network ? ourNetwork : params.network === "testnet" && ourNetwork === "signet" ? "signet" : params.network;
|
||||||
|
|
||||||
|
if (network !== ourNetwork) {
|
||||||
|
|
||||||
// TODO: "testnet" and "signet" are encoded the same I guess?
|
|
||||||
if (params.network === "testnet" || params.network === "signet") {
|
|
||||||
if (ourNetwork === "signet") {
|
|
||||||
// noop
|
|
||||||
}
|
|
||||||
} else if (params.network !== ourNetwork) {
|
|
||||||
if (params.node_pubkey) {
|
|
||||||
// noop
|
|
||||||
} else {
|
|
||||||
return { ok: false, error: new Error(`Destination is for ${params.network} but you're on ${ourNetwork}`) }
|
return { ok: false, error: new Error(`Destination is for ${params.network} but you're on ${ourNetwork}`) }
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ok: true, value: {
|
ok: true, value: {
|
||||||
address: params.address,
|
address: params.address,
|
||||||
invoice: params.invoice,
|
invoice: params.invoice,
|
||||||
amount_sats: params.amount_sats,
|
amount_sats: params.amount_sats,
|
||||||
network: params.network,
|
network,
|
||||||
memo: params.memo,
|
memo: params.memo,
|
||||||
node_pubkey: params.node_pubkey,
|
node_pubkey: params.node_pubkey,
|
||||||
|
lnurl: params.lnurl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,7 +80,7 @@ export default function Scanner() {
|
|||||||
showToast(result.error);
|
showToast(result.error);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey) {
|
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey || result.value?.lnurl) {
|
||||||
actions.setScanResult(result.value);
|
actions.setScanResult(result.value);
|
||||||
navigate("/send")
|
navigate("/send")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useNavigate } from "solid-start";
|
|||||||
import { TagEditor } from "~/components/TagEditor";
|
import { TagEditor } from "~/components/TagEditor";
|
||||||
import { StringShower } from "~/components/ShareCard";
|
import { StringShower } from "~/components/ShareCard";
|
||||||
import { AmountCard } from "~/components/AmountCard";
|
import { AmountCard } from "~/components/AmountCard";
|
||||||
import { MutinyTagItem, UNKNOWN_TAG, sortByLastUsed, tagsToIds } from "~/utils/tags";
|
import { MutinyTagItem } from "~/utils/tags";
|
||||||
import { BackButton } from "~/components/layout/BackButton";
|
import { BackButton } from "~/components/layout/BackButton";
|
||||||
|
|
||||||
type SendSource = "lightning" | "onchain";
|
type SendSource = "lightning" | "onchain";
|
||||||
@@ -30,18 +30,28 @@ type SendSource = "lightning" | "onchain";
|
|||||||
// TODO: better success / fail type
|
// TODO: better success / fail type
|
||||||
type SentDetails = { amount?: bigint, destination?: string, txid?: string, failure_reason?: string }
|
type SentDetails = { amount?: bigint, destination?: string, txid?: string, failure_reason?: string }
|
||||||
|
|
||||||
function MethodChooser(props: { source: SendSource, setSource: (source: string) => void }) {
|
function MethodChooser(props: { source: SendSource, setSource: (source: string) => void, both?: boolean }) {
|
||||||
const [store, _actions] = useMegaStore();
|
const [store, _actions] = useMegaStore();
|
||||||
|
|
||||||
const methods = createMemo(() => {
|
const methods = createMemo(() => {
|
||||||
return [
|
return [
|
||||||
{ value: "lightning", label: "Lightning", caption: store.balance?.lightning ? `${store.balance?.lightning.toLocaleString()} SATS` : "No balance" },
|
{ value: "lightning", label: "Lightning Balance", caption: store.balance?.lightning ? `${store.balance?.lightning.toLocaleString()} SATS` : "No balance" },
|
||||||
{ value: "onchain", label: "On-chain", caption: store.balance?.confirmed ? `${store.balance?.confirmed.toLocaleString()} SATS` : "No balance" }
|
{ value: "onchain", label: "On-chain Balance", caption: store.balance?.confirmed ? `${store.balance?.confirmed.toLocaleString()} SATS` : "No balance" }
|
||||||
]
|
]
|
||||||
|
|
||||||
})
|
})
|
||||||
return (
|
return (
|
||||||
|
<Switch>
|
||||||
|
<Match when={props.both}>
|
||||||
<StyledRadioGroup accent="white" value={props.source} onValueChange={props.setSource} choices={methods()} />
|
<StyledRadioGroup accent="white" value={props.source} onValueChange={props.setSource} choices={methods()} />
|
||||||
|
</Match>
|
||||||
|
<Match when={props.source === "lightning"}>
|
||||||
|
<StyledRadioGroup accent="white" value={props.source} onValueChange={props.setSource} choices={[methods()[0]]} />
|
||||||
|
</Match>
|
||||||
|
<Match when={props.source === "onchain"}>
|
||||||
|
<StyledRadioGroup accent="white" value={props.source} onValueChange={props.setSource} choices={[methods()[1]]} />
|
||||||
|
</Match>
|
||||||
|
</Switch>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +90,7 @@ function DestinationShower(props: {
|
|||||||
address?: string,
|
address?: string,
|
||||||
invoice?: MutinyInvoice,
|
invoice?: MutinyInvoice,
|
||||||
nodePubkey?: string,
|
nodePubkey?: string,
|
||||||
|
lnurl?: string,
|
||||||
clearAll: () => void,
|
clearAll: () => void,
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
@@ -93,6 +104,9 @@ function DestinationShower(props: {
|
|||||||
<Match when={props.nodePubkey && props.source === "lightning"}>
|
<Match when={props.nodePubkey && props.source === "lightning"}>
|
||||||
<StringShower text={props.nodePubkey || ""} />
|
<StringShower text={props.nodePubkey || ""} />
|
||||||
</Match>
|
</Match>
|
||||||
|
<Match when={props.lnurl && props.source === "lightning"}>
|
||||||
|
<StringShower text={props.lnurl || ""} />
|
||||||
|
</Match>
|
||||||
</Switch>
|
</Switch>
|
||||||
|
|
||||||
)
|
)
|
||||||
@@ -113,6 +127,7 @@ export default function Send() {
|
|||||||
// These can only be derived from the "destination" signal
|
// These can only be derived from the "destination" signal
|
||||||
const [invoice, setInvoice] = createSignal<MutinyInvoice>();
|
const [invoice, setInvoice] = createSignal<MutinyInvoice>();
|
||||||
const [nodePubkey, setNodePubkey] = createSignal<string>();
|
const [nodePubkey, setNodePubkey] = createSignal<string>();
|
||||||
|
const [lnurlp, setLnurlp] = createSignal<string>();
|
||||||
const [address, setAddress] = createSignal<string>();
|
const [address, setAddress] = createSignal<string>();
|
||||||
const [description, setDescription] = createSignal<string>();
|
const [description, setDescription] = createSignal<string>();
|
||||||
|
|
||||||
@@ -131,6 +146,7 @@ export default function Send() {
|
|||||||
setAddress(undefined);
|
setAddress(undefined);
|
||||||
setDescription(undefined);
|
setDescription(undefined);
|
||||||
setNodePubkey(undefined);
|
setNodePubkey(undefined);
|
||||||
|
setLnurlp(undefined);
|
||||||
setFieldDestination("");
|
setFieldDestination("");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,7 +170,6 @@ export default function Send() {
|
|||||||
// Rerun every time the destination changes
|
// Rerun every time the destination changes
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const source = destination();
|
const source = destination();
|
||||||
console.log(source)
|
|
||||||
if (!source) return undefined;
|
if (!source) return undefined;
|
||||||
try {
|
try {
|
||||||
if (source.address) setAddress(source.address)
|
if (source.address) setAddress(source.address)
|
||||||
@@ -170,6 +185,14 @@ export default function Send() {
|
|||||||
setAmountSats(source.amount_sats || 0n);
|
setAmountSats(source.amount_sats || 0n);
|
||||||
setNodePubkey(source.node_pubkey);
|
setNodePubkey(source.node_pubkey);
|
||||||
setSource("lightning")
|
setSource("lightning")
|
||||||
|
} else if (source.lnurl) {
|
||||||
|
state.mutiny_wallet?.decode_lnurl(source.lnurl).then((lnurlParams) => {
|
||||||
|
if (lnurlParams.tag === "payRequest") {
|
||||||
|
setAmountSats(source.amount_sats || 0n);
|
||||||
|
setLnurlp(source.lnurl);
|
||||||
|
setSource("lightning")
|
||||||
|
}
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
setAmountSats(source.amount_sats || 0n);
|
setAmountSats(source.amount_sats || 0n);
|
||||||
setSource("onchain")
|
setSource("onchain")
|
||||||
@@ -190,7 +213,7 @@ export default function Send() {
|
|||||||
showToast(result.error);
|
showToast(result.error);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey) {
|
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey || result.value?.lnurl) {
|
||||||
setDestination(result.value);
|
setDestination(result.value);
|
||||||
// Important! we need to clear the scan result once we've used it
|
// Important! we need to clear the scan result once we've used it
|
||||||
actions.setScanResult(undefined);
|
actions.setScanResult(undefined);
|
||||||
@@ -278,6 +301,16 @@ export default function Send() {
|
|||||||
} else {
|
} else {
|
||||||
sentDetails.amount = amountSats();
|
sentDetails.amount = amountSats();
|
||||||
}
|
}
|
||||||
|
} else if (source() === "lightning" && lnurlp()) {
|
||||||
|
const nodes = await state.mutiny_wallet?.list_nodes();
|
||||||
|
const firstNode = nodes[0] as string || ""
|
||||||
|
const payment = await state.mutiny_wallet?.lnurl_pay(firstNode, lnurlp()!, amountSats(), tags);
|
||||||
|
|
||||||
|
if (!payment?.paid) {
|
||||||
|
throw new Error("Lnurl Pay failed")
|
||||||
|
} else {
|
||||||
|
sentDetails.amount = amountSats();
|
||||||
|
}
|
||||||
} else if (source() === "onchain" && address()) {
|
} else if (source() === "onchain" && address()) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tags);
|
const txid = await state.mutiny_wallet?.send_to_address(address()!, amountSats(), tags);
|
||||||
@@ -308,7 +341,7 @@ export default function Send() {
|
|||||||
<MutinyWalletGuard>
|
<MutinyWalletGuard>
|
||||||
<SafeArea>
|
<SafeArea>
|
||||||
<DefaultMain>
|
<DefaultMain>
|
||||||
<Show when={address() || invoice() || nodePubkey()} fallback={<BackLink />}>
|
<Show when={address() || invoice() || nodePubkey() || lnurlp()} fallback={<BackLink />}>
|
||||||
<BackButton onClick={() => clearAll()} title="Start Over" />
|
<BackButton onClick={() => clearAll()} title="Start Over" />
|
||||||
</Show>
|
</Show>
|
||||||
<LargeHeader>Send Bitcoin</LargeHeader>
|
<LargeHeader>Send Bitcoin</LargeHeader>
|
||||||
@@ -339,18 +372,16 @@ export default function Send() {
|
|||||||
</FullscreenModal>
|
</FullscreenModal>
|
||||||
<VStack biggap>
|
<VStack biggap>
|
||||||
<Switch>
|
<Switch>
|
||||||
<Match when={address() || invoice() || nodePubkey()}>
|
<Match when={address() || invoice() || nodePubkey() || lnurlp()}>
|
||||||
<Show when={address() && invoice()}>
|
|
||||||
<MethodChooser source={source()} setSource={setSource} />
|
<MethodChooser source={source()} setSource={setSource} both={!!address() && !!invoice()} />
|
||||||
</Show>
|
|
||||||
<Card>
|
<Card>
|
||||||
<VStack>
|
<VStack>
|
||||||
<DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} clearAll={clearAll} />
|
<DestinationShower source={source()} description={description()} invoice={invoice()} address={address()} nodePubkey={nodePubkey()} lnurl={lnurlp()} clearAll={clearAll} />
|
||||||
<SmallHeader>Private tags</SmallHeader>
|
<SmallHeader>Private tags</SmallHeader>
|
||||||
<TagEditor selectedValues={selectedContacts()} setSelectedValues={setSelectedContacts} placeholder="Add the receiver for your records" />
|
<TagEditor selectedValues={selectedContacts()} setSelectedValues={setSelectedContacts} placeholder="Add the receiver for your records" />
|
||||||
</VStack>
|
</VStack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<AmountCard amountSats={amountSats().toString()} setAmountSats={setAmountSats} fee={feeEstimate()?.toString()} isAmountEditable={!(invoice()?.amount_sats)} />
|
<AmountCard amountSats={amountSats().toString()} setAmountSats={setAmountSats} fee={feeEstimate()?.toString()} isAmountEditable={!(invoice()?.amount_sats)} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={true}>
|
<Match when={true}>
|
||||||
|
|||||||
Reference in New Issue
Block a user