Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fedimint resync #1263

Merged
merged 5 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "mutiny-wallet",
"version": "1.7.13",
"version": "1.7.14",
"license": "MIT",
"packageManager": "[email protected]",
"scripts": {
Expand Down Expand Up @@ -56,7 +56,7 @@
"@kobalte/core": "^0.12.6",
"@kobalte/tailwindcss": "^0.9.0",
"@modular-forms/solid": "^0.20.0",
"@mutinywallet/mutiny-wasm": "1.7.12",
"@mutinywallet/mutiny-wasm": "1.7.13",
"@sentry/browser": "^8.7.0",
"@sentry/vite-plugin": "^2.18.0",
"@solid-primitives/upload": "^0.0.117",
Expand Down
9 changes: 4 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/components/Activity.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { TagItem } from "@mutinywallet/mutiny-wasm";
import { cache, createAsync, useNavigate } from "@solidjs/router";
import { Plus, Save, Search, Shuffle, Users } from "lucide-solid";
import { Plus, Save, Search, Shuffle } from "lucide-solid";
import {
createEffect,
createMemo,
Expand Down
8 changes: 8 additions & 0 deletions src/components/SetupErrorDisplay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,14 @@ export function SetupErrorDisplay(props: {
)}
</ExternalLink>
</NiceP>
<NiceP>
{i18n.t("error.on_boot.loading_failed.in_the_meantime")}{" "}
<a href="/?safe_mode=true">
{" "}
{i18n.t("error.on_boot.loading_failed.safe_mode")}
</a>
.
</NiceP>
<ImportExport emergency />
<ToggleReportDiagnostics />
<Logs />
Expand Down
6 changes: 3 additions & 3 deletions src/components/layout/Misc.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,10 @@ export function ModalCloseButton() {
);
}

const SIMPLE_OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-lg";
const SIMPLE_DIALOG_POSITIONER =
export const SIMPLE_OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-lg";
export const SIMPLE_DIALOG_POSITIONER =
"fixed inset-0 z-50 flex items-center justify-center";
const SIMPLE_DIALOG_CONTENT =
export const SIMPLE_DIALOG_CONTENT =
"max-w-[500px] w-[90vw] max-h-device overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/90 rounded-xl border border-white/10";

export const SimpleDialog: ParentComponent<{
Expand Down
17 changes: 17 additions & 0 deletions src/routes/settings/EmergencyKit.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import {
BackLink,
Button,
DefaultMain,
DeleteEverything,
ExternalLink,
ImportExport,
InnerCard,
LargeHeader,
LoadingIndicator,
Logs,
Expand All @@ -22,6 +24,21 @@ function EmergencyStack() {
<ImportExport emergency />
<ToggleReportDiagnostics />
<Logs />
<InnerCard title={"Safe Mode"}>
<VStack>
<NiceP>
Disable certain wallet functionality to help with
debugging.
</NiceP>
<Button
onClick={() =>
(window.location.href = "/?safe_mode=true")
}
>
Enable Safe Mode
</Button>
</VStack>
</InnerCard>
<div class="flex flex-col gap-2 overflow-x-hidden rounded-xl bg-m-red p-4">
<SmallHeader>{i18n.t("settings.danger_zone")}</SmallHeader>
<DeleteEverything emergency />
Expand Down
174 changes: 172 additions & 2 deletions src/routes/settings/ManageFederations.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Progress } from "@kobalte/core";
import {
createForm,
required,
Expand All @@ -7,12 +8,21 @@ import {
} from "@modular-forms/solid";
import { FederationBalance, TagItem } from "@mutinywallet/mutiny-wasm";
import { A, useNavigate, useSearchParams } from "@solidjs/router";
import { ArrowLeftRight, BadgeCheck, LogOut, Scan, Trash } from "lucide-solid";
import {
ArrowLeftRight,
BadgeCheck,
LogOut,
RefreshCw,
Scan,
Trash
} from "lucide-solid";
import {
createEffect,
createResource,
createSignal,
For,
Match,
onCleanup,
onMount,
Show,
Suspense,
Expand Down Expand Up @@ -45,7 +55,7 @@ import {
} from "~/components";
import { useI18n } from "~/i18n/context";
import { useMegaStore } from "~/state/megaStore";
import { eify, timeAgo } from "~/utils";
import { eify, timeAgo, timeout } from "~/utils";

type FederationForm = {
federation_code: string;
Expand All @@ -68,6 +78,12 @@ export type Metadata = {
about?: string;
};

export type ResyncProgress = {
total: number;
complete: number;
done: boolean;
};

export type DiscoveredFederation = {
id: string;
invite_codes: string[];
Expand Down Expand Up @@ -298,6 +314,26 @@ function RecommendButton(props: { fed: MutinyFederationIdentity }) {
);
}

function ResyncLoadingBar(props: { value: number; max: number }) {
return (
<Progress.Root
value={props.value}
minValue={0}
maxValue={props.max}
getValueLabel={({ value, max }) => {
const percent = Math.round((value / max) * 100);
return `${percent}% - ${value.toLocaleString()} / ${max.toLocaleString()}`;
}}
class="flex w-full flex-col gap-2"
>
<Progress.ValueLabel class="text-sm text-m-grey-400" />
<Progress.Track class="h-6 rounded bg-white/10">
<Progress.Fill class="h-full w-[var(--kb-progress-fill-width)] rounded bg-m-blue transition-[width]" />
</Progress.Track>
</Progress.Root>
);
}

function FederationListItem(props: {
fed: MutinyFederationIdentity;
balance?: bigint;
Expand All @@ -317,10 +353,96 @@ function FederationListItem(props: {
setConfirmLoading(false);
}

// Warn when leaving the page if there's a resync in progress
createEffect(() => {
if (resyncLoading()) {
window.onbeforeunload = function () {
return true;
};
} else {
window.onbeforeunload = null;
}
});

const [shouldPollProgress, setShouldPollProgress] = createSignal(false);

async function resyncFederation() {
setResyncLoading(true);
try {
await sw.resync_federation(props.fed.federation_id);

console.log("RESYNC STARTED");

// This loop is so we can try enough times until the resync actually starts
for (let i = 0; i < 60; i++) {
await timeout(1000);
const progress = await sw.get_federation_resync_progress(
props.fed.federation_id
);

console.log("progress", progress);
if (progress?.total !== 0) {
setResyncProgress(progress);
setResyncOpen(false);
setShouldPollProgress(true);
break;
}
}
} catch (e) {
console.error(e);
setResyncLoading(false);
}
}

function refetch() {
sw.get_federation_resync_progress(props.fed.federation_id).then(
(progress) => {
// if the progress is undefined, it also means we're done
if (progress === undefined) {
setResyncProgress({
total: 100,
complete: 100,
done: true
});
setShouldPollProgress(false);
setResyncLoading(false);
return;
} else if (progress?.done) {
setShouldPollProgress(false);
setResyncLoading(false);
}

setResyncProgress(progress);
}
);
}

createEffect(() => {
let interval = undefined;

if (shouldPollProgress()) {
interval = setInterval(() => {
refetch();
}, 1000); // Poll every second
}

if (!shouldPollProgress()) {
clearInterval(interval);
}

onCleanup(() => {
clearInterval(interval);
});
});

async function confirmRemove() {
setConfirmOpen(true);
}

async function confirmResync() {
setResyncOpen(true);
}

const [transferDialogOpen, setTransferDialogOpen] = createSignal(false);

async function transferFunds() {
Expand All @@ -335,6 +457,13 @@ function FederationListItem(props: {
const [confirmOpen, setConfirmOpen] = createSignal(false);
const [confirmLoading, setConfirmLoading] = createSignal(false);

const [resyncOpen, setResyncOpen] = createSignal(false);
const [resyncLoading, setResyncLoading] = createSignal(false);

const [resyncProgress, setResyncProgress] = createSignal<
ResyncProgress | undefined
>(undefined);

return (
<>
<FancyCard>
Expand Down Expand Up @@ -404,6 +533,12 @@ function FederationListItem(props: {
<LogOut class="h-4 w-4" />
{i18n.t("settings.manage_federations.remove")}
</SubtleButton>
<Show when={state.safe_mode}>
<SubtleButton intent="red" onClick={confirmResync}>
<RefreshCw class="h-4 w-4" />
Resync
</SubtleButton>
</Show>
</VStack>
</FancyCard>
<ConfirmDialog
Expand All @@ -416,6 +551,41 @@ function FederationListItem(props: {
"settings.manage_federations.federation_remove_confirm"
)}
</ConfirmDialog>
<ConfirmDialog
loading={resyncLoading()}
open={resyncOpen()}
onConfirm={resyncFederation}
onCancel={() => setResyncOpen(false)}
>
Are you sure you want to resync this federation? This will
rescan the federation for your ecash, this can take multiple
hours in some cases. If you stop the rescan it can cause your
wallet to be bricked. Please be sure you can run the rescan
before you start it.
</ConfirmDialog>
<Show when={resyncProgress()}>
<SimpleDialog
title={"Resyncing..."}
open={!resyncProgress()!.done}
>
<Show when={!resyncProgress()!.done}>
<NiceP>This could take a couple of hours.</NiceP>
<NiceP>
DO NOT CLOSE THIS WINDOW UNTIL RESYNC IS DONE.
</NiceP>
<ResyncLoadingBar
value={resyncProgress()!.complete}
max={resyncProgress()!.total}
/>
</Show>
<Show when={resyncProgress()!.done}>
<NiceP>Resync complete!</NiceP>
<Button onClick={() => (window.location.href = "/")}>
Nice
</Button>
</Show>
</SimpleDialog>
</Show>
</>
);
}
Expand Down
23 changes: 22 additions & 1 deletion src/workers/walletWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import { MutinyWalletSettingStrings } from "~/logic/mutinyWalletSetup";
import { FakeDirectMessage, OnChainTx } from "~/routes";
import {
DiscoveredFederation,
MutinyFederationIdentity
MutinyFederationIdentity,
ResyncProgress
} from "~/routes/settings";

const RELEASE_VERSION = import.meta.env.__RELEASE_VERSION__;
Expand Down Expand Up @@ -1201,6 +1202,26 @@ export async function remove_federation(federation_id: string): Promise<void> {
await wallet!.remove_federation(federation_id);
}

/**
* Resyncs a federation
* @param {string} federation_id
* @returns {Promise<void>}
*/
export async function resync_federation(federation_id: string): Promise<void> {
await wallet!.resync_federation(federation_id);
}

/**
* Gets the resync progress for a federation
* @param {string} federation_id
* @returns {Promise<ResyncProgress>}
*/
export async function get_federation_resync_progress(
federation_id: string
): Promise<ResyncProgress | undefined> {
return wallet!.get_federation_resync_progress(federation_id);
}

/**
* Opens a channel from our selected node to the given pubkey.
* The amount is in satoshis.
Expand Down
Loading