From a3e356dc090edddee8c74249e7f1e0bfade340a3 Mon Sep 17 00:00:00 2001 From: Tony Giorgio Date: Thu, 1 Feb 2024 05:20:04 -0600 Subject: [PATCH] Add lightning sweep functionality to fedimint --- src/components/BalanceBox.tsx | 45 ++++--- src/components/Failure.tsx | 86 +++++++++++++ src/components/index.ts | 1 + src/i18n/en/translations.ts | 8 ++ src/router.tsx | 4 +- src/routes/Send.tsx | 37 +----- src/routes/SwapLightning.tsx | 228 ++++++++++++++++++++++++++++++++++ src/routes/index.ts | 1 + 8 files changed, 359 insertions(+), 51 deletions(-) create mode 100644 src/components/Failure.tsx create mode 100644 src/routes/SwapLightning.tsx diff --git a/src/components/BalanceBox.tsx b/src/components/BalanceBox.tsx index b0ddba9..f1755f2 100644 --- a/src/components/BalanceBox.tsx +++ b/src/components/BalanceBox.tsx @@ -94,21 +94,38 @@ export function BalanceBox(props: { loading?: boolean }) { }>
-
-
- -
-
- +
+
+
+ +
+
+ +
+ 0n}> +
+ + swaplightning + +
+
diff --git a/src/components/Failure.tsx b/src/components/Failure.tsx new file mode 100644 index 0000000..7c4465b --- /dev/null +++ b/src/components/Failure.tsx @@ -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 ( + + + +

+ {i18n.t("send.payment_pending")} +

+ + {i18n.t("send.payment_pending_description")} + +
+ + +

+ {i18n.t("send.error_channel_reserves")} +

+ + {i18n.t("send.error_channel_reserves_explained")}{" "} + {i18n.t("common.why")} + +
+ + +

+ {props.reason} +

+
+
+ ); +} diff --git a/src/components/index.ts b/src/components/index.ts index 0bd0e28..3b6a779 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -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"; diff --git a/src/i18n/en/translations.ts b/src/i18n/en/translations.ts index c9c9eff..89ccc6b 100644 --- a/src/i18n/en/translations.ts +++ b/src/i18n/en/translations.ts @@ -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: diff --git a/src/router.tsx b/src/router.tsx index 8eea78a..557331e 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -15,7 +15,8 @@ import { Scanner, Search, Send, - Swap + Swap, + SwapLightning } from "~/routes"; import { Admin, @@ -101,6 +102,7 @@ export function Router() { + diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index 4d4164e..14c76f0 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -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 ( - - - -

- {i18n.t("send.payment_pending")} -

- - {i18n.t("send.payment_pending_description")} - -
- - -

- {i18n.t("send.error_channel_reserves")} -

- - {i18n.t("send.error_channel_reserves_explained")}{" "} - {i18n.t("common.why")} - -
- - -

- {props.reason} -

-
-
- ); -} - export function Send() { const [state, actions] = useMegaStore(); const navigate = useNavigate(); diff --git a/src/routes/SwapLightning.tsx b/src/routes/SwapLightning.tsx new file mode 100644 index 0000000..bae3967 --- /dev/null +++ b/src/routes/SwapLightning.tsx @@ -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(); + const [detailsId, setDetailsId] = createSignal(""); + + const [sweepResult, setSweepResult] = createSignal(); + + 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 ( + + + + {i18n.t("swap.header")} + { + if (!open) resetState(); + }} + onConfirm={() => { + resetState(); + navigate("/"); + }} + > + + + + + + + + + +
+

+ {i18n.t("swap.completed")} +

+

+ {i18n.t("swap.sats_added", { + amount: Number( + sweepResult()?.result?.amount + ).toLocaleString() + })} +

+
+ +
+
+
+ +
+
+
+
+
+ + + 0n}> + {amountWarning()} + + +
+ + + +
+ + + + ); +} diff --git a/src/routes/index.ts b/src/routes/index.ts index 923dec3..a47f92d 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -7,4 +7,5 @@ export * from "./Receive"; export * from "./Scanner"; export * from "./Send"; export * from "./Swap"; +export * from "./SwapLightning"; export * from "./Search";