update basic wallet view

This commit is contained in:
hzrd149 2025-03-09 23:11:55 +00:00
parent 789de46c56
commit 92a8f1029c
7 changed files with 614 additions and 683 deletions

View File

@ -19,7 +19,7 @@
"cap-sync-version": "pnpm dlx capacitor-set-version . -v $(jq -r .version package.json) -b 1"
},
"dependencies": {
"@cashu/cashu-ts": "^2.2.0",
"@cashu/cashu-ts": "^2.2.1",
"@chakra-ui/anatomy": "^2.3.4",
"@chakra-ui/breakpoint-utils": "^2.0.8",
"@chakra-ui/icons": "^2.2.4",
@ -31,13 +31,13 @@
"@codemirror/autocomplete": "^6.18.6",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/language": "^6.10.8",
"@codemirror/view": "^6.36.3",
"@codemirror/view": "^6.36.4",
"@emoji-mart/data": "^1.2.1",
"@emoji-mart/react": "^1.1.1",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@getalby/bitcoin-connect": "^3.6.3",
"@getalby/bitcoin-connect-react": "^3.6.3",
"@getalby/bitcoin-connect": "^3.7.0",
"@getalby/bitcoin-connect-react": "^3.7.0",
"@noble/ciphers": "^1.2.1",
"@noble/curves": "^1.8.1",
"@noble/hashes": "^1.7.1",
@ -45,8 +45,8 @@
"@satellite-earth/core": "^0.5.0",
"@scure/base": "^1.2.4",
"@snort/worker-relay": "^1.3.1",
"@uiw/codemirror-theme-github": "^4.23.8",
"@uiw/react-codemirror": "^4.23.8",
"@uiw/codemirror-theme-github": "^4.23.10",
"@uiw/react-codemirror": "^4.23.10",
"@webscopeio/react-textarea-autocomplete": "^4.9.2",
"ansi-to-html": "^0.7.2",
"applesauce-accounts": "next",
@ -55,6 +55,7 @@
"applesauce-factory": "next",
"applesauce-loaders": "next",
"applesauce-react": "next",
"applesauce-relay": "next",
"applesauce-signers": "next",
"bech32": "^2.0.0",
"blossom-client-sdk": "^3.0.1",
@ -66,7 +67,7 @@
"codemirror-json-schema": "^0.7.9",
"dayjs": "^1.11.13",
"debug": "^4.4.0",
"easymde": "^2.19.0",
"easymde": "^2.20.0",
"emoji-regex": "^10.4.0",
"file-saver": "^2.0.5",
"framer-motion": "^10.18.0",
@ -85,7 +86,7 @@
"light-bolt11-decoder": "^3.2.0",
"lodash.throttle": "^4.1.1",
"match-sorter": "^8.0.0",
"nanoid": "^5.1.0",
"nanoid": "^5.1.3",
"ngeohash": "^0.6.3",
"nostr-idb": "^2.2.0",
"nostr-signer-capacitor-plugin": "^0.0.3",
@ -93,7 +94,7 @@
"nostr-typedef": "^0.11.0",
"nostr-wasm": "^0.1.0",
"nuka-carousel": "^8.2.0",
"prettier": "^3.5.1",
"prettier": "^3.5.3",
"react": "^19.0.0",
"react-chartjs-2": "^5.3.0",
"react-diff-viewer-continued": "^3.4.0",
@ -106,8 +107,8 @@
"react-mosaic-component": "^6.1.1",
"react-photo-album": "^2.4.1",
"react-qr-barcode-scanner": "^2.0.0",
"react-router": "^6.29.0",
"react-router-dom": "^6.29.0",
"react-router": "^6.30.0",
"react-router-dom": "^6.30.0",
"react-simplemde-editor": "^5.2.0",
"react-singleton-hook": "^4.0.1",
"react-use": "^17.6.0",
@ -116,7 +117,7 @@
"remark-gfm": "^4.0.1",
"remark-wiki-link": "^2.0.1",
"rx-nostr": "^3.5.0",
"rxjs": "^7.8.1",
"rxjs": "^7.8.2",
"three": "^0.170.0",
"three-spritetext": "^1.9.4",
"three-stdlib": "^2.35.14",
@ -161,7 +162,7 @@
"camelcase": "^8.0.0",
"cheerio": "^1.0.0",
"eventemitter3": "^5.0.1",
"typescript": "^5.7.3",
"typescript": "^5.8.2",
"vite": "^5.4.14",
"vite-plugin-pwa": "^0.21.1",
"vite-tsconfig-paths": "^5.1.4",

1005
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -12,9 +12,9 @@ import {
timer,
} from "rxjs";
import { PrivateNodeConfig } from "@satellite-earth/core/types";
import { Relay } from "applesauce-relay";
import hash_sum from "hash-sum";
import BakeryRelay from "./bakery-relay";
import { LogEntry, NetworkStateResult } from "./types";
import { scanToArray } from "../../helpers/observable";
@ -25,7 +25,7 @@ export default class BakeryControlApi {
network: Observable<NetworkStateResult>;
services: Observable<string[]>;
constructor(public bakery: BakeryRelay) {
constructor(public bakery: Relay) {
this.config = this.query<PrivateNodeConfig>("config", {}).pipe(shareReplay(1));
this.network = this.query<NetworkStateResult>("network-status", {}).pipe(shareReplay(1));
this.services = this.query<{ id: string }>("services", {}).pipe(

View File

@ -1,122 +0,0 @@
import {
BehaviorSubject,
filter,
map,
merge,
NEVER,
Observable,
of,
OperatorFunction,
shareReplay,
take,
takeWhile,
tap,
timeout,
} from "rxjs";
import { Filter, NostrEvent } from "nostr-tools";
import { webSocket, WebSocketSubject } from "rxjs/webSocket";
import { logger } from "../../helpers/debug";
import { simpleTimeout } from "applesauce-core/observable";
export type RequestResponse = { type: "EOSE"; id: string } | { type: "EVENT"; id: string; event: NostrEvent };
/** Filter request responses and only return the events */
export function filterEvents(): OperatorFunction<RequestResponse, NostrEvent> {
return (source) =>
source.pipe(
filter((r) => r.type === "EVENT"),
map((r) => r.event),
);
}
export default class BakeryRelay {
log = logger.extend("Bakery");
public socket$: WebSocketSubject<any[]>;
connected$ = new BehaviorSubject(false);
challenge$: Observable<string>;
authenticated$ = new BehaviorSubject(false);
constructor(public url: string) {
this.socket$ = webSocket({
url,
openObserver: {
next: () => {
this.log("Connected");
this.connected$.next(true);
this.authenticated$.next(false);
},
},
closeObserver: {
next: () => {
this.log("Disconnected");
this.connected$.next(false);
this.authenticated$.next(false);
},
},
});
// create an observable for listening for AUTH
this.challenge$ = this.socket$.pipe(
filter((message) => message[0] === "AUTH"),
map((m) => m[1]),
shareReplay(1),
);
}
req(id: string, filters: Filter[]): Observable<RequestResponse> {
return this.socket$
.multiplex(
() => ["REQ", id, ...filters],
() => ["CLOSE", id],
(message) => (message[0] === "EVENT" || message[0] === "CLOSE" || message[0] === "EOSE") && message[1] === id,
)
.pipe(
// complete when CLOSE is sent
takeWhile((m) => m[0] !== "CLOSE"),
// pick event out of EVENT messages
map<any[], RequestResponse>((message) => {
if (message[0] === "EOSE") return { type: "EOSE", id: message[1] };
else return { type: "EVENT", id: message[1], event: message[2] };
}),
// if no events are seen in 10s, emit EOSE
timeout({
first: 10_000,
with: () => merge(of<RequestResponse>({ type: "EOSE", id }), NEVER),
}),
);
}
protected listenForOk(id: string) {
return this.socket$.pipe(
// look for OK message for event
filter((m) => m[0] === "OK" && m[1] === id),
// format OK message
map((m) => ({ ok: m[2], message: m[3] })),
// complete on first value
take(1),
);
}
/** send an Event message */
event(event: NostrEvent): Observable<{ ok: boolean; message?: string }> {
this.socket$.next(["EVENT", event]);
return this.listenForOk(event.id).pipe(
// Throw timeout error if OK is not seen in 10s
simpleTimeout(10_000, "Timeout"),
);
}
/** send and AUTH message */
auth(event: NostrEvent): Observable<{ ok: boolean; message?: string }> {
this.socket$.next(["AUTH", event]);
return this.listenForOk(event.id).pipe(
// update authenticated
tap((result) => this.authenticated$.next(result.ok)),
// timeout after 5s for AUTH messages
simpleTimeout(5_000, "Timeout"),
);
}
}

View File

@ -1,10 +0,0 @@
import { getTagValue } from "applesauce-core/helpers";
import { NostrEvent } from "nostr-tools";
export function getWalletName(wallet: NostrEvent) {
return getTagValue(wallet, "name");
}
export function getWalletDescription(wallet: NostrEvent) {
return getTagValue(wallet, "description");
}

View File

@ -1,8 +1,8 @@
import { BehaviorSubject, combineLatest, filter, lastValueFrom, map, of, shareReplay, switchMap } from "rxjs";
import { nip42 } from "nostr-tools";
import { Relay } from "applesauce-relay";
import { logger } from "../helpers/debug";
import BakeryRelay from "../classes/bakery/bakery-relay";
import BakeryControlApi from "../classes/bakery/bakery-control";
import localSettings from "./local-settings";
import accounts from "./accounts";
@ -16,14 +16,14 @@ export function clearBakeryURL() {
localSettings.bakeryURL.clear();
}
export const bakery$ = new BehaviorSubject<BakeryRelay | null>(null);
export const bakery$ = new BehaviorSubject<Relay | null>(null);
// connect to the bakery when the URL changes
localSettings.bakeryURL.subscribe((url) => {
if (!URL.canParse(url)) return bakery$.next(null);
try {
bakery$.next(new BakeryRelay(localSettings.bakeryURL.value));
bakery$.next(new Relay(localSettings.bakeryURL.value));
} catch (err) {
log("Failed to create bakery connection, clearing storage");
localSettings.bakeryURL.clear();

View File

@ -1,68 +1,105 @@
import { Badge, Button, Card, CardBody, CardFooter, CardHeader, Flex, Heading } from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import {
getEventUID,
getTagValue,
hasHiddenTags,
HiddenTagsSigner,
isHiddenTagsLocked,
unlockHiddenTags,
} from "applesauce-core/helpers";
Alert,
AlertDescription,
AlertIcon,
AlertTitle,
Badge,
Button,
Card,
CardBody,
CardFooter,
CardHeader,
Flex,
Heading,
Spinner,
} from "@chakra-ui/react";
import { NostrEvent } from "nostr-tools";
import { WalletQuery } from "applesauce-wallet/queries";
import { getWalletMints, unlockWallet, WALLET_KIND } from "applesauce-wallet/helpers";
import { useReadRelays } from "../../hooks/use-client-relays";
import { useActiveAccount } from "applesauce-react/hooks";
import useTimelineLoader from "../../hooks/use-timeline-loader";
import { useActiveAccount, useStoreQuery } from "applesauce-react/hooks";
import useAsyncErrorHandler from "../../hooks/use-async-error-handler";
import { getWalletDescription, getWalletName } from "../../helpers/nostr/wallet";
import DebugEventButton from "../../components/debug-modal/debug-event-button";
import useEventUpdate from "../../hooks/use-event-update";
import { eventStore } from "../../services/event-store";
import useReplaceableEvent from "../../hooks/use-replaceable-event";
import SimpleView from "../../components/layout/presets/simple-view";
function Wallet({ wallet }: { wallet: NostrEvent }) {
useEventUpdate(wallet.id);
const account = useActiveAccount()!;
const locked = hasHiddenTags(wallet) && isHiddenTagsLocked(wallet);
const unlock = useAsyncErrorHandler(async () => {
const signer = account.signer;
if (!signer || !signer.nip04) throw new Error("Missing signer");
await unlockHiddenTags(wallet, signer as HiddenTagsSigner, eventStore);
}, [wallet, account]);
const walletInfo = useStoreQuery(WalletQuery, [account.pubkey]);
return (
<Card>
<CardHeader display="flex" gap="2" p="2" alignItems="center">
<Heading size="md">{getWalletName(wallet) || getTagValue(wallet, "d")}</Heading>
{locked && <Badge colorScheme="orange">Locked</Badge>}
<DebugEventButton event={wallet} variant="ghost" ml="auto" size="sm" />
<Heading size="md">Wallet</Heading>
{walletInfo?.locked && <Badge colorScheme="orange">Locked</Badge>}
{wallet && <DebugEventButton event={wallet} variant="ghost" ml="auto" size="sm" />}
</CardHeader>
<CardBody px="2" py="0">
{getWalletDescription(wallet)}
</CardBody>
<CardFooter p="2">
{locked && (
<Button onClick={unlock} colorScheme="primary">
Unlock
</Button>
)}
</CardFooter>
{walletInfo?.locked === false && (
<CardBody px="2" py="0" whiteSpace="pre-line">
Key: {walletInfo.privateKey}
Mints: {walletInfo.mints.join(", ")}
</CardBody>
)}
</Card>
);
}
export default function WalletHomeView() {
const account = useActiveAccount()!;
const wallet = useReplaceableEvent({ kind: WALLET_KIND, pubkey: account.pubkey });
const readRelays = useReadRelays();
const { timeline } = useTimelineLoader("wallets", readRelays, { kinds: [37375], authors: [account.pubkey] });
const unlock = useAsyncErrorHandler(async () => {
if (!wallet) throw new Error("Missing wallet");
await unlockWallet(wallet, account);
eventStore.update(wallet);
}, [wallet, account]);
const walletInfo = useStoreQuery(WalletQuery, [account.pubkey]);
return (
<Flex direction="column" gap="2">
{timeline.map((wallet) => (
<Wallet key={getEventUID(wallet)} wallet={wallet} />
))}
</Flex>
<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>
);
}