put federations in megastore

This commit is contained in:
Paul Miller
2023-12-19 16:30:53 -06:00
committed by Tony Giorgio
parent 6dcdbae4bc
commit e1cfae2f7f
8 changed files with 147 additions and 149 deletions

View File

@@ -25,8 +25,8 @@ const settingsRoutes = [
"/plus", "/plus",
"/restore", "/restore",
"/servers", "/servers",
"/syncnostrcontacts" "/syncnostrcontacts",
"/managefederations" "/federations"
]; ];
const settingsRoutesPrefixed = settingsRoutes.map((route) => { const settingsRoutesPrefixed = settingsRoutes.map((route) => {
@@ -128,7 +128,7 @@ test("visit each route", async ({ page }) => {
// Manage Federations // Manage Federations
await checkRoute( await checkRoute(
page, page,
"/settings/managefederations", "/settings/federations",
"Manage Federations", "Manage Federations",
checklist checklist
); );

View File

@@ -19,7 +19,6 @@ import currencySwap from "~/assets/icons/currency-swap.svg";
import pencil from "~/assets/icons/pencil.svg"; import pencil from "~/assets/icons/pencil.svg";
import { Button, FeesModal, InfoBox, InlineAmount, VStack } from "~/components"; import { Button, FeesModal, InfoBox, InlineAmount, VStack } from "~/components";
import { useI18n } from "~/i18n/context"; import { useI18n } from "~/i18n/context";
import { Network } from "~/logic/mutinyWalletSetup";
import { useMegaStore } from "~/state/megaStore"; import { useMegaStore } from "~/state/megaStore";
import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs"; import { DIALOG_CONTENT, DIALOG_POSITIONER } from "~/styles/dialogs";
import { Currency, fiatToSats, satsToFiat } from "~/utils"; import { Currency, fiatToSats, satsToFiat } from "~/utils";
@@ -404,17 +403,13 @@ export const AmountEditable: ParentComponent<{
}); });
const warningText = () => { const warningText = () => {
if (state.federations?.length !== 0) {
return undefined;
}
if ((state.balance?.lightning || 0n) === 0n) { if ((state.balance?.lightning || 0n) === 0n) {
const network = state.mutiny_wallet?.get_network() as Network; return i18n.t("receive.amount_editable.receive_too_small", {
if (network === "bitcoin") { amount: "100,000"
return i18n.t("receive.amount_editable.receive_too_small", { });
amount: "100,000"
});
} else {
return i18n.t("receive.amount_editable.receive_too_small", {
amount: "10,000"
});
}
} }
const parsed = Number(localSats()); const parsed = Number(localSats());

View File

@@ -1,5 +1,5 @@
import { A, useNavigate } from "@solidjs/router"; import { A, useNavigate } from "@solidjs/router";
import { createResource, Match, Show, Suspense, Switch } from "solid-js"; import { Match, Show, Switch } from "solid-js";
import shuffle from "~/assets/icons/shuffle.svg"; import shuffle from "~/assets/icons/shuffle.svg";
import { import {
@@ -91,10 +91,25 @@ export function BalanceBox(props: { loading?: boolean }) {
</Match> </Match>
</Switch> </Switch>
</Show> </Show>
<Show when={!props.loading && !state.safe_mode}> <Show when={state.federations && state.federations.length}>
<Suspense> <Show when={!props.loading} fallback={<LoadingShimmer />}>
<FederationsBalance /> <hr class="my-2 border-m-grey-750" />
</Suspense> <div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={state.balance?.federation || 0}
icon="community"
denominationSize="lg"
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={state.balance?.federation || 0n}
denominationSize="sm"
/>
</div>
</div>
</Show>
</Show> </Show>
<hr class="my-2 border-m-grey-750" /> <hr class="my-2 border-m-grey-750" />
<Show when={!props.loading} fallback={<LoadingShimmer />}> <Show when={!props.loading} fallback={<LoadingShimmer />}>
@@ -157,39 +172,3 @@ export function BalanceBox(props: { loading?: boolean }) {
</> </>
); );
} }
function FederationsBalance() {
const [state, _actions] = useMegaStore();
async function fetchFederations() {
const result = await state.mutiny_wallet?.list_federations();
return result ?? [];
}
const [federations] = createResource(fetchFederations);
return (
<Show when={federations() && federations().length}>
<hr class="my-2 border-m-grey-750" />
<Switch>
<Match when={true}>
<div class="flex flex-col gap-1">
<div class="text-2xl">
<AmountSats
amountSats={state.balance?.federation || 0}
icon="community"
denominationSize="lg"
/>
</div>
<div class="text-lg text-white/70">
<AmountFiat
amountSats={state.balance?.federation || 0n}
denominationSize="sm"
/>
</div>
</div>
</Match>
</Switch>
</Show>
);
}

View File

@@ -538,10 +538,15 @@ export default {
federation_code_label: "Federation code", federation_code_label: "Federation code",
federation_code_required: "Federation code can't be blank", federation_code_required: "Federation code can't be blank",
federation_added_success: "Federation added successfully", federation_added_success: "Federation added successfully",
federation_remove_confirm:
"Are you sure you want to remove this federation? Make sure any funds you have are transferred to your lightning balance or another wallet first.",
add: "Add", add: "Add",
remove: "Remove", remove: "Remove",
expires: "Expires", expires: "Expires",
federation_id: "Federation ID" federation_id: "Federation ID",
description:
"Mutiny has experimental support for the Fedimint protocol. You'll need a federation invite code to use this feature. These funds are currently not backed up remotely. Store funds in a federation at your own risk!",
learn_more: "Learn more about Fedimint."
}, },
gift: { gift: {
give_sats_link: "Give sats as a gift", give_sats_link: "Give sats as a gift",

View File

@@ -26,12 +26,12 @@ import {
EmergencyKit, EmergencyKit,
Encrypt, Encrypt,
Gift, Gift,
ManageFederations,
Plus, Plus,
Restore, Restore,
Servers, Servers,
Settings, Settings,
SyncNostrContacts, SyncNostrContacts
ManageFederations
} from "~/routes/settings"; } from "~/routes/settings";
import { useMegaStore } from "./state/megaStore"; import { useMegaStore } from "./state/megaStore";
@@ -119,7 +119,10 @@ export function Router() {
path="/syncnostrcontacts" path="/syncnostrcontacts"
component={SyncNostrContacts} component={SyncNostrContacts}
/> />
<Route path="/managefederations" component={ManageFederations} /> <Route
path="/federations"
component={ManageFederations}
/>
</Route> </Route>
<Route path="/*all" component={NotFound} /> <Route path="/*all" component={NotFound} />
</Routes> </Routes>

View File

@@ -1,10 +1,17 @@
import { createForm, required, SubmitHandler } from "@modular-forms/solid"; import {
import { createResource, createSignal, For, Show } from "solid-js"; createForm,
required,
reset,
SubmitHandler
} from "@modular-forms/solid";
import { createSignal, For, Show } from "solid-js";
import { import {
BackLink, BackLink,
Button, Button,
ConfirmDialog,
DefaultMain, DefaultMain,
ExternalLink,
FancyCard, FancyCard,
InfoBox, InfoBox,
KeyValue, KeyValue,
@@ -12,6 +19,7 @@ import {
MiniStringShower, MiniStringShower,
MutinyWalletGuard, MutinyWalletGuard,
NavBar, NavBar,
NiceP,
SafeArea, SafeArea,
TextField, TextField,
VStack VStack
@@ -24,16 +32,16 @@ type FederationForm = {
federation_code: string; federation_code: string;
}; };
type MutinyFederationIdentity = { export type MutinyFederationIdentity = {
federation_id: string; federation_id: string;
federation_name: string; federation_name: string;
welcome_message: string; welcome_message: string;
federation_expiry_timestamp: number; federation_expiry_timestamp: number;
}; };
function AddFederationForm(props: { refetch: () => void }) { function AddFederationForm() {
const i18n = useI18n(); const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, actions] = useMegaStore();
const [error, setError] = createSignal<Error>(); const [error, setError] = createSignal<Error>();
const [success, setSuccess] = createSignal(""); const [success, setSuccess] = createSignal("");
@@ -56,7 +64,8 @@ function AddFederationForm(props: { refetch: () => void }) {
setSuccess( setSuccess(
i18n.t("settings.manage_federations.federation_added_success") i18n.t("settings.manage_federations.federation_added_success")
); );
await props.refetch(); await actions.refreshFederations();
reset(feedbackForm);
} catch (e) { } catch (e) {
console.error("Error submitting federation:", e); console.error("Error submitting federation:", e);
setError(eify(e)); setError(eify(e));
@@ -89,90 +98,89 @@ function AddFederationForm(props: { refetch: () => void }) {
/> />
)} )}
</Field> </Field>
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<Show when={success()}>
<InfoBox accent="green">{success()}</InfoBox>
</Show>
<Button <Button
loading={false} loading={feedbackForm.submitting}
disabled={!feedbackForm.touched || feedbackForm.invalid} disabled={!feedbackForm.touched || feedbackForm.invalid}
intent="blue" intent="blue"
type="submit" type="submit"
> >
{i18n.t("settings.manage_federations.add")} {i18n.t("settings.manage_federations.add")}
</Button> </Button>
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
<Show when={success()}>
<InfoBox accent="green">{success()}</InfoBox>
</Show>
</VStack> </VStack>
</Form> </Form>
); );
} }
function ListAndRemoveFederations(props: { function FederationListItem(props: { fed: MutinyFederationIdentity }) {
federations?: MutinyFederationIdentity[];
refetch: () => void;
}) {
const i18n = useI18n(); const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, actions] = useMegaStore();
const [error, setError] = createSignal<Error>();
const removeFederation = async (federationId: string) => { async function removeFederation() {
setConfirmLoading(true);
try { try {
await state.mutiny_wallet?.remove_federation(federationId); await state.mutiny_wallet?.remove_federation(
props.refetch(); props.fed.federation_id
);
await actions.refreshFederations();
} catch (e) { } catch (e) {
console.error(e); console.error(e);
setError(eify(e));
} }
}; setConfirmLoading(false);
}
async function confirmRemove() {
setConfirmOpen(true);
}
const [confirmOpen, setConfirmOpen] = createSignal(false);
const [confirmLoading, setConfirmLoading] = createSignal(false);
return ( return (
<VStack> <>
<For each={props.federations ?? []}> <FancyCard>
{(fed) => ( <Show when={props.fed.federation_name}>
<FancyCard> <header class={`font-semibold`}>
<Show when={fed.federation_name}> {props.fed.federation_name}
<header class={`font-semibold`}> </header>
{fed.federation_name} </Show>
</header> <Show when={props.fed.welcome_message}>
</Show> <p>{props.fed.welcome_message}</p>
<Show when={fed.welcome_message}> </Show>
<p>{fed.welcome_message}</p> <Show when={props.fed.federation_expiry_timestamp}>
</Show> <KeyValue
<Show when={fed.federation_expiry_timestamp}> key={i18n.t("settings.manage_federations.expires")}
<KeyValue >
key={i18n.t( <time>
"settings.manage_federations.expires" {timeAgo(props.fed.federation_expiry_timestamp)}
)} </time>
> </KeyValue>
<time> </Show>
{timeAgo(fed.federation_expiry_timestamp)} <KeyValue
</time> key={i18n.t("settings.manage_federations.federation_id")}
</KeyValue> >
</Show> <MiniStringShower text={props.fed.federation_id} />
<KeyValue </KeyValue>
key={i18n.t( <Button intent="red" onClick={confirmRemove}>
"settings.manage_federations.federation_id" {i18n.t("settings.manage_federations.remove")}
)} </Button>
> </FancyCard>
<MiniStringShower text={fed.federation_id} /> <ConfirmDialog
</KeyValue> loading={confirmLoading()}
<Button open={confirmOpen()}
intent="red" onConfirm={removeFederation}
onClick={() => removeFederation(fed.federation_id)} onCancel={() => setConfirmOpen(false)}
> >
{i18n.t("settings.manage_federations.remove")} {i18n.t(
</Button> "settings.manage_federations.federation_remove_confirm"
</FancyCard>
)} )}
</For> </ConfirmDialog>
<Show when={(props.federations ?? []).length === 0}> </>
<div>No federations available.</div>
</Show>
<Show when={error()}>
<InfoBox accent="red">{error()?.message}</InfoBox>
</Show>
</VStack>
); );
} }
@@ -180,19 +188,6 @@ export function ManageFederations() {
const i18n = useI18n(); const i18n = useI18n();
const [state, _actions] = useMegaStore(); const [state, _actions] = useMegaStore();
async function fetchFederations() {
try {
const result =
(await state.mutiny_wallet?.list_federations()) as MutinyFederationIdentity[];
return result ?? [];
} catch (e) {
console.error(e);
return [];
}
}
const [federations, { refetch }] = createResource(fetchFederations);
return ( return (
<MutinyWalletGuard> <MutinyWalletGuard>
<SafeArea> <SafeArea>
@@ -204,11 +199,18 @@ export function ManageFederations() {
<LargeHeader> <LargeHeader>
{i18n.t("settings.manage_federations.title")} {i18n.t("settings.manage_federations.title")}
</LargeHeader> </LargeHeader>
<AddFederationForm refetch={refetch} /> <NiceP>
<ListAndRemoveFederations {i18n.t("settings.manage_federations.description")}{" "}
federations={federations.latest} <ExternalLink href="https://fedimint.org/">
refetch={refetch} {i18n.t("settings.manage_federations.learn_more")}
/> </ExternalLink>
</NiceP>
<AddFederationForm />
<VStack>
<For each={state.federations ?? []}>
{(fed) => <FederationListItem fed={fed} />}
</For>
</VStack>
</DefaultMain> </DefaultMain>
<NavBar activeTab="settings" /> <NavBar activeTab="settings" />
</SafeArea> </SafeArea>

View File

@@ -153,7 +153,7 @@ export function Settings() {
text: "Sync Nostr Contacts" text: "Sync Nostr Contacts"
}, },
{ {
href: "/settings/managefederations", href: "/settings/federations",
text: "Manage Federations" text: "Manage Federations"
} }
]} ]}

View File

@@ -25,6 +25,7 @@ import {
setupMutinyWallet setupMutinyWallet
} from "~/logic/mutinyWalletSetup"; } from "~/logic/mutinyWalletSetup";
import { ParsedParams, toParsedParams } from "~/logic/waila"; import { ParsedParams, toParsedParams } from "~/logic/waila";
import { MutinyFederationIdentity } from "~/routes/settings";
import { import {
BTC_OPTION, BTC_OPTION,
Currency, Currency,
@@ -70,6 +71,7 @@ export type MegaStore = [
betaWarned: boolean; betaWarned: boolean;
testflightPromptDismissed: boolean; testflightPromptDismissed: boolean;
should_zap_hodl: boolean; should_zap_hodl: boolean;
federations?: MutinyFederationIdentity[];
}, },
{ {
setup(password?: string): Promise<void>; setup(password?: string): Promise<void>;
@@ -94,6 +96,7 @@ export type MegaStore = [
setTestFlightPromptDismissed(): void; setTestFlightPromptDismissed(): void;
toggleHodl(): void; toggleHodl(): void;
dropMutinyWallet(): void; dropMutinyWallet(): void;
refreshFederations(): Promise<void>;
} }
]; ];
@@ -134,7 +137,8 @@ export const Provider: ParentComponent = (props) => {
betaWarned: localStorage.getItem("betaWarned") === "true", betaWarned: localStorage.getItem("betaWarned") === "true",
should_zap_hodl: localStorage.getItem("should_zap_hodl") === "true", should_zap_hodl: localStorage.getItem("should_zap_hodl") === "true",
testflightPromptDismissed: testflightPromptDismissed:
localStorage.getItem("testflightPromptDismissed") === "true" localStorage.getItem("testflightPromptDismissed") === "true",
federations: undefined as MutinyFederationIdentity[] | undefined
}); });
const actions = { const actions = {
@@ -211,11 +215,16 @@ export const Provider: ParentComponent = (props) => {
// Get balance // Get balance
const balance = await mutinyWallet.get_balance(); const balance = await mutinyWallet.get_balance();
// Get federations
const federations =
(await mutinyWallet.list_federations()) as MutinyFederationIdentity[];
setState({ setState({
mutiny_wallet: mutinyWallet, mutiny_wallet: mutinyWallet,
wallet_loading: false, wallet_loading: false,
load_stage: "done", load_stage: "done",
balance balance,
federations
}); });
} catch (e) { } catch (e) {
console.error(e); console.error(e);
@@ -408,6 +417,11 @@ export const Provider: ParentComponent = (props) => {
}, },
dropMutinyWallet() { dropMutinyWallet() {
setState({ mutiny_wallet: undefined }); setState({ mutiny_wallet: undefined });
},
async refreshFederations() {
const federations =
(await state.mutiny_wallet?.list_federations()) as MutinyFederationIdentity[];
setState({ federations });
} }
}; };