add receive button to wallet

This commit is contained in:
hzrd149 2025-03-12 20:18:30 +00:00
parent 7e544a5bc9
commit 4ed1a0b528
13 changed files with 546 additions and 477 deletions

View File

@ -50,6 +50,7 @@
"@webscopeio/react-textarea-autocomplete": "^4.9.2", "@webscopeio/react-textarea-autocomplete": "^4.9.2",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
"applesauce-accounts": "next", "applesauce-accounts": "next",
"applesauce-actions": "next",
"applesauce-content": "next", "applesauce-content": "next",
"applesauce-core": "next", "applesauce-core": "next",
"applesauce-factory": "next", "applesauce-factory": "next",

784
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,10 @@
import { WALLET_KIND } from "applesauce-wallet/helpers";
import useReplaceableEvent from "./use-replaceable-event";
import { useStoreQuery } from "applesauce-react/hooks";
import { WalletQuery } from "applesauce-wallet/queries";
export default function useUserWallet(pubkey?: string) {
useReplaceableEvent(pubkey ? { kind: WALLET_KIND, pubkey } : undefined);
return useStoreQuery(WalletQuery, pubkey ? [pubkey] : undefined);
}

View File

@ -1,11 +0,0 @@
import { PropsWithChildren } from "react";
import { useObservable } from "applesauce-react/hooks";
import { FactoryProvider } from "applesauce-react/providers";
import factory$ from "../../services/event-factory";
export default function EventFactoryProvider({ children }: PropsWithChildren) {
const factory = useObservable(factory$);
return <FactoryProvider factory={factory}>{children}</FactoryProvider>;
}

View File

@ -1,6 +1,6 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import { ChakraProvider, localStorageManager } from "@chakra-ui/react"; import { ChakraProvider, localStorageManager } from "@chakra-ui/react";
import { AccountsProvider, QueryStoreProvider } from "applesauce-react/providers"; import { AccountsProvider, QueryStoreProvider, ActionsProvider, FactoryProvider } from "applesauce-react/providers";
import { SigningProvider } from "./signing-provider"; import { SigningProvider } from "./signing-provider";
import buildTheme from "../../theme"; import buildTheme from "../../theme";
@ -10,8 +10,9 @@ import BreakpointProvider from "./breakpoint-provider";
import PublishProvider from "./publish-provider"; import PublishProvider from "./publish-provider";
import WebOfTrustProvider from "./web-of-trust-provider"; import WebOfTrustProvider from "./web-of-trust-provider";
import { queryStore } from "../../services/event-store"; import { queryStore } from "../../services/event-store";
import EventFactoryProvider from "./event-factory-provider";
import accounts from "../../services/accounts"; import accounts from "../../services/accounts";
import actions from "../../services/actions";
import factory from "../../services/event-factory";
function ThemeProviders({ children }: { children: React.ReactNode }) { function ThemeProviders({ children }: { children: React.ReactNode }) {
const { theme: themeName, primaryColor } = useAppSettings(); const { theme: themeName, primaryColor } = useAppSettings();
@ -29,17 +30,19 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) =>
return ( return (
<QueryStoreProvider queryStore={queryStore}> <QueryStoreProvider queryStore={queryStore}>
<AccountsProvider manager={accounts}> <AccountsProvider manager={accounts}>
<ThemeProviders> <ActionsProvider actionHub={actions}>
<SigningProvider> <FactoryProvider factory={factory}>
<PublishProvider> <ThemeProviders>
<UserEmojiProvider> <SigningProvider>
<EventFactoryProvider> <PublishProvider>
<WebOfTrustProvider>{children}</WebOfTrustProvider> <UserEmojiProvider>
</EventFactoryProvider> <WebOfTrustProvider>{children}</WebOfTrustProvider>
</UserEmojiProvider> </UserEmojiProvider>
</PublishProvider> </PublishProvider>
</SigningProvider> </SigningProvider>
</ThemeProviders> </ThemeProviders>
</FactoryProvider>
</ActionsProvider>
</AccountsProvider> </AccountsProvider>
</QueryStoreProvider> </QueryStoreProvider>
); );

18
src/services/actions.ts Normal file
View File

@ -0,0 +1,18 @@
import { ActionHub } from "applesauce-actions";
import { kinds } from "nostr-tools";
import { getOutboxes } from "applesauce-core/helpers";
import { eventStore } from "./event-store";
import factory from "./event-factory";
import rxNostr from "./rx-nostr";
const actions = new ActionHub(eventStore, factory, async (label, event) => {
const mailboxes = eventStore.getReplaceable(kinds.RelayList, event.pubkey);
const outboxes = mailboxes && getOutboxes(mailboxes);
// publish the event
eventStore.add(event);
rxNostr.send(event, { on: { relays: outboxes } });
});
export default actions;

View File

@ -1,31 +1,21 @@
import { BehaviorSubject, combineLatest } from "rxjs";
import { EventFactory } from "applesauce-factory"; import { EventFactory } from "applesauce-factory";
import { map } from "rxjs/operators";
import { getEventRelayHint, getPubkeyRelayHint } from "./relay-hints"; import { getEventRelayHint, getPubkeyRelayHint } from "./relay-hints";
import { NIP_89_CLIENT_APP } from "../const"; import { NIP_89_CLIENT_APP } from "../const";
import localSettings from "./local-settings";
import accounts from "./accounts"; import accounts from "./accounts";
import localSettings from "./local-settings";
const factory$ = new BehaviorSubject<EventFactory>( const factory = new EventFactory({
new EventFactory({ signer: accounts.signer,
signer: accounts.active ?? undefined, getEventRelayHint,
getEventRelayHint, getPubkeyRelayHint: getPubkeyRelayHint,
getPubkeyRelayHint: getPubkeyRelayHint, client: localSettings.addClientTag.value ? NIP_89_CLIENT_APP : undefined,
client: localSettings.addClientTag.value ? NIP_89_CLIENT_APP : undefined, });
}),
);
// update event factory when settings change // update event factory when settings change
combineLatest([accounts.active$, localSettings.addClientTag]).pipe( localSettings.addClientTag.subscribe((client) => {
map(([current, client]) => { if (client) factory.context.client = NIP_89_CLIENT_APP;
return new EventFactory({ else factory.context.client = undefined;
signer: current ? current.signer : undefined, });
getEventRelayHint,
getPubkeyRelayHint: getPubkeyRelayHint,
client: client ? NIP_89_CLIENT_APP : undefined,
});
}),
);
export default factory$; export default factory;

View File

@ -6,6 +6,7 @@ import useReplaceableEvent from "../../hooks/use-replaceable-event";
import { WALLET_KIND } from "applesauce-wallet/helpers"; import { WALLET_KIND } from "applesauce-wallet/helpers";
import useEventUpdate from "../../hooks/use-event-update"; import useEventUpdate from "../../hooks/use-event-update";
import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button"; import QRCodeScannerButton from "../../components/qr-code/qr-code-scanner-button";
import RouterLink from "../../components/router-link";
export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string } & Omit<CardProps, "children">) { export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string } & Omit<CardProps, "children">) {
const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey }); const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey });
@ -27,7 +28,7 @@ export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string
Send Send
</Button> </Button>
<QRCodeScannerButton onData={() => {}} isDisabled size="lg" /> <QRCodeScannerButton onData={() => {}} isDisabled size="lg" />
<Button isDisabled w="full" size="lg"> <Button as={RouterLink} w="full" size="lg" to="/wallet/receive">
Receive Receive
</Button> </Button>
</Flex> </Flex>

View File

@ -1,21 +1,11 @@
import { Button, Card, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react"; import { Button, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import { kinds } from "nostr-tools"; import { kinds } from "nostr-tools";
import { WalletBalanceQuery, WalletQuery } from "applesauce-wallet/queries"; import { WalletBalanceQuery } from "applesauce-wallet/queries";
import { import { UnlockWallet } from "applesauce-wallet/actions";
isHistoryDetailsLocked, import { WALLET_HISTORY_KIND, WALLET_TOKEN_KIND } from "applesauce-wallet/helpers";
isTokenDetailsLocked,
unlockHistoryDetails,
unlockTokenDetails,
unlockWallet,
WALLET_HISTORY_KIND,
WALLET_KIND,
WALLET_TOKEN_KIND,
} from "applesauce-wallet/helpers";
import { useActiveAccount, useStoreQuery } from "applesauce-react/hooks"; import { useActiveAccount, useStoreQuery, useActionHub } from "applesauce-react/hooks";
import useAsyncErrorHandler from "../../hooks/use-async-error-handler"; import useAsyncErrorHandler from "../../hooks/use-async-error-handler";
import { eventStore } from "../../services/event-store";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import SimpleView from "../../components/layout/presets/simple-view"; import SimpleView from "../../components/layout/presets/simple-view";
import useTimelineLoader from "../../hooks/use-timeline-loader"; import useTimelineLoader from "../../hooks/use-timeline-loader";
import useUserMailboxes from "../../hooks/use-user-mailboxes"; import useUserMailboxes from "../../hooks/use-user-mailboxes";
@ -26,10 +16,11 @@ import WalletBalanceCard from "./balance-card";
import WalletTokensTab from "./tabs/tokens"; import WalletTokensTab from "./tabs/tokens";
import WalletHistoryTab from "./tabs/history"; import WalletHistoryTab from "./tabs/history";
import WalletMintsTab from "./tabs/mints"; import WalletMintsTab from "./tabs/mints";
import useUserWallet from "../../hooks/use-user-wallet";
export default function WalletHomeView() { export default function WalletHomeView() {
const account = useActiveAccount()!; const account = useActiveAccount()!;
const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey: account.pubkey }); const wallet = useUserWallet(account.pubkey);
const mailboxes = useUserMailboxes(account.pubkey); const mailboxes = useUserMailboxes(account.pubkey);
const readRelays = useReadRelays(mailboxes?.outboxes); const readRelays = useReadRelays(mailboxes?.outboxes);
@ -42,26 +33,13 @@ export default function WalletHomeView() {
]); ]);
const balance = useStoreQuery(WalletBalanceQuery, [account.pubkey]); const balance = useStoreQuery(WalletBalanceQuery, [account.pubkey]);
const actions = useActionHub();
const unlock = useAsyncErrorHandler(async () => { const unlock = useAsyncErrorHandler(async () => {
if (!wallet) throw new Error("Missing wallet"); if (!wallet) throw new Error("Missing wallet");
await unlockWallet(wallet, account); if (wallet.locked === false) return;
eventStore.update(wallet);
// attempt to unlock all tokens await actions.run(UnlockWallet, { history: true, tokens: true });
for (const event of events) { }, [wallet, actions]);
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, events]);
const walletInfo = useStoreQuery(WalletQuery, [account.pubkey]);
const callback = useTimelineCurserIntersectionCallback(loader); const callback = useTimelineCurserIntersectionCallback(loader);
@ -70,7 +48,7 @@ export default function WalletHomeView() {
<SimpleView <SimpleView
title="Wallet" title="Wallet"
actions={ actions={
walletInfo?.locked && ( wallet?.locked && (
<Button onClick={unlock} colorScheme="primary" ms="auto" size="sm"> <Button onClick={unlock} colorScheme="primary" ms="auto" size="sm">
Unlock Unlock
</Button> </Button>
@ -78,7 +56,7 @@ export default function WalletHomeView() {
} }
> >
<WalletBalanceCard pubkey={account.pubkey} w="full" maxW="2xl" mx="auto" /> <WalletBalanceCard pubkey={account.pubkey} w="full" maxW="2xl" mx="auto" />
{walletInfo?.locked && ( {wallet?.locked && (
<Button onClick={unlock} colorScheme="primary" mx="auto" size="lg" w="sm"> <Button onClick={unlock} colorScheme="primary" mx="auto" size="lg" w="sm">
Unlock Unlock
</Button> </Button>

View File

@ -0,0 +1,57 @@
import { useState } from "react";
import { useActionHub } from "applesauce-react/hooks";
import { Button, Flex, Textarea, useToast } from "@chakra-ui/react";
import { getDecodedToken, Token } from "@cashu/cashu-ts";
import { ReceiveToken } from "applesauce-wallet/actions";
import { useNavigate } from "react-router-dom";
import SimpleView from "../../components/layout/presets/simple-view";
import { getCashuWallet } from "../../services/cashu-mints";
import RouterLink from "../../components/router-link";
export default function WalletReceiveView() {
const actions = useActionHub();
const navigate = useNavigate();
const toast = useToast();
const [input, setInput] = useState("");
const [loading, setLoading] = useState(false);
const receive = async () => {
setLoading(true);
try {
const decoded = getDecodedToken(input.trim());
// swap tokens
const wallet = await getCashuWallet(decoded.mint);
const proofs = await wallet.receive(decoded);
const token: Token = { mint: decoded.mint, proofs };
// save new tokens
await actions.run(ReceiveToken, token);
const amount = token.proofs.reduce((t, p) => t + p.amount, 0);
toast({ status: "success", description: `Received ${amount} sats` });
navigate("/wallet");
} catch (error) {
if (error instanceof Error) toast({ status: "error", description: error.message });
console.log(error);
}
setLoading(false);
};
return (
<SimpleView title="Receive" maxW="2xl" center>
<Textarea value={input} onChange={(e) => setInput(e.target.value)} placeholder="cashuB...." rows={10} />
<Flex gap="2">
<Button as={RouterLink} to="/wallet">
Back
</Button>
<Button colorScheme="primary" onClick={receive} isLoading={loading} ms="auto">
Receive
</Button>
</Flex>
</SimpleView>
);
}

View File

@ -3,6 +3,7 @@ import RequireActiveAccount from "../../components/router/require-active-account
import { lazy } from "react"; import { lazy } from "react";
const WalletHomeView = lazy(() => import(".")); const WalletHomeView = lazy(() => import("."));
const WalletReceiveView = lazy(() => import("./receive"));
export default [ export default [
{ {
@ -13,4 +14,5 @@ export default [
</RequireActiveAccount> </RequireActiveAccount>
), ),
}, },
{ path: "receive", Component: WalletReceiveView },
] satisfies RouteObject[]; ] satisfies RouteObject[];

View File

@ -12,10 +12,10 @@ import {
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks"; import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks";
import { import {
getHistoryDetails, getHistoryContent,
getHistoryRedeemed, getHistoryRedeemed,
isHistoryDetailsLocked, isHistoryContentLocked,
unlockHistoryDetails, unlockHistoryContent,
} from "applesauce-wallet/helpers"; } from "applesauce-wallet/helpers";
import { WalletHistoryQuery } from "applesauce-wallet/queries"; import { WalletHistoryQuery } from "applesauce-wallet/queries";
import { NostrEvent } from "nostr-tools"; import { NostrEvent } from "nostr-tools";
@ -38,8 +38,8 @@ import CashuMintName from "../../../components/cashu/cashu-mint-name";
function HistoryEntry({ entry }: { entry: NostrEvent }) { function HistoryEntry({ entry }: { entry: NostrEvent }) {
const account = useActiveAccount()!; const account = useActiveAccount()!;
const eventStore = useEventStore(); const eventStore = useEventStore();
const locked = isHistoryDetailsLocked(entry); const locked = isHistoryContentLocked(entry);
const details = !locked ? getHistoryDetails(entry) : undefined; const details = !locked ? getHistoryContent(entry) : undefined;
useEventUpdate(entry.id); useEventUpdate(entry.id);
const ref = useEventIntersectionRef(entry); const ref = useEventIntersectionRef(entry);
@ -49,7 +49,7 @@ function HistoryEntry({ entry }: { entry: NostrEvent }) {
const redeemed = useSingleEvents(redeemedIds); const redeemed = useSingleEvents(redeemedIds);
const unlock = useAsyncErrorHandler(async () => { const unlock = useAsyncErrorHandler(async () => {
await unlockHistoryDetails(entry, account); await unlockHistoryContent(entry, account);
eventStore.update(entry); eventStore.update(entry);
}, [entry, account, eventStore]); }, [entry, account, eventStore]);
@ -120,8 +120,8 @@ export default function WalletHistoryTab() {
const unlock = useAsyncErrorHandler(async () => { const unlock = useAsyncErrorHandler(async () => {
for (const entry of locked) { for (const entry of locked) {
if (!isHistoryDetailsLocked(entry)) continue; if (!isHistoryContentLocked(entry)) continue;
await unlockHistoryDetails(entry, account); await unlockHistoryContent(entry, account);
eventStore.update(entry); eventStore.update(entry);
} }
}, [locked, account, eventStore]); }, [locked, account, eventStore]);

View File

@ -13,7 +13,7 @@ import {
} from "@chakra-ui/react"; } from "@chakra-ui/react";
import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks"; import { useActiveAccount, useEventStore, useStoreQuery } from "applesauce-react/hooks";
import { WalletTokensQuery } from "applesauce-wallet/queries"; import { WalletTokensQuery } from "applesauce-wallet/queries";
import { getTokenDetails, isTokenDetailsLocked, unlockTokenDetails } from "applesauce-wallet/helpers"; import { getTokenContent, isTokenContentLocked, unlockTokenContent } from "applesauce-wallet/helpers";
import { NostrEvent } from "nostr-tools"; import { NostrEvent } from "nostr-tools";
import { ProofState } from "@cashu/cashu-ts"; import { ProofState } from "@cashu/cashu-ts";
@ -32,8 +32,8 @@ function TokenEvent({ token }: { token: NostrEvent }) {
useEventUpdate(token.id); useEventUpdate(token.id);
const ref = useEventIntersectionRef(token); const ref = useEventIntersectionRef(token);
const locked = isTokenDetailsLocked(token); const locked = isTokenContentLocked(token);
const details = !locked ? getTokenDetails(token) : undefined; const details = !locked ? getTokenContent(token) : undefined;
const amount = details?.proofs.reduce((t, p) => t + p.amount, 0); const amount = details?.proofs.reduce((t, p) => t + p.amount, 0);
const [spentState, setSpentState] = useState<ProofState[]>(); const [spentState, setSpentState] = useState<ProofState[]>();
@ -49,7 +49,7 @@ function TokenEvent({ token }: { token: NostrEvent }) {
const unlock = useAsyncErrorHandler(async () => { const unlock = useAsyncErrorHandler(async () => {
if (!account) return; if (!account) return;
await unlockTokenDetails(token, account); await unlockTokenContent(token, account);
eventStore.update(token); eventStore.update(token);
}, [token, account, eventStore]); }, [token, account, eventStore]);
@ -109,7 +109,7 @@ export default function WalletTokensTab({ ...props }: Omit<FlexProps, "children"
const unlock = useAsyncErrorHandler(async () => { const unlock = useAsyncErrorHandler(async () => {
if (!locked) return; if (!locked) return;
for (const token of locked) { for (const token of locked) {
await unlockTokenDetails(token, account); await unlockTokenContent(token, account);
eventStore.update(token); eventStore.update(token);
} }
}, [locked, account, eventStore]); }, [locked, account, eventStore]);