method chooser for send

This commit is contained in:
Paul Miller
2024-01-13 16:26:40 +00:00
parent 858c51b102
commit 0748148337
5 changed files with 282 additions and 259 deletions

View File

@@ -7,8 +7,7 @@ import {
Show Show
} from "solid-js"; } from "solid-js";
import { AmountSmall, BigMoney } from "~/components"; import { AmountSats, BigMoney } from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { import {
btcFloatRounding, btcFloatRounding,
@@ -19,17 +18,32 @@ import {
toDisplayHandleNaN toDisplayHandleNaN
} from "~/utils"; } from "~/utils";
export type MethodChoice = {
method: "lightning" | "onchain";
maxAmountSats?: bigint;
};
// Make sure to update this when we get the fedi option in here!
function methodToIcon(method: MethodChoice["method"]) {
if (method === "lightning") {
return "lightning";
} else if (method === "onchain") {
return "chain";
}
}
export const AmountEditable: ParentComponent<{ export const AmountEditable: ParentComponent<{
initialAmountSats: string | bigint; initialAmountSats: string | bigint;
setAmountSats: (s: bigint) => void; setAmountSats: (s: bigint) => void;
maxAmountSats?: bigint;
fee?: string; fee?: string;
frozenAmount?: boolean; frozenAmount?: boolean;
onSubmit?: () => void; onSubmit?: () => void;
activeMethod?: MethodChoice;
methods?: MethodChoice[];
setChosenMethod?: (method: MethodChoice) => void;
}> = (props) => { }> = (props) => {
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
const [mode, setMode] = createSignal<"fiat" | "sats">("sats"); const [mode, setMode] = createSignal<"fiat" | "sats">("sats");
const i18n = useI18n();
const [localSats, setLocalSats] = createSignal( const [localSats, setLocalSats] = createSignal(
props.initialAmountSats.toString() || "0" props.initialAmountSats.toString() || "0"
); );
@@ -229,12 +243,47 @@ export const AmountEditable: ParentComponent<{
onFocus={() => focus()} onFocus={() => focus()}
/> />
</div> </div>
<Show when={props.maxAmountSats}> <Show when={props.methods?.length && props.activeMethod}>
<p class="flex gap-2 px-4 py-2 text-sm font-light text-m-grey-400 md:text-base"> <MethodChooser
{`${i18n.t("receive.amount_editable.balance")} `} methods={props.methods!}
<AmountSmall amountSats={props.maxAmountSats!} /> activeMethod={props.activeMethod!}
</p> setChosenMethod={props.setChosenMethod}
/>
</Show> </Show>
</div> </div>
); );
}; };
function MethodChooser(props: {
activeMethod: MethodChoice;
methods: MethodChoice[];
setChosenMethod?: (method: MethodChoice) => void;
}) {
function setNextMethod() {
const activeIndex = props.methods.findIndex(
(m) => m.method === props.activeMethod.method
);
const nextMethod =
props.methods[
activeIndex === props.methods.length - 1 ? 0 : activeIndex + 1
];
props.setChosenMethod && props.setChosenMethod(nextMethod);
}
return (
<button
onClick={setNextMethod}
disabled={props.methods.length === 1}
class="flex gap-2 rounded px-2 py-1 text-sm font-light text-m-grey-400 md:text-base"
classList={{
"border-b border-t border-b-white/10 border-t-white/50 bg-neutral-700":
props.methods?.length > 1
}}
>
<AmountSats
amountSats={props.activeMethod.maxAmountSats!}
denominationSize="sm"
icon={methodToIcon(props.activeMethod.method)}
/>
</button>
);
}

View File

@@ -1,71 +0,0 @@
import { createMemo, Match, Switch } from "solid-js";
import { StyledRadioGroup } from "~/components";
import { useMegaStore } from "~/state/megaStore";
type SendSource = "lightning" | "onchain";
export function MethodChooser(props: {
source: SendSource;
setSource: (source: string) => void;
both?: boolean;
}) {
const [store, _actions] = useMegaStore();
const methods = createMemo(() => {
const lnBalance =
(store.balance?.lightning || 0n) +
(store.balance?.federation || 0n);
const onchainBalance =
(store.balance?.confirmed || 0n) +
(store.balance?.unconfirmed || 0n);
return [
{
value: "lightning",
label: "Lightning Balance",
caption:
lnBalance > 0n
? `${lnBalance.toLocaleString()} SATS`
: "No balance",
disabled: lnBalance === 0n
},
{
value: "onchain",
label: "On-chain Balance",
caption:
onchainBalance > 0n
? `${onchainBalance.toLocaleString()} SATS`
: "No balance",
disabled: onchainBalance === 0n
}
];
});
return (
<Switch>
<Match when={props.both}>
<StyledRadioGroup
accent="white"
initialValue={props.source}
onValueChange={props.setSource}
choices={methods()}
/>
</Match>
<Match when={props.source === "lightning"}>
<StyledRadioGroup
accent="white"
initialValue={props.source}
onValueChange={props.setSource}
choices={[methods()[0]]}
/>
</Match>
<Match when={props.source === "onchain"}>
<StyledRadioGroup
accent="white"
initialValue={props.source}
onValueChange={props.setSource}
choices={[methods()[1]]}
/>
</Match>
</Switch>
);
}

View File

@@ -49,5 +49,4 @@ export * from "./BigMoney";
export * from "./FeeDisplay"; export * from "./FeeDisplay";
export * from "./ReceiveWarnings"; export * from "./ReceiveWarnings";
export * from "./SimpleInput"; export * from "./SimpleInput";
export * from "./MethodChooser";
export * from "./LabelCircle"; export * from "./LabelCircle";

View File

@@ -33,6 +33,7 @@ import {
MegaCheck, MegaCheck,
MegaClock, MegaClock,
MegaEx, MegaEx,
MethodChoice,
MutinyWalletGuard, MutinyWalletGuard,
NavBar, NavBar,
showToast, showToast,
@@ -637,6 +638,59 @@ export function Send() {
); );
}); });
const lightningMethod = createMemo<MethodChoice>(() => {
return {
method: "lightning",
maxAmountSats: maxLightning()
};
});
const onchainMethod = createMemo<MethodChoice>(() => {
return {
method: "onchain",
maxAmountSats: maxOnchain()
};
});
const sendMethods = createMemo<MethodChoice[]>(() => {
if (lnAddress() || lnurlp() || nodePubkey()) {
return [lightningMethod()];
}
if (invoice() && address()) {
return [lightningMethod(), onchainMethod()];
}
if (invoice()) {
return [lightningMethod()];
}
if (address()) {
return [onchainMethod()];
}
// We should never get here
console.error("No send methods found");
return [];
});
function setSourceFromMethod(method: MethodChoice) {
if (method.method === "lightning") {
setSource("lightning");
} else if (method.method === "onchain") {
setSource("onchain");
}
}
const activeMethod = createMemo(() => {
if (source() === "lightning") {
return lightningMethod();
} else if (source() === "onchain") {
return onchainMethod();
}
});
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<DefaultMain> <DefaultMain>
@@ -726,23 +780,27 @@ export function Send() {
<AmountEditable <AmountEditable
initialAmountSats={amountSats()} initialAmountSats={amountSats()}
setAmountSats={setAmountInput} setAmountSats={setAmountInput}
maxAmountSats={maxAmountSats()}
fee={feeEstimate()?.toString()} fee={feeEstimate()?.toString()}
onSubmit={() => onSubmit={() =>
sendButtonDisabled() ? undefined : handleSend() sendButtonDisabled() ? undefined : handleSend()
} }
activeMethod={activeMethod()}
methods={sendMethods()}
setChosenMethod={setSourceFromMethod}
/> />
</Show> </Show>
<Show when={!isAmtEditable()}> <Show when={!isAmtEditable()}>
<AmountEditable <AmountEditable
initialAmountSats={amountSats()} initialAmountSats={amountSats()}
setAmountSats={setAmountInput} setAmountSats={setAmountInput}
maxAmountSats={maxAmountSats()}
fee={feeEstimate()?.toString()} fee={feeEstimate()?.toString()}
frozenAmount={true} frozenAmount={true}
onSubmit={() => onSubmit={() =>
sendButtonDisabled() ? undefined : handleSend() sendButtonDisabled() ? undefined : handleSend()
} }
activeMethod={activeMethod()}
methods={sendMethods()}
setChosenMethod={setSourceFromMethod}
/> />
</Show> </Show>
<Show when={feeEstimate()}> <Show when={feeEstimate()}>

View File

@@ -25,10 +25,8 @@ import {
LargeHeader, LargeHeader,
MegaCheck, MegaCheck,
MegaEx, MegaEx,
MethodChooser,
MutinyWalletGuard, MutinyWalletGuard,
NavBar, NavBar,
SafeArea,
showToast, showToast,
SuccessModal, SuccessModal,
TextField, TextField,
@@ -36,7 +34,6 @@ import {
} from "~/components"; } from "~/components";
import { useI18n } from "~/i18n/context"; import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup"; import { Network } from "~/logic/mutinyWalletSetup";
import { SendSource } from "~/routes/Send";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { eify, vibrateSuccess } from "~/utils"; import { eify, vibrateSuccess } from "~/utils";
@@ -57,7 +54,6 @@ export function Swap() {
const navigate = useNavigate(); const navigate = useNavigate();
const i18n = useI18n(); const i18n = useI18n();
const [source, setSource] = createSignal<SendSource>("onchain");
const [amountSats, setAmountSats] = createSignal(0n); const [amountSats, setAmountSats] = createSignal(0n);
const [isConnecting, setIsConnecting] = createSignal(false); const [isConnecting, setIsConnecting] = createSignal(false);
@@ -92,7 +88,6 @@ export function Swap() {
} }
function resetState() { function resetState() {
setSource("onchain");
setAmountSats(0n); setAmountSats(0n);
setIsConnecting(false); setIsConnecting(false);
setLoading(false); setLoading(false);
@@ -275,179 +270,172 @@ export function Swap() {
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <DefaultMain>
<DefaultMain> <BackLink />
<BackLink /> <LargeHeader>{i18n.t("swap.header")}</LargeHeader>
<LargeHeader>{i18n.t("swap.header")}</LargeHeader> <SuccessModal
<SuccessModal confirmText={
confirmText={ channelOpenResult()?.channel
channelOpenResult()?.channel ? i18n.t("common.nice")
? i18n.t("common.nice") : i18n.t("common.home")
: i18n.t("common.home") }
} open={!!channelOpenResult()}
open={!!channelOpenResult()} setOpen={(open: boolean) => {
setOpen={(open: boolean) => { if (!open) resetState();
if (!open) resetState(); }}
}} onConfirm={() => {
onConfirm={() => { resetState();
resetState(); navigate("/");
navigate("/"); }}
}} >
> <Switch>
<Switch> <Match when={channelOpenResult()?.failure_reason}>
<Match when={channelOpenResult()?.failure_reason}> <MegaEx />
<MegaEx /> <h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl"> {channelOpenResult()?.failure_reason
{channelOpenResult()?.failure_reason ? channelOpenResult()?.failure_reason
? channelOpenResult()?.failure_reason ?.message
?.message : ""}
: ""} </h1>
</h1> {/*TODO: Error hint needs to be added for possible failure reasons*/}
{/*TODO: Error hint needs to be added for possible failure reasons*/} </Match>
</Match> <Match when={channelOpenResult()?.channel}>
<Match when={channelOpenResult()?.channel}> <Show when={detailsId() && detailsKind()}>
<Show when={detailsId() && detailsKind()}> <ActivityDetailsModal
<ActivityDetailsModal open={detailsOpen()}
open={detailsOpen()} kind={detailsKind()}
kind={detailsKind()} id={detailsId()}
id={detailsId()} setOpen={setDetailsOpen}
setOpen={setDetailsOpen} />
/>
</Show>
<MegaCheck />
<div class="flex flex-col justify-center">
<h1 class="mb-2 mt-4 w-full justify-center text-center text-2xl font-semibold md:text-3xl">
{i18n.t("swap.initiated")}
</h1>
<p class="text-center text-xl">
{i18n.t("swap.sats_added", {
amount: (
Number(
channelOpenResult()?.channel
?.balance
) +
Number(
channelOpenResult()?.channel
?.reserve
)
).toLocaleString()
})}
</p>
<div class="text-center text-sm text-white/70">
<AmountFiat
amountSats={
Number(
channelOpenResult()?.channel
?.balance
) +
Number(
channelOpenResult()?.channel
?.reserve
)
}
/>
</div>
</div>
<hr class="w-16 bg-m-grey-400" />
<p
class="cursor-pointer underline"
onClick={openDetailsModal}
>
{i18n.t("common.view_payment_details")}
</p>
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
</Match>
</Switch>
</SuccessModal>
<VStack biggap>
<MethodChooser
source={source()}
setSource={setSource}
both={false}
/>
<VStack>
<Show when={!hasLsp()}>
<Card>
<VStack>
<div class="flex w-full flex-col gap-2">
<label
for="peerselect"
class="text-sm font-semibold uppercase"
>
{i18n.t("swap.use_existing")}
</label>
<select
name="peerselect"
class="w-full truncate rounded bg-black px-4 py-2"
onChange={handlePeerSelect}
value={selectedPeer()}
>
<option
value=""
class=""
selected
>
{i18n.t("swap.choose_peer")}
</option>
<For each={peers()}>
{(peer) => (
<option
value={peer.pubkey}
>
{peer.alias ??
peer.pubkey}
</option>
)}
</For>
</select>
</div>
<Show when={!selectedPeer()}>
<Form
onSubmit={onSubmit}
class="flex flex-col gap-4"
>
<Field
name="peer"
validate={[required("")]}
>
{(field, props) => (
<TextField
{...props}
value={field.value}
error={field.error}
label={i18n.t(
"swap.peer_connect_label"
)}
placeholder={i18n.t(
"swap.peer_connect_placeholder"
)}
/>
)}
</Field>
<Button
layout="small"
type="submit"
disabled={isConnecting()}
>
{isConnecting()
? i18n.t(
"swap.connecting"
)
: i18n.t(
"swap.connect"
)}
</Button>
</Form>
</Show>
</VStack>
</Card>
</Show> </Show>
</VStack> <MegaCheck />
<div class="flex flex-col justify-center">
<h1 class="mb-2 mt-4 w-full justify-center text-center text-2xl font-semibold md:text-3xl">
{i18n.t("swap.initiated")}
</h1>
<p class="text-center text-xl">
{i18n.t("swap.sats_added", {
amount: (
Number(
channelOpenResult()?.channel
?.balance
) +
Number(
channelOpenResult()?.channel
?.reserve
)
).toLocaleString()
})}
</p>
<div class="text-center text-sm text-white/70">
<AmountFiat
amountSats={
Number(
channelOpenResult()?.channel
?.balance
) +
Number(
channelOpenResult()?.channel
?.reserve
)
}
/>
</div>
</div>
<hr class="w-16 bg-m-grey-400" />
<p
class="cursor-pointer underline"
onClick={openDetailsModal}
>
{i18n.t("common.view_payment_details")}
</p>
{/* <pre>{JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}</pre> */}
</Match>
</Switch>
</SuccessModal>
<div class="flex flex-1 flex-col justify-between gap-2">
<div class="flex-1" />
<VStack biggap>
<Show when={!hasLsp()}>
<Card>
<VStack>
<div class="flex w-full flex-col gap-2">
<label
for="peerselect"
class="text-sm font-semibold uppercase"
>
{i18n.t("swap.use_existing")}
</label>
<select
name="peerselect"
class="w-full truncate rounded bg-black px-4 py-2"
onChange={handlePeerSelect}
value={selectedPeer()}
>
<option value="" class="" selected>
{i18n.t("swap.choose_peer")}
</option>
<For each={peers()}>
{(peer) => (
<option value={peer.pubkey}>
{peer.alias ??
peer.pubkey}
</option>
)}
</For>
</select>
</div>
<Show when={!selectedPeer()}>
<Form
onSubmit={onSubmit}
class="flex flex-col gap-4"
>
<Field
name="peer"
validate={[required("")]}
>
{(field, props) => (
<TextField
{...props}
value={field.value}
error={field.error}
label={i18n.t(
"swap.peer_connect_label"
)}
placeholder={i18n.t(
"swap.peer_connect_placeholder"
)}
/>
)}
</Field>
<Button
layout="small"
type="submit"
disabled={isConnecting()}
>
{isConnecting()
? i18n.t("swap.connecting")
: i18n.t("swap.connect")}
</Button>
</Form>
</Show>
</VStack>
</Card>
</Show>
<AmountEditable <AmountEditable
initialAmountSats={amountSats()} initialAmountSats={amountSats()}
setAmountSats={setAmountSats} setAmountSats={setAmountSats}
fee={feeEstimate()?.toString()} fee={feeEstimate()?.toString()}
maxAmountSats={maxOnchain()} activeMethod={{
method: "onchain",
maxAmountSats: maxOnchain()
}}
methods={[
{
method: "onchain",
maxAmountSats: maxOnchain()
}
]}
/> />
<Show when={feeEstimate() && amountSats() > 0n}> <Show when={feeEstimate() && amountSats() > 0n}>
<FeeDisplay <FeeDisplay
@@ -471,9 +459,9 @@ export function Swap() {
{i18n.t("swap.confirm_swap")} {i18n.t("swap.confirm_swap")}
</Button> </Button>
</VStack> </VStack>
</DefaultMain> </div>
<NavBar activeTab="none" /> </DefaultMain>
</SafeArea> <NavBar activeTab="none" />
</MutinyWalletGuard> </MutinyWalletGuard>
); );
} }