Skip to content

Commit

Permalink
Merge pull request #12 from getAlby/task-zapper
Browse files Browse the repository at this point in the history
feat: add zapper support
  • Loading branch information
im-adithya authored Dec 18, 2024
2 parents 83cecef + aa76124 commit 7ecffde
Show file tree
Hide file tree
Showing 10 changed files with 390 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ LOG_LEVEL=DEBUG
BASE_URL=http://localhost:8080
DATABASE_URL=postgresql://myuser:mypass@localhost:5432/alby_lite
# generate using deno task db:generate:key
ENCRYPTION_KEY=
ENCRYPTION_KEY=
NOSTR_NIP57_PRIVATE_KEY=
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
FROM denoland/deno:1.45.5
FROM denoland/deno:2.1.2
EXPOSE 8080

WORKDIR /app

COPY . .
RUN touch /app/ca-certificate.crt

USER deno

Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ A minimal Lightning address server powered by [NWC](https://nwc.dev)

## Deployment

### Configuration Parameters

- LOG_LEVEL: Sets the amount of detail in logs
- BASE_URL: Base url of the lightning address server
- DATABASE_URI: Postgres connection string
- ENCRYPTION_KEY: Secret used to encrypt NWC connection secrets in the DB
- NOSTR_NIP57_PRIVATE_KEY: private key of zapper service, see [NIP-57](https://github.com/nostr-protocol/nips/blob/master/57.md) for more info

_Environment variables must be setup, including a postgres database connection. Please see .env.example._

### Run with Deno
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"db:generate": "deno run -A --node-modules-dir npm:drizzle-kit generate",
"db:generate:key": "deno run ./src/db/generateKey.ts",
"dev": "deno run --env --allow-net --allow-env --allow-read --allow-write --watch src/main.ts",
"start": "deno run --allow-net --allow-env --allow-read --allow-write src/main.ts",
"start": "deno run --allow-net --allow-env --allow-read --allow-write --cert ./ca-certificate.crt src/main.ts",
"test": "deno test --env --allow-env"
},
"compilerOptions": {
Expand Down
281 changes: 280 additions & 1 deletion deno.lock

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { getPublicKey } from "@nostr/tools";
import { hexToBytes } from "npm:@noble/[email protected]/utils";

export const PORT = parseInt(Deno.env.get("PORT") || "8080");
export const BASE_URL = Deno.env.get("BASE_URL");
if (!BASE_URL) {
Expand All @@ -11,3 +14,6 @@ if (!databaseUrl) {
Deno.exit(1);
}
export const DATABASE_URL = databaseUrl;

export const NOSTR_NIP57_PRIVATE_KEY = Deno.env.get("NOSTR_NIP57_PRIVATE_KEY") || "";
export const NOSTR_NIP57_PUBLIC_KEY = NOSTR_NIP57_PRIVATE_KEY ? getPublicKey(hexToBytes(NOSTR_NIP57_PRIVATE_KEY)) : "";
2 changes: 1 addition & 1 deletion src/db/db.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { drizzle, PostgresJsDatabase } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "https://deno.land/x/[email protected]/mod.js";
import { nwc } from "npm:@getalby/sdk";
import postgres from "postgres";

import { and, eq } from "drizzle-orm";
import { DATABASE_URL } from "../constants.ts";
Expand Down
17 changes: 16 additions & 1 deletion src/lnurlp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { validateZapRequest } from "@nostr/tools/nip57";
import { Hono } from "hono";
import { nwc } from "npm:@getalby/sdk";
import { logger } from "../src/logger.ts";
import { BASE_URL, DOMAIN } from "./constants.ts";
import { BASE_URL, DOMAIN, NOSTR_NIP57_PUBLIC_KEY } from "./constants.ts";
import { DB } from "./db/db.ts";
import "./nwc/nwcPool.ts";

Expand Down Expand Up @@ -35,6 +35,21 @@ export function createLnurlWellKnownApp(db: DB) {
minSendable: 1000,
maxSendable: 10000000000,
metadata: getLnurlMetadata(username),
payerData: {
name: {
mandatory: false
},
email: {
mandatory: false
},
pubkey: {
mandatory: false
}
},
...(NOSTR_NIP57_PUBLIC_KEY ? {
nostrPubkey: NOSTR_NIP57_PUBLIC_KEY,
allowsNostr: true,
} : {})
});
} catch (error) {
return c.json({ status: "ERROR", reason: "" + error });
Expand Down
2 changes: 2 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Hono } from "hono";
import { cors } from "hono/cors";
import { serveStatic } from "hono/deno";
import { secureHeaders } from "hono/secure-headers";
//import { sentry } from "npm:@hono/sentry";
Expand All @@ -22,6 +23,7 @@ const hono = new Hono();

hono.use(loggerMiddleware());
hono.use(secureHeaders());
hono.use(cors());
/*if (SENTRY_DSN) {
hono.use("*", sentry({ dsn: SENTRY_DSN }));
}*/
Expand Down
74 changes: 72 additions & 2 deletions src/nwc/nwcPool.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { Event, finalizeEvent, SimplePool } 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 } 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 pool: SimplePool;
private readonly zapperPrivateKey: string;

constructor(db: DB) {
this._db = db;
this.pool = new SimplePool()
this.zapperPrivateKey = NOSTR_NIP57_PRIVATE_KEY;
}

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

nwcClient.subscribeNotifications(
(notification) => {
async (notification) => {
logger.debug("received notification", { userId, notification });
if (notification.notification_type === "payment_received") {
this._db.markInvoiceSettled(userId, notification.notification)
const transaction = notification.notification
try {
this._db.markInvoiceSettled(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.find(tag => tag[0] === 'relays')?.slice(1);
if (!relays || !relays.length) {
logger.error("no relays specified in zap request", { user_id: userId, transaction });
return;
}

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

const results = await Promise.allSettled(this.pool.publish(relays, signedEvent))

const successfulRelays: string[] = [];
const failedRelays: string[] = [];

results.forEach((result, index) => {
const relay = relays[index];
if (result.status === 'fulfilled') {
successfulRelays.push(relay);
} else {
failedRelays.push(relay);
}
});

if (failedRelays.length === relays.length) {
logger.error("failed to publish zap", {
user_id: userId,
event_id: signedEvent.id,
payment_hash: transaction.payment_hash,
failed_relays: relays,
});
return;
}

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

0 comments on commit 7ecffde

Please sign in to comment.