show incorrect wallet balance

This commit is contained in:
hzrd149 2025-03-10 12:14:44 +00:00
parent 707d7b250a
commit dc138f6037
5 changed files with 207 additions and 135 deletions

106
pnpm-lock.yaml generated
View File

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

View File

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

View File

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

View File

@ -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<CardProps, "children">) {
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 (
<Card {...props}>
<CardHeader gap="2" display="flex" justifyContent="center" alignItems="center" pt="10">
<ECashIcon color="green.400" boxSize={6} />
<Text fontWeight="bold" fontSize="lg">
{balance ? Object.values(balance).reduce((t, v) => t + v, 0) : "--Locked--"}
</Text>
</CardHeader>
<CardBody></CardBody>
{locked && (
<CardFooter display="flex">
<Button colorScheme="primary" onClick={unlock} mx="auto">
Unlock
</Button>
</CardFooter>
)}
</Card>
);
}

View File

@ -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 (
<Card>
<CardHeader display="flex" gap="2" p="2" alignItems="center">
<Heading size="md">Wallet</Heading>
{walletInfo?.locked && <Badge colorScheme="orange">Locked</Badge>}
{wallet && <DebugEventButton event={wallet} variant="ghost" ml="auto" size="sm" />}
</CardHeader>
{walletInfo?.locked === false && (
<CardBody px="2" py="0" whiteSpace="pre-line">
Key: {walletInfo.privateKey}
Mints: {walletInfo.mints.join(", ")}
</CardBody>
<Card ref={ref} w="full">
<CardBody p="2" alignItems="center" flexDirection="row" display="flex" gap="2">
<ECashIcon color="green.400" boxSize={6} />
{amount && <Text>{amount}</Text>}
<ButtonGroup size="sm" ms="auto">
{locked && (
<Button onClick={unlock} variant="link" p="2">
Unlock
</Button>
)}
<DebugEventButton variant="ghost" event={token} />
</ButtonGroup>
</CardBody>
{details && (
<CardFooter px="2" pt="0" pb="0">
<Text fontSize="sm" fontStyle="italic">
{details.mint}
</Text>
</CardFooter>
)}
</Card>
);
@ -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 (
<SimpleView
title="Wallet"
actions={
walletInfo?.locked && (
<Button onClick={unlock} colorScheme="primary" ms="auto" size="sm">
Unlock
</Button>
)
}
>
{walletInfo?.locked && (
<Alert
status="info"
variant="subtle"
flexDirection="column"
alignItems="center"
justifyContent="center"
textAlign="center"
height="xs"
maxW="2xl"
mx="auto"
>
<AlertIcon boxSize="40px" mr={0} />
<AlertTitle mt={4} mb={1} fontSize="lg">
Wallet locked!
</AlertTitle>
<AlertDescription maxWidth="sm">
Your wallet is locked, you need to unlock it in order to use it
</AlertDescription>
<Button onClick={unlock} colorScheme="primary" mt="6">
Unlock
</Button>
</Alert>
)}
{walletInfo?.locked === false && (
<Card p="2" whiteSpace="pre-line">
Key: {walletInfo.privateKey}
<br />
Mints: {walletInfo.mints.join(", ")}
</Card>
)}
</SimpleView>
<IntersectionObserverProvider callback={callback}>
<SimpleView
title="Wallet"
actions={
walletInfo?.locked && (
<Button onClick={unlock} colorScheme="primary" ms="auto" size="sm">
Unlock
</Button>
)
}
>
<WalletBalanceCard pubkey={account.pubkey} w="full" maxW="2xl" mx="auto" />
{walletInfo?.locked === false && (
<Card p="2" whiteSpace="pre-line">
Key: {walletInfo.privateKey}
<br />
Mints: {walletInfo.mints.join(", ")}
</Card>
)}
<Flex direction="column" gap="2" w="full" maxW="lg" mx="auto">
{tokens.map((token) => (
<TokenEvent key={token.id} token={token} />
))}
</Flex>
</SimpleView>
</IntersectionObserverProvider>
);
}