From feed465d67246287819ad997f29ceca7381d91bf Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Tue, 7 May 2024 19:38:52 -0500 Subject: [PATCH] add transfer funds screen --- public/i18n/en.json | 12 +- src/components/Activity.tsx | 21 ++- src/components/AmountEditable.tsx | 4 +- src/router.tsx | 4 +- src/routes/Transfer.tsx | 216 +++++++++++++++++++++ src/routes/index.ts | 1 + src/routes/settings/ManageFederations.tsx | 217 ++++++++++++---------- src/state/megaStore.tsx | 8 +- src/workers/walletWorker.ts | 10 +- 9 files changed, 377 insertions(+), 116 deletions(-) create mode 100644 src/routes/Transfer.tsx diff --git a/public/i18n/en.json b/public/i18n/en.json index 101da1a7..e7ae1eee 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -265,7 +265,8 @@ "back_home": "back home" }, "start_a_chat": "Start a chat?", - "start_a_chat_are_you_sure": "This user isn't in your contact list." + "start_a_chat_are_you_sure": "This user isn't in your contact list.", + "federation_message": "Federation Message" }, "scanner": { "paste": "Paste Something", @@ -563,7 +564,8 @@ "descriptionpart2": "Each one is run by a group of different inviduals or companies. Discover one that you or your friends might trust below.", "join_me": "Join me", "recommend": "Recommend federation", - "recommended_by_you": "Recommended by you" + "recommended_by_you": "Recommended by you", + "transfer_funds": "Transfer funds" }, "gift": { "give_sats_link": "Give sats as a gift", @@ -784,5 +786,11 @@ "nowish": "Nowish", "seconds_future": "Seconds from now", "seconds_past": "Just now" + }, + "transfer": { + "completed": "Transfer Completed", + "sats_moved": "+{{amount}} sats have been moved to {{federation_name}}", + "confirm": "Confirm Transfer", + "title": "Transfer funds" } } diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index c89ea294..b2a54660 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -375,7 +375,7 @@ function NewContactModal(props: { profile: PseudoContact; close: () => void }) { } export function CombinedActivity() { - const [state, _actions, sw] = useMegaStore(); + const [state, actions, sw] = useMegaStore(); const i18n = useI18n(); const [detailsOpen, setDetailsOpen] = createSignal(false); @@ -448,19 +448,28 @@ export function CombinedActivity() { }> { - if (!open) + if (!open) { setShowFederationExpirationWarning(false); + actions.clearExpirationWarning(); + } }} > -
-                            {JSON.stringify(state.expiration_warning, null, 2)}
-                        
{state.expiration_warning?.expiresMessage} + navigate("/settings/federations")} + > +
+ + + {i18n.t("profile.manage_federation")} + +
+
diff --git a/src/components/AmountEditable.tsx b/src/components/AmountEditable.tsx index ce673570..1906e6e7 100644 --- a/src/components/AmountEditable.tsx +++ b/src/components/AmountEditable.tsx @@ -19,7 +19,7 @@ import { } from "~/utils"; export type MethodChoice = { - method: "lightning" | "onchain"; + method: "lightning" | "onchain" | "fedimint"; maxAmountSats?: bigint; }; @@ -29,6 +29,8 @@ function methodToIcon(method: MethodChoice["method"]) { return "lightning"; } else if (method === "onchain") { return "chain"; + } else if (method === "fedimint") { + return "community"; } } diff --git a/src/router.tsx b/src/router.tsx index 474dffb9..0ad459fc 100644 --- a/src/router.tsx +++ b/src/router.tsx @@ -33,7 +33,8 @@ import { Search, Send, Swap, - SwapLightning + SwapLightning, + Transfer } from "~/routes"; import { Admin, @@ -179,6 +180,7 @@ export function Router() { + diff --git a/src/routes/Transfer.tsx b/src/routes/Transfer.tsx new file mode 100644 index 00000000..f15d78e5 --- /dev/null +++ b/src/routes/Transfer.tsx @@ -0,0 +1,216 @@ +import { FedimintSweepResult } from "@mutinywallet/mutiny-wasm"; +import { createAsync, useNavigate, useSearchParams } from "@solidjs/router"; +import { ArrowDown, Users } from "lucide-solid"; +import { createMemo, createSignal, Match, Suspense, Switch } from "solid-js"; + +import { + AmountEditable, + AmountFiat, + AmountSats, + BackLink, + Button, + DefaultMain, + Failure, + Fee, + LargeHeader, + MegaCheck, + MutinyWalletGuard, + SharpButton, + SuccessModal, + VStack +} from "~/components"; +import { useI18n } from "~/i18n/context"; +import { useMegaStore } from "~/state/megaStore"; +import { eify, vibrateSuccess } from "~/utils"; + +type TransferResultDetails = { + result?: FedimintSweepResult; + failure_reason?: string; +}; + +export function Transfer() { + const [state, _actions, sw] = useMegaStore(); + const i18n = useI18n(); + const navigate = useNavigate(); + const [amountSats, setAmountSats] = createSignal(0n); + const [loading, setLoading] = createSignal(false); + const [params] = useSearchParams(); + + const canTransfer = createMemo(() => { + return true; + }); + + const [transferResult, setTransferResult] = + createSignal(); + + const fromFed = () => { + return state.federations?.find((f) => f.federation_id === params.from); + }; + + const toFed = () => { + return state.federations?.find((f) => f.federation_id !== params.from); + }; + + const federationBalances = createAsync(async () => { + try { + const balances = await sw.get_federation_balances(); + return balances?.balances || []; + } catch (e) { + console.error(e); + return []; + } + }); + + const calculateMaxFederation = createAsync(async () => { + return federationBalances()?.find( + (f) => f.identity_federation_id === fromFed()?.federation_id + )?.balance; + }); + + const toBalance = createAsync(async () => { + return federationBalances()?.find( + (f) => f.identity_federation_id === toFed()?.federation_id + )?.balance; + }); + + const isMax = createMemo(() => { + return amountSats() === calculateMaxFederation(); + }); + + async function handleTransfer() { + try { + setLoading(true); + if (!fromFed()) throw new Error("No from federation"); + if (!toFed()) throw new Error("No to federation"); + + if (isMax()) { + const result = await sw.sweep_federation_balance( + undefined, + fromFed()?.federation_id, + toFed()?.federation_id + ); + + setTransferResult({ result: result }); + } else { + const result = await sw.sweep_federation_balance( + amountSats(), + fromFed()?.federation_id, + toFed()?.federation_id + ); + + setTransferResult({ result: result }); + } + + await vibrateSuccess(); + } catch (e) { + const error = eify(e); + setTransferResult({ failure_reason: error.message }); + console.error(e); + } finally { + setLoading(false); + } + } + + // const fromFederatationId = params.from; + + return ( + + + { + if (!open) setTransferResult(undefined); + }} + onConfirm={() => { + setTransferResult(undefined); + navigate("/"); + }} + > + + + + + + +
+

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

+

+ {i18n.t("transfer.sats_moved", { + amount: Number( + transferResult()?.result?.amount + ).toLocaleString(), + federation_name: + toFed()?.federation_name + })} +

+
+ + + +
+
+
+ +
+
+
+ + {i18n.t("transfer.title")} +
+
+
+ + {}}> + + {fromFed()?.federation_name} + + + + {}}> + + {toFed()?.federation_name} + + +
+
+ + + +
+ + + ); +} diff --git a/src/routes/index.ts b/src/routes/index.ts index cd7fa32f..6d6ccb25 100644 --- a/src/routes/index.ts +++ b/src/routes/index.ts @@ -12,3 +12,4 @@ export * from "./Request"; export * from "./EditProfile"; export * from "./Swap"; export * from "./SwapLightning"; +export * from "./Transfer"; diff --git a/src/routes/settings/ManageFederations.tsx b/src/routes/settings/ManageFederations.tsx index 816b70ea..ffa5f839 100644 --- a/src/routes/settings/ManageFederations.tsx +++ b/src/routes/settings/ManageFederations.tsx @@ -7,8 +7,9 @@ import { } from "@modular-forms/solid"; import { FederationBalance, TagItem } from "@mutinywallet/mutiny-wasm"; import { A, useNavigate, useSearchParams } from "@solidjs/router"; -import { BadgeCheck, LogOut, Scan, Trash } from "lucide-solid"; +import { ArrowLeftRight, BadgeCheck, LogOut, Scan, Trash } from "lucide-solid"; import { + createMemo, createResource, createSignal, For, @@ -241,102 +242,12 @@ export function AddFederationForm(props: { {(fed) => ( - - -
-
-                                                {JSON.stringify(fed, null, 2)}
-                                            
- -
-
- {fed.metadata?.name} -
- -

{fed.metadata?.about}

-
-
-
- - - - - - - - - - - 0 - } - > - -
- - {(contact) => ( - - )} - -
-
-
- - - -
-
+ )}
@@ -346,6 +257,94 @@ export function AddFederationForm(props: { ); } +function FederationFormItem(props: { + fed: DiscoveredFederation; + onSelect: (invite_codes: string[]) => void; + loadingFederation: string; + setup: boolean; +}) { + const [state, _actions, _sw] = useMegaStore(); + const i18n = useI18n(); + + const alreadyAdded = createMemo(() => { + const matches = state.federations?.find((f) => + props.fed.invite_codes.includes(f.invite_code) + ); + return matches !== undefined; + }); + return ( + + +
+ +
+
+ {props.fed.metadata?.name} +
+ +

{props.fed.metadata?.about}

+
+
+
+ + + + + + + + + + + 0}> + +
+ + {(contact) => ( + + )} + +
+
+
+ + + +
+
+ ); +} + function RecommendButton(props: { fed: MutinyFederationIdentity }) { const [_state, _actions, sw] = useMegaStore(); const i18n = useI18n(); @@ -429,7 +428,8 @@ function FederationListItem(props: { balance?: bigint; }) { const i18n = useI18n(); - const [_state, actions, sw] = useMegaStore(); + const [state, actions, sw] = useMegaStore(); + const navigate = useNavigate(); async function removeFederation() { setConfirmLoading(true); @@ -446,6 +446,10 @@ function FederationListItem(props: { setConfirmOpen(true); } + async function transferFunds() { + navigate("/transfer?from=" + props.fed.federation_id); + } + const [confirmOpen, setConfirmOpen] = createSignal(false); const [confirmLoading, setConfirmLoading] = createSignal(false); @@ -453,7 +457,7 @@ function FederationListItem(props: { <> -
{JSON.stringify(props.fed, null, 2)}
+ {/*
{JSON.stringify(props.fed, null, 2)}
*/}
{props.fed.federation_name} @@ -495,6 +499,19 @@ function FederationListItem(props: { inviteCode={props.fed.invite_code} /> + + + + {i18n.t( + "settings.manage_federations.transfer_funds" + )} + + @@ -611,7 +628,7 @@ export function ManageFederations() { - + diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx index 7c8db530..1633c04b 100644 --- a/src/state/megaStore.tsx +++ b/src/state/megaStore.tsx @@ -262,10 +262,6 @@ export const makeMegaStoreContext = () => { ); const expiresMessage = specificFederation.popup_countdown_message; - console.log( - "federation message", - expiresMessage - ); expiration_warning = { expiresTimestamp, expiresMessage @@ -557,6 +553,10 @@ export const makeMegaStoreContext = () => { channel.postMessage({ type: "EXISTING_TAB" }); } }; + }, + // Only show the expiration warning once per session + clearExpirationWarning() { + setState({ expiration_warning: undefined }); } }; diff --git a/src/workers/walletWorker.ts b/src/workers/walletWorker.ts index c7b073ad..de67cdd0 100644 --- a/src/workers/walletWorker.ts +++ b/src/workers/walletWorker.ts @@ -1529,9 +1529,15 @@ export async function estimate_sweep_channel_open_fee( * @returns {Promise} */ export async function sweep_federation_balance( - amount?: bigint + amount?: bigint, + from_federation_id?: string, + to_federation_id?: string ): Promise { - const result = await wallet!.sweep_federation_balance(amount); + const result = await wallet!.sweep_federation_balance( + amount, + from_federation_id, + to_federation_id + ); return { ...result.value } as FedimintSweepResult; }