watch for receives

This commit is contained in:
Paul Miller
2023-04-19 14:32:06 -05:00
parent 4be030749a
commit 4dc0a2fa18
7 changed files with 136 additions and 22 deletions

View File

@@ -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 }) {
<a href={mempoolTxUrl(props.item.txid, "signet")} target="_blank" rel="noreferrer">
Mempool Link
</a>
</JsonModal>
<div class={THREE_COLUMNS} onclick={() => setOpen(!open())}>
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />}

View File

@@ -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
};

View File

@@ -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,
</Dialog.CloseButton>
</div>
<Dialog.Description class="flex flex-col gap-4">
<pre class="whitespace-pre-wrap break-all">
{json()}
</pre>
<div class="bg-white/10 rounded-xl max-h-[50vh] overflow-y-scroll disable-scrollbars p-4">
<pre class="whitespace-pre-wrap break-all">
{json()}
</pre>
</div>
{props.children}
<Button onClick={(_) => copy(json() ?? "")}>{copied() ? "Copied" : "Copy"}</Button>
<Button onClick={(_) => props.setOpen(false)}>Close</Button>

View File

@@ -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() {
<LargeHeader>Admin</LargeHeader>
<VStack>
<Card><p>If you know what you're doing you're in the right place!</p></Card>
<Activity />
<KitchenSink />
</VStack>
</DefaultMain>

View File

@@ -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<string>
}>
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<ReceiveState>("edit")
const [bip21Raw, setBip21Raw] = createSignal<MutinyBip21RawMaterials>();
const [unified, setUnified] = createSignal("")
// The data we get after a payment
const [paymentTx, setPaymentTx] = createSignal<OnChainTx>();
const [paymentInvoice, setPaymentInvoice] = createSignal<MutinyInvoice>();
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<PaidState | undefined> {
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 (
<NodeManagerGuard>
<SafeArea>
<DefaultMain>
<LargeHeader>Receive Bitcoin</LargeHeader>
@@ -139,6 +222,16 @@ export default function Receive() {
<code class="break-all">{unified()}</code>
</Card>
</Match>
<Match when={receiveState() === "paid" && paidState() === "lightning_paid"}>
<JsonModal title="They paid with lightning" open={!!paidState()} data={paymentInvoice()} setOpen={(open: boolean) => { if (!open) clearAll() }} />
</Match>
<Match when={receiveState() === "paid" && paidState() === "onchain_paid"}>
<JsonModal title="They paid onchain" open={!!paidState()} data={paymentTx()} setOpen={(open: boolean) => { if (!open) clearAll() }}>
<a href={mempoolTxUrl(paymentTx()?.txid, "signet")} target="_blank" rel="noreferrer">
Mempool Link
</a>
</JsonModal>
</Match>
</Switch>
</DefaultMain>
<NavBar activeTab="none" />

View File

@@ -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)
}

View File

@@ -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<UserStatus>;
setupNodeManager(): Promise<void>;
setWaitlistId(waitlist_id: string): void;
sync(): Promise<void>;
}];
export const Provider: ParentComponent = (props) => {
@@ -61,6 +62,17 @@ export const Provider: ParentComponent = (props) => {
},
setWaitlistId(waitlist_id: string) {
setState({ waitlist_id })
},
async sync(): Promise<void> {
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 (