show fedimint tokens in content

This commit is contained in:
hzrd149 2024-10-16 22:08:06 +01:00
parent f2f8186101
commit e9efa336eb
12 changed files with 161 additions and 31 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Show fedimint tokens in content

View File

@ -2,3 +2,4 @@ node_modules
dist
public/lib
stats.html
pnpm-lock.yaml

View File

@ -105,6 +105,7 @@
"three-spritetext": "^1.8.2",
"three-stdlib": "^2.29.11",
"tiny-lru": "^11.2.11",
"unified": "^11.0.5",
"webln": "^0.3.2",
"workbox-core": "7.0.0",
"workbox-precaching": "7.0.0",

3
pnpm-lock.yaml generated
View File

@ -279,6 +279,9 @@ importers:
tiny-lru:
specifier: ^11.2.11
version: 11.2.11
unified:
specifier: ^11.0.5
version: 11.0.5
webln:
specifier: ^0.3.2
version: 0.3.2

View File

@ -1,5 +1,5 @@
import { useAsync } from "react-use";
import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link, Text } from "@chakra-ui/react";
import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link, Spinner, Text } from "@chakra-ui/react";
import { Token, getEncodedToken } from "@cashu/cashu-ts";
import { CopyIconButton } from "../copy-icon-button";
@ -9,7 +9,6 @@ import { ECashIcon, WalletIcon } from "../icons";
import { getMint } from "../../services/cashu-mints";
import CurrencyDollar from "../icons/currency-dollar";
import CurrencyEthereum from "../icons/currency-ethereum";
import CurrencyRupeeCircle from "../icons/currency-rupee-circle";
import CurrencyEuro from "../icons/currency-euro";
import CurrencyYen from "../icons/currency-yen";
import CurrencyPound from "../icons/currency-pound";
@ -38,14 +37,14 @@ export default function InlineCachuCard({
const account = useCurrentAccount();
encoded = encoded || getEncodedToken(token);
const { value: spendable } = useAsync(async () => {
const { value: spendable, loading } = useAsync(async () => {
if (!token) return;
for (const entry of token.token) {
const mint = await getMint(entry.mint);
const spent = await mint.check({ Ys: entry.proofs.map((p) => p.secret) });
if (spent.states.some((v) => v.state === "SPENT")) return false;
if (spent.states.some((v) => v.state === "UNSPENT")) return true;
}
return true;
return false;
}, [token]);
const amount = token?.token[0].proofs.reduce((acc, v) => acc + v.amount, 0);
@ -98,17 +97,18 @@ export default function InlineCachuCard({
icon={<WalletIcon boxSize={5} />}
title="Open Wallet"
aria-label="Open Wallet"
href={`cashu://` + token}
href={`cashu://` + encoded}
/>
{account && <RedeemButton token={encoded} />}
</ButtonGroup>
<Heading size="md" textDecoration={spendable === false ? "line-through" : undefined}>
{denomination} {spendable === false ? " (Spent)" : ""}
{denomination} {spendable === false ? " (Spent)" : loading ? <Spinner size="xs" /> : undefined}
</Heading>
{token && <Text fontSize="xs">Mint: {new URL(token.token[0].mint).hostname}</Text>}
{token.unit && <Text fontSize="xs">Unit: {token.unit}</Text>}
</Box>
{token.memo && <Box>{token.memo}</Box>}
{loading && <Spinner />}
</Card>
);
}

View File

@ -1,11 +1,14 @@
import { lazy } from "react";
import { Text } from "@chakra-ui/react";
import { ComponentMap } from "applesauce-react";
import Mention from "./mention";
import Cashu from "./cashu";
const InlineFedimintCard = lazy(() => import("../fedimint/inline-fedimint-card"));
export const components: ComponentMap = {
text: ({ node }) => <Text as="span">{node.value}</Text>,
mention: Mention,
cashu: Cashu,
fedimint: ({ node }) => <InlineFedimintCard token={node.token} />,
};

View File

@ -0,0 +1,43 @@
import { Button, ButtonGroup, Card, CardProps, Heading, Link } from "@chakra-ui/react";
import { CopyIconButton } from "../copy-icon-button";
import useCurrentAccount from "../../hooks/use-current-account";
import { ECashIcon, WalletIcon } from "../icons";
export default function InlineFedimintCard({ token, ...props }: Omit<CardProps, "children"> & { token: string }) {
const account = useCurrentAccount();
// const { value: amount } = useAsync(async () => {
// const { FedimintWallet } = await import("@fedimint/core-web");
// const wallet = new FedimintWallet();
// const opened = await wallet.open("noStrudel");
// if (opened) {
// return await wallet.mint.parseNotes(token);
// }
// }, []);
let UnitIcon = ECashIcon;
let unitColor = "green.500";
return (
<Card p="2" flexDirection="row" borderColor="green.500" gap="2" {...props}>
<UnitIcon boxSize={10} color={unitColor} mr="2" mb="1" />
<Heading size="md" alignItems="center">
ecash
</Heading>
<ButtonGroup ml="auto">
<CopyIconButton value={token} title="Copy Token" aria-label="Copy Token" variant="ghost" />
<Button
as={Link}
leftIcon={<WalletIcon boxSize={5} />}
colorScheme="primary"
title="Open Wallet"
aria-label="Open Wallet"
href={`fedi://` + token}
>
Redeem
</Button>
</ButtonGroup>
</Card>
);
}

View File

@ -28,29 +28,9 @@ import { LightboxProvider } from "../../lightbox-provider";
import MediaOwnerProvider from "../../../providers/local/media-owner-provider";
import buildLinkComponent from "../../content/links";
import { components } from "../../content";
import { FedimintTokensTransformer } from "../../../helpers/fedimint";
// function buildContents(event: NostrEvent | EventTemplate, simpleLinks = false) {
// let content: EmbedableContent = [event.content.trim()];
// // image gallery
// content = embedImageGallery(content, event as NostrEvent);
// // bitcoin
// content = embedLightningInvoice(content);
// // cashu
// content = embedCashuTokens(content);
// // nostr
// content = embedNostrLinks(content);
// content = embedNostrMentions(content, event);
// content = embedNostrHashtags(content, event);
// content = embedNipDefinitions(content);
// content = embedEmoji(content, event);
// content = embedNostrWikiLinks(content);
// return content;
// }
const transformers = [FedimintTokensTransformer];
export type TextNoteContentsProps = {
event: NostrEvent | EventTemplate;
@ -98,7 +78,7 @@ export const TextNoteContents = React.memo(
[LinkComponent],
);
const content = useRenderedContent(event, componentsMap);
const content = useRenderedContent(event, componentsMap, { transformers });
return (
<MediaOwnerProvider owner={(event as NostrEvent).pubkey as string | undefined}>

33
src/helpers/fedimint.ts Normal file
View File

@ -0,0 +1,33 @@
import { Transformer } from "unified";
import { Root, findAndReplace, Node } from "applesauce-content/nast";
declare module "applesauce-content/nast" {
export interface FedimintToken extends Node {
type: "fedimint";
token: string;
}
export interface ContentMap {
fedimint: FedimintToken;
}
}
export function FedimintTokensTransformer(): Transformer<Root> {
return (tree) => {
findAndReplace(tree, [
[
/([A-Za-z0-9_+/-]{100,10000}={0,3})/gi,
(_: string, $1: string) => {
try {
return {
type: "fedimint",
token: $1,
};
} catch (error) {}
return false;
},
],
]);
};
}

View File

@ -0,0 +1,15 @@
import { useEffect } from "react";
import replaceableEventsService from "../services/replaceable-events";
import { useReadRelays } from "./use-client-relays";
import { useStoreQuery } from "applesauce-react";
import TrustedMintsQuery from "../queries/trusted-mints";
export default function useTrustedMints(pubkey?: string) {
const relays = useReadRelays();
useEffect(() => {
if (pubkey) replaceableEventsService.requestEvent(relays, 10019, pubkey);
}, [pubkey, relays]);
return useStoreQuery(TrustedMintsQuery, pubkey ? [pubkey] : undefined);
}

View File

@ -0,0 +1,43 @@
import { Query } from "applesauce-core";
import { safeRelayUrl } from "applesauce-core/helpers";
type TrustedMints = {
mints: string[];
relays: Set<string>;
pubkey?: string;
};
export default function TrustedMintsQuery(pubkey: string): Query<TrustedMints | undefined> {
return {
key: pubkey,
run: (events) =>
events.replaceable(10019, pubkey).map((event) => {
if (!event) return undefined;
const relays = new Set<string>();
const mints: string[] = [];
let pubkey: string | undefined = undefined;
for (const tag of event.tags) {
switch (tag[0]) {
case "mint":
if (tag[1]) mints.push(tag[1]);
break;
case "relay":
if (tag[1]) {
let safe = safeRelayUrl(tag[1]);
if (safe) mints.push(safe);
}
break;
case "pubkey":
if (tag[1]) pubkey = tag[1];
break;
}
}
return { relays, mints, pubkey };
}),
};
}

View File

@ -26,6 +26,9 @@ import { renderAudioUrl } from "../../../components/external-embeds/types/audio"
import buildLinkComponent from "../../../components/content/links";
import { components } from "../../../components/content";
import { useKind4Decrypt } from "../../../hooks/use-kind4-decryption";
import { FedimintTokensTransformer } from "../../../helpers/fedimint";
const transformers = [FedimintTokensTransformer];
export default function DirectMessageContent({
event,
@ -64,7 +67,7 @@ export default function DirectMessageContent({
);
const { plaintext } = useKind4Decrypt(event);
const content = useRenderedContent(event, componentsMap, plaintext);
const content = useRenderedContent(event, componentsMap, { overrideContent: plaintext, transformers });
return (
<TrustProvider event={event}>