Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add zapper support #12

Merged
merged 12 commits into from
Dec 18, 2024
4 changes: 4 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ if (!databaseUrl) {
Deno.exit(1);
}
export const DATABASE_URL = databaseUrl;

export const NOSTR_NIP57_PRIVATE_KEY = Deno.env.get("NOSTR_NIP57_PRIVATE_KEY") || "";
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
export const NOSTR_PUBLISHER_API_TOKEN = Deno.env.get("NOSTR_PUBLISHER_API_TOKEN") || "";
export const NOSTR_PUBLISHER_API_URL = Deno.env.get("NOSTR_PUBLISHER_API_URL") || "https://nostr-publisher.getalby.workers.dev";
73 changes: 71 additions & 2 deletions src/nwc/nwcPool.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
import { Event, finalizeEvent } from "@nostr/tools";
import { makeZapReceipt } from "@nostr/tools/nip57";
import { nwc } from "npm:@getalby/sdk";
import { hexToBytes } from "npm:@noble/[email protected]/utils";
import { NOSTR_NIP57_PRIVATE_KEY, NOSTR_PUBLISHER_API_TOKEN, NOSTR_PUBLISHER_API_URL } from "../constants.ts";
import { decrypt } from "../db/aesgcm.ts";
import { DB } from "../db/db.ts";
import { logger } from "../logger.ts";

export class NWCPool {
private readonly _db: DB;
private readonly publisherToken: string;
private readonly publisherUrl: string;
private readonly zapperPrivateKey: string;

constructor(db: DB) {
this._db = db;
this.publisherToken = NOSTR_PUBLISHER_API_TOKEN;
this.publisherUrl = NOSTR_PUBLISHER_API_URL;
this.zapperPrivateKey = NOSTR_NIP57_PRIVATE_KEY;
}

async init() {
Expand All @@ -24,13 +35,71 @@ export class NWCPool {
});

nwcClient.subscribeNotifications(
(notification) => {
async (notification) => {
logger.debug("received notification", { userId, notification });
if (notification.notification_type === "payment_received") {
this._db.updateInvoice(userId, notification.notification)
const transaction = notification.notification
try {
this._db.updateInvoice(userId, transaction)
await this.publishZap(userId, transaction)
} catch (error) {
logger.error("error processing payment_received notification", { userId, transaction, error });
}
}
},
["payment_received"]
);
}

async publishZap(userId: number, transaction: nwc.Nip47Transaction) {
const metadata = transaction.metadata
const requestEvent = metadata?.nostr as Event

if (!requestEvent) {
return;
}

const zapReceipt = makeZapReceipt({
zapRequest: JSON.stringify(requestEvent),
preimage: transaction.preimage,
bolt11: transaction.invoice,
paidAt: new Date(transaction.settled_at * 1000)
})
const relays = requestEvent.tags.filter(tag => tag[0] === 'relays')[0].slice(1);
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
if (!relays.length) {
logger.error("no relays specified in zap request", { user_id: userId, transaction });
return;
}

const signedEvent = finalizeEvent(zapReceipt, hexToBytes(this.zapperPrivateKey))

const response = await fetch(this.publisherUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'API-TOKEN': this.publisherToken,
},
body: JSON.stringify({
relays,
event: signedEvent,
}),
});

if (!response.ok) {
logger.error("failed to publish zap", {
user_id: userId,
event_id: signedEvent.id,
payment_hash: transaction.payment_hash,
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
relays,
response_status: response.status,
});
im-adithya marked this conversation as resolved.
Show resolved Hide resolved
}

logger.debug("published zap", {
user_id: userId,
event_id: signedEvent.id,
payment_hash: transaction.payment_hash,
relays
});
}
}