From d3b2589a40d6f5ab2e629c456afa7d940468f2b7 Mon Sep 17 00:00:00 2001 From: pablof7z Date: Sun, 6 Oct 2024 13:35:36 +0200 Subject: [PATCH] add wallet change events --- ndk-wallet/CHANGELOG.md | 6 +++ ndk-wallet/package.json | 2 +- ndk-wallet/src/cashu/deposit.ts | 10 +++++ ndk-wallet/src/cashu/history.ts | 77 ++++++++++++++++++++++++++++++++- ndk-wallet/src/cashu/pay/ln.ts | 35 ++++++++++++--- ndk-wallet/src/cashu/proofs.ts | 39 ++++++++++------- ndk-wallet/src/cashu/wallet.ts | 17 +++++++- ndk-wallet/src/index.ts | 1 + ndk-wallet/src/wallet/index.ts | 2 +- 9 files changed, 165 insertions(+), 24 deletions(-) diff --git a/ndk-wallet/CHANGELOG.md b/ndk-wallet/CHANGELOG.md index 17c79886..13b2e963 100644 --- a/ndk-wallet/CHANGELOG.md +++ b/ndk-wallet/CHANGELOG.md @@ -1,5 +1,11 @@ # @nostr-dev-kit/ndk-cache-redis +## 0.3.1 + +### Patch Changes + +- publish wallet change events + ## 0.3.0 ### Minor Changes diff --git a/ndk-wallet/package.json b/ndk-wallet/package.json index fdcee39d..4678d293 100644 --- a/ndk-wallet/package.json +++ b/ndk-wallet/package.json @@ -1,6 +1,6 @@ { "name": "@nostr-dev-kit/ndk-wallet", - "version": "0.3.0", + "version": "0.3.1", "description": "NDK Wallet", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/ndk-wallet/src/cashu/deposit.ts b/ndk-wallet/src/cashu/deposit.ts index 056d105f..fc50ea02 100644 --- a/ndk-wallet/src/cashu/deposit.ts +++ b/ndk-wallet/src/cashu/deposit.ts @@ -6,6 +6,7 @@ import { NDKCashuToken } from "./token"; import createDebug from "debug"; import { NDKEvent, NDKKind, NDKTag, NostrEvent } from "@nostr-dev-kit/ndk"; import { getBolt11ExpiresAt } from "../lib/ln"; +import { NDKWalletChange } from "./history"; const d = createDebug("ndk-wallet:cashu:deposit"); @@ -136,6 +137,15 @@ export class NDKCashuDeposit extends EventEmitter<{ await tokenEvent.publish(this.wallet.relaySet); + const historyEvent = new NDKWalletChange(this.wallet.event.ndk); + historyEvent.direction = 'in'; + historyEvent.amount = tokenEvent.amount; + historyEvent.unit = this.unit; + historyEvent.createdTokens = [ tokenEvent ]; + historyEvent.description = "Deposit"; + historyEvent.mint = this.mint; + historyEvent.publish(this.wallet.relaySet); + this.emit("success", tokenEvent); } catch (e: any) { console.log("relayset", this.wallet.relaySet); diff --git a/ndk-wallet/src/cashu/history.ts b/ndk-wallet/src/cashu/history.ts index e6d82fad..90cc28f7 100644 --- a/ndk-wallet/src/cashu/history.ts +++ b/ndk-wallet/src/cashu/history.ts @@ -2,6 +2,7 @@ import type { NDKTag, NostrEvent } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import createDebug from "debug"; +import { NDKCashuToken } from "./token"; const d = createDebug("ndk-wallet:wallet-change"); @@ -11,6 +12,8 @@ const MARKERS = { DESTROYED: "destroyed", }; +export type DIRECTIONS = 'in' | 'out'; + /** * This class represents a balance change in the wallet, whether money being added or removed. */ @@ -45,6 +48,78 @@ export class NDKWalletChange extends NDKEvent { return walletChange; } + set direction(direction: DIRECTIONS | undefined) { + this.removeTag('direction') + if (direction) this.tags.push(['direction', direction]) + } + + get direction(): DIRECTIONS | undefined { + return this.tagValue('direction') as DIRECTIONS | undefined; + } + + set amount(amount: number) { + this.removeTag('amount') + this.tags.push(['amount', amount.toString()]) + } + + get amount(): number | undefined { + return this.tagValue('amount') as number | undefined; + } + + set fee(fee: number) { + this.removeTag('fee') + this.tags.push(['fee', fee.toString()]) + } + + get fee(): number | undefined { + return this.tagValue('fee') as number | undefined; + } + + set unit(unit: string | undefined) { + this.removeTag('unit') + if (unit) this.tags.push(['unit', unit.toString()]) + } + + get unit(): string | undefined { + return this.tagValue('unit'); + } + + set description(description: string | undefined) { + this.removeTag('description') + if (description) this.tags.push(['description', description.toString()]) + } + + get description(): string | undefined { + return this.tagValue('description'); + } + + set mint(mint: string | undefined) { + this.removeTag('mint') + if (mint) this.tags.push(['mint', mint.toString()]) + } + + get mint(): string | undefined { + return this.tagValue('mint'); + } + + /** + * Tags tokens that were created in this history event + */ + set destroyedTokens(events: NDKCashuToken[]) { + for (const event of events) { + this.tag(event, MARKERS.DESTROYED) + } + } + + /** + * Tags tokens that were created in this history event + */ + set createdTokens(events: NDKCashuToken[]) { + for (const event of events) { + this.tag(event, MARKERS.CREATED) + } + } + public addRedeemedNutzap(event: NDKEvent) { this.tag(event, MARKERS.REDEEMED); } @@ -66,7 +141,7 @@ export class NDKWalletChange extends NDKEvent { const user = await this.ndk!.signer!.user(); - await this.encrypt(user); + await this.encrypt(user, undefined, "nip44"); return super.toNostrEvent(pubkey) as unknown as NostrEvent; } diff --git a/ndk-wallet/src/cashu/pay/ln.ts b/ndk-wallet/src/cashu/pay/ln.ts index 7c5c8107..da376eef 100644 --- a/ndk-wallet/src/cashu/pay/ln.ts +++ b/ndk-wallet/src/cashu/pay/ln.ts @@ -1,10 +1,11 @@ -import { CashuWallet, CashuMint } from "@cashu/cashu-ts"; +import { CashuWallet, CashuMint, Proof } from "@cashu/cashu-ts"; import type { LnPaymentInfo } from "@nostr-dev-kit/ndk"; import type { NDKCashuPay } from "../pay"; import type { TokenSelection } from "../proofs"; import { rollOverProofs, chooseProofsForPr } from "../proofs"; import type { MintUrl } from "../mint/utils"; import { NDKCashuWallet } from "../wallet"; +import { NDKWalletChange } from "../history"; export async function payLn(this: NDKCashuPay, useMint?: MintUrl): Promise { const mintBalances = this.wallet.mintBalances; @@ -148,10 +149,13 @@ async function executePayment( debug: NDKCashuPay["debug"] ): Promise { const _wallet = new CashuWallet(new CashuMint(selection.mint)); + + if (!selection.quote) throw new Error("No quote provided") + debug( "Attempting LN payment for %d sats (%d in fees) with proofs %o, %s", - selection.quote!.amount, - selection.quote!.fee_reserve, + selection.quote.amount, + selection.quote.fee_reserve, selection.usedProofs, pr ); @@ -160,16 +164,37 @@ async function executePayment( const result = await _wallet.payLnInvoice(pr, selection.usedProofs, selection.quote); debug("Payment result: %o", result); + const fee = calculateFee(selection.quote.amount, selection.usedProofs, result.change); + + function calculateFee(sentAmount: number, proofs: Proof[], change: Proof[]) { + let fee = -sentAmount; + for (const proof of proofs) fee += proof.amount; + for (const proof of change) fee -= proof.amount; + return fee; + } + + // generate history event if (result.isPaid && result.preimage) { debug("Payment successful"); - rollOverProofs(selection, result.change, selection.mint, wallet); + const { destroyedTokens, createdToken } = await rollOverProofs(selection, result.change, selection.mint, wallet); + const historyEvent = new NDKWalletChange(wallet.ndk); + historyEvent.destroyedTokens = destroyedTokens; + if (createdToken) historyEvent.createdTokens = [createdToken]; + historyEvent.tag(wallet.event); + historyEvent.direction = 'out'; + historyEvent.description = 'Lightning payment'; + historyEvent.tags.push(['preimage', result.preimage]); + historyEvent.amount = selection.quote.amount; + historyEvent.fee = fee; + historyEvent.publish(wallet.relaySet); + return result.preimage; } } catch (e) { debug("Failed to pay with mint %s", e.message); if (e?.message.match(/already spent/i)) { debug("Proofs already spent, rolling over"); - rollOverProofs(selection, [], selection.mint, wallet); + rollOverProofs(selection, [], selection.mint, wallet, 'out', 'Failed Lightning payment'); } throw e; } diff --git a/ndk-wallet/src/cashu/proofs.ts b/ndk-wallet/src/cashu/proofs.ts index b96cb2d1..e85bff19 100644 --- a/ndk-wallet/src/cashu/proofs.ts +++ b/ndk-wallet/src/cashu/proofs.ts @@ -113,6 +113,11 @@ function chooseProofsForQuote( return { ...res, quote }; } +export type ROLL_OVER_RESULT = { + destroyedTokens: NDKCashuToken[], + createdToken: NDKCashuToken | undefined +}; + /** * Deletes and creates new events to reflect the new state of the proofs */ @@ -120,8 +125,8 @@ export async function rollOverProofs( proofs: TokenSelection, changes: Proof[], mint: string, - wallet: NDKCashuWallet -) { + wallet: NDKCashuWallet, +): Promise { const relaySet = wallet.relaySet; if (proofs.usedTokens.length > 0) { @@ -152,20 +157,24 @@ export async function rollOverProofs( proofsToSave.push(change); } - if (proofsToSave.length === 0) { - d("no new proofs to save"); - return; - } + let createdToken: NDKCashuToken | undefined; + + if (proofsToSave.length > 0) { + createdToken = new NDKCashuToken(wallet.ndk); + createdToken.proofs = proofsToSave; + createdToken.mint = mint; + createdToken.wallet = wallet; + await createdToken.sign(); + d("saving %d new proofs", proofsToSave.length); - const tokenEvent = new NDKCashuToken(wallet.ndk); - tokenEvent.proofs = proofsToSave; - tokenEvent.mint = mint; - tokenEvent.wallet = wallet; - await tokenEvent.sign(); - d("saving %d new proofs", proofsToSave.length); + wallet.addToken(createdToken); - wallet.addToken(tokenEvent); + await createdToken.publish(wallet.relaySet); + d("created new token event", createdToken.rawEvent()); + } - tokenEvent.publish(wallet.relaySet); - d("created new token event", tokenEvent.rawEvent()); + return { + destroyedTokens: proofs.usedTokens, + createdToken, + } } diff --git a/ndk-wallet/src/cashu/wallet.ts b/ndk-wallet/src/cashu/wallet.ts index 8c28745a..e4d39c48 100644 --- a/ndk-wallet/src/cashu/wallet.ts +++ b/ndk-wallet/src/cashu/wallet.ts @@ -51,14 +51,17 @@ export class NDKCashuWallet extends EventEmitter implements NDK constructor(ndk: NDK, event?: NDKEvent) { super(); + if (!ndk) throw new Error("no ndk instance"); this.ndk = ndk; if (!event) { event = new NDKEvent(ndk); event.kind = NDKKind.CashuWallet; event.dTag = Math.random().toString(36).substring(3); + event.tags = []; } - + this.event = event; + this.event.ndk = ndk; } set event(e: NDKEvent) { @@ -223,6 +226,7 @@ export class NDKCashuWallet extends EventEmitter implements NDK * Whether this wallet has been deleted */ get isDeleted(): boolean { + if (!this.event?.tags) return false; return this.event.tags.some((t) => t[0] === "deleted"); } @@ -287,6 +291,17 @@ export class NDKCashuWallet extends EventEmitter implements NDK return deposit; } + public async addHistoryItem( + direction: "in" | "out", + amount: number, + token?: NDKCashuToken + ) { + const historyEvent = new NDKWalletChange(this.event.ndk); + historyEvent.tag(this.event); + await historyEvent.sign(); + historyEvent.publish(this.relaySet); + } + /** * Pay a LN invoice with this wallet */ diff --git a/ndk-wallet/src/index.ts b/ndk-wallet/src/index.ts index fe31328a..df8203de 100644 --- a/ndk-wallet/src/index.ts +++ b/ndk-wallet/src/index.ts @@ -5,6 +5,7 @@ export * from "./wallet/index.js"; export * from "./cashu/wallet.js"; export * from "./cashu/token.js"; export * from "./cashu/deposit.js"; +export * from "./cashu/history.js"; export * from "./cashu/mint/utils"; export * from "./ln/index.js"; diff --git a/ndk-wallet/src/wallet/index.ts b/ndk-wallet/src/wallet/index.ts index fc3bb1d7..ab419c57 100644 --- a/ndk-wallet/src/wallet/index.ts +++ b/ndk-wallet/src/wallet/index.ts @@ -41,7 +41,7 @@ export interface NDKWallet /** * Emitted when a balance is known to have been updated. */ - balance_update: (balance: NDKWalletBalance) => void; + balance_updated: (balance?: NDKWalletBalance) => void; }> { get status(): NDKWalletStatus; get type(): string;