Skip to content

Commit

Permalink
Add redeem button for cashu tokens
Browse files Browse the repository at this point in the history
hzrd149 committed Oct 10, 2023
1 parent 45736a4 commit f701942
Showing 10 changed files with 163 additions and 6 deletions.
5 changes: 5 additions & 0 deletions .changeset/blue-shoes-allow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"nostrudel": minor
---

Add redeem button for cashu tokens
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
"build-icons": "node ./scripts/build-icons.mjs"
},
"dependencies": {
"@cashu/cashu-ts": "^0.8.2-rc.7",
"@chakra-ui/anatomy": "^2.2.1",
"@chakra-ui/icons": "^2.1.1",
"@chakra-ui/react": "^2.8.1",
15 changes: 15 additions & 0 deletions src/components/embed-types/cashu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { lazy } from "react";
import { EmbedableContent, embedJSX } from "../../helpers/embeds";
import { getMatchCashu } from "../../helpers/regexp";

const InlineCachuCard = lazy(() => import("../inline-cashu-card"));

export function embedCashuTokens(content: EmbedableContent) {
return embedJSX(content, {
regexp: getMatchCashu(),
render: (match) => {
return <InlineCachuCard token={match[0]} />;
},
name: "emoji",
});
}
1 change: 1 addition & 0 deletions src/components/embed-types/index.ts
Original file line number Diff line number Diff line change
@@ -6,3 +6,4 @@ export * from "./youtube";
export * from "./nostr";
export * from "./emoji";
export * from "./image";
export * from "./cashu";
5 changes: 5 additions & 0 deletions src/components/icons.tsx
Original file line number Diff line number Diff line change
@@ -57,6 +57,8 @@ import UserPlus01 from "./icons/user-plus-01";
import UserX01 from "./icons/user-x-01";
import Plus from "./icons/plus";
import Bookmark from "./icons/bookmark";
import BankNote01 from "./icons/bank-note-01";
import Wallet02 from "./icons/wallet-02";

const defaultProps: IconProps = { boxSize: 4 };

@@ -221,3 +223,6 @@ export const GhostIcon = createIcon({
d: "M12 2C16.9706 2 21 6.02944 21 11V18.5C21 20.433 19.433 22 17.5 22C16.3001 22 15.2413 21.3962 14.6107 20.476C14.0976 21.3857 13.1205 22 12 22C10.8795 22 9.9024 21.3857 9.38728 20.4754C8.75869 21.3962 7.69985 22 6.5 22C4.63144 22 3.10487 20.5357 3.00518 18.692L3 18.5V11C3 6.02944 7.02944 2 12 2ZM12 4C8.21455 4 5.1309 7.00478 5.00406 10.7593L5 11L4.99927 18.4461L5.00226 18.584C5.04504 19.3751 5.70251 20 6.5 20C6.95179 20 7.36652 19.8007 7.64704 19.4648L7.73545 19.3478C8.57033 18.1248 10.3985 18.2016 11.1279 19.4904C11.3053 19.8038 11.6345 20 12 20C12.3651 20 12.6933 19.8044 12.8687 19.4934C13.5692 18.2516 15.2898 18.1317 16.1636 19.2151L16.2606 19.3455C16.5401 19.7534 16.9976 20 17.5 20C18.2797 20 18.9204 19.4051 18.9931 18.6445L19 18.5V11C19 7.13401 15.866 4 12 4ZM12 12C13.1046 12 14 13.1193 14 14.5C14 15.8807 13.1046 17 12 17C10.8954 17 10 15.8807 10 14.5C10 13.1193 10.8954 12 12 12ZM9.5 8C10.3284 8 11 8.67157 11 9.5C11 10.3284 10.3284 11 9.5 11C8.67157 11 8 10.3284 8 9.5C8 8.67157 8.67157 8 9.5 8ZM14.5 8C15.3284 8 16 8.67157 16 9.5C16 10.3284 15.3284 11 14.5 11C13.6716 11 13 10.3284 13 9.5C13 8.67157 13.6716 8 14.5 8Z",
defaultProps,
});

export const ECashIcon = BankNote01;
export const WalletIcon = Wallet02
62 changes: 62 additions & 0 deletions src/components/inline-cashu-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { useEffect, useState } from "react";
import { Box, Button, ButtonGroup, Card, Heading, IconButton, Link } from "@chakra-ui/react";
import { getDecodedToken, Token } from "@cashu/cashu-ts";

import { CopyIconButton } from "./copy-icon-button";
import { useUserMetadata } from "../hooks/use-user-metadata";
import { useCurrentAccount } from "../hooks/use-current-account";
import { ECashIcon, WalletIcon } from "./icons";

function RedeemButton({ token }: { token: string }) {
const account = useCurrentAccount()!;
const metadata = useUserMetadata(account.pubkey);

const lnurl = metadata?.lud16 ?? "";
const url = `https://redeem.cashu.me?token=${encodeURIComponent(token)}&lightning=${encodeURIComponent(
lnurl,
)}&autopay=yes`;
return (
<Button as={Link} href={url} isExternal colorScheme="primary">
Redeem
</Button>
);
}

export default function InlineCachuCard({ token }: { token: string }) {
const account = useCurrentAccount();

const [cashu, setCashu] = useState<Token>();

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 = cashu?.token[0].proofs.reduce((acc, v) => acc + v.amount, 0);
return (
<Card p="4" flexDirection="row" borderColor="green.500" alignItems="center" gap="4">
<ECashIcon boxSize={10} color="green.500" />
<Box>
<Heading size="md">{amount} Cashu sats</Heading>
{cashu && <small>Mint: {new URL(cashu.token[0].mint).hostname}</small>}
</Box>
{cashu.memo && <Box>Memo: {cashu.memo}</Box>}
<ButtonGroup ml="auto">
<CopyIconButton text={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>
</Card>
);
}
4 changes: 4 additions & 0 deletions src/components/note/note-contents.tsx
Original file line number Diff line number Diff line change
@@ -21,6 +21,7 @@ import {
embedImageGallery,
renderGenericUrl,
renderSongDotLinkUrl,
embedCashuTokens,
} from "../embed-types";
import { LightboxProvider } from "../lightbox-provider";
import { renderRedditUrl } from "../embed-types/reddit";
@@ -49,6 +50,9 @@ function buildContents(event: NostrEvent | DraftNostrEvent, simpleLinks = false)
// bitcoin
content = embedLightningInvoice(content);

// cashu
content = embedCashuTokens(content);

// nostr
content = embedNostrLinks(content);
content = embedNostrMentions(content, event);
1 change: 1 addition & 0 deletions src/helpers/regexp.ts
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}]+)/gu;
export const getMatchLink = () =>
/https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{Letter}\p{Number}&\.-\/\?=#\-@%\+_,:!]*)/gu;
export const getMatchEmoji = () => /:([a-zA-Z0-9_-]+):/gi;
export const getMatchCashu = () => /cashuA[A-z0-9]+/g;

// read more https://www.regular-expressions.info/unicode.html#category
export function stripInvisibleChar(str?: string) {
13 changes: 11 additions & 2 deletions src/views/messages/message.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import { useRef } from "react";
import { Box, Card, CardBody, CardHeader, CardProps, Flex, Heading, Text } from "@chakra-ui/react";
import { Box, CardProps, Flex } from "@chakra-ui/react";

import { useCurrentAccount } from "../../hooks/use-current-account";
import { getMessageRecipient } from "../../services/direct-messages";
import { NostrEvent } from "../../types/nostr-event";
import DecryptPlaceholder from "./decrypt-placeholder";
import { EmbedableContent, embedUrls } from "../../helpers/embeds";
import { embedNostrLinks, renderGenericUrl, renderImageUrl, renderVideoUrl } from "../../components/embed-types";
import {
embedCashuTokens,
embedNostrLinks,
renderGenericUrl,
renderImageUrl,
renderVideoUrl,
} from "../../components/embed-types";
import { useRegisterIntersectionEntity } from "../../providers/intersection-observer";
import { UserAvatar } from "../../components/user-avatar";
import { UserLink } from "../../components/user-link";
@@ -19,6 +25,9 @@ export function MessageContent({ event, text }: { event: NostrEvent; text: strin
content = embedNostrLinks(content);
content = embedUrls(content, [renderImageUrl, renderVideoUrl, renderGenericUrl]);

// cashu
content = embedCashuTokens(content);

return <Box whiteSpace="pre-wrap">{content}</Box>;
}

62 changes: 58 additions & 4 deletions yarn.lock
Original file line number Diff line number Diff line change
@@ -975,6 +975,17 @@
"@babel/helper-validator-identifier" "^7.22.19"
to-fast-properties "^2.0.0"

"@cashu/cashu-ts@^0.8.2-rc.7":
version "0.8.2-rc.7"
resolved "https://registry.yarnpkg.com/@cashu/cashu-ts/-/cashu-ts-0.8.2-rc.7.tgz#810109fc5aca8f210cb6865de1b3ab7e245e0771"
integrity sha512-Csvs8BQGdCAqMuoDPYVMAH1h1Z+3qXZfc8V2bxIaMaFWq4O9y/kMIx6uvB1D82+hrjHywpVihMPp9TYCCs3Vjw==
dependencies:
"@gandlaf21/bolt11-decode" "^3.0.6"
"@noble/curves" "^1.0.0"
"@scure/bip32" "^1.3.2"
"@scure/bip39" "^1.2.1"
buffer "^6.0.3"

"@chakra-ui/[email protected]":
version "2.3.1"
resolved "https://registry.yarnpkg.com/@chakra-ui/accordion/-/accordion-2.3.1.tgz#a326509e286a5c4e8478de9bc2b4b05017039e6b"
@@ -2254,6 +2265,15 @@
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz#786c5f41f043b07afb1af37683d7c33668858f6d"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==

"@gandlaf21/bolt11-decode@^3.0.6":
version "3.0.6"
resolved "https://registry.yarnpkg.com/@gandlaf21/bolt11-decode/-/bolt11-decode-3.0.6.tgz#ab514fdcee596ccffbfef7b33bc9cc93afe386ee"
integrity sha512-KUcAK2b9or8J47hzNTM2A+xdU0jCGIL4oC4TDyUlRYMfS5dBVOh4ywg9r3TZD8C/eVx7r14Hp4F79CSDjyCWTQ==
dependencies:
bech32 "^1.1.2"
bn.js "^4.11.8"
buffer "^6.0.3"

"@getalby/bitcoin-connect-react@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@getalby/bitcoin-connect-react/-/bitcoin-connect-react-1.1.0.tgz#d6b97b793b5ae128783e1adafaf7a87f983a6d78"
@@ -2365,12 +2385,19 @@
dependencies:
"@noble/hashes" "1.3.1"

"@noble/curves@^1.0.0", "@noble/curves@~1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35"
integrity sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==
dependencies:
"@noble/hashes" "1.3.2"

"@noble/[email protected]":
version "1.3.1"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.1.tgz#8831ef002114670c603c458ab8b11328406953a9"
integrity sha512-EbqwksQwz9xDRGfDST86whPBgM65E0OH/pCgqW0GBVzO22bNE+NuIbeTb714+IfSjU3aRk47EUvXIb5bTsenKA==

"@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1":
"@noble/hashes@1.3.2", "@noble/hashes@~1.3.0", "@noble/hashes@~1.3.1", "@noble/hashes@~1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39"
integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==
@@ -2463,7 +2490,7 @@
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==

"@scure/base@~1.1.0":
"@scure/base@~1.1.0", "@scure/base@~1.1.2":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.3.tgz#8584115565228290a6c6c4961973e0903bb3df2f"
integrity sha512-/+SgoRjLq7Xlf0CWuLHq2LUZeL/w65kfzAPG5NH9pcmBhs+nunQTn4gvdwgMTIXnt9b2C/1SeL2XiysZEyIC9Q==
@@ -2477,7 +2504,16 @@
"@noble/hashes" "~1.3.1"
"@scure/base" "~1.1.0"

"@scure/[email protected]":
"@scure/bip32@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8"
integrity sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==
dependencies:
"@noble/curves" "~1.2.0"
"@noble/hashes" "~1.3.2"
"@scure/base" "~1.1.2"

"@scure/[email protected]", "@scure/bip39@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a"
integrity sha512-Z3/Fsz1yr904dduJD0NpiyRHhRYHdcnyh73FZWiV+/qhWi83wNJ3NWolYqCEN+ZWsUz2TWwajJggcRE9r1zUYg==
@@ -3027,6 +3063,11 @@ bcrypt-pbkdf@^1.0.0:
dependencies:
tweetnacl "^0.14.3"

bech32@^1.1.2:
version "1.1.4"
resolved "https://registry.yarnpkg.com/bech32/-/bech32-1.1.4.tgz#e38c9f37bf179b8eb16ae3a772b40c356d4832e9"
integrity sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==

bech32@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/bech32/-/bech32-2.0.0.tgz#078d3686535075c8c79709f054b1b226a133b355"
@@ -3054,6 +3095,11 @@ bluebird@^3.7.2:
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f"
integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==

bn.js@^4.11.8:
version "4.12.0"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.12.0.tgz#775b3f278efbb9718eec7361f483fb36fbbfea88"
integrity sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==

boolbase@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
@@ -3116,6 +3162,14 @@ buffer@^5.6.0:
base64-js "^1.3.1"
ieee754 "^1.1.13"

buffer@^6.0.3:
version "6.0.3"
resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6"
integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
dependencies:
base64-js "^1.3.1"
ieee754 "^1.2.1"

builtin-modules@^3.1.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6"
@@ -4605,7 +4659,7 @@ identicon.js@^2.3.3:
resolved "https://registry.yarnpkg.com/identicon.js/-/identicon.js-2.3.3.tgz#c505b8d60ecc6ea13bbd991a33964c44c1ad60a1"
integrity sha512-/qgOkXKZ7YbeCYbawJ9uQQ3XJ3uBg9VDpvHjabCAPp6aRMhjLaFAxG90+1TxzrhKaj6AYpVGrx6UXQfQA41UEA==

ieee754@^1.1.13:
ieee754@^1.1.13, ieee754@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352"
integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==

0 comments on commit f701942

Please sign in to comment.