Add lightning sweep functionality to fedimint

This commit is contained in:
Tony Giorgio
2024-02-01 05:20:04 -06:00
committed by Tony Giorgio
parent 666309fa70
commit a3e356dc09
8 changed files with 359 additions and 51 deletions

View File

@@ -94,21 +94,38 @@ export function BalanceBox(props: { loading?: boolean }) {
<Show when={state.federations && state.federations.length}>
<Show when={!props.loading} fallback={<LoadingShimmer />}>
<hr class="my-2 border-m-grey-750" />
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={state.balance?.federation || 0}
icon="community"
denominationSize="lg"
isFederation
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={state.balance?.federation || 0n}
denominationSize="sm"
/>
<div class="flex justify-between">
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={
state.balance?.federation || 0
}
icon="community"
denominationSize="lg"
isFederation
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={
state.balance?.federation || 0n
}
denominationSize="sm"
/>
</div>
</div>
<Show when={state.balance?.federation || 0n > 0n}>
<div class="self-end justify-self-end">
<A href="/swaplightning" class={STYLE}>
<img
src={shuffle}
alt="swaplightning"
class="h-6 w-6"
/>
</A>
</div>
</Show>
</div>
</Show>
</Show>

View File

@@ -0,0 +1,86 @@
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
import { A, useNavigate, useSearchParams } from "@solidjs/router";
import {
createEffect,
createMemo,
createResource,
createSignal,
JSX,
Match,
onMount,
Show,
Suspense,
Switch
} from "solid-js";
import bolt from "~/assets/icons/bolt.svg";
import chain from "~/assets/icons/chain.svg";
import close from "~/assets/icons/close.svg";
import {
ActivityDetailsModal,
AmountEditable,
AmountFiat,
AmountSats,
BackPop,
Button,
DefaultMain,
Fee,
FeeDisplay,
HackActivityType,
InfoBox,
LabelCircle,
LoadingShimmer,
MegaCheck,
MegaClock,
MegaEx,
MethodChoice,
MutinyWalletGuard,
NavBar,
showToast,
SimpleInput,
SmallHeader,
StringShower,
SuccessModal,
UnstyledBackPop,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { ParsedParams } from "~/logic/waila";
import { useMegaStore } from "~/state/megaStore";
import { eify, vibrateSuccess } from "~/utils";
export function Failure(props: { reason: string }) {
const i18n = useI18n();
return (
<Switch>
<Match when={props.reason === "Payment timed out."}>
<MegaClock />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{i18n.t("send.payment_pending")}
</h1>
<InfoBox accent="white">
{i18n.t("send.payment_pending_description")}
</InfoBox>
</Match>
<Match
when={props.reason === "Channel reserve amount is too high."}
>
<MegaEx />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{i18n.t("send.error_channel_reserves")}
</h1>
<InfoBox accent="white">
{i18n.t("send.error_channel_reserves_explained")}{" "}
<A href="/settings/channels">{i18n.t("common.why")}</A>
</InfoBox>
</Match>
<Match when={true}>
<MegaEx />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{props.reason}
</h1>
</Match>
</Switch>
);
}

View File

@@ -36,6 +36,7 @@ export * from "./Restart";
export * from "./ResyncOnchain";
export * from "./SeedWords";
export * from "./SetupErrorDisplay";
export * from "./Failure";
export * from "./ShareCard";
export * from "./Toaster";
export * from "./NostrActivity";

View File

@@ -600,6 +600,14 @@ export default {
connecting: "Connecting...",
confirm_swap: "Confirm Swap"
},
swap_lightning: {
insufficient_funds: "You don't have enough funds to swap to lightning",
header: "Swap to Lightning",
completed: "Swap Completed",
sats_added: "+{{amount}} sats have been added to your Lightning balance",
sats_fee: "+{{amount}} sats fee",
confirm_swap: "Confirm Swap"
},
reload: {
mutiny_update: "Mutiny Update",
new_version_description:

View File

@@ -15,7 +15,8 @@ import {
Scanner,
Search,
Send,
Swap
Swap,
SwapLightning
} from "~/routes";
import {
Admin,
@@ -101,6 +102,7 @@ export function Router() {
<Route path="/scanner" component={Scanner} />
<Route path="/send" component={Send} />
<Route path="/swap" component={Swap} />
<Route path="/swaplightning" component={SwapLightning} />
<Route path="/search" component={Search} />
<Route path="/settings">
<Route path="/" component={Settings} />

View File

@@ -25,6 +25,7 @@ import {
BackPop,
Button,
DefaultMain,
Failure,
Fee,
FeeDisplay,
HackActivityType,
@@ -190,42 +191,6 @@ function DestinationItem(props: {
);
}
function Failure(props: { reason: string }) {
const i18n = useI18n();
return (
<Switch>
<Match when={props.reason === "Payment timed out."}>
<MegaClock />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{i18n.t("send.payment_pending")}
</h1>
<InfoBox accent="white">
{i18n.t("send.payment_pending_description")}
</InfoBox>
</Match>
<Match
when={props.reason === "Channel reserve amount is too high."}
>
<MegaEx />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{i18n.t("send.error_channel_reserves")}
</h1>
<InfoBox accent="white">
{i18n.t("send.error_channel_reserves_explained")}{" "}
<A href="/settings/channels">{i18n.t("common.why")}</A>
</InfoBox>
</Match>
<Match when={true}>
<MegaEx />
<h1 class="mb-2 mt-4 w-full text-center text-2xl font-semibold md:text-3xl">
{props.reason}
</h1>
</Match>
</Switch>
);
}
export function Send() {
const [state, actions] = useMegaStore();
const navigate = useNavigate();

View File

@@ -0,0 +1,228 @@
import { createForm, required } from "@modular-forms/solid";
import { FedimintSweepResult } from "@mutinywallet/mutiny-wasm";
import { useNavigate } from "@solidjs/router";
import {
createMemo,
createResource,
createSignal,
For,
Match,
Show,
Switch
} from "solid-js";
import {
ActivityDetailsModal,
AmountEditable,
AmountFiat,
BackLink,
Button,
Card,
DefaultMain,
Failure,
Fee,
HackActivityType,
InfoBox,
LargeHeader,
MegaCheck,
MegaEx,
MutinyWalletGuard,
NavBar,
showToast,
SuccessModal,
TextField,
VStack
} from "~/components";
import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { useMegaStore } from "~/state/megaStore";
import { eify, vibrateSuccess } from "~/utils";
type SweepResultDetails = {
result?: FedimintSweepResult;
failure_reason?: string;
};
export function SwapLightning() {
const [state, _actions] = useMegaStore();
const navigate = useNavigate();
const i18n = useI18n();
const [amountSats, setAmountSats] = createSignal(0n);
const [loading, setLoading] = createSignal(false);
// Details Modal
const [detailsOpen, setDetailsOpen] = createSignal(false);
const [detailsKind, setDetailsKind] = createSignal<HackActivityType>();
const [detailsId, setDetailsId] = createSignal("");
const [sweepResult, setSweepResult] = createSignal<SweepResultDetails>();
function resetState() {
setAmountSats(0n);
setLoading(false);
setSweepResult(undefined);
}
const handleSwap = async () => {
if (canSwap()) {
try {
setLoading(true);
if (isMax()) {
const result =
await state.mutiny_wallet?.sweep_federation_balance(
undefined
);
setSweepResult({ result: result });
} else {
const result =
await state.mutiny_wallet?.sweep_federation_balance(
amountSats()
);
setSweepResult({ result: result });
}
await vibrateSuccess();
} catch (e) {
const error = eify(e);
setSweepResult({ failure_reason: error.message });
console.error(e);
} finally {
setLoading(false);
}
}
};
const canSwap = () => {
const balance = state.balance?.federation || 0n;
const network = state.mutiny_wallet?.get_network() as Network;
return amountSats() <= balance;
};
const amountWarning = () => {
if (amountSats() === 0n || !!sweepResult() || loading()) {
return undefined;
}
if (amountSats() > (state.balance?.federation || 0n)) {
return i18n.t("swap.insufficient_funds");
}
return undefined;
};
function calculateMaxFederation() {
return state.balance?.federation ?? 0n;
}
const maxFederationBalance = createMemo(() => {
return calculateMaxFederation();
});
const isMax = createMemo(() => {
return amountSats() === calculateMaxFederation();
});
return (
<MutinyWalletGuard>
<DefaultMain>
<BackLink />
<LargeHeader>{i18n.t("swap.header")}</LargeHeader>
<SuccessModal
confirmText={
sweepResult()?.result
? i18n.t("common.nice")
: i18n.t("common.home")
}
open={!!sweepResult()}
setOpen={(open: boolean) => {
if (!open) resetState();
}}
onConfirm={() => {
resetState();
navigate("/");
}}
>
<Switch>
<Match when={sweepResult()?.failure_reason}>
<Failure reason={sweepResult()?.failure_reason} />
</Match>
<Match when={sweepResult()?.result}>
<Show when={detailsId() && detailsKind()}>
<ActivityDetailsModal
open={detailsOpen()}
kind={detailsKind()}
id={detailsId()}
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.completed")}
</h1>
<p class="text-center text-xl">
{i18n.t("swap.sats_added", {
amount: Number(
sweepResult()?.result?.amount
).toLocaleString()
})}
</p>
<div class="text-center text-sm text-white/70">
<AmountFiat
amountSats={Number(
sweepResult()?.result?.amount
)}
/>
</div>
</div>
<hr class="w-16 bg-m-grey-400" />
<Fee
amountSats={Number(sweepResult()?.result?.fees)}
/>
</Match>
</Switch>
</SuccessModal>
<div class="flex flex-1 flex-col justify-between gap-2">
<div class="flex-1" />
<VStack biggap>
<AmountEditable
initialAmountSats={amountSats()}
setAmountSats={setAmountSats}
activeMethod={{
method: "lightning",
maxAmountSats: maxFederationBalance()
}}
methods={[
{
method: "lightning",
maxAmountSats: maxFederationBalance()
}
]}
/>
<Show when={amountWarning() && amountSats() > 0n}>
<InfoBox accent={"red"}>{amountWarning()}</InfoBox>
</Show>
</VStack>
<div class="flex-1" />
<VStack>
<Button
disabled={!canSwap()}
intent="blue"
onClick={handleSwap}
loading={loading()}
>
{i18n.t("swap.confirm_swap")}
</Button>
</VStack>
</div>
</DefaultMain>
<NavBar activeTab="none" />
</MutinyWalletGuard>
);
}

View File

@@ -7,4 +7,5 @@ export * from "./Receive";
export * from "./Scanner";
export * from "./Send";
export * from "./Swap";
export * from "./SwapLightning";
export * from "./Search";