mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2026-02-12 01:34:42 +01:00
@@ -32,10 +32,8 @@
|
||||
"@kobalte/core": "^0.8.2",
|
||||
"@kobalte/tailwindcss": "^0.5.0",
|
||||
"@modular-forms/solid": "^0.12.0",
|
||||
"@motionone/solid": "^10.16.0",
|
||||
"@mutinywallet/mutiny-wasm": "^0.2.7",
|
||||
"@mutinywallet/waila-wasm": "^0.1.5",
|
||||
"@nostr-dev-kit/ndk": "^0.0.13",
|
||||
"@solidjs/meta": "^0.28.4",
|
||||
"@solidjs/router": "^0.8.2",
|
||||
"@thisbeyond/solid-select": "^0.14.0",
|
||||
|
||||
430
pnpm-lock.yaml
generated
430
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
BIN
public/512.png
BIN
public/512.png
Binary file not shown.
|
Before Width: | Height: | Size: 5.3 KiB After Width: | Height: | Size: 64 KiB |
BIN
public/images/icon.png
Normal file
BIN
public/images/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
@@ -10,8 +10,8 @@ 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'
|
||||
const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full'
|
||||
const MISSING_LABEL = 'py-1 px-2 bg-m-red rounded inline-block text-sm'
|
||||
const RIGHT_COLUMN = 'flex flex-col items-right text-right'
|
||||
const MISSING_LABEL = 'py-1 px-2 bg-white/10 rounded inline-block text-sm'
|
||||
const RIGHT_COLUMN = 'flex flex-col items-right text-right max-w-[8rem]'
|
||||
|
||||
type OnChainTx = {
|
||||
txid: string
|
||||
@@ -19,8 +19,10 @@ type OnChainTx = {
|
||||
sent: number
|
||||
fee?: number
|
||||
confirmation_time?: {
|
||||
height: number
|
||||
timestamp: number
|
||||
"Confirmed": {
|
||||
height: number
|
||||
time: number
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +55,7 @@ function OnChainItem(props: { item: OnChainTx }) {
|
||||
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||
{isReceive() ? <img src={receive} alt="receive arrow" /> : <img src={send} alt="send arrow" />}
|
||||
<div class={CENTER_COLUMN}>
|
||||
<h2 class={MISSING_LABEL}>Label Missing</h2>
|
||||
<h2 class={MISSING_LABEL}>Unknown</h2>
|
||||
{isReceive() ? <SmallAmount amount={props.item.received} /> : <SmallAmount amount={props.item.sent} />}
|
||||
{/* <h2 class="truncate">Txid: {props.item.txid}</h2> */}
|
||||
</div>
|
||||
@@ -61,7 +63,7 @@ function OnChainItem(props: { item: OnChainTx }) {
|
||||
<SmallHeader class={isReceive() ? "text-m-green" : "text-m-red"}>
|
||||
{isReceive() ? "RECEIVE" : "SEND"}
|
||||
</SmallHeader>
|
||||
<SubtleText>{props.item.confirmation_time ? prettyPrintTime(props.item.confirmation_time.timestamp) : "Unconfirmed"}</SubtleText>
|
||||
<SubtleText>{props.item.confirmation_time ? prettyPrintTime(props.item.confirmation_time.Confirmed.time) : "Unconfirmed"}</SubtleText>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
@@ -79,7 +81,7 @@ function InvoiceItem(props: { item: MutinyInvoice }) {
|
||||
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||
{isSend() ? <img src={send} alt="send arrow" /> : <img src={receive} alt="receive arrow" />}
|
||||
<div class={CENTER_COLUMN}>
|
||||
<h2 class={MISSING_LABEL}>Label Missing</h2>
|
||||
<h2 class={MISSING_LABEL}>Unknown</h2>
|
||||
<SmallAmount amount={props.item.amount_sats || 0} />
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
@@ -104,7 +106,7 @@ function Utxo(props: { item: Utxo }) {
|
||||
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||
<img src={receive} alt="receive arrow" />
|
||||
<div class={CENTER_COLUMN}>
|
||||
<h2 class={MISSING_LABEL}>Label Missing</h2>
|
||||
<h2 class={MISSING_LABEL}>Unknown</h2>
|
||||
<SmallAmount amount={props.item.txout.value} />
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
@@ -148,7 +150,7 @@ export function Activity() {
|
||||
<Card title="On-chain">
|
||||
<Switch>
|
||||
<Match when={transactions.loading}>
|
||||
<LoadingSpinner big />
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match when={transactions.state === "ready" && transactions().length === 0}>
|
||||
<code>No transactions (empty state)</code>
|
||||
@@ -165,7 +167,7 @@ export function Activity() {
|
||||
<Card title="Lightning">
|
||||
<Switch>
|
||||
<Match when={invoices.loading}>
|
||||
<LoadingSpinner big />
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match when={invoices.state === "ready" && invoices().length === 0}>
|
||||
<code>No invoices (empty state)</code>
|
||||
@@ -182,7 +184,7 @@ export function Activity() {
|
||||
<Card title="UTXOs">
|
||||
<Switch>
|
||||
<Match when={utxos.loading}>
|
||||
<LoadingSpinner big />
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match when={utxos.state === "ready" && utxos().length === 0}>
|
||||
<code>No utxos (empty state)</code>
|
||||
|
||||
@@ -17,11 +17,11 @@ export function Amount(props: { amountSats: bigint | number | undefined, showFia
|
||||
return (
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-4xl font-light">
|
||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)} <span class='text-xl'>SATS</span>
|
||||
{props.loading ? "..." : prettyPrintAmount(props.amountSats)} <span class='text-xl'>SATS</span>
|
||||
</h1>
|
||||
<Show when={props.showFiat}>
|
||||
<h2 class="text-xl font-light text-white/70" >
|
||||
≈ {props.loading ? "..." : amountInUsd()} <span class="text-sm">USD</span>
|
||||
≈ {props.loading ? "..." : amountInUsd()} <span class="text-sm">USD</span>
|
||||
</h2>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
@@ -131,7 +131,7 @@ export function AmountEditable(props: { initialAmountSats: string, setAmountSats
|
||||
}
|
||||
|
||||
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
|
||||
const DIALOG_CONTENT = "h-screen-safe flex flex-col justify-between p-4 backdrop-blur-md bg-neutral-800/70"
|
||||
const DIALOG_CONTENT = "h-full safe-bottom flex flex-col justify-between p-4 backdrop-blur-md bg-neutral-800/70"
|
||||
|
||||
return (
|
||||
<Dialog.Root isOpen={isOpen()}>
|
||||
|
||||
@@ -5,7 +5,7 @@ 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"
|
||||
const DIALOG_CONTENT = "max-w-[600px] max-h-full 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,7 +29,7 @@ export function showToast(arg: ToastArg) {
|
||||
|
||||
export function ToastItem(props: { toastId: number, title: string, description: string, isError?: boolean }) {
|
||||
return (
|
||||
<Toast.Root toastId={props.toastId} class={`w-[80vw] max-w-[400px] p-4 bg-neutral-900/80 backdrop-blur-md shadow-xl rounded-xl border ${props.isError ? "border-m-red/50" : "border-white/10"} `}>
|
||||
<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">
|
||||
<div>
|
||||
<Toast.Title>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Button, LargeHeader, SmallHeader } from "~/components/layout";
|
||||
import close from "~/assets/icons/close.svg";
|
||||
|
||||
const DIALOG_POSITIONER = "fixed inset-0 safe-top safe-bottom z-50"
|
||||
const DIALOG_CONTENT = "h-screen-safe p-4 bg-gray/50 backdrop-blur-md bg-black/80"
|
||||
const DIALOG_CONTENT = "h-full p-4 bg-gray/50 backdrop-blur-md bg-black/80"
|
||||
|
||||
type FullscreenModalProps = {
|
||||
title: string,
|
||||
|
||||
@@ -75,8 +75,8 @@ const NodeManagerGuard: ParentComponent = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const LoadingSpinner = (props: { big?: boolean }) => {
|
||||
return (<div role="status" class={props.big ? "w-full h-full grid" : ""} >
|
||||
const LoadingSpinner = (props: { big?: boolean, wide?: boolean }) => {
|
||||
return (<div role="status" class={props.big ? "w-full h-full grid" : props.wide ? "w-full" : ""} >
|
||||
<svg aria-hidden="true" class="w-8 h-8 mr-2 text-gray-200 animate-spin dark:text-gray-600 fill-m-red place-self-center" viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function Root() {
|
||||
<Meta name="theme-color" content="#000000" />
|
||||
<Meta name="description" content="Lightning wallet for the web" />
|
||||
<Link rel="icon" href="/favicon.ico" />
|
||||
<Link rel="apple-touch-icon" href="/180.png" sizes="180x180" />
|
||||
<Link rel="apple-touch-icon" href="/images/icon.png" sizes="512x512" />
|
||||
<Link rel="mask-icon" href="/mutiny_logo_mask.svg" color="#000" />
|
||||
</Head>
|
||||
<Body>
|
||||
|
||||
@@ -138,7 +138,7 @@ export default function Receive() {
|
||||
async function getUnifiedQr(amount: string) {
|
||||
const bigAmount = BigInt(amount);
|
||||
try {
|
||||
const raw = await state.node_manager?.create_bip21(bigAmount, "TODO DELETE ME");
|
||||
const raw = await state.node_manager?.create_bip21(bigAmount);
|
||||
// Save the raw info so we can watch the address and invoice
|
||||
setBip21Raw(raw);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ export type ParsedParams = {
|
||||
amount_sats?: bigint;
|
||||
network?: string;
|
||||
memo?: string;
|
||||
node_pubkey?: string;
|
||||
}
|
||||
|
||||
export function toParsedParams(str: string, ourNetwork: string): Result<ParsedParams> {
|
||||
@@ -24,16 +25,23 @@ export function toParsedParams(str: string, ourNetwork: string): Result<ParsedPa
|
||||
return { ok: false, error: new Error("Invalid payment request") }
|
||||
}
|
||||
|
||||
console.log("params:", params.node_pubkey)
|
||||
console.log("params network:", params.network)
|
||||
console.log("our network:", ourNetwork)
|
||||
|
||||
|
||||
|
||||
// TODO: "testnet" and "signet" are encoded the same I guess?
|
||||
if (params.network === "testnet" || params.network === "signet") {
|
||||
if (ourNetwork === "signet") {
|
||||
// noop
|
||||
}
|
||||
} else if (params.network !== ourNetwork) {
|
||||
return { ok: false, error: new Error(`Destination is for ${params.network} but you're on ${ourNetwork}`) }
|
||||
if (params.node_pubkey) {
|
||||
// noop
|
||||
} else {
|
||||
return { ok: false, error: new Error(`Destination is for ${params.network} but you're on ${ourNetwork}`) }
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -42,7 +50,8 @@ export function toParsedParams(str: string, ourNetwork: string): Result<ParsedPa
|
||||
invoice: params.invoice,
|
||||
amount_sats: params.amount_sats,
|
||||
network: params.network,
|
||||
memo: params.memo
|
||||
memo: params.memo,
|
||||
node_pubkey: params.node_pubkey,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +93,7 @@ export default function Scanner() {
|
||||
showToast(result.error);
|
||||
return;
|
||||
} else {
|
||||
if (result.value?.address || result.value?.invoice) {
|
||||
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey) {
|
||||
actions.setScanResult(result.value);
|
||||
navigate("/send")
|
||||
}
|
||||
@@ -93,7 +102,7 @@ export default function Scanner() {
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="safe-top safe-left safe-right safe-bottom h-screen-safe">
|
||||
<div class="safe-top safe-left safe-right safe-bottom h-full">
|
||||
<Reader onResult={onResult} />
|
||||
<div class="w-full flex flex-col items-center fixed bottom-[2rem] gap-8 px-8">
|
||||
<div class="w-full max-w-[800px] flex flex-col gap-2">
|
||||
|
||||
@@ -39,6 +39,7 @@ export default function Send() {
|
||||
|
||||
// These can only be derived from the "destination" signal
|
||||
const [invoice, setInvoice] = createSignal<MutinyInvoice>();
|
||||
const [nodePubkey, setNodePubkey] = createSignal<string>();
|
||||
const [address, setAddress] = createSignal<string>();
|
||||
const [description, setDescription] = createSignal<string>();
|
||||
|
||||
@@ -54,6 +55,7 @@ export default function Send() {
|
||||
setInvoice(undefined);
|
||||
setAddress(undefined);
|
||||
setDescription(undefined);
|
||||
setNodePubkey(undefined);
|
||||
setFieldDestination("");
|
||||
}
|
||||
|
||||
@@ -84,6 +86,10 @@ export default function Send() {
|
||||
setInvoice(invoice)
|
||||
setSource("lightning")
|
||||
});
|
||||
} else if (source.node_pubkey) {
|
||||
setAmountSats(source.amount_sats || 0n);
|
||||
setNodePubkey(source.node_pubkey);
|
||||
setSource("lightning")
|
||||
} else {
|
||||
setAmountSats(source.amount_sats || 0n);
|
||||
setSource("onchain")
|
||||
@@ -104,7 +110,7 @@ export default function Send() {
|
||||
showToast(result.error);
|
||||
return;
|
||||
} else {
|
||||
if (result.value?.address || result.value?.invoice) {
|
||||
if (result.value?.address || result.value?.invoice || result.value?.node_pubkey) {
|
||||
setDestination(result.value);
|
||||
// Important! we need to clear the scan result once we've used it
|
||||
actions.setScanResult(undefined);
|
||||
@@ -146,6 +152,12 @@ export default function Send() {
|
||||
await state.node_manager?.pay_invoice(firstNode, bolt11, amountSats());
|
||||
sentDetails.amount = amountSats();
|
||||
}
|
||||
} else if (source() === "lightning" && nodePubkey()) {
|
||||
const nodes = await state.node_manager?.list_nodes();
|
||||
const firstNode = nodes[0] as string || ""
|
||||
const invoice = await state.node_manager?.keysend(firstNode, nodePubkey()!, amountSats());
|
||||
console.log(invoice?.value)
|
||||
sentDetails.amount = amountSats();
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const txid = await state.node_manager?.send_to_address(address()!, amountSats());
|
||||
@@ -165,6 +177,10 @@ export default function Send() {
|
||||
}
|
||||
}
|
||||
|
||||
const sendButtonDisabled = createMemo(() => {
|
||||
return !destination() || sending() || amountSats() === 0n;
|
||||
})
|
||||
|
||||
return (
|
||||
<NodeManagerGuard>
|
||||
<SafeArea>
|
||||
@@ -188,7 +204,7 @@ export default function Send() {
|
||||
</dt>
|
||||
<dd>
|
||||
<Switch>
|
||||
<Match when={address() || invoice()}>
|
||||
<Match when={address() || invoice() || nodePubkey()}>
|
||||
<div class="flex gap-2 items-center">
|
||||
<Show when={address() && source() === "onchain"}>
|
||||
<code class="truncate text-sm break-all">{"Address: "} {address()}
|
||||
@@ -206,9 +222,14 @@ export default function Send() {
|
||||
</Show>
|
||||
</code>
|
||||
</Show>
|
||||
<Show when={nodePubkey() && source() === "lightning"}>
|
||||
<code class="truncate text-sm break-all">{"Node Pubkey: "} {nodePubkey()}
|
||||
|
||||
</code>
|
||||
</Show>
|
||||
<Button class="flex-0" intent="glowy" layout="xs" onClick={clearAll}>Clear</Button>
|
||||
</div>
|
||||
<div class="my-8 flex gap-4 w-full items-center justify-around">
|
||||
<div class="my-8 flex flex-col md:flex-row md:justify-center gap-4 w-full items-start">
|
||||
{/* if the amount came with the invoice we can't allow setting it */}
|
||||
<Show when={!(invoice()?.amount_sats)} fallback={<Amount amountSats={amountSats() || 0} showFiat />}>
|
||||
<AmountEditable initialAmountSats={amountSats().toString() || "0"} setAmountSats={setAmountSats} />
|
||||
@@ -217,13 +238,13 @@ export default function Send() {
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="text-neutral-400 font-semibold uppercase">+ Fee</h2>
|
||||
<h3 class="text-xl font-light text-neutral-300">
|
||||
{fakeFee().toLocaleString()} <span class='text-lg'>SATS</span>
|
||||
{fakeFee().toLocaleString()} <span class='text-lg'>SATS</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div class="flex gap-2 items-center">
|
||||
<h2 class="font-semibold uppercase text-white">Total</h2>
|
||||
<h3 class="text-xl font-light text-white">
|
||||
{(amountSats().valueOf() + fakeFee().valueOf()).toLocaleString()} <span class='text-lg'>SATS</span>
|
||||
{(amountSats().valueOf() + fakeFee().valueOf()).toLocaleString()} <span class='text-lg'>SATS</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
@@ -263,7 +284,7 @@ export default function Send() {
|
||||
</Show>
|
||||
</dl>
|
||||
<Show when={destination()}>
|
||||
<Button disabled={!destination() || sending()} intent="blue" onClick={handleSend} loading={sending()}>{sending() ? "Sending..." : "Confirm Send"}</Button>
|
||||
<Button disabled={sendButtonDisabled()} intent="blue" onClick={handleSend} loading={sending()}>{sending() ? "Sending..." : "Confirm Send"}</Button>
|
||||
</Show>
|
||||
</DefaultMain>
|
||||
<NavBar activeTab="send" />
|
||||
|
||||
@@ -68,13 +68,11 @@ export const Provider: ParentComponent = (props) => {
|
||||
},
|
||||
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")
|
||||
},
|
||||
setScanResult(scan_result: ParsedParams) {
|
||||
|
||||
@@ -51,8 +51,9 @@ export default defineConfig({
|
||||
alias: [{ find: '~', replacement: path.resolve(__dirname, './src') }]
|
||||
},
|
||||
optimizeDeps: {
|
||||
// Don't want vite to bundle these late during dev causing reload
|
||||
include: ["qr-scanner", "nostr-tools", "class-variance-authority"],
|
||||
// This is necessary because otherwise `vite dev` can't find the wasm
|
||||
exclude: ["@mutinywallet/mutiny-wasm", "@mutinywallet/waila-wasm"],
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user