${keyed(
this.currentRoute.params,
diff --git a/src/style.css b/src/style.css
index bb02569..a6a8efb 100644
--- a/src/style.css
+++ b/src/style.css
@@ -43,8 +43,6 @@
--border: 2px;
--depth: 1;
--noise: 1;
-
- --font-mono: 'SF Mono', 'Monaco', 'Inconsolata', 'Fira Mono', 'Droid Sans Mono', 'Source Code Pro', monospace;
}
body.dark {
diff --git a/src/wallet.ts b/src/wallet.ts
deleted file mode 100644
index 3d1fc09..0000000
--- a/src/wallet.ts
+++ /dev/null
@@ -1,306 +0,0 @@
-import { CashuMint, CashuWallet, type Proof, type SendOptions } from '@cashu/cashu-ts';
-import { bytesToHex } from '@noble/ciphers/utils';
-import { secp256k1 } from '@noble/curves/secp256k1';
-import { NDKEvent, type NDKEventId, NDKKind, type NDKTag, type NDKUser } from '@nostr-dev-kit/ndk';
-import { getSigner, ndk } from './ndk';
-
-import { State, property } from '@lit-app/state';
-
-export type WalletHistory = {
- id: string;
- amount: number;
- created_at: number;
- direction: 'in' | 'out';
- tokenEventId: NDKEventId;
- nutZapEventId?: NDKEventId;
- senderPubkey?: string;
- recipientPubkey: string;
-};
-
-class Wallet extends State {
- static mintUrl = 'https://testnut.cashu.space'; // TODO: this should be user controlled
-
- private walletEvent: NDKEvent | null = null;
- private cashuWallet: CashuWallet = new CashuWallet(new CashuMint(Wallet.mintUrl));
-
- private proofs: Proof[] = [];
- private eventIdToProofIdMap: Map = new Map();
-
- private isHandlingZapEvent = false;
- private zapEventsQueue: NDKEvent[] = [];
- private latestHistoryTimestamp = 0;
-
- @property() public balance = 0;
- @property() public history: WalletHistory[] = [];
- @property() public sortedHistory: WalletHistory[] = [];
-
- async loadWallet() {
- if (this.walletEvent) return;
- await getSigner();
- const user = await ndk.signer!.user()!;
- const privateWalletEvent = await ndk.fetchEvent({
- kinds: [17375 as NDKKind],
- authors: [user.pubkey],
- });
- if (!privateWalletEvent) {
- const walletPrivateKey = secp256k1.utils.randomPrivateKey();
- const walletPublicKey = secp256k1.getPublicKey(walletPrivateKey);
- const newPrivateWalletEvent = new NDKEvent(ndk);
- newPrivateWalletEvent.kind = NDKKind.CashuWallet;
- newPrivateWalletEvent.content = await ndk.signer!.encrypt(
- user,
- JSON.stringify([
- ['privkey', bytesToHex(walletPrivateKey)],
- ['mint', Wallet.mintUrl],
- ]),
- 'nip44',
- );
- const publicWalletEvent = new NDKEvent(ndk);
- publicWalletEvent.kind = NDKKind.CashuMintList;
- publicWalletEvent.content = '';
- publicWalletEvent.tags = [
- ['mint', Wallet.mintUrl, 'sat'],
- ['pubkey', bytesToHex(walletPublicKey)],
- ];
- await newPrivateWalletEvent.sign();
- await newPrivateWalletEvent.publish();
- await publicWalletEvent.sign();
- await publicWalletEvent.publish();
- this.walletEvent = newPrivateWalletEvent;
- } else this.walletEvent = privateWalletEvent;
-
- const walletHistory = await ndk.fetchEvents({
- kinds: [NDKKind.CashuWalletTx],
- authors: [user.pubkey],
- since: this.walletEvent?.created_at,
- });
-
- for (const event of walletHistory) {
- await this.processHistoryEvent(event);
- }
-
- const proofsEvents = await ndk.fetchEvents({
- kinds: [NDKKind.CashuToken],
- authors: [user.pubkey],
- });
-
- for (const proofsEvent of proofsEvents) {
- const {
- mint,
- proofs = [],
- del = [],
- } = JSON.parse(await ndk.signer!.decrypt(user, proofsEvent.content, 'nip44')) as {
- mint: string;
- proofs: Proof[];
- del?: NDKEventId[];
- };
-
- if (mint !== this.cashuWallet.mint.mintUrl) continue;
-
- for (const eventId of del) {
- const toDeletes = this.eventIdToProofIdMap.get(eventId);
- if (toDeletes)
- for (const proof of toDeletes)
- this.proofs = this.proofs.filter((p) => p.secret !== proof[0] || p.C !== proof[1]);
- this.eventIdToProofIdMap.delete(eventId);
- }
-
- for (const proof of proofs) this.proofs.push(proof);
-
- this.eventIdToProofIdMap.set(
- proofsEvent.id,
- proofs.map((p) => [p.secret, p.C]),
- );
- }
-
- ndk
- .subscribe({
- kinds: [NDKKind.Nutzap],
- '#p': [user.pubkey],
- since: this.latestHistoryTimestamp,
- })
- .on('event', async (event) => {
- this.zapEventsQueue.push(event);
- this.handleZapEvent();
- });
-
- ndk
- .subscribe({
- kinds: [NDKKind.CashuWalletTx],
- authors: [user.pubkey],
- since: this.latestHistoryTimestamp,
- })
- .on('event', async (event) => {
- await this.processHistoryEvent(event);
- });
-
- this.recalculateBalance();
- }
-
- private async processHistoryEvent(event: NDKEvent) {
- const user = await ndk.signer!.user()!;
- const parsedContent = JSON.parse(await ndk.signer!.decrypt(user, event.content, 'nip44'));
- const direction = parsedContent.find((p: [string, string]) => p[0] === 'direction')?.[1];
- const amount = parsedContent.find((p: [string, string]) => p[0] === 'amount')?.[1];
- const tokenEventId = parsedContent.find((p: [string, string]) => p[0] === 'e')?.[1];
- const nutZapEventId = event.tags.find((t: NDKTag) => t[0] === 'e')?.[1];
- const senderPubkey = event.tags.find((t: NDKTag) => t[0] === 'p')?.[1];
- if (!nutZapEventId || !senderPubkey) return;
- this.history.push({
- id: event.id,
- amount: Number.parseInt(amount),
- created_at: event.created_at ?? 0,
- direction: direction as 'in' | 'out',
- tokenEventId,
- nutZapEventId: nutZapEventId as NDKEventId,
- senderPubkey,
- recipientPubkey: event.pubkey,
- });
- this.latestHistoryTimestamp = Math.max(this.latestHistoryTimestamp, event.created_at ?? 0);
- this.sortedHistory = [...this.history.sort((a, b) => b.created_at - a.created_at)];
- }
-
- private async handleZapEvent() {
- if (this.isHandlingZapEvent) return;
- const event = this.zapEventsQueue.shift();
- if (!event) return;
- this.isHandlingZapEvent = true;
- const proofs = JSON.parse(event.tags.find((t) => t[0] === 'proof')?.[1] ?? '[]') as Proof[];
- this.proofs.push(...proofs);
-
- const newProofsEvent = await this.createProofsEvent();
- await this.createHistoryEvent(
- newProofsEvent,
- this.proofs.reduce((acc, curr) => acc + curr.amount, 0),
- 'in',
- );
- this.recalculateBalance();
- this.isHandlingZapEvent = false;
- }
-
- private async createProofsEvent(): Promise {
- const newProofsEvent = new NDKEvent(ndk);
- newProofsEvent.kind = NDKKind.CashuToken;
- newProofsEvent.content = await ndk.signer?.encrypt(
- await ndk.signer!.user()!,
- JSON.stringify({
- mint: this.cashuWallet.mint.mintUrl,
- proofs: this.proofs,
- del: Array.from(this.eventIdToProofIdMap.keys()),
- }),
- 'nip44',
- )!;
- await newProofsEvent.sign();
- await newProofsEvent.publish();
- return newProofsEvent;
- }
-
- recalculateBalance() {
- this.proofs = this.proofs.filter(
- (p, index, self) => index === self.findIndex((t) => t.secret === p.secret && t.C === p.C),
- );
- this.balance = this.proofs.reduce((acc, curr) => acc + curr.amount, 0);
- }
-
- private async createHistoryEvent(newProofsEvent: NDKEvent, amount: number, direction: 'in' | 'out') {
- const historyEvent = new NDKEvent(ndk);
- historyEvent.kind = NDKKind.CashuWalletTx;
- historyEvent.content = await ndk.signer?.encrypt(
- await ndk.signer!.user()!,
- JSON.stringify([
- ['direction', direction],
- ['amount', amount.toString()],
- ['e', newProofsEvent.id],
- ]),
- 'nip44',
- )!;
- historyEvent.tags = [
- ['e', newProofsEvent.id],
- ['p', newProofsEvent.pubkey],
- ];
- await historyEvent.sign();
- await historyEvent.publish();
- }
-
- /** @throws Error if token is invalid */
- async receiveToken(token: string) {
- const proofs = await this.cashuWallet.receive(token);
- if (proofs.length === 0) throw new Error('Invalid token');
- for (const proof of proofs) this.proofs.push(proof);
- const newProofsEvent = await this.createProofsEvent();
- const amount = proofs.reduce((acc, curr) => acc + curr.amount, 0);
- await this.createHistoryEvent(newProofsEvent, amount, 'in');
- this.recalculateBalance();
- }
-
- private async createToken(amount: number, recipient?: NDKUser) {
- if (amount < 10) throw new Error('Amount must be greater than 10');
- const sendOptions: SendOptions = {
- includeFees: true,
- };
- if (recipient) {
- const mintListEvent = await ndk.fetchEvent({
- kinds: [NDKKind.CashuMintList],
- authors: [recipient.pubkey],
- });
- if (!mintListEvent) throw new Error('Recipient has no mint list');
- const pubkey = mintListEvent.tags.find((t) => t[0] === 'pubkey')?.[1];
- if (!pubkey) throw new Error('Recipient has no mint list');
- sendOptions.pubkey = pubkey;
- }
- await this.cashuWallet.getKeySets();
- const sendResult = await this.cashuWallet.send(amount, this.proofs, sendOptions);
- this.proofs = sendResult.keep;
- const proofsEvent = new NDKEvent(ndk);
- proofsEvent.kind = NDKKind.CashuToken;
- proofsEvent.content = await ndk.signer?.encrypt(
- await ndk.signer!.user()!,
- JSON.stringify({
- mint: this.cashuWallet.mint.mintUrl,
- proofs: this.proofs,
- del: Array.from(this.eventIdToProofIdMap.keys()),
- }),
- 'nip44',
- )!;
- await proofsEvent.sign();
- await proofsEvent.publish();
- this.recalculateBalance();
- return sendResult.send;
- }
-
- async nutZap(amount: number, recipient: NDKUser, eventId?: NDKEventId, message?: string) {
- const token = await this.createToken(amount, recipient);
- const zapEvent = new NDKEvent(ndk);
- zapEvent.kind = NDKKind.Nutzap;
- zapEvent.content = message ?? '';
- zapEvent.tags = [
- ['proof', JSON.stringify(token)],
- ['u', Wallet.mintUrl],
- ['p', recipient.pubkey],
- ];
- if (eventId) zapEvent.tags.push(['e', eventId]);
- await zapEvent.sign();
- await zapEvent.publish();
-
- const spendingHistoryEvent = new NDKEvent(ndk);
- spendingHistoryEvent.kind = NDKKind.CashuWalletTx;
- const historyContent = [
- ['direction', 'out'],
- ['amount', amount.toString()],
- ];
- if (eventId) historyContent.push(['e', eventId]);
- spendingHistoryEvent.content = await ndk.signer?.encrypt(
- await ndk.signer!.user()!,
- JSON.stringify(historyContent),
- 'nip44',
- )!;
- spendingHistoryEvent.tags = [
- ['e', zapEvent.id],
- ['p', recipient.pubkey],
- ];
- await spendingHistoryEvent.sign();
- await spendingHistoryEvent.publish();
- }
-}
-
-export const wallet = new Wallet();