mirror of
https://github.com/aljazceru/mutiny-web.git
synced 2025-12-29 11:54:22 +01:00
Merge pull request #158 from MutinyWallet/receive-details-min
details modal for onchain
This commit is contained in:
@@ -1,194 +1,246 @@
|
||||
import { LoadingSpinner, NiceP, SmallAmount, SmallHeader } from './layout';
|
||||
import { For, Match, Show, Switch, createEffect, createMemo, createResource, createSignal } from 'solid-js';
|
||||
import { useMegaStore } from '~/state/megaStore';
|
||||
import { MutinyInvoice } from '@mutinywallet/mutiny-wasm';
|
||||
import { JsonModal } from '~/components/JsonModal';
|
||||
import mempoolTxUrl from '~/utils/mempoolTxUrl';
|
||||
import utxoIcon from '~/assets/icons/coin.svg';
|
||||
import { getRedshifted } from '~/utils/fakeLabels';
|
||||
import { ActivityItem } from './ActivityItem';
|
||||
import { MutinyTagItem } from '~/utils/tags';
|
||||
import { Network } from '~/logic/mutinyWalletSetup';
|
||||
import { DetailsModal } from './DetailsModal';
|
||||
import { LoadingSpinner, NiceP, SmallAmount, SmallHeader } from "./layout"
|
||||
import {
|
||||
For,
|
||||
Match,
|
||||
Show,
|
||||
Switch,
|
||||
createEffect,
|
||||
createMemo,
|
||||
createResource,
|
||||
createSignal,
|
||||
} from "solid-js"
|
||||
import { useMegaStore } from "~/state/megaStore"
|
||||
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"
|
||||
import { JsonModal } from "~/components/JsonModal"
|
||||
import utxoIcon from "~/assets/icons/coin.svg"
|
||||
import { getRedshifted } from "~/utils/fakeLabels"
|
||||
import { ActivityItem } from "./ActivityItem"
|
||||
import { MutinyTagItem } from "~/utils/tags"
|
||||
import { Network } from "~/logic/mutinyWalletSetup"
|
||||
import { DetailsModal } from "./DetailsModal"
|
||||
|
||||
export const THREE_COLUMNS = 'grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0'
|
||||
export const CENTER_COLUMN = 'min-w-0 overflow-hidden max-w-full'
|
||||
export const MISSING_LABEL = 'py-1 px-2 bg-white/10 rounded inline-block text-sm'
|
||||
export const REDSHIFT_LABEL = 'py-1 px-2 bg-white text-m-red rounded inline-block text-sm'
|
||||
export const RIGHT_COLUMN = 'flex flex-col items-right text-right max-w-[8rem]'
|
||||
export const THREE_COLUMNS =
|
||||
"grid grid-cols-[auto,1fr,auto] gap-4 py-2 px-2 border-b border-neutral-800 last:border-b-0"
|
||||
export const CENTER_COLUMN = "min-w-0 overflow-hidden max-w-full"
|
||||
export const MISSING_LABEL =
|
||||
"py-1 px-2 bg-white/10 rounded inline-block text-sm"
|
||||
export const REDSHIFT_LABEL =
|
||||
"py-1 px-2 bg-white text-m-red rounded inline-block text-sm"
|
||||
export const RIGHT_COLUMN = "flex flex-col items-right text-right max-w-[8rem]"
|
||||
|
||||
export type OnChainTx = {
|
||||
txid: string
|
||||
received: number
|
||||
sent: number
|
||||
fee?: number
|
||||
confirmation_time?: {
|
||||
"Confirmed"?: {
|
||||
height: number
|
||||
time: number
|
||||
}
|
||||
},
|
||||
labels: string[]
|
||||
txid: string
|
||||
received: number
|
||||
sent: number
|
||||
fee?: number
|
||||
confirmation_time?: {
|
||||
Confirmed?: {
|
||||
height: number
|
||||
time: number
|
||||
}
|
||||
}
|
||||
labels: string[]
|
||||
}
|
||||
|
||||
export type UtxoItem = {
|
||||
outpoint: string
|
||||
txout: {
|
||||
value: number
|
||||
script_pubkey: string
|
||||
}
|
||||
keychain: string
|
||||
is_spent: boolean,
|
||||
redshifted?: boolean,
|
||||
outpoint: string
|
||||
txout: {
|
||||
value: number
|
||||
script_pubkey: string
|
||||
}
|
||||
keychain: string
|
||||
is_spent: boolean
|
||||
redshifted?: boolean
|
||||
}
|
||||
|
||||
function OnChainItem(props: { item: OnChainTx, labels: MutinyTagItem[], network: Network }) {
|
||||
const isReceive = () => props.item.received > props.item.sent
|
||||
function OnChainItem(props: {
|
||||
item: OnChainTx
|
||||
labels: MutinyTagItem[]
|
||||
network: Network
|
||||
}) {
|
||||
const isReceive = () => props.item.received > props.item.sent
|
||||
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonModal open={open()} data={props.item} title="On-Chain Transaction" setOpen={setOpen}>
|
||||
<a href={mempoolTxUrl(props.item.txid, props.network)} target="_blank" rel="noreferrer">
|
||||
Mempool Link
|
||||
</a>
|
||||
</JsonModal>
|
||||
<ActivityItem
|
||||
kind={"onchain"}
|
||||
labels={props.labels}
|
||||
// FIXME: is this something we can put into node logic?
|
||||
amount={isReceive() ? props.item.received - props.item.sent : props.item.sent - props.item.received}
|
||||
date={props.item.confirmation_time?.Confirmed?.time}
|
||||
positive={isReceive()}
|
||||
onClick={() => setOpen(!open())}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<DetailsModal open={open()} data={props.item} setOpen={setOpen} />
|
||||
<ActivityItem
|
||||
kind={"onchain"}
|
||||
labels={props.labels}
|
||||
// FIXME: is this something we can put into node logic?
|
||||
amount={
|
||||
isReceive()
|
||||
? props.item.received - props.item.sent
|
||||
: props.item.sent - props.item.received
|
||||
}
|
||||
date={props.item.confirmation_time?.Confirmed?.time}
|
||||
positive={isReceive()}
|
||||
onClick={() => setOpen(!open())}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function InvoiceItem(props: { item: MutinyInvoice, labels: MutinyTagItem[] }) {
|
||||
const isSend = createMemo(() => props.item.is_send);
|
||||
function InvoiceItem(props: { item: MutinyInvoice; labels: MutinyTagItem[] }) {
|
||||
const isSend = createMemo(() => props.item.is_send)
|
||||
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
<DetailsModal open={open()} data={props.item} title="Lightning Transaction" setOpen={setOpen} />
|
||||
<ActivityItem kind={"lightning"} labels={props.labels} amount={props.item.amount_sats || 0n} date={props.item.last_updated} positive={!isSend()} onClick={() => setOpen(!open())} />
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<DetailsModal open={open()} data={props.item} setOpen={setOpen} />
|
||||
<ActivityItem
|
||||
kind={"lightning"}
|
||||
labels={props.labels}
|
||||
amount={props.item.amount_sats || 0n}
|
||||
date={props.item.last_updated}
|
||||
positive={!isSend()}
|
||||
onClick={() => setOpen(!open())}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function Utxo(props: { item: UtxoItem }) {
|
||||
const spent = createMemo(() => props.item.is_spent);
|
||||
const spent = createMemo(() => props.item.is_spent)
|
||||
|
||||
const [open, setOpen] = createSignal(false)
|
||||
const [open, setOpen] = createSignal(false)
|
||||
|
||||
const redshifted = createMemo(() => getRedshifted(props.item.outpoint));
|
||||
const redshifted = createMemo(() => getRedshifted(props.item.outpoint))
|
||||
|
||||
return (
|
||||
<>
|
||||
<JsonModal open={open()} data={props.item} title="Unspent Transaction Output" setOpen={setOpen} />
|
||||
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||
<div class="flex items-center">
|
||||
<img src={utxoIcon} alt="coin" />
|
||||
</div>
|
||||
<div class={CENTER_COLUMN}>
|
||||
<div class="flex gap-2">
|
||||
<Show when={redshifted()} fallback={<h2 class={MISSING_LABEL}>Unknown</h2>}>
|
||||
<h2 class={REDSHIFT_LABEL}>Redshift</h2>
|
||||
</Show>
|
||||
</div>
|
||||
<SmallAmount amount={props.item.txout.value} />
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
<SmallHeader class={spent() ? "text-m-red" : "text-m-green"}>
|
||||
{/* {spent() ? "SPENT" : "UNSPENT"} */}
|
||||
</SmallHeader>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<JsonModal
|
||||
open={open()}
|
||||
data={props.item}
|
||||
title="Unspent Transaction Output"
|
||||
setOpen={setOpen}
|
||||
/>
|
||||
<div class={THREE_COLUMNS} onClick={() => setOpen(!open())}>
|
||||
<div class="flex items-center">
|
||||
<img src={utxoIcon} alt="coin" />
|
||||
</div>
|
||||
<div class={CENTER_COLUMN}>
|
||||
<div class="flex gap-2">
|
||||
<Show
|
||||
when={redshifted()}
|
||||
fallback={<h2 class={MISSING_LABEL}>Unknown</h2>}
|
||||
>
|
||||
<h2 class={REDSHIFT_LABEL}>Redshift</h2>
|
||||
</Show>
|
||||
</div>
|
||||
<SmallAmount amount={props.item.txout.value} />
|
||||
</div>
|
||||
<div class={RIGHT_COLUMN}>
|
||||
<SmallHeader class={spent() ? "text-m-red" : "text-m-green"}>
|
||||
{/* {spent() ? "SPENT" : "UNSPENT"} */}
|
||||
</SmallHeader>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
type ActivityItem = { type: "onchain" | "lightning", item: OnChainTx | MutinyInvoice, time: number, labels: MutinyTagItem[] }
|
||||
type ActivityItem = {
|
||||
type: "onchain" | "lightning"
|
||||
item: OnChainTx | MutinyInvoice
|
||||
time: number
|
||||
labels: MutinyTagItem[]
|
||||
}
|
||||
|
||||
function sortByTime(a: ActivityItem, b: ActivityItem) {
|
||||
return b.time - a.time;
|
||||
return b.time - a.time
|
||||
}
|
||||
|
||||
export function CombinedActivity(props: { limit?: number }) {
|
||||
const [state, actions] = useMegaStore();
|
||||
const [state, actions] = useMegaStore()
|
||||
|
||||
const getAllActivity = async () => {
|
||||
console.log("Getting all activity");
|
||||
const txs = await state.mutiny_wallet?.list_onchain() as OnChainTx[];
|
||||
const invoices = await state.mutiny_wallet?.list_invoices() as MutinyInvoice[];
|
||||
const tags = await actions.listTags();
|
||||
const getAllActivity = async () => {
|
||||
console.log("Getting all activity")
|
||||
const txs = (await state.mutiny_wallet?.list_onchain()) as OnChainTx[]
|
||||
const invoices =
|
||||
(await state.mutiny_wallet?.list_invoices()) as MutinyInvoice[]
|
||||
const tags = await actions.listTags()
|
||||
|
||||
let activity: ActivityItem[] = [];
|
||||
let activity: ActivityItem[] = []
|
||||
|
||||
for (let i = 0; i < txs.length; i++) {
|
||||
activity.push({ type: "onchain", item: txs[i], time: txs[i].confirmation_time?.Confirmed?.time || Date.now(), labels: [] })
|
||||
}
|
||||
|
||||
for (let i = 0; i < invoices.length; i++) {
|
||||
if (invoices[i].paid) {
|
||||
activity.push({ type: "lightning", item: invoices[i], time: Number(invoices[i].last_updated), labels: [] })
|
||||
}
|
||||
}
|
||||
|
||||
if (props.limit) {
|
||||
activity = activity.sort(sortByTime).slice(0, props.limit);
|
||||
} else {
|
||||
activity.sort(sortByTime);
|
||||
}
|
||||
|
||||
for (let i = 0; i < activity.length; i++) {
|
||||
// filter the tags to only include the ones that have an id matching one of the labels
|
||||
activity[i].labels = tags.filter((tag) => activity[i].item.labels.includes(tag.id));
|
||||
}
|
||||
|
||||
return activity;
|
||||
for (let i = 0; i < txs.length; i++) {
|
||||
activity.push({
|
||||
type: "onchain",
|
||||
item: txs[i],
|
||||
time: txs[i].confirmation_time?.Confirmed?.time || Date.now(),
|
||||
labels: [],
|
||||
})
|
||||
}
|
||||
|
||||
const [activity, { refetch }] = createResource(getAllActivity);
|
||||
for (let i = 0; i < invoices.length; i++) {
|
||||
if (invoices[i].paid) {
|
||||
activity.push({
|
||||
type: "lightning",
|
||||
item: invoices[i],
|
||||
time: Number(invoices[i].last_updated),
|
||||
labels: [],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network;
|
||||
if (props.limit) {
|
||||
activity = activity.sort(sortByTime).slice(0, props.limit)
|
||||
} else {
|
||||
activity.sort(sortByTime)
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
// After every sync we should refetch the activity
|
||||
if (!state.is_syncing) {
|
||||
refetch();
|
||||
}
|
||||
})
|
||||
for (let i = 0; i < activity.length; i++) {
|
||||
// filter the tags to only include the ones that have an id matching one of the labels
|
||||
activity[i].labels = tags.filter((tag) =>
|
||||
activity[i].item.labels.includes(tag.id)
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={activity.loading}>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match when={activity.state === "ready" && activity().length === 0}>
|
||||
<NiceP>No activity to show</NiceP>
|
||||
</Match>
|
||||
<Match when={activity.state === "ready" && activity().length >= 0}>
|
||||
<For each={activity.latest}>
|
||||
{(activityItem) =>
|
||||
<Switch>
|
||||
<Match when={activityItem.type === "onchain"}>
|
||||
<OnChainItem item={activityItem.item as OnChainTx} labels={activityItem.labels} network={network} />
|
||||
</Match>
|
||||
<Match when={activityItem.type === "lightning"}>
|
||||
<InvoiceItem item={activityItem.item as MutinyInvoice} labels={activityItem.labels} />
|
||||
</Match>
|
||||
</Switch>
|
||||
}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
return activity
|
||||
}
|
||||
|
||||
)
|
||||
const [activity, { refetch }] = createResource(getAllActivity)
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network
|
||||
|
||||
}
|
||||
createEffect(() => {
|
||||
// After every sync we should refetch the activity
|
||||
if (!state.is_syncing) {
|
||||
refetch()
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<Switch>
|
||||
<Match when={activity.loading}>
|
||||
<LoadingSpinner wide />
|
||||
</Match>
|
||||
<Match when={activity.state === "ready" && activity().length === 0}>
|
||||
<NiceP>No activity to show</NiceP>
|
||||
</Match>
|
||||
<Match when={activity.state === "ready" && activity().length >= 0}>
|
||||
<For each={activity.latest}>
|
||||
{(activityItem) => (
|
||||
<Switch>
|
||||
<Match when={activityItem.type === "onchain"}>
|
||||
<OnChainItem
|
||||
item={activityItem.item as OnChainTx}
|
||||
labels={activityItem.labels}
|
||||
network={network}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={activityItem.type === "lightning"}>
|
||||
<InvoiceItem
|
||||
item={activityItem.item as MutinyInvoice}
|
||||
labels={activityItem.labels}
|
||||
/>
|
||||
</Match>
|
||||
</Switch>
|
||||
)}
|
||||
</For>
|
||||
</Match>
|
||||
</Switch>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -3,27 +3,45 @@ import { useMegaStore } from "~/state/megaStore"
|
||||
import { satsToUsd } from "~/utils/conversions"
|
||||
|
||||
function prettyPrintAmount(n?: number | bigint): string {
|
||||
if (!n || n.valueOf() === 0) {
|
||||
return "0"
|
||||
}
|
||||
return n.toLocaleString()
|
||||
if (!n || n.valueOf() === 0) {
|
||||
return "0"
|
||||
}
|
||||
return n.toLocaleString()
|
||||
}
|
||||
|
||||
export function Amount(props: { amountSats: bigint | number | undefined, showFiat?: boolean, loading?: boolean }) {
|
||||
const [state, _] = useMegaStore()
|
||||
export function Amount(props: {
|
||||
amountSats: bigint | number | undefined
|
||||
showFiat?: boolean
|
||||
loading?: boolean
|
||||
}) {
|
||||
const [state, _] = useMegaStore()
|
||||
|
||||
const amountInUsd = () => satsToUsd(state.price, Number(props.amountSats) || 0, true)
|
||||
const amountInUsd = () =>
|
||||
satsToUsd(state.price, Number(props.amountSats) || 0, true)
|
||||
|
||||
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>
|
||||
</h1>
|
||||
<Show when={props.showFiat}>
|
||||
<h2 class="text-xl font-light text-white/70" >
|
||||
≈ {props.loading ? "..." : amountInUsd()} <span class="text-sm">USD</span>
|
||||
</h2>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
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>
|
||||
</h1>
|
||||
<Show when={props.showFiat}>
|
||||
<h2 class="text-xl font-light text-white/70">
|
||||
≈ {props.loading ? "..." : amountInUsd()}
|
||||
<span class="text-sm">USD</span>
|
||||
</h2>
|
||||
</Show>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AmountSmall(props: {
|
||||
amountSats: bigint | number | undefined
|
||||
}) {
|
||||
return (
|
||||
<span class="font-light">
|
||||
{prettyPrintAmount(props.amountSats)}
|
||||
<span class="text-sm">SATS</span>
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,145 +1,299 @@
|
||||
import { Dialog } from "@kobalte/core";
|
||||
import { For, JSX, ParentComponent, Show, createMemo } from "solid-js";
|
||||
import { Hr, TinyButton, VStack } from "~/components/layout";
|
||||
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm";
|
||||
import { OnChainTx } from "./Activity";
|
||||
import { Dialog } from "@kobalte/core"
|
||||
import {
|
||||
For,
|
||||
JSX,
|
||||
Match,
|
||||
ParentComponent,
|
||||
Show,
|
||||
Switch,
|
||||
createMemo,
|
||||
} from "solid-js"
|
||||
import { Hr, TinyButton, VStack } from "~/components/layout"
|
||||
import { MutinyInvoice } from "@mutinywallet/mutiny-wasm"
|
||||
import { OnChainTx } from "./Activity"
|
||||
|
||||
import close from "~/assets/icons/close.svg";
|
||||
import bolt from "~/assets/icons/bolt-black.svg";
|
||||
import close from "~/assets/icons/close.svg"
|
||||
import bolt from "~/assets/icons/bolt-black.svg"
|
||||
import chain from "~/assets/icons/chain-black.svg"
|
||||
import copyIcon from "~/assets/icons/copy.svg"
|
||||
|
||||
import { ActivityAmount } from "./ActivityItem";
|
||||
import { CopyButton } from "./ShareCard";
|
||||
import { prettyPrintTime } from "~/utils/prettyPrintTime";
|
||||
import { useMegaStore } from "~/state/megaStore";
|
||||
import { tagToMutinyTag } from "~/utils/tags";
|
||||
import { useCopy } from "~/utils/useCopy";
|
||||
import { ActivityAmount } from "./ActivityItem"
|
||||
import { CopyButton } from "./ShareCard"
|
||||
import { prettyPrintTime } from "~/utils/prettyPrintTime"
|
||||
import { useMegaStore } from "~/state/megaStore"
|
||||
import { tagToMutinyTag } from "~/utils/tags"
|
||||
import { useCopy } from "~/utils/useCopy"
|
||||
import mempoolTxUrl from "~/utils/mempoolTxUrl"
|
||||
import { Network } from "~/logic/mutinyWalletSetup"
|
||||
import { AmountSmall } from "./Amount"
|
||||
|
||||
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-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||
const DIALOG_CONTENT =
|
||||
"max-w-[500px] w-[90vw] max-h-[100dvh] overflow-y-scroll disable-scrollbars mx-4 p-4 bg-neutral-800/80 backdrop-blur-md shadow-xl rounded-xl border border-white/10"
|
||||
|
||||
function LightningHeader(props: { info: MutinyInvoice }) {
|
||||
const [state, _actions] = useMegaStore();
|
||||
const [state, _actions] = useMegaStore()
|
||||
|
||||
const tags = createMemo(() => {
|
||||
if (props.info.labels.length) {
|
||||
let contact = state.mutiny_wallet?.get_contact(props.info.labels[0]);
|
||||
if (contact) {
|
||||
return [tagToMutinyTag(contact)]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
const tags = createMemo(() => {
|
||||
if (props.info.labels.length) {
|
||||
let contact = state.mutiny_wallet?.get_contact(props.info.labels[0])
|
||||
if (contact) {
|
||||
return [tagToMutinyTag(contact)]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="p-4 bg-neutral-100 rounded-full">
|
||||
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">{props.info.is_send ? "Lightning send" : "Lightning receive"}</h1>
|
||||
<ActivityAmount center amount={props.info.amount_sats?.toString() ?? "0"} price={state.price} positive={!props.info.is_send} />
|
||||
<For each={tags()}>
|
||||
{(tag) => (
|
||||
<TinyButton tag={tag} onClick={() => { }}>
|
||||
{tag.name}
|
||||
</TinyButton>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="p-4 bg-neutral-100 rounded-full">
|
||||
<img src={bolt} alt="lightning bolt" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">
|
||||
{props.info.is_send ? "Lightning send" : "Lightning receive"}
|
||||
</h1>
|
||||
<ActivityAmount
|
||||
center
|
||||
amount={props.info.amount_sats?.toString() ?? "0"}
|
||||
price={state.price}
|
||||
positive={!props.info.is_send}
|
||||
/>
|
||||
<For each={tags()}>
|
||||
{(tag) => (
|
||||
<TinyButton tag={tag} onClick={() => {}}>
|
||||
{tag.name}
|
||||
</TinyButton>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function OnchainHeader(props: { info: OnChainTx }) {
|
||||
const [state, _actions] = useMegaStore()
|
||||
|
||||
const tags = createMemo(() => {
|
||||
if (props.info.labels.length) {
|
||||
let contact = state.mutiny_wallet?.get_contact(props.info.labels[0])
|
||||
if (contact) {
|
||||
return [tagToMutinyTag(contact)]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const isSend = () => {
|
||||
return props.info.sent > props.info.received
|
||||
}
|
||||
|
||||
const amount = () => {
|
||||
if (isSend()) {
|
||||
return (props.info.sent - props.info.received).toString()
|
||||
} else {
|
||||
return (props.info.received - props.info.sent).toString()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col items-center gap-4">
|
||||
<div class="p-4 bg-neutral-100 rounded-full">
|
||||
<img src={chain} alt="blockchain" class="w-8 h-8" />
|
||||
</div>
|
||||
<h1 class="uppercase font-semibold">
|
||||
{isSend() ? "On-chain send" : "On-chain receive"}
|
||||
</h1>
|
||||
<ActivityAmount
|
||||
center
|
||||
amount={amount() ?? "0"}
|
||||
price={state.price}
|
||||
positive={!isSend()}
|
||||
/>
|
||||
<For each={tags()}>
|
||||
{(tag) => (
|
||||
<TinyButton tag={tag} onClick={() => {}}>
|
||||
{tag.name}
|
||||
</TinyButton>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const KeyValue: ParentComponent<{ key: string }> = (props) => {
|
||||
return (
|
||||
<li class="flex justify-between items-center gap-4">
|
||||
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
|
||||
{props.children}
|
||||
</li>
|
||||
|
||||
)
|
||||
|
||||
return (
|
||||
<li class="flex justify-between items-center gap-4">
|
||||
<span class="uppercase font-semibold whitespace-nowrap">{props.key}</span>
|
||||
<span class="font-light">{props.children}</span>
|
||||
</li>
|
||||
)
|
||||
}
|
||||
|
||||
function MiniStringShower(props: { text: string }) {
|
||||
const [copy, _copied] = useCopy({ copiedTimeout: 1000 });
|
||||
const [copy, _copied] = useCopy({ copiedTimeout: 1000 })
|
||||
|
||||
return (
|
||||
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
||||
<pre class="truncate text-neutral-300">{props.text}</pre>
|
||||
<button class="w-[1rem]" onClick={() => copy(props.text)}>
|
||||
<img src={copyIcon} alt="copy" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
return (
|
||||
<div class="w-full grid gap-1 grid-cols-[minmax(0,_1fr)_auto]">
|
||||
<pre class="truncate text-neutral-300 font-light">{props.text}</pre>
|
||||
<button class="w-[1rem]" onClick={() => copy(props.text)}>
|
||||
<img src={copyIcon} alt="copy" class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LightningDetails(props: { info: MutinyInvoice }) {
|
||||
return (
|
||||
<VStack>
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Status">
|
||||
<span class="text-neutral-300">{props.info.paid ? "Paid" : "Unpaid"}</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="When">
|
||||
<span class="text-neutral-300">{prettyPrintTime(Number(props.info.last_updated))}</span>
|
||||
</KeyValue>
|
||||
<Show when={props.info.description}>
|
||||
<KeyValue key="Description">
|
||||
<span class="text-neutral-300 truncate">{props.info.description}</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key="Fees">
|
||||
<span class="text-neutral-300">{props.info.fees_paid?.toLocaleString() ?? 0}</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="Bolt11">
|
||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key="Payment Hash">
|
||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key="Preimage">
|
||||
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||
</KeyValue>
|
||||
</ul>
|
||||
</VStack>
|
||||
|
||||
)
|
||||
return (
|
||||
<VStack>
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Status">
|
||||
<span class="text-neutral-300">
|
||||
{props.info.paid ? "Paid" : "Unpaid"}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="When">
|
||||
<span class="text-neutral-300">
|
||||
{prettyPrintTime(Number(props.info.last_updated))}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={props.info.description}>
|
||||
<KeyValue key="Description">
|
||||
<span class="text-neutral-300 truncate">
|
||||
{props.info.description}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key="Fees">
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall amountSats={props.info.fees_paid} />
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="Bolt11">
|
||||
<MiniStringShower text={props.info.bolt11 ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key="Payment Hash">
|
||||
<MiniStringShower text={props.info.payment_hash ?? ""} />
|
||||
</KeyValue>
|
||||
<KeyValue key="Preimage">
|
||||
<MiniStringShower text={props.info.preimage ?? ""} />
|
||||
</KeyValue>
|
||||
</ul>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
export function DetailsModal(props: { title: string, open: boolean, data?: MutinyInvoice | OnChainTx, setOpen: (open: boolean) => void, children?: JSX.Element }) {
|
||||
const json = createMemo(() => JSON.stringify(props.data, null, 2));
|
||||
function OnchainDetails(props: { info: OnChainTx }) {
|
||||
const [state, _actions] = useMegaStore()
|
||||
|
||||
return (
|
||||
<Dialog.Root open={props.open} onOpenChange={(isOpen) => props.setOpen(isOpen)}>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<div />
|
||||
<Dialog.CloseButton>
|
||||
<button tabindex="-1" class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue ">
|
||||
<img src={close} alt="Close" class="w-8 h-8" />
|
||||
</button>
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Title>
|
||||
<LightningHeader info={props.data as MutinyInvoice} />
|
||||
</Dialog.Title>
|
||||
<Hr />
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<LightningDetails info={props.data as MutinyInvoice} />
|
||||
<div class="flex justify-center">
|
||||
<CopyButton title="Copy" text={json()} />
|
||||
</div>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root >
|
||||
)
|
||||
const confirmationTime = () => {
|
||||
return props.info.confirmation_time?.Confirmed?.time
|
||||
}
|
||||
|
||||
const network = state.mutiny_wallet?.get_network() as Network
|
||||
|
||||
return (
|
||||
<VStack>
|
||||
<ul class="flex flex-col gap-4">
|
||||
<KeyValue key="Status">
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime() ? "Confirmed" : "Unconfirmed"}
|
||||
</span>
|
||||
</KeyValue>
|
||||
<Show when={confirmationTime()}>
|
||||
<KeyValue key="When">
|
||||
<span class="text-neutral-300">
|
||||
{confirmationTime()
|
||||
? prettyPrintTime(Number(confirmationTime()))
|
||||
: "Pending"}
|
||||
</span>
|
||||
</KeyValue>
|
||||
</Show>
|
||||
<KeyValue key="Fee">
|
||||
<span class="text-neutral-300">
|
||||
<AmountSmall amountSats={props.info.fee} />
|
||||
</span>
|
||||
</KeyValue>
|
||||
<KeyValue key="Txid">
|
||||
<MiniStringShower text={props.info.txid ?? ""} />
|
||||
</KeyValue>
|
||||
</ul>
|
||||
<a
|
||||
class="uppercase font-light text-center"
|
||||
href={mempoolTxUrl(props.info.txid, network)}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Mempool.space
|
||||
</a>
|
||||
</VStack>
|
||||
)
|
||||
}
|
||||
|
||||
export function DetailsModal(props: {
|
||||
open: boolean
|
||||
data: MutinyInvoice | OnChainTx
|
||||
setOpen: (open: boolean) => void
|
||||
children?: JSX.Element
|
||||
}) {
|
||||
const json = createMemo(() => JSON.stringify(props.data, null, 2))
|
||||
|
||||
const isInvoice = () => {
|
||||
return ("bolt11" in props.data) as boolean
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog.Root
|
||||
open={props.open}
|
||||
onOpenChange={(isOpen) => props.setOpen(isOpen)}
|
||||
>
|
||||
<Dialog.Portal>
|
||||
<Dialog.Overlay class={OVERLAY} />
|
||||
<div class={DIALOG_POSITIONER}>
|
||||
<Dialog.Content class={DIALOG_CONTENT}>
|
||||
<div class="flex justify-between mb-2">
|
||||
<div />
|
||||
<Dialog.CloseButton>
|
||||
<button
|
||||
tabindex="-1"
|
||||
class="self-center hover:bg-white/10 rounded-lg active:bg-m-blue "
|
||||
>
|
||||
<img src={close} alt="Close" class="w-8 h-8" />
|
||||
</button>
|
||||
</Dialog.CloseButton>
|
||||
</div>
|
||||
<Dialog.Title>
|
||||
<Switch>
|
||||
<Match when={isInvoice()}>
|
||||
<LightningHeader info={props.data as MutinyInvoice} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<OnchainHeader info={props.data as OnChainTx} />
|
||||
</Match>
|
||||
</Switch>
|
||||
</Dialog.Title>
|
||||
<Hr />
|
||||
<Dialog.Description class="flex flex-col gap-4">
|
||||
<Switch>
|
||||
<Match when={isInvoice()}>
|
||||
<LightningDetails info={props.data as MutinyInvoice} />
|
||||
</Match>
|
||||
<Match when={true}>
|
||||
<OnchainDetails info={props.data as OnChainTx} />
|
||||
</Match>
|
||||
</Switch>
|
||||
<div class="flex justify-center">
|
||||
<CopyButton title="Copy" text={json()} />
|
||||
</div>
|
||||
</Dialog.Description>
|
||||
</Dialog.Content>
|
||||
</div>
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user