diff --git a/docs/wallet/nutzaps.md b/docs/wallet/nutzaps.md new file mode 100644 index 00000000..57abec1c --- /dev/null +++ b/docs/wallet/nutzaps.md @@ -0,0 +1,13 @@ +# Sweeping NIP-61 nutzaps +When a user receives a nutzap, they should sweep the public tokens into their wallet, the `@nostr-dev-kit/ndk-wallet` package takes care of this for you when +the `NDKWalletService` is running by default. + +```typescript +const walletService = new NDKWalletService(ndk); +walletService.start(); +walletService.on("nutzap", (nutzap: NDKNutzap) => { + console.log("Received a nutzap from " + nutzap.pubkey + " for " + nutzap.amount + " " + nutzap.unit + " on mint " + nutzap.mint); + // -> Received a nutzap from fa98..... for 1 usd on mint https://... +}); +``` + diff --git a/ndk-wallet/src/cashu/decrypt.ts b/ndk-wallet/src/cashu/decrypt.ts new file mode 100644 index 00000000..d8ad7d40 --- /dev/null +++ b/ndk-wallet/src/cashu/decrypt.ts @@ -0,0 +1,18 @@ +import { NDKEvent } from "@nostr-dev-kit/ndk"; +import createDebug from "debug"; + +const debug = createDebug("ndk-wallet:cashu:decrypt"); + +/** + * Decrypts an NDKEvent using nip44, and if that fails, using nip04. + */ +export async function decrypt(event: NDKEvent) { + try { + await event.decrypt(undefined, undefined, "nip44"); + return; + } catch (e) { + debug("unable to decerypt with nip44, attempting with nip04", e); + await event.decrypt(undefined, undefined, "nip04"); + debug("✅ decrypted with nip04"); + } +} diff --git a/ndk-wallet/src/cashu/deposit.ts b/ndk-wallet/src/cashu/deposit.ts index 6ab5ebb0..03c16030 100644 --- a/ndk-wallet/src/cashu/deposit.ts +++ b/ndk-wallet/src/cashu/deposit.ts @@ -4,6 +4,8 @@ import type { NDKCashuWallet } from "./wallet"; import { EventEmitter } from "tseep"; import { NDKCashuToken } from "./token"; import createDebug from "debug"; +import { NDKEvent, NDKKind, NDKTag, NostrEvent } from "@nostr-dev-kit/ndk"; +import { getBolt11ExpiresAt } from "../lib/ln"; const d = createDebug("ndk-wallet:cashu:deposit"); @@ -43,10 +45,46 @@ export class NDKCashuDeposit extends EventEmitter<{ this.quoteId = quote.quote; this.check(); + this.createQuoteEvent(quote.quote, quote.request); return quote.request; } + /** + * This generates a 7374 event containing the quote ID + * with an optional expiration set to the bolt11 expiry (if there is one) + */ + private async createQuoteEvent( + quoteId: string, + bolt11: string + ) { + const { ndk } = this.wallet; + const bolt11Expiry = getBolt11ExpiresAt(bolt11); + let tags: NDKTag[] = [ + ["a", this.wallet.tagId()], + ["mint", this.mint], + ]; + + // if we have a bolt11 expiry, expire this event at that time + if (bolt11Expiry) tags.push(["expiration", bolt11Expiry.toString()]); + + const event = new NDKEvent(ndk, { + kind: NDKKind.CashuQuote, + content: quoteId, + tags + } as NostrEvent); + d("saving quote ID: %o", event.rawEvent()) + await event.encrypt(ndk.activeUser, undefined, "nip44"); + await event.sign(); + try { + await event.publish(this.wallet.relaySet); + d("saved quote on event %s", event.encode()) + } catch (e: any) { + d("error saving quote on event %s", e.relayErrors) + } + return event; + } + private async runCheck() { if (!this.finalized) await this.finalize(); if (!this.finalized) this.delayCheck(); diff --git a/ndk-wallet/src/cashu/token.ts b/ndk-wallet/src/cashu/token.ts index 539aef69..0faaed22 100644 --- a/ndk-wallet/src/cashu/token.ts +++ b/ndk-wallet/src/cashu/token.ts @@ -4,9 +4,11 @@ import type { NDKRelay, NDKRelaySet, NostrEvent } from "@nostr-dev-kit/ndk"; import type NDK from "@nostr-dev-kit/ndk"; import { NDKEvent, NDKKind } from "@nostr-dev-kit/ndk"; import type { NDKCashuWallet } from "./wallet"; +import { decrypt } from "./decrypt"; export function proofsTotalBalance(proofs: Proof[]): number { for (const proof of proofs) { + console.log("proof", proof.secret); if (proof.amount < 0) { throw new Error("proof amount is negative"); } @@ -29,7 +31,7 @@ export class NDKCashuToken extends NDKEvent { token.original = event; try { - await token.decrypt(); + await decrypt(token); } catch { token.content = token.original.content; } @@ -51,7 +53,7 @@ export class NDKCashuToken extends NDKEvent { }); const user = await this.ndk!.signer!.user(); - await this.encrypt(user); + await this.encrypt(user, undefined, "nip44"); return super.toNostrEvent(pubkey); } diff --git a/ndk-wallet/src/cashu/wallet.ts b/ndk-wallet/src/cashu/wallet.ts index 0fee08b7..9e6d202e 100644 --- a/ndk-wallet/src/cashu/wallet.ts +++ b/ndk-wallet/src/cashu/wallet.ts @@ -3,6 +3,7 @@ import type { NDKEventId, NDKPaymentConfirmationCashu, NDKPaymentConfirmationLN, + NDKSubscription, NDKTag, NDKZapDetails} from "@nostr-dev-kit/ndk"; import NDK, { @@ -23,9 +24,13 @@ import { NDKWalletChange } from "./history.js"; import { checkTokenProofs } from "./validate.js"; import { NDKWallet, NDKWalletBalance, NDKWalletEvents, NDKWalletStatus } from "../wallet/index.js"; import { EventEmitter } from "tseep"; +import { decrypt } from "./decrypt.js"; const d = createDebug("ndk-wallet:cashu:wallet"); +/** + * This class tracks state of a NIP-60 wallet + */ export class NDKCashuWallet extends EventEmitter implements NDKWallet { readonly type = 'nip-60'; @@ -34,6 +39,8 @@ export class NDKCashuWallet extends EventEmitter implements NDK private knownTokens: Set = new Set(); private skipPrivateKey: boolean = false; public p2pk: string | undefined; + private sub?: NDKSubscription; + public ndk: NDK; public status: NDKWalletStatus = NDKWalletStatus.INITIAL; @@ -46,8 +53,9 @@ export class NDKCashuWallet extends EventEmitter implements NDK public _event?: NDKEvent; public walletId: string = 'unset'; - constructor(event?: NDKEvent, ndk?: NDK) { + constructor(ndk: NDK, event?: NDKEvent) { super(); + this.ndk = ndk; if (!event) { event = new NDKEvent(ndk); event.kind = NDKKind.CashuWallet; @@ -67,9 +75,7 @@ export class NDKCashuWallet extends EventEmitter implements NDK return this._event; } - tagId(): string { - return this.event.tagId(); - } + tagId() { return this.event.tagId(); } /** * Returns the tokens that are available for spending @@ -92,17 +98,17 @@ export class NDKCashuWallet extends EventEmitter implements NDK public checkProofs = checkTokenProofs.bind(this); static async from(event: NDKEvent): Promise { - const wallet = new NDKCashuWallet(event); + if (!event.ndk) throw new Error("no ndk instance on event"); + const wallet = new NDKCashuWallet(event.ndk, event); if (wallet.isDeleted) return; const prevContent = wallet.event.content; wallet.publicTags = wallet.event.tags; try { - await wallet.event.decrypt(); - + await decrypt(wallet.event); wallet.privateTags = JSON.parse(wallet.event.content); } catch (e) { - d("unable to decrypt wallet", e); + throw e; } wallet.event.content ??= prevContent; @@ -181,6 +187,9 @@ export class NDKCashuWallet extends EventEmitter implements NDK this.setPrivateTag("unit", unit); } + /** + * Returns the p2pk of this wallet + */ async getP2pk(): Promise { if (this.p2pk) return this.p2pk; if (this.privkey) { @@ -191,6 +200,9 @@ export class NDKCashuWallet extends EventEmitter implements NDK } } + /** + * Returns the private key of this wallet + */ get privkey(): string | undefined { const privkey = this.getPrivateTag("privkey"); if (privkey) return privkey; @@ -238,7 +250,7 @@ export class NDKCashuWallet extends EventEmitter implements NDK // encrypt private tags this.event.content = JSON.stringify(this.privateTags); const user = await this.event.ndk!.signer!.user(); - await this.event.encrypt(user); + await this.event.encrypt(user, undefined, "nip44"); } return this.event.publishReplaceable( @@ -252,16 +264,36 @@ export class NDKCashuWallet extends EventEmitter implements NDK return NDKRelaySet.fromRelayUrls(this.relays, this.event.ndk!); } + /** + * Prepares a deposit + * @param amount + * @param mint + * @param unit + * + * @example + * const wallet = new NDKCashuWallet(...); + * const deposit = wallet.deposit(1000, "https://mint.example.com", "sats"); + * deposit.on("success", (token) => { + * console.log("deposit successful", token); + * }); + * deposit.on("error", (error) => { + * console.log("deposit failed", error); + * }); + * + * // start monitoring the deposit + * deposit.start(); + */ public deposit(amount: number, mint?: string, unit?: string): NDKCashuDeposit { const deposit = new NDKCashuDeposit(this, amount, mint, unit); deposit.on("success", (token) => { - this.tokens.push(token); - this.knownTokens.add(token.id); - this.emit("balance_updated"); + this.addToken(token); }); return deposit; } + /** + * Pay a LN invoice with this wallet + */ async lnPay({pr}: {pr:string}, useMint?: MintUrl): Promise { const pay = new NDKCashuPay(this, { pr }); const preimage = await pay.payLn(useMint); @@ -354,6 +386,7 @@ export class NDKCashuWallet extends EventEmitter implements NDK } public addToken(token: NDKCashuToken) { + console.trace("adding token", token.id, token.rawEvent()); if (!this.knownTokens.has(token.id)) { this.knownTokens.add(token.id); this.tokens.push(token); diff --git a/ndk-wallet/src/service/index.ts b/ndk-wallet/src/service/index.ts index 94275421..d3269e99 100644 --- a/ndk-wallet/src/service/index.ts +++ b/ndk-wallet/src/service/index.ts @@ -9,9 +9,15 @@ import type { MintUrl } from "../cashu/mint/utils.js"; import type { NDKCashuToken } from "../cashu/token.js"; import { NDKWebLNWallet } from "../ln/index.js"; import { NDKWallet } from "../wallet/index.js"; +import { NutzapMonitor } from "./nutzap-monitor/index.js"; const d = createDebug("ndk-wallet:wallet"); +/** + * The NDKWalletService provides a shortcut to discover and interact with + * users wallets. + * + */ class NDKWalletService extends EventEmitter<{ /** * New default was has been established @@ -28,7 +34,7 @@ class NDKWalletService extends EventEmitter<{ "wallet:balance": (wallet: NDKWallet) => void; "nutzap:seen": (nutzap: NDKNutzap) => void; - "nutzap:redeemed": (nutzap: NDKNutzap) => void; + "nutzap": (nutzap: NDKNutzap) => void; "nutzap:failed": (nutzap: NDKNutzap) => void; ready: () => void; @@ -36,54 +42,117 @@ class NDKWalletService extends EventEmitter<{ public ndk: NDK; public wallets: NDKWallet[] = []; public state: 'loading' | 'ready' = 'loading'; + public defaultWallet?: NDKWallet; private lifecycle: NDKWalletLifecycle | undefined; + private nutzapMonitor: NutzapMonitor | undefined; constructor(ndk: NDK) { super(); this.ndk = ndk; - this.ndk.walletConfig ??= { onPaymentComplete: () => {}} + this.ndk.walletConfig ??= {}; this.ndk.walletConfig.onCashuPay = this.onCashuPay.bind(this) this.ndk.walletConfig.onLnPay = this.onLnPay.bind(this) } async onCashuPay(payment: NDKZapDetails): Promise { - const wallet = this.wallets[0] - if (!wallet) throw new Error("No wallet available"); + if (!this.defaultWallet) throw new Error("No wallet available"); - return wallet.cashuPay(payment); - - - console.log('attempt to pay with default wallet', payment); - return undefined; + return this.defaultWallet.cashuPay(payment); } async onLnPay(payment: NDKZapDetails): Promise { - console.log('attempt to pay with ln', payment) - return undefined; + if (!this.defaultWallet) throw new Error("No wallet available"); + + return this.defaultWallet.lnPay(payment); } - public createCashuWallet() { - return new NDKCashuWallet(undefined, this.ndk); + private alreadyHasWallet(wallet: NDKWallet): boolean { + if (wallet instanceof NDKCashuWallet) { + return this.wallets.some(w => w instanceof NDKCashuWallet && w.event.id === wallet.event.id); + } + + return false; } /** * Starts monitoring changes for the user's wallets */ public start(user?: NDKUser) { - // try to load a webln wallet - const weblnWallet = new NDKWebLNWallet(); - weblnWallet.on("ready", () => { - this.wallets.push(weblnWallet); - this.emit("wallet", weblnWallet); + // todo: check NIP-78 configuration for webln/nwc/nip-61 settings + this.lifecycle = new NDKWalletLifecycle(this.ndk, user ?? this.ndk.activeUser!); + this.lifecycle.on("mintlist:ready", (mintList: NDKCashuMintList) => { + this.startNutzapMonitor(mintList); + }); + + this.lifecycle.on("wallet:default", (wallet: NDKWallet) => { + d("default wallet ready", wallet.type); + this.defaultWallet = wallet; + this.emit("wallet:default", wallet); + }); + + this.lifecycle.on("wallet", (wallet: NDKWallet) => { + d("wallet ready", wallet.type); + if (this.alreadyHasWallet(wallet)) { + return; + } + + this.wallets.push(wallet); + this.emit("wallets"); + + // if we have a new wallet and the nutzap monitor + // is already running, add the wallet to the monitor + if (this.nutzapMonitor) { + this.nutzapMonitor.addWallet(wallet as NDKCashuWallet); + } + }); + + this.lifecycle.on("ready", () => { + this.state = 'ready'; this.emit("ready"); }); - - this.lifecycle = new NDKWalletLifecycle(this, this.ndk, user ?? this.ndk.activeUser!); + this.lifecycle.start(); } + /** + * Configures NDK to use a webln wallet + */ + public useWebLN() { + const wallet = new NDKWebLNWallet(); + this.ndk.walletConfig ??= {}; + this.ndk.walletConfig.onCashuPay = wallet.cashuPay.bind(wallet); + this.ndk.walletConfig.onLnPay = wallet.lnPay.bind(wallet); + } + + /** + * Starts monitoring for nutzaps + * @param mintList User's mint list (kind:10019) + */ + public startNutzapMonitor( + mintList: NDKCashuMintList + ) { + d("starting nutzap monitor"); + if (this.nutzapMonitor) + throw new Error("Nutzap monitor already started"); + + const relaysSet = mintList.relaySet; + if (!relaysSet) + throw new Error("Mint list has no relay set"); + + this.nutzapMonitor = new NutzapMonitor(this.ndk, this.ndk.activeUser!, relaysSet); + this.wallets. + filter(w => w instanceof NDKCashuWallet) + .forEach(w => this.nutzapMonitor!.addWallet(w)); + + this.nutzapMonitor.on("redeem", (nutzap: NDKNutzap) => { + this.emit("nutzap", nutzap); + }); + + this.nutzapMonitor.start(); + } + /** * Publishes the mint list tying to a specific wallet */ @@ -92,7 +161,7 @@ class NDKWalletService extends EventEmitter<{ mintList.relays = wallet.relays; mintList.mints = wallet.mints; mintList.p2pk = await wallet.getP2pk(); - return mintList.publish(); + return mintList.publishReplaceable(); } async transfer(wallet: NDKCashuWallet, fromMint: MintUrl, toMint: MintUrl) { diff --git a/ndk-wallet/src/service/lifecycle/index.ts b/ndk-wallet/src/service/lifecycle/index.ts index f16ac1ae..38aacc2c 100644 --- a/ndk-wallet/src/service/lifecycle/index.ts +++ b/ndk-wallet/src/service/lifecycle/index.ts @@ -1,6 +1,7 @@ import type { NDKEvent, NDKEventId, + NDKFilter, NDKRelay, NDKSubscription, NDKUser} from "@nostr-dev-kit/ndk"; @@ -12,7 +13,6 @@ import { NDKRelaySet, NDKSubscriptionCacheUsage } from "@nostr-dev-kit/ndk"; -import type NDKWallet from "../index.js"; import handleMintList from "./mint-list.js"; import handleWalletEvent from "./wallet.js"; import handleTokenEvent from "./token.js"; @@ -21,16 +21,19 @@ import type { NDKCashuWallet} from "../../cashu/wallet.js"; import type { NDKCashuToken } from "../../cashu/token.js"; import createDebug from "debug"; import { NDKWalletChange } from "../../cashu/history.js"; -import NutzapHandler from "./nutzap.js"; -import { NDKWalletStatus } from "../../wallet/index.js"; -import NDKWalletService from "../index.js"; +import { EventEmitter } from "tseep"; +import { NDKWallet } from "../../wallet/index.js"; /** * This class is responsible for managing the lifecycle of a user wallets. * It fetches the user wallets, tokens and nutzaps and keeps them up to date. */ -class NDKWalletLifecycle { - public service: NDKWalletService; +class NDKWalletLifecycle extends EventEmitter<{ + "wallet:default": (wallet: NDKWallet) => void; + "mintlist:ready": (mintList: NDKCashuMintList) => void; + "wallet": (wallet: NDKWallet) => void; + "ready": () => void; +}> { private sub: NDKSubscription | undefined; public eosed = false; public ndk: NDK; @@ -46,25 +49,32 @@ class NDKWalletLifecycle { public debug = createDebug("ndk-wallet:lifecycle"); public state: "loading" | "ready" = "loading"; - public nutzap: NutzapHandler; - - constructor(service: NDKWalletService, ndk: NDK, user: NDKUser) { - this.service = service; + constructor(ndk: NDK, user: NDKUser) { + super(); this.ndk = ndk; this.user = user; - this.nutzap = new NutzapHandler(this); } + /** + * Check for wallets + */ async start() { const userRelayList = await getRelayListForUser(this.user.pubkey, this.ndk); + const filters: NDKFilter[] = [ + { + kinds: [NDKKind.CashuMintList, NDKKind.CashuWallet], + authors: [this.user.pubkey], + }, + { kinds: [NDKKind.WalletChange], authors: [this.user!.pubkey], limit: 10 }, + ] + + // if we have a clientName, also get NIP-78 AppSpecificData + if (this.ndk.clientName) { + filters.push({ kinds: [NDKKind.AppSpecificData], authors: [this.user.pubkey], "#d": [this.ndk.clientName] }) + } + this.sub = this.ndk.subscribe( - [ - { - kinds: [NDKKind.CashuMintList, NDKKind.CashuWallet], - authors: [this.user.pubkey], - }, - { kinds: [NDKKind.WalletChange], authors: [this.user!.pubkey], limit: 10 }, - ], + filters, { subId: "ndk-wallet", groupable: false, @@ -94,15 +104,18 @@ class NDKWalletLifecycle { case NDKKind.EventDeletion: handleEventDeletion.bind(this, event).call(this); break; - case NDKKind.Nutzap: - this.nutzap.addNutzap(event); + // case NDKKind.Nutzap: + // this.nutzap.addNutzap(event); + // break; + // case NDKKind.WalletChange: + // NDKWalletChange.from(event).then((wc) => { + // if (wc) { + // this.nutzap.addWalletChange(wc); + // } + // }); + case NDKKind.AppSpecificData: + // handleAppSpecificData.bind(this, event, relay).call(this); break; - case NDKKind.WalletChange: - NDKWalletChange.from(event).then((wc) => { - if (wc) { - this.nutzap.addWalletChange(wc); - } - }); } } @@ -142,8 +155,7 @@ class NDKWalletLifecycle { this.debug("oldest wallet timestamp", oldestWalletTimestamp); } - this.service.wallets.push(wallet) - this.service.emit("wallet", wallet); + this.emit("wallet", wallet); } this.debug("oldest wallet timestamp", oldestWalletTimestamp, this.wallets.values()); @@ -153,14 +165,14 @@ class NDKWalletLifecycle { { kinds: [NDKKind.CashuToken], authors: [this.user!.pubkey] }, { kinds: [NDKKind.EventDeletion], authors: [this.user!.pubkey], limit: 0 }, { kinds: [NDKKind.WalletChange], authors: [this.user!.pubkey] }, - { - kinds: [NDKKind.Nutzap], - "#p": [this.user!.pubkey], - since: oldestWalletTimestamp, - }, + // { + // kinds: [NDKKind.Nutzap], + // "#p": [this.user!.pubkey], + // since: oldestWalletTimestamp, + // }, ], { - subId: "ndk-wallet-tokens2", + subId: "ndk-wallet-tokens", groupable: false, cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY, }, @@ -174,24 +186,17 @@ class NDKWalletLifecycle { private tokensSubEose() { this.state = "ready"; - console.log("EMITTING READY"); this.tokensSubEosed = true; - this.service.emit("ready"); - - this.nutzap.eosed().then(() => { - // once we finish processing nutzaps - // we can update the wallet's cached balance - for (const wallet of this.wallets.values()) { - wallet.status = NDKWalletStatus.READY; - wallet.updateBalance(); - } - }); - } - - // private handleMintList = handleMintList.bind - - public emit(event: string, ...args: any[]) { - this.service.emit(event as unknown as any, ...args); + this.emit("ready"); + + // this.nutzap.eosed().then(() => { + // // once we finish processing nutzaps + // // we can update the wallet's cached balance + // for (const wallet of this.wallets.values()) { + // wallet.status = NDKWalletStatus.READY; + // wallet.updateBalance(); + // } + // }); } // Sets the default wallet as seen by the mint list diff --git a/ndk-wallet/src/service/lifecycle/mint-list.ts b/ndk-wallet/src/service/lifecycle/mint-list.ts index dd699b11..13d4310e 100644 --- a/ndk-wallet/src/service/lifecycle/mint-list.ts +++ b/ndk-wallet/src/service/lifecycle/mint-list.ts @@ -9,6 +9,6 @@ export default function handleMintList(this: NDKWalletLifecycle, event: NDKEvent const prevPubkey = this._mintList?.p2pk; this._mintList = mintList; - if (this.eosed && this._mintList) this.emit("mintlist", this._mintList); + if (this.eosed && this._mintList) this.emit("mintlist:ready", this._mintList); if (this._mintList.p2pk) this.setDefaultWallet(this._mintList.p2pk); } diff --git a/ndk-wallet/src/service/lifecycle/nutzap.ts b/ndk-wallet/src/service/lifecycle/nutzap.ts index 5fe619da..5db7b86f 100644 --- a/ndk-wallet/src/service/lifecycle/nutzap.ts +++ b/ndk-wallet/src/service/lifecycle/nutzap.ts @@ -43,21 +43,6 @@ class NutzapHandler { redeemedIds.forEach((id) => this.knownRedeemedTokens.add(id)); } - async eosed() { - this._eosed = true; - - // start processing queue of nutzaps - await this.processRedeemQueue(); - } - - private pushToRedeemQueue(event: NDKEvent) { - if (this.redeemQueue.has(event.id)) return; - - const nutzap = NDKNutzap.from(event); - if (!nutzap) return; - this.redeemQueue.set(nutzap.id, nutzap); - } - private async processRedeemQueue() { // go through knownRedeemedTokens and remove them from the queue for (const id of this.knownRedeemedTokens) { @@ -102,70 +87,12 @@ class NutzapHandler { } } - private findWalletForNutzap(nutzap: NDKNutzap): NDKCashuWallet | undefined { - const p2pk = nutzap.p2pk; - let wallet: NDKCashuWallet | undefined; + - if (p2pk) wallet = this.lifecycle.walletsByP2pk.get(p2pk); + + - return wallet ?? this.lifecycle.defaultWallet; - } - - private cashuWallets: Map = new Map(); - private cashuWallet(mint: MintUrl, unit: string = "sat"): CashuWallet { - const key = `${mint}:${unit}`; - let wallet = this.cashuWallets.get(key); - if (!wallet) { - wallet = new CashuWallet(new CashuMint(mint), { unit }); - this.cashuWallets.set(key, wallet); - } - - return wallet; - } - - private async redeem(event: NDKEvent) { - if (this.knownRedeemedTokens.has(event.id)) return; - this.knownRedeemedTokens.add(event.id); - - const nutzap = await NDKNutzap.from(event); - if (!nutzap) return; - - try { - const { proofs, mint } = nutzap; - const wallet = this.findWalletForNutzap(nutzap); - if (!wallet) { - const p2pk = nutzap.p2pk; - throw new Error( - "wallet not found for nutzap (p2pk: " + p2pk + ") " + nutzap.content - ); - } - - // we emit a nutzap:seen event only once we know that we have the private key to attempt to redeem it - this.lifecycle.emit("nutzap:seen", nutzap); - - const _wallet = this.cashuWallet(mint); - - try { - const res = await _wallet.receiveTokenEntry( - { proofs, mint }, - { - privkey: wallet.privkey, - } - ); - d("redeemed nutzap %o", nutzap.rawEvent()); - this.lifecycle.emit("nutzap:redeemed", nutzap); - - // save new proofs in wallet - wallet.saveProofs(res, mint, nutzap); - } catch (e: any) { - console.error(e.message); - this.lifecycle.emit("nutzap:failed", nutzap, e.message); - } - } catch (e) { - console.trace(e); - this.lifecycle.emit("nutzap:failed", nutzap, e); - } - } + } export default NutzapHandler; diff --git a/ndk-wallet/src/service/lifecycle/token.ts b/ndk-wallet/src/service/lifecycle/token.ts index a902cddf..eb91476c 100644 --- a/ndk-wallet/src/service/lifecycle/token.ts +++ b/ndk-wallet/src/service/lifecycle/token.ts @@ -21,6 +21,7 @@ async function handleToken(this: NDKWalletLifecycle, event: NDKEvent, relay?: ND this.debug("no wallet found for token %s", token.id); this.orphanedTokens.set(token.id, token); } else { + this.debug("adding token %s to wallet %s", token.id, wallet.name); wallet.addToken(token); } } diff --git a/ndk-wallet/src/service/lifecycle/wallet.ts b/ndk-wallet/src/service/lifecycle/wallet.ts index 3a29d978..44fd8ba5 100644 --- a/ndk-wallet/src/service/lifecycle/wallet.ts +++ b/ndk-wallet/src/service/lifecycle/wallet.ts @@ -5,7 +5,6 @@ import { NDKCashuWallet } from "../../cashu/wallet"; function removeDeletedWallet(this: NDKWalletLifecycle, walletId: string) { this.wallets.delete(walletId); if (this.defaultWallet?.walletId === walletId) this.setDefaultWallet(undefined); - this.emit("wallets"); } const seenWallets: Record = {}; @@ -47,7 +46,7 @@ async function handleWalletEvent(this: NDKWalletLifecycle, event: NDKEvent, rela return; } } else { - this.debug.extend(dTag)("Relay %s sent a new wallet %s", relay?.url, dTag); + if (relay) this.debug.extend(dTag)("Relay %s sent a new wallet %s", relay.url, dTag); seenWallets[dTag] = { events: [event], mostRecentTime: event.created_at!, @@ -86,7 +85,7 @@ async function handleWalletEvent(this: NDKWalletLifecycle, event: NDKEvent, rela } // check if this is the most up to date version of this wallet we have - if (existingEvent && existingEvent.created_at! >= wallet.created_at!) return; + if (existingEvent && existingEvent.event.created_at! >= wallet.event.created_at!) return; this.wallets.set(wallet.walletId, wallet); this.emit("wallet", wallet); diff --git a/ndk-wallet/src/service/nutzap-monitor/index.ts b/ndk-wallet/src/service/nutzap-monitor/index.ts new file mode 100644 index 00000000..e60f2005 --- /dev/null +++ b/ndk-wallet/src/service/nutzap-monitor/index.ts @@ -0,0 +1,181 @@ +import NDK, { NDKEvent, type NDKEventId, NDKKind, NDKNutzap, NDKRelaySet, NDKSubscription, NDKSubscriptionCacheUsage, NDKUser } from "@nostr-dev-kit/ndk"; +import { CashuMint, CashuWallet } from "@cashu/cashu-ts"; +import { EventEmitter } from "tseep"; +import createDebug from "debug"; +import { NDKCashuWallet } from "../../cashu/wallet"; + +const d = createDebug("ndk-wallet:nutzap-monitor"); + +enum PROCESSING_STATUS { + initial = 1, + processing = 2, + processed = 3, + failed = 4, +} + +/** + * This class monitors a user's nutzap inbox relays + * for new nutzaps and processes them. + */ +export class NutzapMonitor extends EventEmitter<{ + /** + * Emitted when a new nutzap is successfully redeemed + */ + redeem: (event: NDKNutzap) => void; + + /** + * Emitted when a nutzap has been seen + */ + seen: (event: NDKNutzap) => void; + + /** + * Emitted when a nutzap has failed to be redeemed + */ + failed: (event: NDKNutzap, error: string) => void; +}> { + private ndk: NDK; + private user: NDKUser; + public relaySet?: NDKRelaySet; + private sub?: NDKSubscription; + private eosed = false; + private redeemQueue = new Map(); + private knownTokens = new Map(); + + /** + * Known wallets. This is necessary to be able to find the private key + * that is needed to redeem the nutzap. + */ + private walletByP2pk = new Map(); + + addWallet(wallet: NDKCashuWallet) { + const p2pk = wallet.p2pk; + if (p2pk) { + d("adding wallet with p2pk %o", p2pk); + this.walletByP2pk.set(p2pk, wallet); + } + } + + constructor( + ndk: NDK, + user: NDKUser, + relaySet?: NDKRelaySet + ) { + super(); + this.ndk = ndk; + this.user = user; + this.relaySet = relaySet; + } + + /** + * Start the monitor. + */ + public start() { + if (!this.relaySet) { + d("no relay set provided"); + throw new Error("no relay set provided"); + } + + this.sub = this.ndk.subscribe( + { kinds: [NDKKind.Nutzap], "#p": [this.user.pubkey] }, + { subId: "ndk-wallet:nutzap-monitor", cacheUsage: NDKSubscriptionCacheUsage.ONLY_RELAY }, + this.relaySet, + false + ); + + this.sub.on("event", this.eventHandler.bind(this)); + this.sub.on("eose", this.eoseHandler.bind(this)); + this.sub.start(); + } + + public stop() { + this.sub?.stop(); + } + + private eoseHandler() { + this.eosed = true; + this.processQueue(); + } + + private async eventHandler(event: NDKEvent) { + if (this.knownTokens.has(event.id)) + return; + this.knownTokens.set(event.id, PROCESSING_STATUS.initial); + const nutzapEvent = await NDKNutzap.from(event); + if (!nutzapEvent) return; + this.emit("seen", nutzapEvent); + + if (!this.eosed) { + this.pushToRedeemQueue(nutzapEvent); + } else { + this.redeem(nutzapEvent); + } + } + + private pushToRedeemQueue(event: NDKEvent) { + if (this.redeemQueue.has(event.id)) return; + + const nutzap = NDKNutzap.from(event); + if (!nutzap) return; + this.redeemQueue.set(nutzap.id, nutzap); + } + + private async redeem(nutzap: NDKNutzap) { + const currentStatus = this.knownTokens.get(nutzap.id); + if (!currentStatus || currentStatus > PROCESSING_STATUS.initial) return; + this.knownTokens.set(nutzap.id, PROCESSING_STATUS.processing); + + try { + const { proofs, mint } = nutzap; + const wallet = this.findWalletForNutzap(nutzap); + if (!wallet) { + const p2pk = nutzap.p2pk; + throw new Error( + "wallet not found for nutzap (p2pk: " + p2pk + ") " + nutzap.content + ); + } + + const _wallet = this.cashuWallet(mint); + + try { + const res = await _wallet.receiveTokenEntry( + { proofs, mint }, + { + privkey: wallet.privkey, + } + ); + d("redeemed nutzap %o", nutzap.rawEvent()); + this.lifecycle.emit("nutzap:redeemed", nutzap); + + // save new proofs in wallet + wallet.saveProofs(res, mint, nutzap); + } catch (e: any) { + console.error(e.message); + this.lifecycle.emit("nutzap:failed", nutzap, e.message); + } + } catch (e) { + console.trace(e); + this.lifecycle.emit("nutzap:failed", nutzap, e); + } + } + + private findWalletForNutzap(nutzap: NDKNutzap): NDKCashuWallet | undefined { + const p2pk = nutzap.p2pk; + let wallet: NDKCashuWallet | undefined; + + if (p2pk) wallet = this.lifecycle.walletsByP2pk.get(p2pk); + + return wallet ?? this.lifecycle.defaultWallet; + } + + private cashuWallets: Map = new Map(); + private cashuWallet(mint: MintUrl, unit: string = "sat"): CashuWallet { + const key = `${mint}:${unit}`; + let wallet = this.cashuWallets.get(key); + if (!wallet) { + wallet = new CashuWallet(new CashuMint(mint), { unit }); + this.cashuWallets.set(key, wallet); + } + + return wallet; + } +} diff --git a/ndk/src/events/kinds/index.ts b/ndk/src/events/kinds/index.ts index a8c88cf7..486b305d 100644 --- a/ndk/src/events/kinds/index.ts +++ b/ndk/src/events/kinds/index.ts @@ -54,6 +54,7 @@ export enum NDKKind { Unsubscribe = 7002, SubscriptionReceipt = 7003, + CashuQuote = 7374, CashuToken = 7375, WalletChange = 7376, diff --git a/ndk/src/ndk/index.ts b/ndk/src/ndk/index.ts index 9fbd3367..6c531297 100644 --- a/ndk/src/ndk/index.ts +++ b/ndk/src/ndk/index.ts @@ -39,7 +39,7 @@ export type NDKValidationRatioFn = ( export interface NDKWalletConfig { onLnPay?: LnPayCb; onCashuPay?: CashuPayCb; - onPaymentComplete: ( + onPaymentComplete?: ( results: Map ) => void; } @@ -209,7 +209,7 @@ export class NDK extends EventEmitter<{ relays: WebSocket["url"][] ) => void; }> { - public explicitRelayUrls?: WebSocket["url"][]; + private _explicitRelayUrls?: WebSocket["url"][]; public pool: NDKPool; public outboxPool?: NDKPool; private _signer?: NDKSigner; @@ -278,7 +278,7 @@ export class NDK extends EventEmitter<{ super(); this.debug = opts.debug || debug("ndk"); - this.explicitRelayUrls = opts.explicitRelayUrls || []; + this._explicitRelayUrls = opts.explicitRelayUrls || []; this.subManager = new NDKSubscriptionManager(this.debug); this.pool = new NDKPool( opts.explicitRelayUrls || [], @@ -334,6 +334,15 @@ export class NDK extends EventEmitter<{ } catch {} } + set explicitRelayUrls(urls: WebSocket["url"][]) { + this._explicitRelayUrls = urls; + this.pool.relayUrls = urls; + } + + get explicitRelayUrls() { + return this._explicitRelayUrls || []; + } + set signatureVerificationWorker(worker: Worker | undefined) { this.asyncSigVerification = !!worker; if (worker) { diff --git a/ndk/src/subscription/index.ts b/ndk/src/subscription/index.ts index 6764bb56..aa32bdd0 100644 --- a/ndk/src/subscription/index.ts +++ b/ndk/src/subscription/index.ts @@ -383,7 +383,7 @@ export class NDKSubscription extends EventEmitter<{ * Send REQ to relays */ private startWithRelays(): void { - if (!this.relaySet) { + if (!this.relaySet || this.relaySet.relays.size === 0) { this.relayFilters = calculateRelaySetsFromFilters(this.ndk, this.filters, this.pool); } else { this.relayFilters = new Map(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b877ef99..5cf97358 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,8 +353,8 @@ importers: specifier: workspace:* version: link:../ndk svelte: - specifier: ^4.2.1 - version: 4.2.1 + specifier: 5.0.0-next.260 + version: 5.0.0-next.260 devDependencies: '@nostr-dev-kit/eslint-config-custom': specifier: workspace:* @@ -715,10 +715,6 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} - '@ampproject/remapping@2.2.1': - resolution: {integrity: sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==} - engines: {node: '>=6.0.0'} - '@ampproject/remapping@2.3.0': resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -2434,18 +2430,10 @@ packages: resolution: {integrity: sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - '@jridgewell/gen-mapping@0.3.3': - resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} - engines: {node: '>=6.0.0'} - '@jridgewell/gen-mapping@0.3.5': resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.1.1': - resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} - engines: {node: '>=6.0.0'} - '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} @@ -2457,8 +2445,8 @@ packages: '@jridgewell/sourcemap-codec@1.4.15': resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - '@jridgewell/trace-mapping@0.3.19': - resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -3808,6 +3796,11 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-typescript@1.4.13: + resolution: {integrity: sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==} + peerDependencies: + acorn: '>=8.9.0' + acorn-walk@8.2.0: resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} engines: {node: '>=0.4.0'} @@ -3822,6 +3815,11 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + acorn@8.12.1: + resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + engines: {node: '>=0.4.0'} + hasBin: true + address@1.2.2: resolution: {integrity: sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==} engines: {node: '>= 10.0.0'} @@ -3908,6 +3906,10 @@ packages: aria-query@5.3.0: resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.0: resolution: {integrity: sha512-LPuwb2P+NrQw3XhxGc36+XSvuBPopovXYTR9Ew++Du9Yb/bx5AzBfrIsBoj0EZUifjQU+sHL21sseZ3jerWO/A==} @@ -3957,12 +3959,13 @@ packages: resolution: {integrity: sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==} engines: {node: '>= 0.4'} - axobject-query@3.2.1: - resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} - axobject-query@4.0.0: resolution: {integrity: sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==} + axobject-query@4.1.0: + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} + babel-core@7.0.0-bridge.0: resolution: {integrity: sha512-poPX9mZH/5CSanm50Q+1toVci6pv5KSRv/5TWCwtzQS5XEwn40BcCrgIeMFWP9CKKIniKXNxoIOnOq4VVlGXhg==} peerDependencies: @@ -5064,6 +5067,9 @@ packages: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} + esrap@1.2.2: + resolution: {integrity: sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==} + esrecurse@4.3.0: resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} engines: {node: '>=4.0'} @@ -6202,9 +6208,8 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} - magic-string@0.30.3: - resolution: {integrity: sha512-B7xGbll2fG/VjP+SWg4sX3JynwIU0mjoTc6MPpKNuIvftk6u6vqhDnk1R80b8C2GBR6ywqy+1DcKBrevBg+bmw==} - engines: {node: '>=12'} + magic-string@0.30.11: + resolution: {integrity: sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==} make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} @@ -7788,14 +7793,14 @@ packages: svelte: ^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0 typescript: ^4.9.4 || ^5.0.0 - svelte@4.2.1: - resolution: {integrity: sha512-LpLqY2Jr7cRxkrTc796/AaaoMLF/1ax7cto8Ot76wrvKQhrPmZ0JgajiWPmg9mTSDqO16SSLiD17r9MsvAPTmw==} - engines: {node: '>=16'} - svelte@4.2.19: resolution: {integrity: sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==} engines: {node: '>=16'} + svelte@5.0.0-next.260: + resolution: {integrity: sha512-TGcvG71DUklf5P4UmJxOQiVxWYLPp4c6o+NUjmVMsAXKsCMXOTXw+QpnmEWw5D95Sj7SrmAGeIT+p/uvHAUZXg==} + engines: {node: '>=18'} + sveltedoc-parser@4.2.1: resolution: {integrity: sha512-sWJRa4qOfRdSORSVw9GhfDEwsbsYsegnDzBevUCF6k/Eis/QqCu9lJ6I0+d/E2wOWCjOhlcJ3+jl/Iur+5mmCw==} engines: {node: '>=10.0.0'} @@ -8775,6 +8780,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zimmerframe@1.1.2: + resolution: {integrity: sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==} + zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -8911,11 +8919,6 @@ snapshots: '@alloc/quick-lru@5.2.0': {} - '@ampproject/remapping@2.2.1': - dependencies: - '@jridgewell/gen-mapping': 0.3.3 - '@jridgewell/trace-mapping': 0.3.19 - '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 @@ -10930,30 +10933,19 @@ snapshots: '@types/yargs': 17.0.24 chalk: 4.1.2 - '@jridgewell/gen-mapping@0.3.3': - dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 - '@jridgewell/resolve-uri@3.1.1': {} - '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} '@jridgewell/sourcemap-codec@1.4.15': {} - '@jridgewell/trace-mapping@0.3.19': - dependencies: - '@jridgewell/resolve-uri': 3.1.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': dependencies: @@ -10963,7 +10955,7 @@ snapshots: '@jridgewell/trace-mapping@0.3.9': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@juggle/resize-observer@3.4.0': {} @@ -11717,7 +11709,7 @@ snapshots: express: 4.18.2 find-cache-dir: 3.3.2 fs-extra: 11.1.1 - magic-string: 0.30.10 + magic-string: 0.30.11 rollup: 3.29.4 vite: 4.5.5(@types/node@22.6.1) optionalDependencies: @@ -12020,7 +12012,7 @@ snapshots: '@storybook/node-logger': 7.6.20 '@storybook/svelte': 7.6.20(svelte@4.2.19) '@sveltejs/vite-plugin-svelte': 2.4.6(svelte@4.2.19)(vite@4.5.5(@types/node@22.6.1)) - magic-string: 0.30.10 + magic-string: 0.30.11 svelte: 4.2.19 svelte-preprocess: 5.1.4(@babel/core@7.25.2)(postcss-load-config@4.0.1(postcss@8.4.47)(ts-node@10.9.2(@types/node@22.6.1)(typescript@5.5.4)))(postcss@8.4.47)(svelte@4.2.19)(typescript@5.5.4) sveltedoc-parser: 4.2.1 @@ -12177,7 +12169,7 @@ snapshots: debug: 4.3.6 deepmerge: 4.3.1 kleur: 4.1.5 - magic-string: 0.30.10 + magic-string: 0.30.11 svelte: 4.2.19 svelte-hmr: 0.15.3(svelte@4.2.19) vite: 4.5.5(@types/node@22.6.1) @@ -12720,7 +12712,7 @@ snapshots: '@vue/compiler-ssr': 3.4.27 '@vue/shared': 3.4.27 estree-walker: 2.0.2 - magic-string: 0.30.10 + magic-string: 0.30.11 postcss: 8.4.47 source-map-js: 1.2.0 @@ -12827,12 +12819,22 @@ snapshots: dependencies: acorn: 8.11.3 + acorn-jsx@5.3.2(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + + acorn-typescript@1.4.13(acorn@8.12.1): + dependencies: + acorn: 8.12.1 + acorn-walk@8.2.0: {} acorn@8.10.0: {} acorn@8.11.3: {} + acorn@8.12.1: {} + address@1.2.2: {} agent-base@5.1.1: {} @@ -12926,6 +12928,8 @@ snapshots: dependencies: dequal: 2.0.3 + aria-query@5.3.2: {} + array-buffer-byte-length@1.0.0: dependencies: call-bind: 1.0.2 @@ -12984,14 +12988,12 @@ snapshots: available-typed-arrays@1.0.5: {} - axobject-query@3.2.1: - dependencies: - dequal: 2.0.3 - axobject-query@4.0.0: dependencies: dequal: 2.0.3 + axobject-query@4.1.0: {} + babel-core@7.0.0-bridge.0(@babel/core@7.25.2): dependencies: '@babel/core': 7.25.2 @@ -14494,8 +14496,8 @@ snapshots: espree@9.2.0: dependencies: - acorn: 8.11.3 - acorn-jsx: 5.3.2(acorn@8.11.3) + acorn: 8.12.1 + acorn-jsx: 5.3.2(acorn@8.12.1) eslint-visitor-keys: 3.4.3 espree@9.6.1: @@ -14514,6 +14516,11 @@ snapshots: dependencies: estraverse: 5.3.0 + esrap@1.2.2: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.6 + esrecurse@4.3.0: dependencies: estraverse: 5.3.0 @@ -15244,7 +15251,7 @@ snapshots: is-reference@3.0.2: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.6 is-regex@1.1.4: dependencies: @@ -16364,9 +16371,9 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 - magic-string@0.30.3: + magic-string@0.30.11: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 make-dir@2.1.0: dependencies: @@ -17980,7 +17987,7 @@ snapshots: sorcery@0.11.0: dependencies: - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 buffer-crc32: 0.2.13 minimist: 1.2.8 sander: 0.5.1 @@ -18247,22 +18254,6 @@ snapshots: svelte: 4.2.19 typescript: 5.5.4 - svelte@4.2.1: - dependencies: - '@ampproject/remapping': 2.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.19 - acorn: 8.10.0 - aria-query: 5.3.0 - axobject-query: 3.2.1 - code-red: 1.0.4 - css-tree: 2.3.1 - estree-walker: 3.0.3 - is-reference: 3.0.2 - locate-character: 3.0.0 - magic-string: 0.30.3 - periscopic: 3.1.0 - svelte@4.2.19: dependencies: '@ampproject/remapping': 2.3.0 @@ -18280,6 +18271,22 @@ snapshots: magic-string: 0.30.10 periscopic: 3.1.0 + svelte@5.0.0-next.260: + dependencies: + '@ampproject/remapping': 2.3.0 + '@jridgewell/sourcemap-codec': 1.5.0 + '@types/estree': 1.0.6 + acorn: 8.12.1 + acorn-typescript: 1.4.13(acorn@8.12.1) + aria-query: 5.3.2 + axobject-query: 4.1.0 + esm-env: 1.0.0 + esrap: 1.2.2 + is-reference: 3.0.2 + locate-character: 3.0.0 + magic-string: 0.30.11 + zimmerframe: 1.1.2 + sveltedoc-parser@4.2.1: dependencies: eslint: 8.4.1 @@ -19169,7 +19176,7 @@ snapshots: unplugin@1.5.0: dependencies: - acorn: 8.11.3 + acorn: 8.12.1 chokidar: 3.5.3 webpack-sources: 3.2.3 webpack-virtual-modules: 0.5.0 @@ -19582,4 +19589,6 @@ snapshots: yocto-queue@0.1.0: {} + zimmerframe@1.1.2: {} + zwitch@2.0.4: {}