Skip to content

Commit

Permalink
generate NIP-60 quote events and wip refactor of ndk-wallet's service
Browse files Browse the repository at this point in the history
  • Loading branch information
pablof7z committed Sep 29, 2024
1 parent 722345b commit d2c323c
Show file tree
Hide file tree
Showing 16 changed files with 552 additions and 247 deletions.
13 changes: 13 additions & 0 deletions docs/wallet/nutzaps.md
Original file line number Diff line number Diff line change
@@ -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://...
});
```

18 changes: 18 additions & 0 deletions ndk-wallet/src/cashu/decrypt.ts
Original file line number Diff line number Diff line change
@@ -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");
}
}
38 changes: 38 additions & 0 deletions ndk-wallet/src/cashu/deposit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions ndk-wallet/src/cashu/token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Expand All @@ -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;
}
Expand All @@ -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);
}
Expand Down
57 changes: 45 additions & 12 deletions ndk-wallet/src/cashu/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
NDKEventId,
NDKPaymentConfirmationCashu,
NDKPaymentConfirmationLN,
NDKSubscription,
NDKTag,
NDKZapDetails} from "@nostr-dev-kit/ndk";
import NDK, {
Expand All @@ -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<NDKWalletEvents> implements NDKWallet {
readonly type = 'nip-60';

Expand All @@ -34,6 +39,8 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> implements NDK
private knownTokens: Set<NDKEventId> = new Set();
private skipPrivateKey: boolean = false;
public p2pk: string | undefined;
private sub?: NDKSubscription;
public ndk: NDK;

public status: NDKWalletStatus = NDKWalletStatus.INITIAL;

Expand All @@ -46,8 +53,9 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> 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;
Expand All @@ -67,9 +75,7 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> implements NDK
return this._event;
}

tagId(): string {
return this.event.tagId();
}
tagId() { return this.event.tagId(); }

/**
* Returns the tokens that are available for spending
Expand All @@ -92,17 +98,17 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> implements NDK
public checkProofs = checkTokenProofs.bind(this);

static async from(event: NDKEvent): Promise<NDKCashuWallet | undefined> {
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;

Expand Down Expand Up @@ -181,6 +187,9 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> implements NDK
this.setPrivateTag("unit", unit);
}

/**
* Returns the p2pk of this wallet
*/
async getP2pk(): Promise<string | undefined> {
if (this.p2pk) return this.p2pk;
if (this.privkey) {
Expand All @@ -191,6 +200,9 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> implements NDK
}
}

/**
* Returns the private key of this wallet
*/
get privkey(): string | undefined {
const privkey = this.getPrivateTag("privkey");
if (privkey) return privkey;
Expand Down Expand Up @@ -238,7 +250,7 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> 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(
Expand All @@ -252,16 +264,36 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> 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<NDKPaymentConfirmationLN | undefined> {
const pay = new NDKCashuPay(this, { pr });
const preimage = await pay.payLn(useMint);
Expand Down Expand Up @@ -354,6 +386,7 @@ export class NDKCashuWallet extends EventEmitter<NDKWalletEvents> 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);
Expand Down
Loading

0 comments on commit d2c323c

Please sign in to comment.