Merge pull request #53 from MutinyWallet/complaints-pt2

Complaints pt2
This commit is contained in:
Paul Miller
2023-04-27 09:47:42 -05:00
committed by GitHub
17 changed files with 218 additions and 313 deletions

View File

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

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 64 KiB

BIN
public/images/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

View File

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

View File

@@ -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)}&nbsp;<span class='text-xl'>SATS</span>
</h1>
<Show when={props.showFiat}>
<h2 class="text-xl font-light text-white/70" >
&#8776; {props.loading ? "..." : amountInUsd()} <span class="text-sm">USD</span>
&#8776; {props.loading ? "..." : amountInUsd()}&nbsp;<span class="text-sm">USD</span>
</h2>
</Show>
</div>

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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" />

View File

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

View File

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

View File

@@ -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">

View File

@@ -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()}&nbsp;<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()}&nbsp;<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" />

View File

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

View File

@@ -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"],
},
});