check cashu tokens

This commit is contained in:
hzrd149 2024-01-24 08:29:30 +00:00
parent 1469b7dd05
commit 7d3c4ea3b0
9 changed files with 158 additions and 16 deletions

View File

@ -17,7 +17,7 @@
"postinstall": "patch-package"
},
"dependencies": {
"@cashu/cashu-ts": "^0.8.2-rc.7",
"@cashu/cashu-ts": "^0.9.0",
"@chakra-ui/anatomy": "^2.2.2",
"@chakra-ui/breakpoint-utils": "^2.0.8",
"@chakra-ui/icons": "^2.1.1",
@ -30,6 +30,7 @@
"@getalby/bitcoin-connect": "^3.2.1",
"@getalby/bitcoin-connect-react": "^3.2.1",
"@noble/hashes": "^1.3.2",
"@noble/curves": "^1.3.0",
"@noble/secp256k1": "^1.7.0",
"@react-three/drei": "^9.92.5",
"@react-three/fiber": "^8.15.12",

View File

@ -1,11 +1,13 @@
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 } from "@cashu/cashu-ts";
import { getDecodedToken, Token, CashuMint } 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";
import { getMint } from "../services/cashu-mints";
function RedeemButton({ token }: { token: string }) {
const account = useCurrentAccount()!;
@ -26,6 +28,15 @@ export default function InlineCachuCard({ token, ...props }: Omit<CardProps, "ch
const account = useCurrentAccount();
const [cashu, setCashu] = useState<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;
}
return true;
}, [cashu]);
useEffect(() => {
if (!token.startsWith("cashuA") || token.length < 10) return;
@ -42,7 +53,9 @@ export default function InlineCachuCard({ token, ...props }: Omit<CardProps, "ch
<Card p="4" flexDirection="row" borderColor="green.500" alignItems="center" gap="4" flexWrap="wrap" {...props}>
<ECashIcon boxSize={10} color="green.500" />
<Box>
<Heading size="md">{amount} Cashu sats</Heading>
<Heading size="md" textDecoration={spendable === false ? "line-through" : undefined}>
{amount} Cashu sats{spendable === false ? " (Spent)" : ""}
</Heading>
{cashu && <small>Mint: {new URL(cashu.token[0].mint).hostname}</small>}
</Box>
{cashu.memo && <Box>Memo: {cashu.memo}</Box>}

View File

@ -56,6 +56,7 @@ import { useTextAreaUploadFileWithForm } from "../../hooks/use-textarea-upload-f
import { useThrottle } from "react-use";
import MinePOW from "../mine-pow";
import useAppSettings from "../../hooks/use-app-settings";
import { ErrorBoundary } from "../error-boundary";
type FormValues = {
subject: string;
@ -213,9 +214,11 @@ export default function PostModal({
<Box>
<Heading size="sm">Preview:</Heading>
<Box borderWidth={1} borderRadius="md" p="2">
<TrustProvider trust>
<NoteContents event={previewDraft} />
</TrustProvider>
<ErrorBoundary>
<TrustProvider trust>
<NoteContents event={previewDraft} />
</TrustProvider>
</ErrorBoundary>
</Box>
</Box>
)}

View File

@ -89,7 +89,7 @@ function AddRelayForm() {
const submit = handleSubmit((values) => {
const url = safeRelayUrl(values.url);
if (!url) return;
clientRelaysService.addRelay(url, RelayMode.READ);
clientRelaysService.addRelay(url, RelayMode.ALL);
reset();
});

View File

@ -64,8 +64,6 @@ function GenericNoteTimeline({ timeline }: { timeline: TimelineLoader }) {
setMinDate(getCachedNumber("min") ?? timeline.timeline.value[NOTE_BUFFER]?.created_at ?? Infinity);
}, [timeline, setPinDate, setMinDate, setMaxDate, setLatest, getCachedNumber]);
console.log(minDate, pinDate);
const updateNoteMinHeight = useCallback(
(id: string, element: Element) => {
const rect = element.getBoundingClientRect();

View File

@ -4,7 +4,7 @@ export const getMatchHashtag = () => /(^|[^\p{L}])#([\p{L}\p{N}\p{M}]+)/gu;
export const getMatchLink = () =>
/https?:\/\/([a-zA-Z0-9\.\-]+\.[a-zA-Z]+)([\p{L}\p{N}\p{M}&\.-\/\?=#\-@%\+_,:!~*]*)/gu;
export const getMatchEmoji = () => /:([a-zA-Z0-9_-]+):/gi;
export const getMatchCashu = () => /cashuA[A-z0-9]+/g;
export const getMatchCashu = () => /(cashuA[A-Za-z0-9_-]{0,10000}={0,3})/gi;
// read more https://www.regular-expressions.info/unicode.html#category
export function stripInvisibleChar(str?: string) {

127
src/services/cashu-mints.ts Normal file
View File

@ -0,0 +1,127 @@
import { secp256k1 } from "@noble/curves/secp256k1";
import { ProjPointType } from "@noble/curves/abstract/weierstrass";
import { bytesToHex, randomBytes } from "@noble/hashes/utils";
import {
BlindedMessageData,
CashuMint,
CashuWallet,
MintKeys,
Proof,
SerializedBlindedMessage,
getEncodedToken,
getDecodedToken,
SerializedBlindedSignature,
} from "@cashu/cashu-ts";
import { bytesToNumber, splitAmount } from "@cashu/cashu-ts/dist/lib/es6/utils";
import { hashToCurve, pointFromHex, unblindSignature } from "@cashu/cashu-ts/dist/lib/es6/DHKE";
import { BlindedMessage } from "@cashu/cashu-ts/dist/lib/es6/model/BlindedMessage";
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
/** Copied from @cashu/cashu-ts/src/DHKE and modified to use textDecoder instead of encodeUint8toBase64 */
function blindMessage(secret: Uint8Array, r?: bigint): { B_: ProjPointType<bigint>; r: bigint } {
const secretMessageBase64 = textDecoder.decode(secret); //encodeUint8toBase64(secret);
const secretMessage = new TextEncoder().encode(secretMessageBase64);
const Y = hashToCurve(secretMessage);
if (!r) {
r = bytesToNumber(secp256k1.utils.randomPrivateKey());
}
const rG = secp256k1.ProjectivePoint.BASE.multiply(r);
const B_ = Y.add(rG);
return { B_, r };
}
/** Copied from @cashu/cashu-ts/src/DHKE and modified to use textDecoder instead of encodeUint8toBase64 */
function constructProofs(
promises: Array<SerializedBlindedSignature>,
rs: Array<bigint>,
secrets: Array<Uint8Array>,
keys: MintKeys,
): Array<Proof> {
return promises.map((p: SerializedBlindedSignature, i: number) => {
const C_ = pointFromHex(p.C_);
const A = pointFromHex(keys[p.amount]);
const C = unblindSignature(C_, rs[i], A);
const proof = {
id: p.id,
amount: p.amount,
secret: textDecoder.decode(secrets[i]), // encodeUint8toBase64(secrets[i]),
C: C.toHex(true),
};
return proof;
});
}
class P2PKCashuWallet extends CashuWallet {
p2pkCreateRandomBlindedMessages(amount: number, pubkey: string): BlindedMessageData & { amounts: Array<number> } {
const amounts = splitAmount(amount);
return this.p2pkCreateBlindedMessages(amounts, pubkey);
}
p2pkCreateBlindedMessages(amounts: Array<number>, pubkey: string): BlindedMessageData & { amounts: Array<number> } {
const blindedMessages: Array<SerializedBlindedMessage> = [];
const secrets: Array<Uint8Array> = [];
const rs: Array<bigint> = [];
for (let i = 0; i < amounts.length; i++) {
let deterministicR = undefined;
let secret = undefined;
secret = textEncoder.encode(
JSON.stringify([
"P2PK",
{
nonce: bytesToHex(randomBytes(16)),
data: pubkey,
},
]),
);
secrets.push(secret);
const { B_, r } = blindMessage(secret, deterministicR);
rs.push(r);
const blindedMessage = new BlindedMessage(amounts[i], B_);
blindedMessages.push(blindedMessage.getSerializedBlindedMessage());
}
return { blindedMessages, secrets, rs, amounts };
}
async p2pkRequestTokens(
amount: number,
id: string,
pubkey: string,
): Promise<{ proofs: Array<Proof>; newKeys?: MintKeys }> {
const { blindedMessages, secrets, rs } = this.p2pkCreateRandomBlindedMessages(amount, pubkey);
const payloads = { outputs: blindedMessages };
const { promises } = await this.mint.mint(payloads, id);
return {
proofs: constructProofs(
promises,
rs,
secrets,
//@ts-ignore
await this.getKeys(promises),
),
//@ts-ignore
newKeys: await this.changedKeys(promises),
};
}
}
//@ts-ignore
const wallet = new P2PKCashuWallet(new CashuMint("https://8333.space:3338"));
//@ts-ignore
window.wallet = wallet;
//@ts-ignore
window.getDecodedToken = getDecodedToken;
//@ts-ignore
window.getEncodedToken = getEncodedToken;
const mints = new Map<string, CashuMint>();
export async function getMint(url: string) {
const formatted = new URL(url).toString();
if (!mints.has(formatted)) {
const mint = new CashuMint(formatted);
mints.set(formatted, mint);
}
return mints.get(formatted)!;
}

View File

@ -87,7 +87,7 @@ function AddRelayForm({ onSubmit }: { onSubmit: (url: string) => void }) {
function MailboxesPage() {
const toast = useToast();
const account = useCurrentAccount()!;
const { inbox, outbox, event } = useUserMailboxes(account.pubkey) || {};
const { inbox, outbox, event } = useUserMailboxes(account.pubkey, { alwaysRequest: true }) || {};
const { requestSignature } = useSigningContext();
const addRelay = useCallback(

View File

@ -976,10 +976,10 @@
"@babel/helper-validator-identifier" "^7.22.20"
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==
"@cashu/cashu-ts@^0.9.0":
version "0.9.0"
resolved "https://registry.yarnpkg.com/@cashu/cashu-ts/-/cashu-ts-0.9.0.tgz#425b60282d257a243d5a77e024a1a370dab2bd1d"
integrity sha512-DacSnpv3dJIGzo6A1nHIp2guFuDcmoPB5CX9m0SXA60bQxoIa4srHKLkjxUZ8GFCD9RaI+60UZ2+hZS635Ro2w==
dependencies:
"@gandlaf21/bolt11-decode" "^3.0.6"
"@noble/curves" "^1.0.0"
@ -2382,7 +2382,7 @@
dependencies:
"@noble/hashes" "1.3.2"
"@noble/curves@^1.0.0", "@noble/curves@~1.3.0":
"@noble/curves@^1.0.0", "@noble/curves@^1.3.0", "@noble/curves@~1.3.0":
version "1.3.0"
resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.3.0.tgz#01be46da4fd195822dab821e72f71bf4aeec635e"
integrity sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==