mirror of
https://github.com/hzrd149/nostrudel.git
synced 2025-04-11 05:09:36 +02:00
check cashu tokens
This commit is contained in:
parent
1469b7dd05
commit
7d3c4ea3b0
@ -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",
|
||||
|
@ -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>}
|
||||
|
@ -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>
|
||||
)}
|
||||
|
@ -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();
|
||||
});
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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
127
src/services/cashu-mints.ts
Normal 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)!;
|
||||
}
|
@ -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(
|
||||
|
10
yarn.lock
10
yarn.lock
@ -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==
|
||||
|
Loading…
x
Reference in New Issue
Block a user