mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-01-19 14:14:22 +01:00
Add lightning sweep functionality to fedimint
This commit is contained in:
committed by
Tony Giorgio
parent
666309fa70
commit
a3e356dc09
@@ -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>
|
||||
|
||||
86
src/components/Failure.tsx
Normal file
86
src/components/Failure.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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();
|
||||
|
||||
228
src/routes/SwapLightning.tsx
Normal file
228
src/routes/SwapLightning.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -7,4 +7,5 @@ export * from "./Receive";
|
||||
export * from "./Scanner";
|
||||
export * from "./Send";
|
||||
export * from "./Swap";
|
||||
export * from "./SwapLightning";
|
||||
export * from "./Search";
|
||||
|
||||
Reference in New Issue
Block a user