mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 13:21:44 +01:00
add receive button to wallet
This commit is contained in:
parent
7e544a5bc9
commit
4ed1a0b528
@ -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
784
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
10
src/hooks/use-user-wallet.ts
Normal file
10
src/hooks/use-user-wallet.ts
Normal 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);
|
||||
}
|
@ -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>;
|
||||
}
|
@ -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
18
src/services/actions.ts
Normal 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;
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
57
src/views/wallet/receive.tsx
Normal file
57
src/views/wallet/receive.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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[];
|
||||
|
@ -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]);
|
||||
|
@ -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]);
|
||||
|
Loading…
x
Reference in New Issue
Block a user