From fa6a547bc89551fdeb9253ce457d9accb75c9c8d Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Tue, 7 May 2024 10:22:19 -0500 Subject: [PATCH] add transfer funds screen --- public/i18n/en.json | 12 +- src/components/Activity.tsx | 40 +++- src/components/AmountEditable.tsx | 4 +- src/router.tsx | 4 +- src/routes/Transfer.tsx | 216 ++++++++++++++++++++++ src/routes/index.ts | 1 + src/routes/settings/ManageFederations.tsx | 214 +++++++++++---------- src/state/megaStore.tsx | 59 +++++- src/workers/walletWorker.ts | 11 +- 9 files changed, 454 insertions(+), 107 deletions(-) create mode 100644 src/routes/Transfer.tsx diff --git a/public/i18n/en.json b/public/i18n/en.json index bb0d2611..5ae7b896 100644 --- a/public/i18n/en.json +++ b/public/i18n/en.json @@ -263,7 +263,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", @@ -561,7 +562,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", @@ -782,5 +784,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 5bd5dcd0..592b8511 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -8,6 +8,7 @@ import { createSignal, For, Match, + onMount, Show, Suspense, Switch @@ -364,7 +365,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); @@ -407,6 +408,17 @@ export function CombinedActivity() { const [newContact, setNewContact] = createSignal(); + const [ + showFederationExpirationWarning, + setShowFederationExpirationWarning + ] = createSignal(false); + + onMount(() => { + if (state.expiration_warning) { + setShowFederationExpirationWarning(true); + } + }); + return ( <> @@ -424,6 +436,32 @@ export function CombinedActivity() { /> }> + + { + if (!open) { + setShowFederationExpirationWarning(false); + actions.clearExpirationWarning(); + } + }} + > + + {state.expiration_warning?.expiresMessage} + + navigate("/settings/federations")} + > +
+ + + {i18n.t("profile.manage_federation")} + +
+
+
+
+ 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 fe339615..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, @@ -57,6 +58,7 @@ export type MutinyFederationIdentity = { welcome_message: string; federation_expiry_timestamp: number; invite_code: string; + meta_external_url?: string; }; export type Metadata = { @@ -240,99 +242,12 @@ export function AddFederationForm(props: { {(fed) => ( - - -
- -
-
- {fed.metadata?.name} -
- -

{fed.metadata?.about}

-
-
-
- - - - - - - - - - - 0 - } - > - -
- - {(contact) => ( - - )} - -
-
-
- - - -
-
+ )}
@@ -342,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(); @@ -425,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); @@ -442,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); @@ -449,6 +457,7 @@ function FederationListItem(props: { <> + {/*
{JSON.stringify(props.fed, null, 2)}
*/}
{props.fed.federation_name} @@ -490,6 +499,19 @@ function FederationListItem(props: { inviteCode={props.fed.invite_code} /> + + + + {i18n.t( + "settings.manage_federations.transfer_funds" + )} + + @@ -606,7 +628,7 @@ export function ManageFederations() { - + diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx index 8bd49230..1633c04b 100644 --- a/src/state/megaStore.tsx +++ b/src/state/megaStore.tsx @@ -87,7 +87,10 @@ export const makeMegaStoreContext = () => { testflightPromptDismissed: localStorage.getItem("testflightPromptDismissed") === "true", federations: undefined as MutinyFederationIdentity[] | undefined, - balanceView: localStorage.getItem("balanceView") || "sats" + balanceView: localStorage.getItem("balanceView") || "sats", + expiration_warning: undefined as + | { expiresTimestamp: number; expiresMessage: string } + | undefined }); const actions = { @@ -226,15 +229,59 @@ export const makeMegaStoreContext = () => { const balance = await sw.get_balance(); // Get federations - const federations = - (await sw.list_federations()) as MutinyFederationIdentity[]; + const federations = await sw.list_federations(); + + let expiration_warning: + | { expiresTimestamp: number; expiresMessage: string } + | undefined = undefined; + + try { + if (federations.length) { + const activeFederation = federations[0]; + const metadataUrl = activeFederation.meta_external_url; + console.log("federation metadata url", metadataUrl); + if (metadataUrl) { + const response = await fetch(metadataUrl); + if (response.ok) { + const metadata = await response.json(); + console.log( + "all federation metadata", + metadata + ); + const specificFederation = + metadata[activeFederation.federation_id]; + console.log( + "specific federation metadata", + specificFederation + ); + const expiresTimestamp = + specificFederation.popup_end_timestamp; + console.log( + "federation expires", + expiresTimestamp + ); + const expiresMessage = + specificFederation.popup_countdown_message; + expiration_warning = { + expiresTimestamp, + expiresMessage + }; + } + } + } + } catch (e) { + console.error("Error getting federation metadata", e); + } + + console.log("expiration_warning", expiration_warning); setState({ wallet_loading: false, load_stage: "done", balance, federations, - network: network as Network + network: network as Network, + expiration_warning }); // Timestamp our initialization for double init defense @@ -506,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 8f5d54b5..c75fdafa 100644 --- a/src/workers/walletWorker.ts +++ b/src/workers/walletWorker.ts @@ -217,6 +217,7 @@ export async function get_balance(): Promise { */ export async function list_federations(): Promise { const federations = await wallet!.list_federations(); + console.log("list_federations", federations); return federations as MutinyFederationIdentity[]; } @@ -1548,9 +1549,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; }