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",
"ansi-to-html": "^0.7.2",
"applesauce-accounts": "next",
"applesauce-actions": "next",
"applesauce-content": "next",
"applesauce-core": "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 { 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 buildTheme from "../../theme";
@ -10,8 +10,9 @@ import BreakpointProvider from "./breakpoint-provider";
import PublishProvider from "./publish-provider";
import WebOfTrustProvider from "./web-of-trust-provider";
import { queryStore } from "../../services/event-store";
import EventFactoryProvider from "./event-factory-provider";
import accounts from "../../services/accounts";
import actions from "../../services/actions";
import factory from "../../services/event-factory";
function ThemeProviders({ children }: { children: React.ReactNode }) {
const { theme: themeName, primaryColor } = useAppSettings();
@ -29,17 +30,19 @@ export const GlobalProviders = ({ children }: { children: React.ReactNode }) =>
return (
<QueryStoreProvider queryStore={queryStore}>
<AccountsProvider manager={accounts}>
<ThemeProviders>
<SigningProvider>
<PublishProvider>
<UserEmojiProvider>
<EventFactoryProvider>
<WebOfTrustProvider>{children}</WebOfTrustProvider>
</EventFactoryProvider>
</UserEmojiProvider>
</PublishProvider>
</SigningProvider>
</ThemeProviders>
<ActionsProvider actionHub={actions}>
<FactoryProvider factory={factory}>
<ThemeProviders>
<SigningProvider>
<PublishProvider>
<UserEmojiProvider>
<WebOfTrustProvider>{children}</WebOfTrustProvider>
</UserEmojiProvider>
</PublishProvider>
</SigningProvider>
</ThemeProviders>
</FactoryProvider>
</ActionsProvider>
</AccountsProvider>
</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 { map } from "rxjs/operators";
import { getEventRelayHint, getPubkeyRelayHint } from "./relay-hints";
import { NIP_89_CLIENT_APP } from "../const";
import localSettings from "./local-settings";
import accounts from "./accounts";
import localSettings from "./local-settings";
const factory$ = new BehaviorSubject<EventFactory>(
new EventFactory({
signer: accounts.active ?? undefined,
getEventRelayHint,
getPubkeyRelayHint: getPubkeyRelayHint,
client: localSettings.addClientTag.value ? NIP_89_CLIENT_APP : undefined,
}),
);
const factory = new EventFactory({
signer: accounts.signer,
getEventRelayHint,
getPubkeyRelayHint: getPubkeyRelayHint,
client: localSettings.addClientTag.value ? NIP_89_CLIENT_APP : undefined,
});
// update event factory when settings change
combineLatest([accounts.active$, localSettings.addClientTag]).pipe(
map(([current, client]) => {
return new EventFactory({
signer: current ? current.signer : undefined,
getEventRelayHint,
getPubkeyRelayHint: getPubkeyRelayHint,
client: client ? NIP_89_CLIENT_APP : undefined,
});
}),
);
localSettings.addClientTag.subscribe((client) => {
if (client) factory.context.client = NIP_89_CLIENT_APP;
else factory.context.client = 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 useEventUpdate from "../../hooks/use-event-update";
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">) {
const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey });
@ -27,7 +28,7 @@ export default function WalletBalanceCard({ pubkey, ...props }: { pubkey: string
Send
</Button>
<QRCodeScannerButton onData={() => {}} isDisabled size="lg" />
<Button isDisabled w="full" size="lg">
<Button as={RouterLink} w="full" size="lg" to="/wallet/receive">
Receive
</Button>
</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 { WalletBalanceQuery, WalletQuery } from "applesauce-wallet/queries";
import {
isHistoryDetailsLocked,
isTokenDetailsLocked,
unlockHistoryDetails,
unlockTokenDetails,
unlockWallet,
WALLET_HISTORY_KIND,
WALLET_KIND,
WALLET_TOKEN_KIND,
} from "applesauce-wallet/helpers";
import { WalletBalanceQuery } from "applesauce-wallet/queries";
import { UnlockWallet } from "applesauce-wallet/actions";
import { WALLET_HISTORY_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 { 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";
@ -26,10 +16,11 @@ import WalletBalanceCard from "./balance-card";
import WalletTokensTab from "./tabs/tokens";
import WalletHistoryTab from "./tabs/history";
import WalletMintsTab from "./tabs/mints";
import useUserWallet from "../../hooks/use-user-wallet";
export default function WalletHomeView() {
const account = useActiveAccount()!;
const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey: account.pubkey });
const wallet = useUserWallet(account.pubkey);
const mailboxes = useUserMailboxes(account.pubkey);
const readRelays = useReadRelays(mailboxes?.outboxes);
@ -42,26 +33,13 @@ export default function WalletHomeView() {
]);
const balance = useStoreQuery(WalletBalanceQuery, [account.pubkey]);
const actions = useActionHub();
const unlock = useAsyncErrorHandler(async () => {
if (!wallet) throw new Error("Missing wallet");
await unlockWallet(wallet, account);
eventStore.update(wallet);
if (wallet.locked === false) return;
// attempt to unlock all tokens
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, events]);
const walletInfo = useStoreQuery(WalletQuery, [account.pubkey]);
await actions.run(UnlockWallet, { history: true, tokens: true });
}, [wallet, actions]);
const callback = useTimelineCurserIntersectionCallback(loader);
@ -70,7 +48,7 @@ export default function WalletHomeView() {
<SimpleView
title="Wallet"
actions={
walletInfo?.locked && (
wallet?.locked && (
<Button onClick={unlock} colorScheme="primary" ms="auto" size="sm">
Unlock
</Button>
@ -78,7 +56,7 @@ export default function WalletHomeView() {
}
>
<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">
Unlock
</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";
const WalletHomeView = lazy(() => import("."));
const WalletReceiveView = lazy(() => import("./receive"));
export default [
{
@ -13,4 +14,5 @@ export default [
</RequireActiveAccount>
),
},
{ path: "receive", Component: WalletReceiveView },
] satisfies RouteObject[];

View File

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

View File

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