mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-26 18:34:26 +01:00
12
package.json
12
package.json
@@ -9,9 +9,9 @@
|
||||
},
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.16.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||
"@typescript-eslint/parser": "^5.59.1",
|
||||
"@types/node": "^18.16.3",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.2",
|
||||
"@typescript-eslint/parser": "^5.59.2",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"esbuild": "^0.14.54",
|
||||
"eslint": "^8.39.0",
|
||||
@@ -23,7 +23,7 @@
|
||||
"solid-start-node": "^0.2.26",
|
||||
"tailwindcss": "^3.3.2",
|
||||
"typescript": "^4.9.5",
|
||||
"vite": "^4.3.3",
|
||||
"vite": "^4.3.4",
|
||||
"vite-plugin-pwa": "^0.14.7",
|
||||
"vite-plugin-wasm": "^3.2.2",
|
||||
"workbox-window": "^6.5.4"
|
||||
@@ -31,7 +31,7 @@
|
||||
"dependencies": {
|
||||
"@kobalte/core": "^0.8.2",
|
||||
"@kobalte/tailwindcss": "^0.5.0",
|
||||
"@modular-forms/solid": "^0.12.0",
|
||||
"@modular-forms/solid": "^0.13.1",
|
||||
"@mutinywallet/mutiny-wasm": "^0.2.8",
|
||||
"@mutinywallet/waila-wasm": "^0.1.5",
|
||||
"@solidjs/meta": "^0.28.4",
|
||||
@@ -40,7 +40,7 @@
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"nostr-tools": "^1.10.1",
|
||||
"qr-scanner": "^1.4.2",
|
||||
"solid-js": "^1.7.3",
|
||||
"solid-js": "^1.7.4",
|
||||
"solid-qr-code": "^0.0.8",
|
||||
"solid-start": "^0.2.26",
|
||||
"undici": "^5.22.0"
|
||||
|
||||
1358
pnpm-lock.yaml
generated
1358
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
src/assets/icons/megacheck.png
Normal file
BIN
src/assets/icons/megacheck.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 113 KiB |
BIN
src/assets/icons/megaex.png
Normal file
BIN
src/assets/icons/megaex.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@@ -6,6 +6,7 @@ import ReloadPrompt from "~/components/Reload";
|
||||
import { A } from 'solid-start';
|
||||
import { Activity } from './Activity';
|
||||
import settings from '~/assets/icons/settings.svg';
|
||||
import { OnboardWarning } from './OnboardWarning';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
@@ -16,6 +17,7 @@ export default function App() {
|
||||
<img src={logo} class="h-10" alt="logo" />
|
||||
<A class="md:hidden p-2 hover:bg-white/5 rounded-lg active:bg-m-blue" href="/settings"><img src={settings} alt="Settings" /></A>
|
||||
</header>
|
||||
<OnboardWarning />
|
||||
<ReloadPrompt />
|
||||
<BalanceBox />
|
||||
<Activity />
|
||||
|
||||
@@ -17,54 +17,26 @@ function SyncingIndicator() {
|
||||
}
|
||||
|
||||
export default function BalanceBox() {
|
||||
const [state, _] = useMegaStore();
|
||||
|
||||
const fetchOnchainBalance = async () => {
|
||||
console.log("Refetching onchain balance");
|
||||
await state.node_manager?.sync();
|
||||
const balance = await state.node_manager?.get_balance();
|
||||
return balance
|
||||
};
|
||||
|
||||
// TODO: it's hacky to do these separately, but ln doesn't need the sync so I don't want to wait
|
||||
const fetchLnBalance = async () => {
|
||||
console.log("Refetching ln balance");
|
||||
const balance = await state.node_manager?.get_balance();
|
||||
return balance
|
||||
};
|
||||
|
||||
const [onChainBalance, { refetch: refetchOnChainBalance }] = createResource(fetchOnchainBalance);
|
||||
const [lnBalance, { refetch: refetchLnBalance }] = createResource(fetchLnBalance);
|
||||
|
||||
function refetchBalance() {
|
||||
refetchLnBalance();
|
||||
refetchOnChainBalance();
|
||||
}
|
||||
const [state, actions] = useMegaStore();
|
||||
|
||||
return (
|
||||
<>
|
||||
<FancyCard title="Lightning">
|
||||
<Suspense fallback={<Amount amountSats={0} showFiat loading={true} />}>
|
||||
<Show when={lnBalance()}>
|
||||
<Amount amountSats={lnBalance()?.lightning} showFiat />
|
||||
</Show>
|
||||
</Suspense>
|
||||
<Amount amountSats={state.balance?.lightning || 0} showFiat />
|
||||
</FancyCard>
|
||||
|
||||
<FancyCard title="On-Chain" tag={onChainBalance.loading && <SyncingIndicator />}>
|
||||
<Suspense fallback={<Amount amountSats={0} showFiat loading={true} />}>
|
||||
<div onClick={refetchBalance}>
|
||||
<Amount amountSats={onChainBalance()?.confirmed} showFiat loading={onChainBalance.loading} />
|
||||
</div>
|
||||
</Suspense>
|
||||
<FancyCard title="On-Chain" tag={state.is_syncing && <SyncingIndicator />}>
|
||||
<div onClick={actions.sync}>
|
||||
<Amount amountSats={state.balance?.confirmed} showFiat />
|
||||
</div>
|
||||
<Suspense>
|
||||
<Show when={onChainBalance()?.unconfirmed}>
|
||||
<Show when={state.balance?.unconfirmed}>
|
||||
<div class="flex flex-col gap-2">
|
||||
<header class='text-sm font-semibold uppercase text-white/50'>
|
||||
Unconfirmed Balance
|
||||
</header>
|
||||
<div class="text-white/50">
|
||||
{prettyPrintAmount(onChainBalance()?.unconfirmed)} <span class='text-sm'>SATS</span>
|
||||
{prettyPrintAmount(state.balance?.unconfirmed)} <span class='text-sm'>SATS</span>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
48
src/components/OnboardWarning.tsx
Normal file
48
src/components/OnboardWarning.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { Show, createSignal, onMount } from "solid-js";
|
||||
import { Button, ButtonLink, SmallHeader, VStack } from "./layout";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
|
||||
export function OnboardWarning() {
|
||||
const [state, actions] = useMegaStore();
|
||||
const [dismissedBackup, setDismissedBackup] = createSignal(false);
|
||||
|
||||
onMount(() => {
|
||||
actions.sync()
|
||||
})
|
||||
|
||||
function hasMoney() {
|
||||
return state.balance?.confirmed || state.balance?.lightning || state.balance?.unconfirmed
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* TODO: show this once we have a restore flow */}
|
||||
<Show when={!state.dismissed_restore_prompt && false}>
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
|
||||
<SmallHeader>Welcome!</SmallHeader>
|
||||
<VStack>
|
||||
<p class="text-2xl font-light">
|
||||
Do you want to restore an existing Mutiny Wallet?
|
||||
</p>
|
||||
<div class="w-full flex gap-2">
|
||||
<Button intent="green" onClick={() => { }}>Restore</Button>
|
||||
<Button onClick={actions.dismissRestorePrompt}>Nope</Button>
|
||||
</div>
|
||||
</VStack>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={!state.has_backed_up && hasMoney() && !dismissedBackup()}>
|
||||
<div class='rounded-xl p-4 flex flex-col gap-2 bg-neutral-950 overflow-x-hidden'>
|
||||
<SmallHeader>Secure your funds</SmallHeader>
|
||||
<p class="text-2xl font-light">
|
||||
You have money stored in this browser. Let's make sure you have a backup.
|
||||
</p>
|
||||
<div class="w-full flex gap-2">
|
||||
<ButtonLink intent="blue" href="/backup">Backup</ButtonLink>
|
||||
<Button onClick={() => { setDismissedBackup(true) }}>Nope</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,27 +1,36 @@
|
||||
import { Match, Switch, createSignal } from "solid-js"
|
||||
import { For, Match, Switch, createMemo, createSignal } from "solid-js"
|
||||
|
||||
export function SeedWords(props: { words: string }) {
|
||||
export function SeedWords(props: { words: string, setHasSeen?: (hasSeen: boolean) => void }) {
|
||||
const [shouldShow, setShouldShow] = createSignal(false)
|
||||
|
||||
function toggleShow() {
|
||||
setShouldShow(!shouldShow())
|
||||
if (shouldShow()) {
|
||||
props.setHasSeen?.(true)
|
||||
}
|
||||
}
|
||||
|
||||
return (<pre class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden">
|
||||
const splitWords = createMemo(() => props.words.split(" "))
|
||||
|
||||
return (<button class="flex items-center gap-4 bg-m-red p-4 rounded-xl overflow-hidden" onClick={toggleShow}>
|
||||
<Switch>
|
||||
<Match when={!shouldShow()}>
|
||||
<div onClick={toggleShow} class="cursor-pointer">
|
||||
<div class="cursor-pointer">
|
||||
<code class="text-red">TAP TO REVEAL SEED WORDS</code>
|
||||
</div>
|
||||
</Match>
|
||||
|
||||
<Match when={shouldShow()}>
|
||||
<div onClick={toggleShow} class="cursor-pointer overflow-hidden">
|
||||
<p class="font-mono w-full whitespace-pre-wrap">
|
||||
{props.words}
|
||||
</p>
|
||||
</div>
|
||||
<ol class="cursor-pointer overflow-hidden grid grid-cols-2 w-full list-decimal list-inside">
|
||||
<For each={splitWords()}>
|
||||
{(word) => (
|
||||
<li class="font-mono text-left">
|
||||
{word}
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
</ol>
|
||||
</Match>
|
||||
</Switch>
|
||||
</pre >)
|
||||
</button >)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export function ToastItem(props: { toastId: number, title: string, description:
|
||||
return (
|
||||
<Toast.Root toastId={props.toastId} class={`w-[80vw] max-w-[400px] mx-auto p-4 bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border ${props.isError ? "border-m-red/50" : "border-white/10"} `}>
|
||||
<div class="flex gap-4 w-full justify-between items-start">
|
||||
<div>
|
||||
<div class="flex-1">
|
||||
<Toast.Title>
|
||||
<SmallHeader>
|
||||
{props.title}
|
||||
@@ -43,7 +43,7 @@ export function ToastItem(props: { toastId: number, title: string, description:
|
||||
</p>
|
||||
</Toast.Description>
|
||||
</div>
|
||||
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue w-[5rem]">
|
||||
<Toast.CloseButton class="hover:bg-white/10 rounded-lg active:bg-m-blue w-[5rem] flex-0">
|
||||
<img src={close} alt="Close" />
|
||||
</Toast.CloseButton>
|
||||
</div>
|
||||
|
||||
@@ -103,6 +103,10 @@ const SmallAmount: ParentComponent<{ amount: number | bigint }> = (props) => {
|
||||
return (<h2 class="font-light text-lg">{props.amount.toLocaleString()} <span class="text-sm">SATS</span></h2>)
|
||||
}
|
||||
|
||||
export const NiceP: ParentComponent = (props) => {
|
||||
return (<p class="text-2xl font-light">{props.children}</p>)
|
||||
}
|
||||
|
||||
export {
|
||||
SmallHeader,
|
||||
Card,
|
||||
|
||||
44
src/routes/Backup.tsx
Normal file
44
src/routes/Backup.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Button, DefaultMain, LargeHeader, NiceP, NodeManagerGuard, SafeArea, VStack } from "~/components/layout";
|
||||
import NavBar from "~/components/NavBar";
|
||||
import { useNavigate } from 'solid-start';
|
||||
import { BackButton } from '~/components/layout/BackButton';
|
||||
import { SeedWords } from '~/components/SeedWords';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { Show, createSignal } from 'solid-js';
|
||||
|
||||
export default function App() {
|
||||
const [store, actions] = useMegaStore();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [hasSeenBackup, setHasSeenBackup] = createSignal(false);
|
||||
|
||||
function wroteDownTheWords() {
|
||||
actions.setHasBackedUp()
|
||||
navigate("/")
|
||||
}
|
||||
|
||||
return (
|
||||
<NodeManagerGuard>
|
||||
<SafeArea>
|
||||
<DefaultMain>
|
||||
<BackButton />
|
||||
<LargeHeader>Backup</LargeHeader>
|
||||
<VStack>
|
||||
<NiceP>Let's get these funds secured.</NiceP>
|
||||
<NiceP>We'll show you 12 words. You write down the 12 words.</NiceP>
|
||||
<NiceP>
|
||||
If you clear your browser history, or lose your device, these 12 words are the only way you can restore your wallet.
|
||||
</NiceP>
|
||||
<NiceP>Mutiny is self-custodial. It's all up to you...</NiceP>
|
||||
<SeedWords words={store.node_manager?.show_seed() || ""} setHasSeen={setHasSeenBackup} />
|
||||
<Show when={hasSeenBackup()}>
|
||||
<NiceP>You are responsible for your funds!</NiceP>
|
||||
</Show>
|
||||
<Button disabled={!hasSeenBackup()} intent="blue" onClick={wroteDownTheWords}>I wrote down the words</Button>
|
||||
</VStack>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="none" />
|
||||
</SafeArea>
|
||||
</NodeManagerGuard>
|
||||
);
|
||||
}
|
||||
@@ -8,14 +8,14 @@ import { useMegaStore } from "~/state/megaStore";
|
||||
import { objectToSearchParams } from "~/utils/objectToSearchParams";
|
||||
import { useCopy } from "~/utils/useCopy";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
|
||||
import party from '~/assets/hands/handsup.png';
|
||||
import { Amount } from "~/components/Amount";
|
||||
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
||||
import { BackButton } from "~/components/layout/BackButton";
|
||||
import { TagEditor, TagItem } from "~/components/TagEditor";
|
||||
import { StyledRadioGroup } from "~/components/layout/Radio";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
import { useNavigate } from "solid-start";
|
||||
import megacheck from "~/assets/icons/megacheck.png";
|
||||
|
||||
type OnChainTx = {
|
||||
transaction: {
|
||||
@@ -81,6 +81,7 @@ type PaidState = "lightning_paid" | "onchain_paid";
|
||||
|
||||
export default function Receive() {
|
||||
const [state, _] = useMegaStore()
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [amount, setAmount] = createSignal("")
|
||||
const [receiveState, setReceiveState] = createSignal<ReceiveState>("edit")
|
||||
@@ -263,17 +264,27 @@ export default function Receive() {
|
||||
</Card>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
|
||||
<FullscreenModal title="Payment Received" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<FullscreenModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
setOpen={(open: boolean) => { if (!open) clearAll() }}
|
||||
onConfirm={() => { clearAll(); navigate("/"); }}
|
||||
>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentInvoice()?.amount_sats} showFiat />
|
||||
</div>
|
||||
</FullscreenModal>
|
||||
</Match>
|
||||
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
|
||||
<FullscreenModal title="Payment Received" open={!!paidState()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
|
||||
<FullscreenModal
|
||||
title="Payment Received"
|
||||
open={!!paidState()}
|
||||
setOpen={(open: boolean) => { if (!open) clearAll() }}
|
||||
onConfirm={() => { clearAll(); navigate("/"); }}
|
||||
>
|
||||
<div class="flex flex-col items-center gap-8">
|
||||
<img src={party} alt="party" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh] aspect-square" />
|
||||
<Amount amountSats={paymentTx()?.received} showFiat />
|
||||
<a href={mempoolTxUrl(paymentTx()?.txid, "signet")} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
|
||||
@@ -12,10 +12,11 @@ import { ParsedParams, toParsedParams } from "./Scanner";
|
||||
import { showToast } from "~/components/Toaster";
|
||||
import eify from "~/utils/eify";
|
||||
import { FullscreenModal } from "~/components/layout/FullscreenModal";
|
||||
import handshake from "~/assets/hands/handshake.png";
|
||||
import thumbsdown from "~/assets/hands/thumbsdown.png";
|
||||
import megacheck from "~/assets/icons/megacheck.png"
|
||||
import megaex from "~/assets/icons/megaex.png";
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl";
|
||||
import { BackButton } from "~/components/layout/BackButton";
|
||||
import { useNavigate } from "solid-start";
|
||||
|
||||
type SendSource = "lightning" | "onchain";
|
||||
|
||||
@@ -29,6 +30,7 @@ type SentDetails = { amount?: bigint, destination?: string, txid?: string, failu
|
||||
|
||||
export default function Send() {
|
||||
const [state, actions] = useMegaStore();
|
||||
const navigate = useNavigate()
|
||||
|
||||
// These can only be set by the user
|
||||
const [fieldDestination, setFieldDestination] = createSignal("");
|
||||
@@ -204,16 +206,16 @@ export default function Send() {
|
||||
confirmText={sentDetails()?.amount ? "Nice" : "Too Bad"}
|
||||
open={!!sentDetails()}
|
||||
setOpen={(open: boolean) => { if (!open) setSentDetails(undefined) }}
|
||||
onConfirm={() => setSentDetails(undefined)}
|
||||
onConfirm={() => { setSentDetails(undefined); navigate("/"); }}
|
||||
>
|
||||
<div class="flex flex-col items-center gap-8 h-full">
|
||||
<Switch>
|
||||
<Match when={sentDetails()?.failure_reason}>
|
||||
<img src={thumbsdown} alt="thumbs down" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<img src={megaex} alt="fail" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<p class="text-xl font-light py-2 px-4 rounded-xl bg-white/10">{sentDetails()?.failure_reason}</p>
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<img src={handshake} alt="handshake" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<img src={megacheck} alt="success" class="w-1/2 mx-auto max-w-[50vh]" />
|
||||
<Amount amountSats={sentDetails()?.amount} showFiat />
|
||||
<Show when={sentDetails()?.txid}>
|
||||
<a href={mempoolTxUrl(sentDetails()?.txid, state.node_manager?.get_network())} target="_blank" rel="noreferrer">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
|
||||
/* @refresh reload */
|
||||
|
||||
// Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js
|
||||
import { ParentComponent, createContext, createEffect, onCleanup, onMount, useContext } from "solid-js";
|
||||
import { createStore } from "solid-js/store";
|
||||
import { NodeManagerSettingStrings, setupNodeManager } from "~/logic/nodeManagerSetup";
|
||||
@@ -16,14 +17,19 @@ export type MegaStore = [{
|
||||
user_status: UserStatus;
|
||||
scan_result?: ParsedParams;
|
||||
balance?: MutinyBalance;
|
||||
is_syncing?: boolean;
|
||||
last_sync?: number;
|
||||
price: number
|
||||
has_backed_up: boolean,
|
||||
dismissed_restore_prompt: boolean
|
||||
}, {
|
||||
fetchUserStatus(): Promise<UserStatus>;
|
||||
setupNodeManager(settings?: NodeManagerSettingStrings): Promise<void>;
|
||||
setWaitlistId(waitlist_id: string): void;
|
||||
setScanResult(scan_result: ParsedParams | undefined): void;
|
||||
sync(): Promise<void>;
|
||||
dismissRestorePrompt(): void;
|
||||
setHasBackedUp(): void;
|
||||
}];
|
||||
|
||||
export const Provider: ParentComponent = (props) => {
|
||||
@@ -33,7 +39,12 @@ export const Provider: ParentComponent = (props) => {
|
||||
user_status: undefined as UserStatus,
|
||||
scan_result: undefined as ParsedParams | undefined,
|
||||
// TODO: wire this up to real price once we have caching
|
||||
price: 30000
|
||||
price: 30000,
|
||||
has_backed_up: localStorage.getItem("has_backed_up") === "true",
|
||||
balance: undefined as MutinyBalance | undefined,
|
||||
last_sync: undefined as number | undefined,
|
||||
is_syncing: false,
|
||||
dismissed_restore_prompt: localStorage.getItem("dismissed_restore_prompt") === "true"
|
||||
});
|
||||
|
||||
const actions = {
|
||||
@@ -69,14 +80,29 @@ export const Provider: ParentComponent = (props) => {
|
||||
async sync(): Promise<void> {
|
||||
console.time("BDK Sync Time")
|
||||
try {
|
||||
await state.node_manager?.sync()
|
||||
if (state.node_manager && !state.is_syncing) {
|
||||
setState({ is_syncing: true })
|
||||
await state.node_manager?.sync()
|
||||
const balance = await state.node_manager?.get_balance();
|
||||
setState({ balance, last_sync: Date.now() })
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
setState({ is_syncing: false })
|
||||
}
|
||||
console.timeEnd("BDK Sync Time")
|
||||
},
|
||||
setScanResult(scan_result: ParsedParams) {
|
||||
setState({ scan_result })
|
||||
},
|
||||
setHasBackedUp() {
|
||||
localStorage.setItem("has_backed_up", "true")
|
||||
setState({ has_backed_up: true })
|
||||
},
|
||||
dismissRestorePrompt() {
|
||||
localStorage.setItem("dismissed_restore_prompt", "true")
|
||||
setState({ dismissed_restore_prompt: true })
|
||||
}
|
||||
};
|
||||
|
||||
@@ -101,8 +127,8 @@ export const Provider: ParentComponent = (props) => {
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
const interval = setInterval(() => {
|
||||
if (state.node_manager) actions.sync();
|
||||
const interval = setInterval(async () => {
|
||||
await actions.sync();
|
||||
}, 60 * 1000); // Poll every minute
|
||||
|
||||
onCleanup(() => {
|
||||
|
||||
Reference in New Issue
Block a user