mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-03-17 13:21:44 +01:00
update basic wallet view
This commit is contained in:
parent
789de46c56
commit
92a8f1029c
27
package.json
27
package.json
@ -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
1005
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -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(
|
||||
|
@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
@ -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();
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user