diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6736fe85c..2afb29811 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,31 +104,31 @@ importers: version: 0.7.2 applesauce-accounts: specifier: next - version: 0.0.0-next-20250309231023(typescript@5.8.2) + version: 0.0.0-next-20250310121307(typescript@5.8.2) applesauce-content: specifier: next - version: 0.0.0-next-20250309231023(typescript@5.8.2) + version: 0.0.0-next-20250310121307(typescript@5.8.2) applesauce-core: specifier: next - version: 0.0.0-next-20250309231023(typescript@5.8.2) + version: 0.0.0-next-20250310121307(typescript@5.8.2) applesauce-factory: specifier: next - version: 0.0.0-next-20250309231023(typescript@5.8.2) + version: 0.0.0-next-20250310121307(typescript@5.8.2) applesauce-loaders: specifier: next - version: 0.0.0-next-20250309231023(typescript@5.8.2) + version: 0.0.0-next-20250310121307(typescript@5.8.2) applesauce-react: specifier: next - version: 0.0.0-next-20250309231023(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2) + version: 0.0.0-next-20250310121307(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2) applesauce-relay: specifier: next - version: 0.0.0-next-20250309231023(typescript@5.8.2) + version: 0.0.0-next-20250310121307(typescript@5.8.2) applesauce-signers: specifier: next - version: 0.0.0-next-20250309231023(typescript@5.8.2) + version: 0.0.0-next-20250310121307(typescript@5.8.2) applesauce-wallet: - specifier: 0.0.0-next-20250309231023 - version: 0.0.0-next-20250309231023(typescript@5.8.2) + specifier: next + version: 0.0.0-next-20250310121307(typescript@5.8.2) bech32: specifier: ^2.0.0 version: 2.0.0 @@ -2195,32 +2195,32 @@ packages: engines: {node: '>=8.0.0'} hasBin: true - applesauce-accounts@0.0.0-next-20250309231023: - resolution: {integrity: sha512-7oOZ7AQ667VncranhPI1oowec2uHshq4JLdNzp6PlQq2L6/aZoCdOHrXa+mJ6V6YWRQsq1ekvqRIHEpZ0cKFfA==} + applesauce-accounts@0.0.0-next-20250310121307: + resolution: {integrity: sha512-Q1vEjYznvkxRDM2C/gN+UJpjva2QvFLrIlfMTyrfm7s1nDEIKP6wJ4Oj72PsnyZREP0/UnH7fP5olvJMakBOBg==} - applesauce-content@0.0.0-next-20250309231023: - resolution: {integrity: sha512-8MigSRS3hG1/93TxBOGWpRaDoIXycnR44NbGirOYX4NG7r3UjcGMgQ1O8hG2XAz3+RKsOA3umj/0FRO7j+JgmQ==} + applesauce-content@0.0.0-next-20250310121307: + resolution: {integrity: sha512-oxMgVy1hS0ZWGL+0Skrt25vcMUwY8T3A1p45vc1q17oXWbIKBqZPtenoHarluEBdY9iUt0QBVVIFWSQtphudxw==} - applesauce-core@0.0.0-next-20250309231023: - resolution: {integrity: sha512-QG0PT88QIkjbHWuJ+a3B3KEIJdKcCaPEHvYqXPbCiY73K37j5W1xVQByn2kOP8yHDk//ZOyBjrN8EsnWLSnxPg==} + applesauce-core@0.0.0-next-20250310121307: + resolution: {integrity: sha512-u6YNDyPy2v+B8Iv6WFkTTnilLJfNgFVeDs+aGy4KpItq0tdEQcnwTIFRcTovSaCNn2YYNJ1WHw30/a622SmshA==} - applesauce-factory@0.0.0-next-20250309231023: - resolution: {integrity: sha512-PzFFXZgMs9q6LCP0zvUO0k6+3x5zUrtImFABBmOxsCFzMkU6a0h7SEhs8fJIXDtAz0weeC40rhyYRUWgR8jeSA==} + applesauce-factory@0.0.0-next-20250310121307: + resolution: {integrity: sha512-rJNnwwxIdX1s4vdmAmIDvappQT/0J0XH+xIpMDnuf1yvMmwK/lH+DvGWPdC2h/TNI4Lbd3c3774jUkgBZoKMrQ==} - applesauce-loaders@0.0.0-next-20250309231023: - resolution: {integrity: sha512-c5RsCyOIpMKv3CKqgi7QCO7Blx6D1EZnaAfoitmqtWf7IfR50uEie3/R4myMxOjWVZa4I6bqWQba+Imqfpg8PQ==} + applesauce-loaders@0.0.0-next-20250310121307: + resolution: {integrity: sha512-KiqVGqzIEOuGfLU6TGGjRFru7/0e1HATSyWsXyvyPJd0Pt5Ud9tkJidI+bAY+jZthDcBlFdEPT4HTPjKJ63AOg==} - applesauce-react@0.0.0-next-20250309231023: - resolution: {integrity: sha512-WEw6ykfnghhwK22kziRoSISfpXpeMF9gzT0Khe/ZaSs+U/qVVml62TlKeVuYn0u95SlrMHrSB/OmkDSj8koNWw==} + applesauce-react@0.0.0-next-20250310121307: + resolution: {integrity: sha512-MrWVYJUuKGAuW0ZjUTBgNP/o4AVth1vPlmAra0xYsH5y8puQvU0APqXpH60yrOGqMvtWiZbpMpnhPKjmjytxfw==} - applesauce-relay@0.0.0-next-20250309231023: - resolution: {integrity: sha512-ga3fLhqu4RVZQkf6xM/Y/gCIg+pzw6s17PQczLtE+k+Iz5utHk0dpOAHChoA94xMt1je3U1MZbnmZUmzD3lf0A==} + applesauce-relay@0.0.0-next-20250310121307: + resolution: {integrity: sha512-5QyyRQc5vEHGUWJgHRw6hM5zSkvkqt+UYrwkL9yGcP1rQ/IyM0kcoog1VobKrSGjlQOyXxhw0cShSKoTzGWreA==} - applesauce-signers@0.0.0-next-20250309231023: - resolution: {integrity: sha512-FOsD05Ymj9pNPj4LvC4xy3L3z9CKZT6LlbkEbAltw/YMubcEuqALZH9dCY69D7xiUUJvk0HqDpfDyq+iXmiiFA==} + applesauce-signers@0.0.0-next-20250310121307: + resolution: {integrity: sha512-dfppzzFIT3FtLF9JmBNAQToD8euOsWqt940itYMvesP5gV7BEHCSPiBzHdaXPNfK5/ay/kN9WHleBdwVcKTW5A==} - applesauce-wallet@0.0.0-next-20250309231023: - resolution: {integrity: sha512-QxdomzWOMd/x6ip3cxrT6jXqcLUPfBb31vafTDDJM5cCNVby4IElUyZCIh0SWEdauK53xv7dkOYrsWzpKDL/4w==} + applesauce-wallet@0.0.0-next-20250310121307: + resolution: {integrity: sha512-9rwmR4RsJ1hv5nM7uOo/HWKHzhkhuueEtL99ZJleOGnQWm6fTz0LlMRDCT8OLlX7ZCC9Oho7p38YBqISQzJHew==} arg@4.1.3: resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} @@ -3036,8 +3036,8 @@ packages: engines: {node: '>=0.10.0'} hasBin: true - electron-to-chromium@1.5.113: - resolution: {integrity: sha512-wjT2O4hX+wdWPJ76gWSkMhcHAV2PTMX+QetUCPYEdCIe+cxmgzzSSiGRCKW8nuh4mwKZlpv0xvoW7OF2X+wmHg==} + electron-to-chromium@1.5.114: + resolution: {integrity: sha512-DFptFef3iktoKlFQK/afbo274/XNWD00Am0xa7M8FZUepHlHT8PEuiNBoRfFHbH1okqN58AlhbJ4QTkcnXorjA==} elementtree@0.1.7: resolution: {integrity: sha512-wkgGT6kugeQk/P6VZ/f4T+4HB41BVgNBq5CDIZVbQ02nvTVqAiVTbskxxu3eA/X96lMlfYOwnLQpN2v5E1zDEg==} @@ -8435,10 +8435,10 @@ snapshots: dependencies: entities: 2.2.0 - applesauce-accounts@0.0.0-next-20250309231023(typescript@5.8.2): + applesauce-accounts@0.0.0-next-20250310121307(typescript@5.8.2): dependencies: '@noble/hashes': 1.7.1 - applesauce-signers: 0.0.0-next-20250309231023(typescript@5.8.2) + applesauce-signers: 0.0.0-next-20250310121307(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-20250309231023(typescript@5.8.2): + applesauce-content@0.0.0-next-20250310121307(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-20250309231023(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310121307(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-20250309231023(typescript@5.8.2): + applesauce-core@0.0.0-next-20250310121307(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-20250309231023(typescript@5.8.2): + applesauce-factory@0.0.0-next-20250310121307(typescript@5.8.2): dependencies: - applesauce-content: 0.0.0-next-20250309231023(typescript@5.8.2) - applesauce-core: 0.0.0-next-20250309231023(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) nanoid: 5.1.3 nostr-tools: 2.10.4(typescript@5.8.2) transitivePeerDependencies: - supports-color - typescript - applesauce-loaders@0.0.0-next-20250309231023(typescript@5.8.2): + applesauce-loaders@0.0.0-next-20250310121307(typescript@5.8.2): dependencies: - applesauce-core: 0.0.0-next-20250309231023(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310121307(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-20250309231023(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2): + applesauce-react@0.0.0-next-20250310121307(react-dom@19.0.0(react@19.0.0))(typescript@5.8.2): dependencies: - applesauce-accounts: 0.0.0-next-20250309231023(typescript@5.8.2) - applesauce-content: 0.0.0-next-20250309231023(typescript@5.8.2) - applesauce-core: 0.0.0-next-20250309231023(typescript@5.8.2) - applesauce-factory: 0.0.0-next-20250309231023(typescript@5.8.2) + 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) 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-20250309231023(typescript@5.8.2): + applesauce-relay@0.0.0-next-20250310121307(typescript@5.8.2): dependencies: - applesauce-core: 0.0.0-next-20250309231023(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310121307(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-20250309231023(typescript@5.8.2): + applesauce-signers@0.0.0-next-20250310121307(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-20250309231023(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310121307(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-20250309231023(typescript@5.8.2): + applesauce-wallet@0.0.0-next-20250310121307(typescript@5.8.2): dependencies: - applesauce-core: 0.0.0-next-20250309231023(typescript@5.8.2) + applesauce-core: 0.0.0-next-20250310121307(typescript@5.8.2) nostr-tools: 2.10.4(typescript@5.8.2) rxjs: 7.8.2 transitivePeerDependencies: @@ -8763,7 +8763,7 @@ snapshots: browserslist@4.24.4: dependencies: caniuse-lite: 1.0.30001703 - electron-to-chromium: 1.5.113 + electron-to-chromium: 1.5.114 node-releases: 2.0.19 update-browserslist-db: 1.1.3(browserslist@4.24.4) @@ -9460,7 +9460,7 @@ snapshots: dependencies: jake: 10.9.2 - electron-to-chromium@1.5.113: {} + electron-to-chromium@1.5.114: {} elementtree@0.1.7: dependencies: diff --git a/src/hooks/use-event-update.ts b/src/hooks/use-event-update.ts index 94d1d74ed..5f8050d4b 100644 --- a/src/hooks/use-event-update.ts +++ b/src/hooks/use-event-update.ts @@ -1,13 +1,14 @@ -import { useEffect, useMemo, useState } from "react"; +import { useEffect, useMemo } from "react"; import { eventStore } from "../services/event-store"; +import useForceUpdate from "./use-force-update"; export default function useEventUpdate(id?: string) { - const [_count, setCount] = useState(0); + const update = useForceUpdate(); - const observable = useMemo(() => (id ? eventStore.event(id) : undefined), [id]); + const observable = useMemo(() => (id ? eventStore.updated(id) : undefined), [id]); useEffect(() => { if (!observable) return; - const sub = observable.subscribe(() => setCount((v) => v + 1)); + const sub = observable.subscribe(update); return () => sub.unsubscribe(); - }, [observable]); + }, [observable, update]); } diff --git a/src/hooks/use-force-update.ts b/src/hooks/use-force-update.ts index 027f2f7dd..11850224e 100644 --- a/src/hooks/use-force-update.ts +++ b/src/hooks/use-force-update.ts @@ -1,7 +1,7 @@ import { useCallback, useState } from "react"; export default function useForceUpdate() { - const [count, setCount] = useState(0); + const [_count, setCount] = useState(0); const update = useCallback(() => { setCount((v) => v + 1); }, [setCount]); diff --git a/src/views/wallet/balance-card.tsx b/src/views/wallet/balance-card.tsx new file mode 100644 index 000000000..63184721f --- /dev/null +++ b/src/views/wallet/balance-card.tsx @@ -0,0 +1,44 @@ +import { Button, Card, CardBody, CardFooter, CardHeader, CardProps, Text } from "@chakra-ui/react"; +import { useActiveAccount, useEventStore, 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 useEventUpdate from "../../hooks/use-event-update"; + +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 && ( + + + + )} + + ); +} diff --git a/src/views/wallet/index.tsx b/src/views/wallet/index.tsx index 430deaead..92add2aa6 100644 --- a/src/views/wallet/index.tsx +++ b/src/views/wallet/index.tsx @@ -1,46 +1,68 @@ -import { - Alert, - AlertDescription, - AlertIcon, - AlertTitle, - Badge, - Button, - Card, - CardBody, - CardFooter, - CardHeader, - Flex, - Heading, - Spinner, -} from "@chakra-ui/react"; -import { NostrEvent } from "nostr-tools"; +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 { unlockWallet, WALLET_KIND } from "applesauce-wallet/helpers"; +import { + getTokenDetails, + isTokenDetailsLocked, + unlockTokenDetails, + unlockWallet, + WALLET_KIND, + WALLET_TOKEN_KIND, +} from "applesauce-wallet/helpers"; -import { useActiveAccount, useStoreQuery } from "applesauce-react/hooks"; +import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks"; import useAsyncErrorHandler from "../../hooks/use-async-error-handler"; -import DebugEventButton from "../../components/debug-modal/debug-event-button"; import { eventStore } from "../../services/event-store"; import useReplaceableEvent from "../../hooks/use-replaceable-event"; import SimpleView from "../../components/layout/presets/simple-view"; +import useTimelineLoader from "../../hooks/use-timeline-loader"; +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 Wallet({ wallet }: { wallet: NostrEvent }) { - const account = useActiveAccount()!; +function TokenEvent({ token }: { token: NostrEvent }) { + const account = useActiveAccount(); + const eventStore = useEventStore(); + useEventUpdate(token.id); + const ref = useEventIntersectionRef(token); - const walletInfo = useStoreQuery(WalletQuery, [account.pubkey]); + 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 ( - - - Wallet - {walletInfo?.locked && Locked} - {wallet && } - - {walletInfo?.locked === false && ( - - Key: {walletInfo.privateKey} - Mints: {walletInfo.mints.join(", ")} - + + + + {amount && {amount}} + + {locked && ( + + )} + + + + {details && ( + + + {details.mint} + + )} ); @@ -50,56 +72,61 @@ export default function WalletHomeView() { const account = useActiveAccount()!; const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey: account.pubkey }); + const mailboxes = useUserMailboxes(account.pubkey); + const readRelays = useReadRelays(mailboxes?.outboxes); + const { timeline: events, loader } = useTimelineLoader(`${account.pubkey}-wallet-tokens`, readRelays, [ + { + kinds: [WALLET_TOKEN_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 unlock = useAsyncErrorHandler(async () => { if (!wallet) throw new Error("Missing wallet"); await unlockWallet(wallet, account); eventStore.update(wallet); - }, [wallet, account]); + + // attempt to unlock all tokens + for (const token of tokens) { + await unlockTokenDetails(token, account); + eventStore.update(token); + } + }, [wallet, account, tokens]); const walletInfo = useStoreQuery(WalletQuery, [account.pubkey]); + const callback = useTimelineCurserIntersectionCallback(loader); + return ( - - Unlock - - ) - } - > - {walletInfo?.locked && ( - - - - Wallet locked! - - - Your wallet is locked, you need to unlock it in order to use it - - - - )} - {walletInfo?.locked === false && ( - - Key: {walletInfo.privateKey} -
- Mints: {walletInfo.mints.join(", ")} -
- )} -
+ + + Unlock + + ) + } + > + + {walletInfo?.locked === false && ( + + Key: {walletInfo.privateKey} +
+ Mints: {walletInfo.mints.join(", ")} +
+ )} + + + {tokens.map((token) => ( + + ))} + +
+
); }