diff --git a/src/components/Activity.tsx b/src/components/Activity.tsx index 34986129..d8f55920 100644 --- a/src/components/Activity.tsx +++ b/src/components/Activity.tsx @@ -11,8 +11,8 @@ import { import { A } from "solid-start"; import { + ActivityDetailsModal, ActivityItem, - DetailsIdModal, HackActivityType, LoadingShimmer, NiceP @@ -30,7 +30,7 @@ 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]"; -interface IActivityItem { +export interface IActivityItem { kind: HackActivityType; id: string; amount_sats: number; @@ -50,7 +50,6 @@ function UnifiedActivityItem(props: { props.item.kind as unknown as HackActivityType ); }; - return ( } > - +
+ {props.info.inbound + ? i18n.t("activity.transaction_details.lightning_receive") + : i18n.t("activity.transaction_details.lightning_send")} + lightning bolt +
+
+
+ +
+
+ +
+
+ + ); +} + +function OnchainHeader(props: { info: OnChainTx; kind?: HackActivityType }) { + const i18n = useI18n(); + + 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 ( +
+
+ {props.kind === "ChannelOpen" + ? i18n.t("activity.transaction_details.channel_open") + : props.kind === "ChannelClose" + ? i18n.t("activity.transaction_details.channel_close") + : isSend() + ? i18n.t("activity.transaction_details.onchain_send") + : i18n.t("activity.transaction_details.onchain_receive")} + + + swap + + + blockchain + + +
+ +
+
+ +
+
+ +
+
+
+
+ ); +} + +export function MiniStringShower(props: { text: string }) { + const [copy, copied] = useCopy({ copiedTimeout: 1000 }); + + return ( +
+ + +
+ ); +} + +export function FormatPrettyPrint(props: { ts: number }) { + return ( +
+ {prettyPrintTime(props.ts).split(",", 2).join(",")} +
+ {prettyPrintTime(props.ts).split(", ")[2]} +
+
+ ); +} + +function LightningDetails(props: { info: MutinyInvoice; tags?: TagItem }) { + const i18n = useI18n(); + const [state, _actions] = useMegaStore(); + return ( + +
    + + + + + + { + // noop + }} + > + {props.tags?.name || props.info.labels[0]} + + + + + {props.info.paid + ? i18n.t("activity.transaction_details.paid") + : i18n.t("activity.transaction_details.unpaid")} + + + + + + + {props.info.description} + + + + + + + + + + + +
+
+ ); +} + +function OnchainDetails(props: { + info: OnChainTx; + kind?: HackActivityType; + tags?: TagItem; +}) { + const i18n = useI18n(); + const [state, _actions] = useMegaStore(); + const [copy, copied] = useCopy({ copiedTimeout: 1000 }); + + const confirmationTime = () => { + return props.info.confirmation_time?.Confirmed?.time; + }; + + const network = state.mutiny_wallet?.get_network() as Network; + + // Can return nothing if the channel is already closed + const [channelInfo] = createResource(async () => { + if (props.kind === "ChannelOpen") { + try { + const channels = + await (state.mutiny_wallet?.list_channels() as Promise< + MutinyChannel[] + >); + const channel = channels.find( + (channel) => channel.outpoint?.startsWith(props.info.txid) + ); + return channel; + } catch (e) { + console.error(e); + } + } else { + return undefined; + } + }); + + return ( + + {/*
{JSON.stringify(channelInfo() || "", null, 2)}
*/} +
    + + + + + + + + + + + + + {i18n.t("activity.transaction_details.no_details")} + + + + 0 + } + > + + + + + + + { + // noop + }} + > + {props.tags?.name || props.info.labels[0]} + + + + + {confirmationTime() + ? i18n.t("activity.transaction_details.confirmed") + : i18n.t("activity.transaction_details.unconfirmed")} + + + {confirmationTime() ? ( + + ) : ( + "Pending" + )} + + + + + + + +
    + {/* Have to do all these shenanigans because css / html is hard */} + + +
    +
    +
+
+ ); +} + +function ChannelCloseDetails(props: { info: ChannelClosure }) { + const i18n = useI18n(); + return ( + + {/*
{JSON.stringify(props.info.value, null, 2)}
*/} +
    + +

    {i18n.t("activity.transaction_details.sweep_delay")}

    +
    + + + + + + {props.info.timestamp ? ( + + ) : ( + i18n.t("common.pending") + )} + + + +

    + {props.info.reason ?? ""} +

    +
    +
+
+ ); +} + +export function ActivityDetailsModal(props: { + open: boolean; + kind?: HackActivityType; + id: string; + setOpen: (open: boolean) => void; +}) { + const [state, _actions] = useMegaStore(); + const id = () => props.id; + const kind = () => props.kind; + + const [data, { refetch }] = createResource(async () => { + try { + if (kind() === "Lightning") { + console.debug("reading invoice: ", id()); + const invoice = await state.mutiny_wallet?.get_invoice_by_hash( + id() + ); + return invoice; + } else if (kind() === "ChannelClose") { + console.debug("reading channel close: ", id()); + const closeItem = + await state.mutiny_wallet?.get_channel_closure(id()); + + return closeItem; + } else { + console.debug("reading tx: ", id()); + const tx = await state.mutiny_wallet?.get_transaction(id()); + + return tx; + } + } catch (e) { + console.error(e); + return undefined; + } + }); + const tags = createMemo(() => { + if ( + !!data() && + data()?.labels !== undefined && + typeof data()?.labels[0] === "string" + ) { + try { + // find if there's just one for now + const contacts = state.mutiny_wallet?.get_contact( + data().labels[0] + ); + if (contacts) { + return contacts; + } else { + return; + } + } catch (e) { + console.error(e); + } + } else { + return; + } + }); + + createEffect(() => { + if (props.id && props.kind && props.open) { + refetch(); + } + }); + + return ( + + + +
+ + +
+
+
+ + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + +
+ + +
+ + + ); +} diff --git a/src/components/Amount.tsx b/src/components/Amount.tsx index f48de39b..92f693cb 100644 --- a/src/components/Amount.tsx +++ b/src/components/Amount.tsx @@ -102,9 +102,9 @@ export function AmountSmall(props: { }) { const i18n = useI18n(); return ( - + {prettyPrintAmount(props.amountSats)}  - + {props.amountSats === 1 || props.amountSats === 1n ? i18n.t("common.sat") : i18n.t("common.sats")} diff --git a/src/components/AmountCard.tsx b/src/components/AmountCard.tsx index 4af7287f..b899e8d8 100644 --- a/src/components/AmountCard.tsx +++ b/src/components/AmountCard.tsx @@ -9,7 +9,9 @@ const noop = () => { // do nothing }; -const KeyValue: ParentComponent<{ key: string; gray?: boolean }> = (props) => { +const AmountKeyValue: ParentComponent<{ key: string; gray?: boolean }> = ( + props +) => { return (
- +
{amountInFiat()}  {state.fiat.value}
-
+ ); } @@ -99,7 +101,7 @@ export function AmountCard(props: {
- + - - + + - +

- + - +
- + - - + - +

- + - +
- + - +
diff --git a/src/components/BalanceBox.tsx b/src/components/BalanceBox.tsx index 86511dad..8106a9a7 100644 --- a/src/components/BalanceBox.tsx +++ b/src/components/BalanceBox.tsx @@ -13,13 +13,19 @@ import { import { useI18n } from "~/i18n/context"; import { useMegaStore } from "~/state/megaStore"; -export function LoadingShimmer() { +export function LoadingShimmer(props: { center?: boolean }) { return (
-

+

-

+

diff --git a/src/components/ContactViewer.tsx b/src/components/ContactViewer.tsx index 7012e481..88c70c62 100644 --- a/src/components/ContactViewer.tsx +++ b/src/components/ContactViewer.tsx @@ -171,9 +171,9 @@ export function ContactViewer(props: { } > -
- lightning bolt -
-

- {props.info.inbound - ? i18n.t("modals.transaction_details.lightning_receive") - : i18n.t("modals.transaction_details.lightning_send")} -

- - - {(tag) => ( - { - // noop - }} - > - {tag.name} - - )} - -
- ); -} - -function OnchainHeader(props: { - info: OnChainTx; - tags: MutinyTagItem[]; - kind?: HackActivityType; -}) { - const i18n = useI18n(); - const [state, _actions] = useMegaStore(); - - 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 ( -
-
- - - swap - - - blockchain - - -
-

- {props.kind === "ChannelOpen" - ? i18n.t("modals.transaction_details.channel_open") - : props.kind === "ChannelClose" - ? i18n.t("modals.transaction_details.channel_close") - : isSend() - ? i18n.t("modals.transaction_details.onchain_send") - : i18n.t("modals.transaction_details.onchain_receive")} -

- - - - - {(tag) => ( - { - // noop - }} - > - {tag.name} - - )} - -
- ); -} - -export const KeyValue: ParentComponent<{ key: string }> = (props) => { - return ( -
  • - - {props.key} - - {props.children} -
  • - ); -}; - -export function MiniStringShower(props: { text: string }) { - const [copy, copied] = useCopy({ copiedTimeout: 1000 }); - - return ( -
    - - {/*
    {props.text}
    */} - -
    - ); -} - -function LightningDetails(props: { info: MutinyInvoice }) { - const i18n = useI18n(); - return ( - -
      - - - {props.info.paid - ? i18n.t("modals.transaction_details.paid") - : i18n.t("modals.transaction_details.unpaid")} - - - - - {prettyPrintTime(Number(props.info.last_updated))} - - - - - - {props.info.description} - - - - - - - - - - - - - - - - - -
    -
    - ); -} - -function OnchainDetails(props: { info: OnChainTx; kind?: HackActivityType }) { - const i18n = useI18n(); - const [state, _actions] = useMegaStore(); - - const confirmationTime = () => { - return props.info.confirmation_time?.Confirmed?.time; - }; - - const network = state.mutiny_wallet?.get_network() as Network; - - // Can return nothing if the channel is already closed - const [channelInfo] = createResource(async () => { - if (props.kind === "ChannelOpen") { - try { - const channels = - await (state.mutiny_wallet?.list_channels() as Promise< - MutinyChannel[] - >); - const channel = channels.find( - (channel) => channel.outpoint?.startsWith(props.info.txid) - ); - return channel; - } catch (e) { - console.error(e); - } - } else { - return undefined; - } - }); - - return ( - - {/*
    {JSON.stringify(channelInfo() || "", null, 2)}
    */} -
      - - - {confirmationTime() - ? i18n.t("modals.transaction_details.confirmed") - : i18n.t("modals.transaction_details.unconfirmed")} - - - - - - {confirmationTime() - ? prettyPrintTime(Number(confirmationTime())) - : "Pending"} - - - - 0}> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {i18n.t("modals.transaction_details.no_details")} - - - -
    -
    - - {i18n.t("common.view_transaction")} - -
    -
    - ); -} - -function ChannelCloseDetails(props: { info: ChannelClosure }) { - const i18n = useI18n(); - return ( - - {/*
    {JSON.stringify(props.info.value, null, 2)}
    */} -
      - - - - - - - {props.info.timestamp - ? prettyPrintTime(Number(props.info.timestamp)) - : i18n.t("common.pending")} - - - - -

      - {props.info.reason ?? ""} -

      -
      -
    -
    - ); -} - -export function DetailsIdModal(props: { - open: boolean; - kind?: HackActivityType; - id: string; - setOpen: (open: boolean) => void; -}) { - const i18n = useI18n(); - const [state, _actions] = useMegaStore(); - - const id = () => props.id; - const kind = () => props.kind; - - // TODO: is there a cleaner way to do refetch when id changes? - const [data, { refetch }] = createResource(async () => { - try { - if (kind() === "Lightning") { - console.debug("reading invoice: ", id()); - const invoice = await state.mutiny_wallet?.get_invoice_by_hash( - id() - ); - return invoice; - } else if (kind() === "ChannelClose") { - console.debug("reading channel close: ", id()); - const closeItem = - await state.mutiny_wallet?.get_channel_closure(id()); - - return closeItem; - } else { - console.debug("reading tx: ", id()); - const tx = await state.mutiny_wallet?.get_transaction(id()); - - return tx; - } - } catch (e) { - console.error(e); - return undefined; - } - }); - - const tags = createMemo(() => { - if (data() && data().labels && data().labels.length > 0) { - try { - const contact = state.mutiny_wallet?.get_contact( - data().labels[0] - ); - if (contact) { - return [tagToMutinyTag(contact)]; - } else { - return []; - } - } catch (e) { - console.error(e); - return []; - } - } else { - return []; - } - }); - - createEffect(() => { - if (props.id && props.kind && props.open) { - refetch(); - } - }); - - const json = createMemo(() => JSON.stringify(data() || "", null, 2)); - - return ( - - - -
    - - -
    -
    - - - -
    - - - - - - - - - - -
    - - - - - - - - - - - - - -
    - -
    -
    -
    - - -
    - - - ); -} diff --git a/src/components/ShareCard.tsx b/src/components/ShareCard.tsx index 92890bd6..2100f346 100644 --- a/src/components/ShareCard.tsx +++ b/src/components/ShareCard.tsx @@ -46,8 +46,7 @@ export function TruncateMiddle(props: { text: string; whiteBg?: boolean }) {
    {props.text} diff --git a/src/components/index.ts b/src/components/index.ts index 1cf4531c..53cfc36f 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -2,6 +2,7 @@ export * from "./layout"; export * from "./successfail"; export * from "./Activity"; +export * from "./ActivityDetailsModal"; export * from "./ActivityItem"; export * from "./Amount"; export * from "./AmountCard"; @@ -14,7 +15,6 @@ export * from "./ContactForm"; export * from "./ContactViewer"; export * from "./DecryptDialog"; export * from "./DeleteEverything"; -export * from "./DetailsModal"; export * from "./ErrorDisplay"; export * from "./Fee"; export * from "./I18nProvider"; diff --git a/src/components/layout/BackPop.tsx b/src/components/layout/BackPop.tsx index 3458adb1..fb6cc479 100644 --- a/src/components/layout/BackPop.tsx +++ b/src/components/layout/BackPop.tsx @@ -21,7 +21,11 @@ export function BackPop() { return ( navigate(backPath())} showOnDesktop /> diff --git a/src/components/layout/Misc.tsx b/src/components/layout/Misc.tsx index 7f7e510b..6f998302 100644 --- a/src/components/layout/Misc.tsx +++ b/src/components/layout/Misc.tsx @@ -184,6 +184,17 @@ export const MutinyWalletGuard: ParentComponent = (props) => { export const Hr = () => ; +export const KeyValue: ParentComponent<{ key: string }> = (props) => { + return ( +
  • + + {props.key} + + {props.children} +
  • + ); +}; + export const LargeHeader: ParentComponent<{ action?: JSX.Element; centered?: boolean; diff --git a/src/i18n/en/translations.ts b/src/i18n/en/translations.ts index 7016f5fc..3015a53d 100644 --- a/src/i18n/en/translations.ts +++ b/src/i18n/en/translations.ts @@ -17,7 +17,8 @@ export default { error_unimplemented: "Unimplemented", why: "Why?", private_tags: "Private tags", - view_transaction: "View Transaction", + view_transaction: "View transaction", + view_payment_details: "View payment details", pending: "Pending", error_safe_mode: "Mutiny is running in safe mode. Lightning is disabled.", @@ -36,6 +37,7 @@ export default { pay: "Pay", name: "Name", placeholder: "Satoshi", + lightning_address: "Lightning Address", unimplemented: "Unimplemented", not_available: "We don't do that yet", error_name: "We at least need a name" @@ -163,7 +165,40 @@ export default { coming_soon: "Coming soon", private: "Private", anonymous: "Anonymous", - from: "From:" + from: "From:", + transaction_details: { + lightning_receive: "Received via Lightning", + lightning_send: "Sent via Lightning", + channel_open: "Channel open", + channel_close: "Channel close", + onchain_receive: "On-chain receive", + onchain_send: "On-chain send", + paid: "Paid", + unpaid: "Unpaid", + status: "Status", + date: "Date", + tagged_to: "Tagged to", + description: "Description", + fee: "Fee", + onchain_fee: "On-chain Fee", + invoice: "Invoice", + payment_hash: "Payment Hash", + payment_preimage: "Preimage", + txid: "Txid", + total: "Amount Requested", + balance: "Balance", + reserve: "Reserve", + peer: "Peer", + channel_id: "Channel ID", + reason: "Reason", + confirmed: "Confirmed", + unconfirmed: "Unconfirmed", + sweep_delay: + "Funds may take a few days to be swept back into the wallet", + no_details: + "No channel details found, which means this channel has likely been closed.", + back_home: "back home" + } }, redshift: { title: "Redshift", @@ -625,34 +660,6 @@ export default { "If you want to use pretend money to test out Mutiny without risk,", signet_link: "check out our Signet version." }, - transaction_details: { - lightning_receive: "Lightning receive", - lightning_send: "Lightning send", - channel_open: "Channel open", - channel_close: "Channel close", - onchain_receive: "On-chain receive", - onchain_send: "On-chain send", - paid: "Paid", - unpaid: "Unpaid", - status: "Status", - when: "When", - description: "Description", - fee: "Fee", - fees: "Fees", - bolt11: "Bolt11", - payment_hash: "Payment Hash", - preimage: "Preimage", - txid: "Txid", - balance: "Balance", - reserve: "Reserve", - peer: "Peer", - channel_id: "Channel ID", - reason: "Reason", - confirmed: "Confirmed", - unconfirmed: "Unconfirmed", - no_details: - "No channel details found, which means this channel has likely been closed." - }, more_info: { whats_with_the_fees: "What's with the fees?", self_custodial: diff --git a/src/i18n/ko/translations.ts b/src/i18n/ko/translations.ts index 0bd1caa6..376131c2 100644 --- a/src/i18n/ko/translations.ts +++ b/src/i18n/ko/translations.ts @@ -145,7 +145,35 @@ export default { unknown: "알 수 없음", import_contacts: "Nostr에서 연락처를 가져와 누가 체널을 열고 있는지 확인하세요.", - coming_soon: "곧 출시 예정" + coming_soon: "곧 출시 예정", + transaction_details: { + lightning_receive: "라이트닝 입금", + lightning_send: "라이트닝 송금", + channel_open: "채널 개설", + channel_close: "채널 종료", + onchain_receive: "체인상 입금", + onchain_send: "체인상 송금", + paid: "지불 완료", + unpaid: "미지불", + status: "상태", + when: "시간", + description: "설명", + fee: "수수료", + fees: "수수료", + bolt11: "Bolt11", + payment_hash: "지불 해시", + preimage: "사전 이미지", + txid: "거래 ID", + balance: "잔고", + reserve: "리저브", + peer: "피어", + channel_id: "채널 ID", + reason: "이유", + confirmed: "확인됨", + unconfirmed: "확인 대기", + no_details: + "채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다." + } }, redshift: { title: "레드시프트", @@ -497,34 +525,6 @@ export default { "위험 없이 Mutiny를 테스트하려면 가상 자금을 사용하려면", signet_link: "Signet 버전을 확인하세요." }, - transaction_details: { - lightning_receive: "라이트닝 입금", - lightning_send: "라이트닝 송금", - channel_open: "채널 개설", - channel_close: "채널 종료", - onchain_receive: "체인상 입금", - onchain_send: "체인상 송금", - paid: "지불 완료", - unpaid: "미지불", - status: "상태", - when: "시간", - description: "설명", - fee: "수수료", - fees: "수수료", - bolt11: "Bolt11", - payment_hash: "지불 해시", - preimage: "사전 이미지", - txid: "거래 ID", - balance: "잔고", - reserve: "리저브", - peer: "피어", - channel_id: "채널 ID", - reason: "이유", - confirmed: "확인됨", - unconfirmed: "확인 대기", - no_details: - "채널 상세정보를 찾을 수 없습니다. 이는 해당 채널이 종료된 것으로 보입니다." - }, more_info: { whats_with_the_fees: "수수료는 어떻게 되나요?", self_custodial: diff --git a/src/root.css b/src/root.css index e18954de..41966a36 100644 --- a/src/root.css +++ b/src/root.css @@ -29,7 +29,7 @@ html { } a { - @apply underline decoration-light-text hover:decoration-white; + @apply underline decoration-m-grey-400 hover:decoration-white; } p { diff --git a/src/routes/Receive.tsx b/src/routes/Receive.tsx index 931c9e79..e90ef414 100644 --- a/src/routes/Receive.tsx +++ b/src/routes/Receive.tsx @@ -17,6 +17,7 @@ import { useNavigate } from "solid-start"; import side2side from "~/assets/icons/side-to-side.svg"; import { + ActivityDetailsModal, AmountCard, AmountFiat, AmountSats, @@ -26,9 +27,9 @@ import { Card, Checkbox, DefaultMain, - ExternalLink, Fee, FeesModal, + HackActivityType, Indicator, InfoBox, IntegratedQr, @@ -45,11 +46,9 @@ import { VStack } from "~/components"; import { useI18n } from "~/i18n/context"; -import { Network } from "~/logic/mutinyWalletSetup"; import { useMegaStore } from "~/state/megaStore"; import { eify, - mempoolTxUrl, MutinyTagItem, objectToSearchParams, vibrateSuccess @@ -142,6 +141,11 @@ export default function Receive() { const [loading, setLoading] = createSignal(false); const [error, setError] = createSignal(""); + // Details Modal + const [detailsOpen, setDetailsOpen] = createSignal(false); + const [detailsKind, setDetailsKind] = createSignal(); + const [detailsId, setDetailsId] = createSignal(""); + const RECEIVE_FLAVORS = [ { value: "unified", @@ -184,6 +188,30 @@ export default function Receive() { setSelectedValues([]); } + function openDetailsModal() { + const paymentTxId = + paidState() === "onchain_paid" + ? paymentTx() + ? paymentTx()?.txid + : undefined + : paymentInvoice() + ? paymentInvoice()?.payment_hash + : undefined; + const kind = paidState() === "onchain_paid" ? "OnChain" : "Lightning"; + + console.log("Opening details modal: ", paymentTxId, kind); + + if (!paymentTxId) { + console.warn("No id provided to openDetailsModal"); + return; + } + if (paymentTxId !== undefined) { + setDetailsId(paymentTxId); + } + setDetailsKind(kind); + setDetailsOpen(true); + } + async function processContacts( contacts: Partial[] ): Promise { @@ -346,8 +374,6 @@ export default function Receive() { const [paidState, { refetch }] = createResource(bip21Raw, checkIfPaid); - const network = state.mutiny_wallet?.get_network() as Network; - createEffect(() => { const interval = setInterval(() => { if (receiveState() === "show") refetch(); @@ -483,6 +509,14 @@ export default function Receive() { navigate("/"); }} > + + +

    {receiveState() === "paid" && @@ -525,22 +559,14 @@ export default function Receive() { > - {/*TODO: Confirmation time estimate still not possible needs to be implemented in mutiny-node first - {/*TODO: add internal payment detail page* for lightning*/} - - +

    - {i18n.t("common.view_transaction")} - + {i18n.t("common.view_payment_details")} +

    diff --git a/src/routes/Send.tsx b/src/routes/Send.tsx index 7b7b7690..369d07c7 100644 --- a/src/routes/Send.tsx +++ b/src/routes/Send.tsx @@ -15,6 +15,7 @@ import { useNavigate } from "solid-start"; import { Paste } from "~/assets/svg/Paste"; import { Scan } from "~/assets/svg/Scan"; import { + ActivityDetailsModal, AmountCard, AmountFiat, AmountSats, @@ -24,9 +25,9 @@ import { ButtonLink, Card, DefaultMain, - ExternalLink, Fee, GiftLink, + HackActivityType, HStack, InfoBox, LargeHeader, @@ -44,10 +45,9 @@ import { VStack } from "~/components"; import { useI18n } from "~/i18n/context"; -import { Network } from "~/logic/mutinyWalletSetup"; import { ParsedParams } from "~/logic/waila"; import { useMegaStore } from "~/state/megaStore"; -import { eify, mempoolTxUrl, MutinyTagItem, vibrateSuccess } from "~/utils"; +import { eify, MutinyTagItem, vibrateSuccess } from "~/utils"; export type SendSource = "lightning" | "onchain"; @@ -59,6 +59,7 @@ type SentDetails = { amount?: bigint; destination?: string; txid?: string; + payment_hash?: string; failure_reason?: string; fee_estimate?: bigint | number; }; @@ -227,6 +228,11 @@ export default function Send() { Partial[] >([]); + // Details Modal + const [detailsOpen, setDetailsOpen] = createSignal(false); + const [detailsKind, setDetailsKind] = createSignal(); + const [detailsId, setDetailsId] = createSignal(""); + // Errors const [error, setError] = createSignal(); @@ -243,6 +249,29 @@ export default function Send() { setFieldDestination(""); } + function openDetailsModal() { + const paymentTxId = sentDetails()?.txid + ? sentDetails() + ? sentDetails()?.txid + : undefined + : sentDetails() + ? sentDetails()?.payment_hash + : undefined; + const kind = sentDetails()?.txid ? "OnChain" : "Lightning"; + + console.log("Opening details modal: ", paymentTxId, kind); + + if (!paymentTxId) { + console.warn("No id provided to openDetailsModal"); + return; + } + if (paymentTxId !== undefined) { + setDetailsId(paymentTxId); + } + setDetailsKind(kind); + setDetailsOpen(true); + } + // If we got here from a scan result we want to set the destination and clean up that scan result onMount(() => { if (state.scan_result) { @@ -478,6 +507,7 @@ export default function Send() { tags ); sentDetails.amount = invoice()?.amount_sats; + sentDetails.payment_hash = invoice()?.payment_hash; sentDetails.fee_estimate = payment?.fees_paid || 0; } else { const payment = await state.mutiny_wallet?.pay_invoice( @@ -487,6 +517,7 @@ export default function Send() { tags ); sentDetails.amount = amountSats(); + sentDetails.payment_hash = invoice()?.payment_hash; sentDetails.fee_estimate = payment?.fees_paid || 0; } } else if (source() === "lightning" && nodePubkey()) { @@ -505,6 +536,7 @@ export default function Send() { throw new Error(i18n.t("send.error_keysend")); } else { sentDetails.amount = amountSats(); + sentDetails.payment_hash = invoice()?.payment_hash; sentDetails.fee_estimate = payment?.fees_paid || 0; } } else if (source() === "lightning" && lnurlp()) { @@ -517,11 +549,13 @@ export default function Send() { undefined, // zap_npub tags ); + sentDetails.payment_hash = invoice()?.payment_hash; if (!payment?.paid) { throw new Error(i18n.t("send.error_LNURL")); } else { sentDetails.amount = amountSats(); + sentDetails.payment_hash = invoice()?.payment_hash; sentDetails.fee_estimate = payment?.fees_paid || 0; } } else if (source() === "onchain" && address()) { @@ -568,8 +602,6 @@ export default function Send() { return !destination() || sending() || amountSats() === 0n || !!error(); }); - const network = state.mutiny_wallet?.get_network() as Network; - return ( @@ -614,6 +646,14 @@ export default function Send() { {/*TODO: add failure hint logic for different failure conditions*/} + + +

    {sentDetails()?.amount @@ -638,16 +678,12 @@ export default function Send() {


    - - - {i18n.t("common.view_transaction")} - - +

    + {i18n.t("common.view_payment_details")} +

    diff --git a/src/routes/Swap.tsx b/src/routes/Swap.tsx index 6fca8223..741a8b92 100644 --- a/src/routes/Swap.tsx +++ b/src/routes/Swap.tsx @@ -12,13 +12,14 @@ import { import { useNavigate } from "solid-start"; import { + ActivityDetailsModal, AmountCard, AmountFiat, BackLink, Button, Card, DefaultMain, - ExternalLink, + HackActivityType, InfoBox, LargeHeader, MegaCheck, @@ -35,7 +36,7 @@ import { useI18n } from "~/i18n/context"; import { Network } from "~/logic/mutinyWalletSetup"; import { MethodChooser, SendSource } from "~/routes/Send"; import { useMegaStore } from "~/state/megaStore"; -import { eify, mempoolTxUrl, vibrateSuccess } from "~/utils"; +import { eify, vibrateSuccess } from "~/utils"; const CHANNEL_FEE_ESTIMATE_ADDRESS = "bc1qf7546vg73ddsjznzq57z3e8jdn6gtw6au576j07kt6d9j7nz8mzsyn6lgf"; @@ -62,9 +63,32 @@ export default function Swap() { const [selectedPeer, setSelectedPeer] = createSignal(""); + // Details Modal + const [detailsOpen, setDetailsOpen] = createSignal(false); + const [detailsKind, setDetailsKind] = createSignal(); + const [detailsId, setDetailsId] = createSignal(""); + const [channelOpenResult, setChannelOpenResult] = createSignal(); + function openDetailsModal() { + const paymentTxId = + channelOpenResult()?.channel?.outpoint?.split(":")[0]; + const kind: HackActivityType = "ChannelOpen"; + + console.log("Opening details modal: ", paymentTxId, kind); + + if (!paymentTxId) { + console.warn("No id provided to openDetailsModal"); + return; + } + if (paymentTxId !== undefined) { + setDetailsId(paymentTxId); + } + setDetailsKind(kind); + setDetailsOpen(true); + } + function resetState() { setSource("onchain"); setAmountSats(0n); @@ -258,8 +282,6 @@ export default function Swap() { return undefined; }); - const network = state.mutiny_wallet?.get_network() as Network; - return ( @@ -293,6 +315,14 @@ export default function Swap() { {/*TODO: Error hint needs to be added for possible failure reasons*/} + + +

    @@ -328,22 +358,12 @@ export default function Swap() {


    - - - {i18n.t("common.view_transaction")} - - + {i18n.t("common.view_payment_details")} +

    {/*
    {JSON.stringify(channelOpenResult()?.channel?.value, null, 2)}
    */}
    diff --git a/src/routes/settings/Channels.tsx b/src/routes/settings/Channels.tsx index 815fe2e3..a4bb4088 100644 --- a/src/routes/settings/Channels.tsx +++ b/src/routes/settings/Channels.tsx @@ -17,7 +17,7 @@ import { import { useI18n } from "~/i18n/context"; import { useMegaStore } from "~/state/megaStore"; -function BalanceBar(props: { +export function BalanceBar(props: { inbound: number; reserve: number; outbound: number;