From 4dc0a2fa1878a8f35d0b4500a22309c157118d38 Mon Sep 17 00:00:00 2001 From: Paul Miller Date: Wed, 19 Apr 2023 14:32:06 -0500 Subject: [PATCH] watch for receives --- src/components/Activity.tsx | 3 +- src/components/BalanceBox.tsx | 1 - src/components/JsonModal.tsx | 12 ++-- src/routes/Admin.tsx | 2 - src/routes/Receive.tsx | 111 +++++++++++++++++++++++++++++++--- src/routes/Send.tsx | 5 +- src/state/megaStore.tsx | 24 +++++++- 7 files changed, 136 insertions(+), 22 deletions(-) diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index d308886..e25b944 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -5,7 +5,7 @@ import { For, JSX, Match, Show, Suspense, Switch, createMemo, createResource, cr import { useMegaStore } from '~/state/megaStore'; import { MutinyInvoice } from '@mutinywallet/mutiny-wasm'; import { prettyPrintTime } from '~/utils/prettyPrintTime'; -import { JsonModal } from './JsonModal'; +import { JsonModal } from '~/components/JsonModal'; import mempoolTxUrl from '~/utils/mempoolTxUrl'; const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0' @@ -49,7 +49,6 @@ function OnChainItem(props: { item: OnChainTx }) { Mempool Link -
setOpen(!open())}> {isReceive() ? receive arrow : send arrow} diff --git a/src/components/BalanceBox.tsx b/src/components/BalanceBox.tsx index f11aab9..6bf5b33 100644 --- a/src/components/BalanceBox.tsx +++ b/src/components/BalanceBox.tsx @@ -21,7 +21,6 @@ export default function BalanceBox() { const fetchOnchainBalance = async () => { console.log("Refetching onchain balance"); - await state.node_manager?.sync(); const balance = await state.node_manager?.get_balance(); return balance }; diff --git a/src/components/JsonModal.tsx b/src/components/JsonModal.tsx index 3e04e7a..87e64b2 100644 --- a/src/components/JsonModal.tsx +++ b/src/components/JsonModal.tsx @@ -1,11 +1,11 @@ import { Dialog } from "@kobalte/core"; import { JSX, createMemo } from "solid-js"; -import { Button, ButtonLink, SmallHeader } from "~/components/layout"; +import { Button, SmallHeader } from "~/components/layout"; import { useCopy } from "~/utils/useCopy"; const OVERLAY = "fixed inset-0 z-50 bg-black/50 backdrop-blur-sm" const DIALOG_POSITIONER = "fixed inset-0 z-50 flex items-center justify-center" -const DIALOG_CONTENT = "max-w-[600px] max-h-screen-safe p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10 overflow-y-scroll disable-scrollbars" +const DIALOG_CONTENT = "max-w-[600px] max-h-screen-safe p-4 bg-gray/50 backdrop-blur-md shadow-xl rounded-xl border border-white/10" export function JsonModal(props: { title: string, open: boolean, data?: unknown, setOpen: (open: boolean) => void, children?: JSX.Element }) { const json = createMemo(() => JSON.stringify(props.data, null, 2)); @@ -29,9 +29,11 @@ export function JsonModal(props: { title: string, open: boolean, data?: unknown,
-
-                                {json()}
-                            
+
+
+                                    {json()}
+                                
+
{props.children} diff --git a/src/routes/Admin.tsx b/src/routes/Admin.tsx index e059580..d92dff0 100644 --- a/src/routes/Admin.tsx +++ b/src/routes/Admin.tsx @@ -1,4 +1,3 @@ -import { Activity } from "~/components/Activity"; import KitchenSink from "~/components/KitchenSink"; import NavBar from "~/components/NavBar"; import { Card, DefaultMain, LargeHeader, SafeArea, VStack } from "~/components/layout"; @@ -10,7 +9,6 @@ export default function Admin() { Admin

If you know what you're doing you're in the right place!

-
diff --git a/src/routes/Receive.tsx b/src/routes/Receive.tsx index ba18fb0..db69f0b 100644 --- a/src/routes/Receive.tsx +++ b/src/routes/Receive.tsx @@ -1,5 +1,6 @@ import { TextField } from "@kobalte/core"; -import { createMemo, createResource, createSignal, Match, Switch } from "solid-js"; +import { MutinyBip21RawMaterials, MutinyInvoice } from "@mutinywallet/mutiny-wasm"; +import { createEffect, createMemo, createResource, createSignal, Match, onCleanup, Switch } from "solid-js"; import { QRCodeSVG } from "solid-qr-code"; import { AmountEditable } from "~/components/AmountEditable"; import { Button, Card, DefaultMain, LargeHeader, NodeManagerGuard, SafeArea, SmallHeader } from "~/components/layout"; @@ -8,6 +9,32 @@ import { useMegaStore } from "~/state/megaStore"; import { satsToUsd } from "~/utils/conversions"; import { objectToSearchParams } from "~/utils/objectToSearchParams"; import { useCopy } from "~/utils/useCopy"; +import { JsonModal } from '~/components/JsonModal'; +import mempoolTxUrl from "~/utils/mempoolTxUrl"; + +type OnChainTx = { + transaction: { + version: number + lock_time: number + input: Array<{ + previous_output: string + script_sig: string + sequence: number + witness: Array + }> + output: Array<{ + value: number + script_pubkey: string + }> + } + txid: string + received: number + sent: number + confirmation_time: { + height: number + timestamp: number + } +} function ShareButton(props: { receiveString: string }) { async function share(receiveString: string) { @@ -31,7 +58,8 @@ function ShareButton(props: { receiveString: string }) { ) } -type ReceiveState = "edit" | "show" +type ReceiveState = "edit" | "show" | "paid" +type PaidState = "lightning_paid" | "onchain_paid"; export default function Receive() { const [state, _] = useMegaStore() @@ -41,6 +69,24 @@ export default function Receive() { const [receiveState, setReceiveState] = createSignal("edit") + const [bip21Raw, setBip21Raw] = createSignal(); + + const [unified, setUnified] = createSignal("") + + // The data we get after a payment + const [paymentTx, setPaymentTx] = createSignal(); + const [paymentInvoice, setPaymentInvoice] = createSignal(); + + function clearAll() { + setAmount("") + setLabel("") + setReceiveState("edit") + setBip21Raw(undefined) + setUnified("") + setPaymentTx(undefined) + setPaymentInvoice(undefined) + } + let amountInput!: HTMLInputElement; let labelInput!: HTMLInputElement; @@ -56,21 +102,24 @@ export default function Receive() { labelInput.focus(); } - const [unified, setUnified] = createSignal("") + const [copy, copied] = useCopy({ copiedTimeout: 1000 }); async function getUnifiedQr(amount: string, label: string) { const bigAmount = BigInt(amount); - const bip21Raw = await state.node_manager?.create_bip21(bigAmount, label); + const raw = await state.node_manager?.create_bip21(bigAmount, label); + + // Save the raw info so we can watch the address and invoice + setBip21Raw(raw); const params = objectToSearchParams({ - amount: bip21Raw?.btc_amount, - label: bip21Raw?.description, - lightning: bip21Raw?.invoice + amount: raw?.btc_amount, + label: raw?.description, + lightning: raw?.invoice }) - return `bitcoin:${bip21Raw?.address}?${params}` + return `bitcoin:${raw?.address}?${params}` } async function onSubmit(e: Event) { @@ -90,9 +139,43 @@ export default function Receive() { labelInput.focus(); } + async function checkIfPaid(bip21?: MutinyBip21RawMaterials): Promise { + if (bip21) { + console.log("checking if paid...") + const lightning = bip21.invoice + const address = bip21.address + + const invoice = await state.node_manager?.get_invoice(lightning) + + if (invoice && invoice.paid) { + setReceiveState("paid") + setPaymentInvoice(invoice) + return "lightning_paid" + } + + const tx = await state.node_manager?.check_address(address) as OnChainTx | undefined; + + if (tx) { + setReceiveState("paid") + setPaymentTx(tx) + return "onchain_paid" + } + } + } + + const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid); + + createEffect(() => { + const interval = setInterval(() => { + if (receiveState() === "show") refetch(); + }, 1000); // Poll every second + onCleanup(() => { + clearInterval(interval); + }); + }); + return ( - Receive Bitcoin @@ -139,6 +222,16 @@ export default function Receive() { {unified()} + + { if (!open) clearAll() }} /> + + + { if (!open) clearAll() }}> + + Mempool Link + + + diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index 540be91..1e092b9 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -23,8 +23,7 @@ const PAYMENT_METHODS = [{ value: "lightning", label: "Lightning", caption: "Fas type SentDetails = { nice: string } export default function Send() { - const [state, _] = useMegaStore(); - + const [state, actions] = useMegaStore(); // These can only be set by the user const [destination, setDestination] = createSignal(""); @@ -133,6 +132,8 @@ export default function Send() { } else { // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const txid = await state.node_manager?.send_to_address(address()!, amountSats()); + // TODO: figure out if this is necessary, it takes forever + await actions.sync(); console.error(txid) } diff --git a/src/state/megaStore.tsx b/src/state/megaStore.tsx index 3e3a963..d0d60e8 100644 --- a/src/state/megaStore.tsx +++ b/src/state/megaStore.tsx @@ -1,6 +1,6 @@ // Inspired by https://github.com/solidjs/solid-realworld/blob/main/src/store/index.js -import { ParentComponent, createContext, createEffect, onMount, useContext } from "solid-js"; +import { ParentComponent, createContext, createEffect, onCleanup, onMount, useContext } from "solid-js"; import { createStore } from "solid-js/store"; import { setupNodeManager } from "~/logic/nodeManagerSetup"; import { MutinyBalance, NodeManager } from "@mutinywallet/mutiny-wasm"; @@ -21,6 +21,7 @@ export type MegaStore = [{ fetchUserStatus(): Promise; setupNodeManager(): Promise; setWaitlistId(waitlist_id: string): void; + sync(): Promise; }]; export const Provider: ParentComponent = (props) => { @@ -61,6 +62,17 @@ export const Provider: ParentComponent = (props) => { }, setWaitlistId(waitlist_id: string) { setState({ waitlist_id }) + }, + async sync(): Promise { + console.time("BDK Sync Time") + console.groupCollapsed("BDK Sync") + try { + await state.node_manager?.sync() + } catch (e) { + console.error(e); + } + console.groupEnd(); + console.timeEnd("BDK Sync Time") } }; @@ -84,6 +96,16 @@ export const Provider: ParentComponent = (props) => { state.waitlist_id ? localStorage.setItem("waitlist_id", state.waitlist_id) : localStorage.removeItem("waitlist_id"); }); + createEffect(() => { + const interval = setInterval(() => { + if (state.node_manager) actions.sync(); + }, 60 * 1000); // Poll every minute + + onCleanup(() => { + clearInterval(interval); + }); + }) + const store = [state, actions] as MegaStore; return (