diff --git a/package.json b/package.json index a17ffaf40..2158bf933 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "cap-sync-version": "pnpm dlx capacitor-set-version . -v $(jq -r .version package.json) -b 1" }, "dependencies": { - "@cashu/cashu-ts": "^2.2.1", + "@cashu/cashu-ts": "^2.2.2", "@chakra-ui/anatomy": "^2.3.4", "@chakra-ui/breakpoint-utils": "^2.0.8", "@chakra-ui/icons": "^2.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2afb29811..e8668f094 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,8 +13,8 @@ importers: .: dependencies: '@cashu/cashu-ts': - specifier: ^2.2.1 - version: 2.2.1 + specifier: ^2.2.2 + version: 2.2.2 '@chakra-ui/anatomy': specifier: ^2.3.4 version: 2.3.4 @@ -104,31 +104,31 @@ importers: version: 0.7.2 applesauce-accounts: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) applesauce-content: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) applesauce-core: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) applesauce-factory: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) applesauce-loaders: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) applesauce-react: specifier: next - version: 0.0.0-next-20250310121307(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2) + version: 0.0.0-next-20250310162525(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2) applesauce-relay: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) applesauce-signers: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) applesauce-wallet: specifier: next - version: 0.0.0-next-20250310121307(typescript@5.8.2) + version: 0.0.0-next-20250310162525(typescript@5.8.2) bech32: specifier: ^2.0.0 version: 2.0.0 @@ -1038,8 +1038,8 @@ packages: '@cashu/cashu-ts@2.0.0-rc1': resolution: {integrity: sha512-39459l7x/fUMEgOsCdGLLl6rMekO4nbv+wEuavmyElh8hgN8t66wcb29AJvdFTb6K3lPACKF2rs/jAlPYrN7Ng==} - '@cashu/cashu-ts@2.2.1': - resolution: {integrity: sha512-/A8Lfkf7nexldcAcTbqrITXxwgiCYTTnrthB8DoipLVeDfyUXer48FJdUmXpRp87Aijn2BNklo8qA0yO0kHXaA==} + '@cashu/cashu-ts@2.2.2': + resolution: {integrity: sha512-s4DRaIZOh2MC0qi0G1Te4KfOzzw91EZiIVupssKDmaPSUbT1ggLonj5qyzB4OCpI7uZ4lDpnxh43xkRZyAqotw==} '@cashu/crypto@0.2.7': resolution: {integrity: sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig==} @@ -2195,32 +2195,32 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - applesauce-accounts@0.0.0-next-20250310121307: - resolution: {integrity: sha512-Q1vEjYznvkxRDM2C/gN+UJpjva2QvFLrIlfMTyrfm7s1nDEIKP6wJ4Oj72PsnyZREP0/UnH7fP5olvJMakBOBg==} + applesauce-accounts@0.0.0-next-20250310162525: + resolution: {integrity: sha512-Gn+PJ3RgSOJGqKLcC9LRm7KgrwL64D0ZQBIZ7XORk/dF61kaF88gW3TnL72aUByjp8jJHxOTFxaR+HTR22aHZA==} - applesauce-content@0.0.0-next-20250310121307: - resolution: {integrity: sha512-oxMgVy1hS0ZWGL+0Skrt25vcMUwY8T3A1p45vc1q17oXWbIKBqZPtenoHarluEBdY9iUt0QBVVIFWSQtphudxw==} + applesauce-content@0.0.0-next-20250310162525: + resolution: {integrity: sha512-Bc5GS+taB29zewyH/t8Cfq3CdCF47xE20sVurNplrUyJ6jS99im+BFsgKQY1OX41xSsK6h3TDVjBYRUSvSwRtA==} - applesauce-core@0.0.0-next-20250310121307: - resolution: {integrity: sha512-u6YNDyPy2v+B8Iv6WFkTTnilLJfNgFVeDs+aGy4KpItq0tdEQcnwTIFRcTovSaCNn2YYNJ1WHw30/a622SmshA==} + applesauce-core@0.0.0-next-20250310162525: + resolution: {integrity: sha512-8E1PAfH4UPbS/GMNrKyrWO2DDtx+PlzMVnL5UskUX5YcEy3ziPCjXY10U4V32p+DKH+aGzFsWkaSVN4OR/TXaw==} - applesauce-factory@0.0.0-next-20250310121307: - resolution: {integrity: sha512-rJNnwwxIdX1s4vdmAmIDvappQT/0J0XH+xIpMDnuf1yvMmwK/lH+DvGWPdC2h/TNI4Lbd3c3774jUkgBZoKMrQ==} + applesauce-factory@0.0.0-next-20250310162525: + resolution: {integrity: sha512-avUTztNHvuZJe44OCoeI5aA8vcrokuqXPjsux+8xkrFOpLJ3LWcXOWQEhQJJ0sj9idiWoHa7P6kZVk7mQ+lhTw==} - applesauce-loaders@0.0.0-next-20250310121307: - resolution: {integrity: sha512-KiqVGqzIEOuGfLU6TGGjRFru7/0e1HATSyWsXyvyPJd0Pt5Ud9tkJidI+bAY+jZthDcBlFdEPT4HTPjKJ63AOg==} + applesauce-loaders@0.0.0-next-20250310162525: + resolution: {integrity: sha512-lwJvrruMeeNo25WK87GSDHb3QkxEmgM90rLrLSAdNVxhNW99ZhnHWmJr3bwMnRgRQoOTqNHAHhBV8Uk2TGsWCA==} - applesauce-react@0.0.0-next-20250310121307: - resolution: {integrity: sha512-MrWVYJUuKGAuW0ZjUTBgNP/o4AVth1vPlmAra0xYsH5y8puQvU0APqXpH60yrOGqMvtWiZbpMpnhPKjmjytxfw==} + applesauce-react@0.0.0-next-20250310162525: + resolution: {integrity: sha512-ZOewc/bvC2EiO5lOhNyiOBPzh1NsNA1fEDdHLI5oyUs76n4Pl/2sIz2Rhm9i+wMyxvrpqd26itglILibnMh+tw==} - applesauce-relay@0.0.0-next-20250310121307: - resolution: {integrity: sha512-5QyyRQc5vEHGUWJgHRw6hM5zSkvkqt+UYrwkL9yGcP1rQ/IyM0kcoog1VobKrSGjlQOyXxhw0cShSKoTzGWreA==} + applesauce-relay@0.0.0-next-20250310162525: + resolution: {integrity: sha512-JOq2SktlRGr6EIBJS3aZXxo+ySDcdySFdjlrNtSAMRfH6jGKHEkjbqgaMPdNQ0QGAkIYnnLTWwFSQJEYpH4H0w==} - applesauce-signers@0.0.0-next-20250310121307: - resolution: {integrity: sha512-dfppzzFIT3FtLF9JmBNAQToD8euOsWqt940itYMvesP5gV7BEHCSPiBzHdaXPNfK5/ay/kN9WHleBdwVcKTW5A==} + applesauce-signers@0.0.0-next-20250310162525: + resolution: {integrity: sha512-phmk143NUPEwu7h4bEL9Fpofx12C9lawhoC7+47CGYjgOdfAT2xjH+Xicot7qqUYjlE0QzYikS5rLzUXA/m93w==} - applesauce-wallet@0.0.0-next-20250310121307: - resolution: {integrity: sha512-9rwmR4RsJ1hv5nM7uOo/HWKHzhkhuueEtL99ZJleOGnQWm6fTz0LlMRDCT8OLlX7ZCC9Oho7p38YBqISQzJHew==} + applesauce-wallet@0.0.0-next-20250310162525: + resolution: {integrity: sha512-Bcp/HUyGyW3057wtK0y/eWLeEmV2WfuwpMNFWAHjs/Ro50FFNLt9YA7tEjiMQe42KQTwn+YjBXOpm2FFxDxAfg==} arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -6961,7 +6961,7 @@ snapshots: '@scure/bip32': 1.6.2 buffer: 6.0.3 - '@cashu/cashu-ts@2.2.1': + '@cashu/cashu-ts@2.2.2': dependencies: '@cashu/crypto': 0.3.4 '@noble/curves': 1.8.1 @@ -8435,10 +8435,10 @@ snapshots: dependencies: entities: 2.2.0 - applesauce-accounts@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-accounts@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: '@noble/hashes': 1.7.1 - applesauce-signers: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-signers: 0.0.0-next-20250310162525(typescript@5.8.2) nanoid: 5.1.3 nostr-tools: 2.10.4(typescript@5.8.2) rxjs: 7.8.2 @@ -8446,13 +8446,13 @@ snapshots: - supports-color - typescript - applesauce-content@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-content@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: '@cashu/cashu-ts': 2.0.0-rc1 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310162525(typescript@5.8.2) mdast-util-find-and-replace: 3.0.2 nostr-tools: 2.10.4(typescript@5.8.2) remark: 15.0.1 @@ -8463,7 +8463,7 @@ snapshots: - supports-color - typescript - applesauce-core@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-core@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: '@noble/hashes': 1.7.1 '@scure/base': 1.2.4 @@ -8478,19 +8478,19 @@ snapshots: - supports-color - typescript - applesauce-factory@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-factory@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: - applesauce-content: 0.0.0-next-20250310121307(typescript@5.8.2) - applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-content: 0.0.0-next-20250310162525(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310162525(typescript@5.8.2) nanoid: 5.1.3 nostr-tools: 2.10.4(typescript@5.8.2) transitivePeerDependencies: - supports-color - typescript - applesauce-loaders@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-loaders@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: - applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310162525(typescript@5.8.2) nanoid: 5.1.3 nostr-tools: 2.10.4(typescript@5.8.2) rx-nostr: 3.5.0 @@ -8499,12 +8499,12 @@ snapshots: - supports-color - typescript - applesauce-react@0.0.0-next-20250310121307(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2): + applesauce-react@0.0.0-next-20250310162525(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2): dependencies: - applesauce-accounts: 0.0.0-next-20250310121307(typescript@5.8.2) - applesauce-content: 0.0.0-next-20250310121307(typescript@5.8.2) - applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) - applesauce-factory: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-accounts: 0.0.0-next-20250310162525(typescript@5.8.2) + applesauce-content: 0.0.0-next-20250310162525(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310162525(typescript@5.8.2) + applesauce-factory: 0.0.0-next-20250310162525(typescript@5.8.2) nostr-tools: 2.10.4(typescript@5.8.2) observable-hooks: 4.2.4(react-dom@19.0.0(react@19.0.0))(react@18.3.1)(rxjs@7.8.2) react: 18.3.1 @@ -8514,9 +8514,9 @@ snapshots: - supports-color - typescript - applesauce-relay@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-relay@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: - applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310162525(typescript@5.8.2) nanoid: 5.1.3 nostr-tools: 2.10.4(typescript@5.8.2) rxjs: 7.8.2 @@ -8524,12 +8524,12 @@ snapshots: - supports-color - typescript - applesauce-signers@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-signers@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: '@noble/hashes': 1.7.1 '@noble/secp256k1': 1.7.1 '@scure/base': 1.2.4 - applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310162525(typescript@5.8.2) debug: 4.4.0 nanoid: 5.1.3 nostr-tools: 2.10.4(typescript@5.8.2) @@ -8537,9 +8537,9 @@ snapshots: - supports-color - typescript - applesauce-wallet@0.0.0-next-20250310121307(typescript@5.8.2): + applesauce-wallet@0.0.0-next-20250310162525(typescript@5.8.2): dependencies: - applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310162525(typescript@5.8.2) nostr-tools: 2.10.4(typescript@5.8.2) rxjs: 7.8.2 transitivePeerDependencies: @@ -8700,7 +8700,7 @@ snapshots: blossom-client-sdk@3.0.1: dependencies: - '@cashu/cashu-ts': 2.2.1 + '@cashu/cashu-ts': 2.2.2 '@noble/hashes': 1.7.1 blossom-server-sdk@0.4.0: diff --git a/src/components/cashu/cashu-mint-favicon.tsx b/src/components/cashu/cashu-mint-favicon.tsx new file mode 100644 index 000000000..9ecbe56f9 --- /dev/null +++ b/src/components/cashu/cashu-mint-favicon.tsx @@ -0,0 +1,20 @@ +import { useMemo } from "react"; +import { useAsync } from "react-use"; +import { Avatar, AvatarProps } from "@chakra-ui/react"; + +import { MediaServerIcon } from "../icons"; +import { getCashuMint } from "../../services/cashu-mints"; + +export default function CashuMintFavicon({ mint, ...props }: { mint: string } & Omit) { + const { value: cashuMint } = useAsync(() => getCashuMint(mint), [mint]); + const { value: info } = useAsync(async () => cashuMint?.getInfo(), [cashuMint]); + + const url = useMemo(() => { + const url = new URL(mint); + url.protocol = "https:"; + url.pathname = "/favicon.ico"; + return url.toString(); + }, [mint]); + + return } overflow="hidden" {...props} />; +} diff --git a/src/components/cashu/cashu-mint-name.tsx b/src/components/cashu/cashu-mint-name.tsx new file mode 100644 index 000000000..f7143adc9 --- /dev/null +++ b/src/components/cashu/cashu-mint-name.tsx @@ -0,0 +1,14 @@ +import { AvatarProps, Text } from "@chakra-ui/react"; +import { useObservable } from "applesauce-react/hooks"; + +import { cashuMintInfo } from "../../services/cashu-mints"; + +export default function CashuMintName({ mint, ...props }: { mint: string } & Omit) { + const info = useObservable(cashuMintInfo(mint)); + + return ( + + {info?.name || mint} + + ); +} diff --git a/src/components/cashu/inline-cashu-card.tsx b/src/components/cashu/inline-cashu-card.tsx index 3c5b37813..74d902759 100644 --- a/src/components/cashu/inline-cashu-card.tsx +++ b/src/components/cashu/inline-cashu-card.tsx @@ -10,7 +10,7 @@ import CurrencyEuro from "../icons/currency-euro"; import CurrencyYen from "../icons/currency-yen"; import CurrencyPound from "../icons/currency-pound"; import CurrencyBitcoin from "../icons/currency-bitcoin"; -import { getMintWallet } from "../../services/cashu-mints"; +import { getCashuWallet } from "../../services/cashu-mints"; export default function InlineCachuCard({ token, @@ -20,7 +20,7 @@ export default function InlineCachuCard({ encoded = encoded || getEncodedToken(token); const { value: spendable, loading } = useAsync(async () => { if (!token) return; - const wallet = await getMintWallet(token.mint); + const wallet = await getCashuWallet(token.mint); const status = await wallet.checkProofsStates(token.proofs); return status.some((s) => s.state !== CheckStateEnum.UNSPENT); }, [token]); diff --git a/src/components/media-server/media-server-favicon.tsx b/src/components/favicon/media-server-favicon.tsx similarity index 100% rename from src/components/media-server/media-server-favicon.tsx rename to src/components/favicon/media-server-favicon.tsx diff --git a/src/components/qr-code/qr-code-scanner-button.tsx b/src/components/qr-code/qr-code-scanner-button.tsx index 8ceacee99..c5d16ea42 100644 --- a/src/components/qr-code/qr-code-scanner-button.tsx +++ b/src/components/qr-code/qr-code-scanner-button.tsx @@ -1,5 +1,5 @@ import { Suspense, lazy, useCallback } from "react"; -import { IconButton, useDisclosure, useToast } from "@chakra-ui/react"; +import { IconButton, IconButtonProps, useDisclosure, useToast } from "@chakra-ui/react"; import { type QrScannerModalProps } from "./qr-scanner-modal"; import { CAP_IS_NATIVE } from "../../env"; @@ -71,7 +71,10 @@ async function scanWithNative() { } } -export default function QRCodeScannerButton({ onData }: { onData: QrScannerModalProps["onData"] }) { +export default function QRCodeScannerButton({ + onData, + ...props +}: { onData: QrScannerModalProps["onData"] } & Omit) { const toast = useToast(); const modal = useDisclosure(); @@ -91,7 +94,7 @@ export default function QRCodeScannerButton({ onData }: { onData: QrScannerModal return ( <> - } aria-label="Qr Scanner" /> + } aria-label="Qr Scanner" {...props} /> {modal.isOpen && ( diff --git a/src/services/cashu-mints.ts b/src/services/cashu-mints.ts index d94c23866..833e7f01c 100644 --- a/src/services/cashu-mints.ts +++ b/src/services/cashu-mints.ts @@ -1,13 +1,46 @@ -import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import { CashuMint, CashuWallet, GetInfoResponse } from "@cashu/cashu-ts"; +import { normalizeURL } from "applesauce-core/helpers"; +import { from, Observable, ReplaySubject, share, switchMap } from "rxjs"; +const mints = new Map(); const wallets = new Map(); -export async function getMintWallet(url: string) { +export async function getCashuMint(url: string) { + const formatted = new URL(url).toString(); + if (!mints.has(formatted)) { + const mint = new CashuMint(formatted); + mints.set(formatted, mint); + } + return mints.get(formatted)!; +} + +export async function getCashuWallet(url: string) { const formatted = new URL(url).toString(); if (!wallets.has(formatted)) { - const mint = new CashuMint(formatted); + const mint = await getCashuMint(url); const wallet = new CashuWallet(mint); wallets.set(formatted, wallet); } return wallets.get(formatted)!; } + +const mintInfo = new Map>(); +export function cashuMintInfo(mint: string): Observable { + mint = normalizeURL(mint); + const existing = mintInfo.get(mint); + if (existing) return existing; + + const observable = from(getCashuMint(mint)).pipe( + // fetch mint info + switchMap((m) => from(m.getInfo())), + // share value and keep warm for 2 minutes + share({ + connector: () => new ReplaySubject(1), + resetOnRefCountZero: false, + resetOnComplete: false, + resetOnError: false, + }), + ); + mintInfo.set(mint, observable); + return observable; +} diff --git a/src/views/settings/mailboxes/index.tsx b/src/views/settings/mailboxes/index.tsx index 20ea7f3a6..775a81ca8 100644 --- a/src/views/settings/mailboxes/index.tsx +++ b/src/views/settings/mailboxes/index.tsx @@ -8,7 +8,7 @@ import RequireActiveAccount from "../../../components/router/require-active-acco import useUserMailboxes from "../../../hooks/use-user-mailboxes"; import { useActiveAccount } from "applesauce-react/hooks"; import { InboxIcon, OutboxIcon } from "../../../components/icons"; -import MediaServerFavicon from "../../../components/media-server/media-server-favicon"; +import MediaServerFavicon from "../../../components/favicon/media-server-favicon"; import { NostrEvent } from "../../../types/nostr-event"; import useAsyncErrorHandler from "../../../hooks/use-async-error-handler"; import { usePublishEvent } from "../../../providers/global/publish-provider"; diff --git a/src/views/settings/media-servers/index.tsx b/src/views/settings/media-servers/index.tsx index 156edc0b2..3f105e261 100644 --- a/src/views/settings/media-servers/index.tsx +++ b/src/views/settings/media-servers/index.tsx @@ -19,7 +19,7 @@ import { useForm } from "react-hook-form"; import RequireActiveAccount from "../../../components/router/require-active-account"; import { useActiveAccount } from "applesauce-react/hooks"; -import MediaServerFavicon from "../../../components/media-server/media-server-favicon"; +import MediaServerFavicon from "../../../components/favicon/media-server-favicon"; import { usePublishEvent } from "../../../providers/global/publish-provider"; import useUsersMediaServers from "../../../hooks/use-user-media-servers"; import DebugEventButton from "../../../components/debug-modal/debug-event-button"; diff --git a/src/views/wallet/balance-card.tsx b/src/views/wallet/balance-card.tsx index 63184721f..2e965dd50 100644 --- a/src/views/wallet/balance-card.tsx +++ b/src/views/wallet/balance-card.tsx @@ -1,44 +1,37 @@ -import { Button, Card, CardBody, CardFooter, CardHeader, CardProps, Text } from "@chakra-ui/react"; -import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks"; +import { Button, Card, CardBody, CardHeader, CardProps, Flex, Text } from "@chakra-ui/react"; +import { useStoreQuery } from "applesauce-react/hooks"; import { WalletBalanceQuery } from "applesauce-wallet/queries"; import { ECashIcon } from "../../components/icons"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; -import { isWalletLocked, unlockWallet, WALLET_KIND } from "applesauce-wallet/helpers"; -import useAsyncErrorHandler from "../../hooks/use-async-error-handler"; +import { WALLET_KIND } from "applesauce-wallet/helpers"; import useEventUpdate from "../../hooks/use-event-update"; +import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button"; export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string } & Omit) { - const account = useActiveAccount(); - const eventStore = useEventStore(); const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey }); useEventUpdate(wallet?.id); - const locked = !wallet || isWalletLocked(wallet); const balance = useStoreQuery(WalletBalanceQuery, [pubkey]); - const unlock = useAsyncErrorHandler(async () => { - if (!account) throw new Error("Missing account"); - if (!wallet) throw new Error("Missing wallet"); - await unlockWallet(wallet, account); - eventStore.update(wallet); - }, [wallet, account]); - return ( - - - + + + {balance ? Object.values(balance).reduce((t, v) => t + v, 0) : "--Locked--"} - - {locked && ( - - - - )} + {}} isDisabled size="lg" /> + + + ); } diff --git a/src/views/wallet/index.tsx b/src/views/wallet/index.tsx index 92add2aa6..efd6599ac 100644 --- a/src/views/wallet/index.tsx +++ b/src/views/wallet/index.tsx @@ -1,16 +1,18 @@ -import { Button, ButtonGroup, Card, CardBody, CardFooter, Flex, Text } from "@chakra-ui/react"; -import { kinds, NostrEvent } from "nostr-tools"; -import { WalletQuery } from "applesauce-wallet/queries"; +import { Button, Card, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; +import { kinds } from "nostr-tools"; +import { WalletBalanceQuery, WalletQuery } from "applesauce-wallet/queries"; import { - getTokenDetails, + isHistoryDetailsLocked, isTokenDetailsLocked, + unlockHistoryDetails, unlockTokenDetails, unlockWallet, + WALLET_HISTORY_KIND, WALLET_KIND, WALLET_TOKEN_KIND, } from "applesauce-wallet/helpers"; -import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks"; +import { useActiveAccount, useStoreQuery } from "applesauce-react/hooks"; import useAsyncErrorHandler from "../../hooks/use-async-error-handler"; import { eventStore } from "../../services/event-store"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; @@ -20,53 +22,10 @@ import useUserMailboxes from "../../hooks/use-user-mailboxes"; import { useReadRelays } from "../../hooks/use-client-relays"; import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback"; import IntersectionObserverProvider from "../../providers/local/intersection-observer"; -import useEventIntersectionRef from "../../hooks/use-event-intersection-ref"; -import useEventUpdate from "../../hooks/use-event-update"; -import DebugEventButton from "../../components/debug-modal/debug-event-button"; -import { ECashIcon } from "../../components/icons"; import WalletBalanceCard from "./balance-card"; -import { useMemo } from "react"; - -function TokenEvent({ token }: { token: NostrEvent }) { - const account = useActiveAccount(); - const eventStore = useEventStore(); - useEventUpdate(token.id); - const ref = useEventIntersectionRef(token); - - const locked = isTokenDetailsLocked(token); - const details = !locked ? getTokenDetails(token) : undefined; - const amount = details?.proofs.reduce((t, p) => t + p.amount, 0); - - const unlock = useAsyncErrorHandler(async () => { - if (!account) return; - await unlockTokenDetails(token, account); - eventStore.update(token); - }, [token, account, eventStore]); - - return ( - - - - {amount && {amount}} - - {locked && ( - - )} - - - - {details && ( - - - {details.mint} - - - )} - - ); -} +import WalletTokensTab from "./tabs/tokens"; +import WalletHistoryTab from "./tabs/history"; +import WalletMintsTab from "./tabs/mints"; export default function WalletHomeView() { const account = useActiveAccount()!; @@ -76,13 +35,12 @@ export default function WalletHomeView() { const readRelays = useReadRelays(mailboxes?.outboxes); const { timeline: events, loader } = useTimelineLoader(`${account.pubkey}-wallet-tokens`, readRelays, [ { - kinds: [WALLET_TOKEN_KIND], + kinds: [WALLET_TOKEN_KIND, WALLET_HISTORY_KIND], authors: [account.pubkey], }, { kinds: [kinds.EventDeletion], "#k": [String(WALLET_TOKEN_KIND)], authors: [account.pubkey] }, ]); - - const tokens = useMemo(() => events.filter((e) => e.kind === WALLET_TOKEN_KIND), [events]); + const balance = useStoreQuery(WalletBalanceQuery, [account.pubkey]); const unlock = useAsyncErrorHandler(async () => { if (!wallet) throw new Error("Missing wallet"); @@ -90,11 +48,18 @@ export default function WalletHomeView() { eventStore.update(wallet); // attempt to unlock all tokens - for (const token of tokens) { - await unlockTokenDetails(token, account); - eventStore.update(token); + for (const event of events) { + if (event.kind === WALLET_TOKEN_KIND) { + if (!isTokenDetailsLocked(event)) continue; + await unlockTokenDetails(event, account); + eventStore.update(event); + } else if (event.kind === WALLET_HISTORY_KIND) { + if (!isHistoryDetailsLocked(event)) continue; + await unlockHistoryDetails(event, account); + eventStore.update(event); + } } - }, [wallet, account, tokens]); + }, [wallet, account, events]); const walletInfo = useStoreQuery(WalletQuery, [account.pubkey]); @@ -113,19 +78,30 @@ export default function WalletHomeView() { } > - {walletInfo?.locked === false && ( - - Key: {walletInfo.privateKey} -
- Mints: {walletInfo.mints.join(", ")} -
+ {walletInfo?.locked && ( + )} - - {tokens.map((token) => ( - - ))} - + + + History ({events.filter((e) => e.kind === WALLET_HISTORY_KIND).length}) + Tokens ({events.filter((e) => e.kind === WALLET_TOKEN_KIND).length}) + Mints ({balance ? Object.keys(balance).length : 0}) + + + + + + + + + + + + + ); diff --git a/src/views/wallet/tabs/history.tsx b/src/views/wallet/tabs/history.tsx new file mode 100644 index 000000000..5d7a9ffea --- /dev/null +++ b/src/views/wallet/tabs/history.tsx @@ -0,0 +1,122 @@ +import { + AvatarGroup, + Button, + ButtonGroup, + Card, + CardBody, + CardFooter, + Flex, + IconButton, + Spacer, + Text, +} from "@chakra-ui/react"; +import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks"; +import { + getHistoryDetails, + getHistoryRedeemed, + isHistoryDetailsLocked, + unlockHistoryDetails, +} from "applesauce-wallet/helpers"; +import { WalletHistoryQuery } from "applesauce-wallet/queries"; +import { NostrEvent } from "nostr-tools"; + +import Lock01 from "../../../components/icons/lock-01"; +import DebugEventButton from "../../../components/debug-modal/debug-event-button"; +import ArrowBlockUp from "../../../components/icons/arrow-block-up"; +import ArrowBlockDown from "../../../components/icons/arrow-block-down"; +import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref"; +import useAsyncErrorHandler from "../../../hooks/use-async-error-handler"; +import { useDeleteEventContext } from "../../../providers/route/delete-event-provider"; +import { TrashIcon } from "../../../components/icons"; +import useEventUpdate from "../../../hooks/use-event-update"; +import Timestamp from "../../../components/timestamp"; +import useSingleEvents from "../../../hooks/use-single-events"; +import UserAvatarLink from "../../../components/user/user-avatar-link"; + +function HistoryEntry({ entry }: { entry: NostrEvent }) { + const account = useActiveAccount()!; + const eventStore = useEventStore(); + const locked = isHistoryDetailsLocked(entry); + const details = !locked ? getHistoryDetails(entry) : undefined; + useEventUpdate(entry.id); + + const ref = useEventIntersectionRef(entry); + const { deleteEvent } = useDeleteEventContext(); + + const redeemedIds = getHistoryRedeemed(entry); + const redeemed = useSingleEvents(redeemedIds); + + const unlock = useAsyncErrorHandler(async () => { + await unlockHistoryDetails(entry, account); + eventStore.update(entry); + }, [entry, account, eventStore]); + + return ( + + + {locked ? ( + + ) : details?.direction === "in" ? ( + + ) : ( + + )} + {details?.amount} + + + {locked && ( + + )} + + + } + aria-label="Delete entry" + onClick={() => deleteEvent(entry)} + colorScheme="red" + variant="ghost" + /> + + + {redeemed.length > 0 && ( + + Redeemed zaps from: + + {redeemed.map((event) => ( + + ))} + + + )} + + ); +} + +export default function WalletHistoryTab() { + const account = useActiveAccount()!; + const eventStore = useEventStore(); + + const history = useStoreQuery(WalletHistoryQuery, [account.pubkey]) ?? []; + const locked = useStoreQuery(WalletHistoryQuery, [account.pubkey, true]) ?? []; + + const unlock = useAsyncErrorHandler(async () => { + for (const entry of locked) { + if (!isHistoryDetailsLocked(entry)) continue; + await unlockHistoryDetails(entry, account); + eventStore.update(entry); + } + }, [locked, account, eventStore]); + + return ( + + {locked && locked.length > 0 && ( + + )} + {history?.map((entry) => )} + + ); +} diff --git a/src/views/wallet/tabs/mints.tsx b/src/views/wallet/tabs/mints.tsx new file mode 100644 index 000000000..29d717df7 --- /dev/null +++ b/src/views/wallet/tabs/mints.tsx @@ -0,0 +1,30 @@ +import { Box, Card, Flex, Link, Text } from "@chakra-ui/react"; +import { useActiveAccount, useStoreQuery } from "applesauce-react/hooks"; +import { WalletBalanceQuery } from "applesauce-wallet/queries"; +import CashuMintFavicon from "../../../components/cashu/cashu-mint-favicon"; +import CashuMintName from "../../../components/cashu/cashu-mint-name"; + +export default function WalletMintsTab() { + const account = useActiveAccount()!; + const balance = useStoreQuery(WalletBalanceQuery, [account.pubkey]); + + return ( + + {balance && + Object.entries(balance).map(([mint, total]) => ( + + + + + + + {mint} + + + Amount: {total} + + + ))} + + ); +} diff --git a/src/views/wallet/tabs/tokens.tsx b/src/views/wallet/tabs/tokens.tsx new file mode 100644 index 000000000..3f3d40777 --- /dev/null +++ b/src/views/wallet/tabs/tokens.tsx @@ -0,0 +1,124 @@ +import { + Button, + ButtonGroup, + Card, + CardFooter, + CardHeader, + Flex, + FlexProps, + IconButton, + Spacer, + Text, +} from "@chakra-ui/react"; +import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks"; +import { WalletTokensQuery } from "applesauce-wallet/queries"; +import { getTokenDetails, isTokenDetailsLocked, unlockTokenDetails } from "applesauce-wallet/helpers"; +import { NostrEvent } from "nostr-tools"; + +import useAsyncErrorHandler from "../../../hooks/use-async-error-handler"; +import useEventUpdate from "../../../hooks/use-event-update"; +import useEventIntersectionRef from "../../../hooks/use-event-intersection-ref"; +import { ECashIcon, TrashIcon } from "../../../components/icons"; +import DebugEventButton from "../../../components/debug-modal/debug-event-button"; +import { useDeleteEventContext } from "../../../providers/route/delete-event-provider"; +import Timestamp from "../../../components/timestamp"; +import { useState } from "react"; +import { getCashuWallet } from "../../../services/cashu-mints"; + +function TokenEvent({ token }: { token: NostrEvent }) { + const account = useActiveAccount(); + const eventStore = useEventStore(); + useEventUpdate(token.id); + const ref = useEventIntersectionRef(token); + + const locked = isTokenDetailsLocked(token); + const details = !locked ? getTokenDetails(token) : undefined; + const amount = details?.proofs.reduce((t, p) => t + p.amount, 0); + + const [spent, setSpent] = useState(); + const check = useAsyncErrorHandler(async () => { + if (!details) return; + const wallet = await getCashuWallet(details.mint); + const state = await wallet.checkProofsStates(details.proofs); + + setSpent(!state.some((t) => t.state === "UNSPENT")); + }, [details, setSpent]); + + const { deleteEvent } = useDeleteEventContext(); + + const unlock = useAsyncErrorHandler(async () => { + if (!account) return; + await unlockTokenDetails(token, account); + eventStore.update(token); + }, [token, account, eventStore]); + + return ( + + + + {amount && {amount}} + + {locked && ( + + )} + + + } + aria-label="Delete entry" + onClick={() => deleteEvent(token)} + colorScheme="red" + variant="ghost" + /> + + + {details && ( + + + + + {details.mint} + + + )} + + ); +} + +export default function WalletTokensTab({ ...props }: Omit) { + const account = useActiveAccount()!; + const eventStore = useEventStore(); + + const tokens = useStoreQuery(WalletTokensQuery, [account.pubkey]) ?? []; + const locked = useStoreQuery(WalletTokensQuery, [account.pubkey, true]) ?? []; + + const unlock = useAsyncErrorHandler(async () => { + if (!locked) return; + for (const token of locked) { + await unlockTokenDetails(token, account); + eventStore.update(token); + } + }, [locked, account, eventStore]); + + return ( + + {locked && locked.length > 0 && ( + + )} + + {tokens.map((token) => ( + + ))} + + ); +}