mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-30 12:24:20 +01:00
watch for receives
This commit is contained in:
@@ -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" />}
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user