Add support for cashu v4 tokens

This commit is contained in:
hzrd149 2024-10-16 19:03:44 +01:00
parent 1282aee6f4
commit f2f8186101
7 changed files with 3899 additions and 4895 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add support for cashu v4 tokens

View File

@ -16,7 +16,7 @@
"build-icons": "node ./scripts/build-icons.mjs"
},
"dependencies": {
"@cashu/cashu-ts": "^0.9.0",
"@cashu/cashu-ts": "^1.1.0",
"@chakra-ui/anatomy": "^2.2.2",
"@chakra-ui/breakpoint-utils": "^2.0.8",
"@chakra-ui/icons": "^2.1.1",
@ -31,6 +31,7 @@
"@codemirror/view": "^6.28.6",
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@fedimint/core-web": "^0.0.7",
"@getalby/bitcoin-connect": "^3.6.2",
"@getalby/bitcoin-connect-react": "^3.6.2",
"@noble/ciphers": "^1.0.0",

8662
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,19 @@
import { useAsync } from "react-use";
import { useEffect, useState } from "react";
import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link } from "@chakra-ui/react";
import { getDecodedToken, Token, CashuMint } from "@cashu/cashu-ts";
import { Box, Button, ButtonGroup, Card, CardProps, Heading, IconButton, Link, Text } from "@chakra-ui/react";
import { Token, getEncodedToken } from "@cashu/cashu-ts";
import { CopyIconButton } from "../copy-icon-button";
import useUserProfile from "../../hooks/use-user-profile";
import useCurrentAccount from "../../hooks/use-current-account";
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";
import CurrencyBitcoin from "../icons/currency-bitcoin";
function RedeemButton({ token }: { token: string }) {
const account = useCurrentAccount()!;
@ -24,52 +30,85 @@ function RedeemButton({ token }: { token: string }) {
);
}
export default function InlineCachuCard({ token, ...props }: Omit<CardProps, "children"> & { token: string }) {
export default function InlineCachuCard({
token,
encoded,
...props
}: Omit<CardProps, "children"> & { token: Token; encoded?: string }) {
const account = useCurrentAccount();
const [cashu, setCashu] = useState<Token>();
encoded = encoded || getEncodedToken(token);
const { value: spendable } = useAsync(async () => {
if (!cashu) return;
for (const token of cashu.token) {
const mint = await getMint(token.mint);
const spent = await mint.check({ proofs: token.proofs.map((p) => ({ secret: p.secret })) });
if (spent.spendable.some((v) => v === false)) return false;
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;
}
return true;
}, [cashu]);
useEffect(() => {
if (!token.startsWith("cashuA") || token.length < 10) return;
try {
const cashu = getDecodedToken(token);
setCashu(cashu);
} catch (e) {}
}, [token]);
if (!cashu) return null;
const amount = token?.token[0].proofs.reduce((acc, v) => acc + v.amount, 0);
let UnitIcon = ECashIcon;
let unitColor = "green.500";
let denomination = `${amount} tokens`;
switch (token.unit) {
case "usd":
UnitIcon = CurrencyDollar;
denomination = `$${(amount / 100).toFixed(2)}`;
break;
case "eur":
UnitIcon = CurrencyEuro;
unitColor = "blue.500";
denomination = `${(amount / 100).toFixed(2)}`;
break;
case "gpb":
UnitIcon = CurrencyPound;
denomination = `£${(amount / 100).toFixed(2)}`;
break;
case "yen":
UnitIcon = CurrencyYen;
denomination = `¥${(amount / 100).toFixed(2)}`;
break;
case "eth":
UnitIcon = CurrencyEthereum;
break;
case "sat":
unitColor = "orange.300";
UnitIcon = CurrencyBitcoin;
denomination = `${amount} sats`;
break;
}
const amount = cashu?.token[0].proofs.reduce((acc, v) => acc + v.amount, 0);
return (
<Card p="4" flexDirection="row" borderColor="green.500" alignItems="center" gap="4" flexWrap="wrap" {...props}>
<ECashIcon boxSize={10} color="green.500" />
<Card p="2" flexDirection="column" borderColor="green.500" gap="2" {...props}>
<Box>
<UnitIcon boxSize={10} color={unitColor} float="left" mr="2" mb="1" />
<ButtonGroup float="right">
<CopyIconButton value={encoded} title="Copy Token" aria-label="Copy Token" variant="ghost" />
<IconButton
as={Link}
icon={<WalletIcon boxSize={5} />}
title="Open Wallet"
aria-label="Open Wallet"
href={`cashu://` + token}
/>
{account && <RedeemButton token={encoded} />}
</ButtonGroup>
<Heading size="md" textDecoration={spendable === false ? "line-through" : undefined}>
{amount} Cashu sats{spendable === false ? " (Spent)" : ""}
{denomination} {spendable === false ? " (Spent)" : ""}
</Heading>
{cashu && <small>Mint: {new URL(cashu.token[0].mint).hostname}</small>}
{token && <Text fontSize="xs">Mint: {new URL(token.token[0].mint).hostname}</Text>}
{token.unit && <Text fontSize="xs">Unit: {token.unit}</Text>}
</Box>
{cashu.memo && <Box>Memo: {cashu.memo}</Box>}
<ButtonGroup ml="auto">
<CopyIconButton value={token} title="Copy Token" aria-label="Copy Token" />
<IconButton
as={Link}
icon={<WalletIcon />}
title="Open Wallet"
aria-label="Open Wallet"
href={`cashu://` + token}
/>
{account && <RedeemButton token={token} />}
</ButtonGroup>
{token.memo && <Box>{token.memo}</Box>}
</Card>
);
}

View File

@ -0,0 +1,7 @@
import { CashuToken } from "applesauce-content/nast";
import InlineCachuCard from "../cashu/inline-cashu-card";
export default function Cashu({ node }: { node: CashuToken }) {
return <InlineCachuCard token={node.token} encoded={node.raw} />;
}

View File

@ -2,8 +2,10 @@ import { Text } from "@chakra-ui/react";
import { ComponentMap } from "applesauce-react";
import Mention from "./mention";
import Cashu from "./cashu";
export const components: ComponentMap = {
text: ({ node }) => <Text as="span">{node.value}</Text>,
mention: Mention,
cashu: Cashu,
};

View File

@ -25,6 +25,7 @@ import { LightboxProvider } from "../../../components/lightbox-provider";
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";
export default function DirectMessageContent({
event,
@ -62,7 +63,8 @@ export default function DirectMessageContent({
[LinkComponent],
);
const content = useRenderedContent(event, componentsMap);
const { plaintext } = useKind4Decrypt(event);
const content = useRenderedContent(event, componentsMap, plaintext);
return (
<TrustProvider event={event}>