diff --git a/README.md b/README.md index 806a178..3f2e9c3 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ A minimal Lightning address server powered by [NWC](https://nwc.dev) ```json { - "connectionSecret": "nostr+walletconnect://..." + "connectionSecret": "nostr+walletconnect://..." } ``` @@ -26,7 +26,6 @@ A minimal Lightning address server powered by [NWC](https://nwc.dev) - [Install Deno](https://docs.deno.com/runtime/manual/getting_started/installation/) - Copy `.env.example` to `.env` -- Setup DB: `deno task db:migrate` - Run in dev mode: `deno task dev` ### Creating a new migration diff --git a/deno.lock b/deno.lock index ffb510c..22c71d0 100644 --- a/deno.lock +++ b/deno.lock @@ -4,6 +4,7 @@ "jsr:@hono/hono@^4.5.5": "4.5.5", "jsr:@nostr/tools@^2.10.3": "2.10.3", "jsr:@std/assert@^1.0.6": "1.0.6", + "jsr:@std/dotenv@*": "0.225.2", "jsr:@std/expect@*": "1.0.4", "jsr:@std/internal@^1.0.4": "1.0.4", "npm:@getalby/sdk@*": "3.7.1", @@ -39,6 +40,9 @@ "jsr:@std/internal" ] }, + "@std/dotenv@0.225.2": { + "integrity": "e2025dce4de6c7bca21dece8baddd4262b09d5187217e231b033e088e0c4dd23" + }, "@std/expect@1.0.4": { "integrity": "97f68a445a9de0d9670200d2b7a19a7505a01b2cb390a983ba8d97d90ce30c4f", "dependencies": [ diff --git a/drizzle/0001_fluffy_black_tom.sql b/drizzle/0001_white_prism.sql similarity index 74% rename from drizzle/0001_fluffy_black_tom.sql rename to drizzle/0001_white_prism.sql index d205d7e..a2cc79f 100644 --- a/drizzle/0001_fluffy_black_tom.sql +++ b/drizzle/0001_white_prism.sql @@ -1,9 +1,8 @@ CREATE TABLE IF NOT EXISTS "invoices" ( "id" serial PRIMARY KEY NOT NULL, "user_id" integer NOT NULL, - "amount" integer NOT NULL, + "amount" bigint NOT NULL, "description" text, - "description_hash" text, "payment_request" text NOT NULL, "payment_hash" text NOT NULL, "preimage" text, @@ -21,6 +20,4 @@ EXCEPTION END $$; --> statement-breakpoint CREATE INDEX IF NOT EXISTS "user_id_idx" ON "invoices" USING btree ("user_id");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "payment_hash_idx" ON "invoices" USING btree ("payment_hash");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "user_payment_hash_idx" ON "invoices" USING btree ("user_id","payment_hash");--> statement-breakpoint -CREATE INDEX IF NOT EXISTS "username_idx" ON "users" USING btree ("username"); \ No newline at end of file +CREATE INDEX IF NOT EXISTS "user_payment_hash_idx" ON "invoices" USING btree ("user_id","payment_hash"); \ No newline at end of file diff --git a/drizzle/meta/0001_snapshot.json b/drizzle/meta/0001_snapshot.json index 72495e3..d0f04a0 100644 --- a/drizzle/meta/0001_snapshot.json +++ b/drizzle/meta/0001_snapshot.json @@ -1,5 +1,5 @@ { - "id": "a90a8c91-5f2c-48da-9109-ae0a37820ceb", + "id": "52607bd8-7a1a-4a34-ad27-91b78733e85c", "prevId": "e292703e-d08b-4f8b-a9eb-3937fe872be7", "version": "7", "dialect": "postgresql", @@ -22,7 +22,7 @@ }, "amount": { "name": "amount", - "type": "integer", + "type": "bigint", "primaryKey": false, "notNull": true }, @@ -32,12 +32,6 @@ "primaryKey": false, "notNull": false }, - "description_hash": { - "name": "description_hash", - "type": "text", - "primaryKey": false, - "notNull": false - }, "payment_request": { "name": "payment_request", "type": "text", @@ -92,21 +86,6 @@ "method": "btree", "with": {} }, - "payment_hash_idx": { - "name": "payment_hash_idx", - "columns": [ - { - "expression": "payment_hash", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, "user_payment_hash_idx": { "name": "user_payment_hash_idx", "columns": [ @@ -192,23 +171,7 @@ "default": "now()" } }, - "indexes": { - "username_idx": { - "name": "username_idx", - "columns": [ - { - "expression": "username", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, + "indexes": {}, "foreignKeys": {}, "compositePrimaryKeys": {}, "uniqueConstraints": { diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 6980b63..8c625fe 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -12,8 +12,8 @@ { "idx": 1, "version": "7", - "when": 1732639452062, - "tag": "0001_fluffy_black_tom", + "when": 1733813329314, + "tag": "0001_white_prism", "breakpoints": true } ] diff --git a/src/db/db.ts b/src/db/db.ts index 92a4962..520e6fd 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -29,7 +29,7 @@ export class DB { async createUser( connectionSecret: string, username?: string - ): Promise<{ username: string }> { + ) { const parsed = nwc.NWCClient.parseWalletConnectUrl(connectionSecret); if (!parsed.secret) { throw new Error("no secret found in connection secret"); @@ -40,12 +40,12 @@ export class DB { const encryptedConnectionSecret = await encrypt(connectionSecret); - await this._db.insert(users).values({ + const [newUser] = await this._db.insert(users).values({ encryptedConnectionSecret, username, - }); + }).returning({ id: users.id, username: users.username }); - return { username }; + return newUser; } getAllUsers() { @@ -69,23 +69,22 @@ export class DB { async createInvoice( userId: number, transaction: nwc.Nip47Transaction - ): Promise<{ identifier: string }> { + ) { await this._db.insert(invoices).values({ userId, amount: transaction.amount, description: transaction.description, - descriptionHash: transaction.description_hash, paymentRequest: transaction.invoice, paymentHash: transaction.payment_hash, metadata: transaction.metadata, }); - return { identifier: transaction.payment_hash }; + return; } - async findInvoice(identifier: string) { + async findInvoice(paymentHash: string) { const result = await this._db.query.invoices.findFirst({ - where: eq(invoices.paymentHash, identifier), + where: eq(invoices.paymentHash, paymentHash), }); if (!result) { throw new Error("invoice not found"); @@ -93,7 +92,7 @@ export class DB { return result; } - async updateInvoice( + async markInvoiceSettled( userId: number, transaction: nwc.Nip47Transaction ): Promise { diff --git a/src/db/schema.ts b/src/db/schema.ts index 6fff944..cea1949 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -1,22 +1,17 @@ -import { index, integer, jsonb, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; +import { bigint, index, integer, jsonb, pgTable, serial, text, timestamp } from "drizzle-orm/pg-core"; export const users = pgTable("users", { id: serial("id").primaryKey(), encryptedConnectionSecret: text("connection_secret").notNull(), username: text("username").unique().notNull(), createdAt: timestamp("created_at").notNull().defaultNow(), -}, (table) => { - return { - usernameIdx: index("username_idx").on(table.username), - }; }); export const invoices = pgTable("invoices", { id: serial("id").primaryKey(), userId: integer("user_id").references(() => users.id, { onDelete: "cascade" }).notNull(), - amount: integer("amount").notNull(), + amount: bigint("amount", { mode: "number" }).notNull(), description: text("description"), - descriptionHash: text("description_hash"), paymentRequest: text("payment_request").unique().notNull(), paymentHash: text("payment_hash").unique().notNull(), preimage: text("preimage"), @@ -26,7 +21,6 @@ export const invoices = pgTable("invoices", { }, (table) => { return { userIdIdx: index("user_id_idx").on(table.userId), - paymentHashIdx: index("payment_hash_idx").on(table.paymentHash), userPaymentHashIdx: index("user_payment_hash_idx").on(table.userId, table.paymentHash), }; }); diff --git a/src/lnurlp.ts b/src/lnurlp.ts index 0db3efd..e901114 100644 --- a/src/lnurlp.ts +++ b/src/lnurlp.ts @@ -1,6 +1,6 @@ import { Event } from "@nostr/tools"; import { validateZapRequest } from "@nostr/tools/nip57"; -import { Context, Hono } from "hono"; +import { Hono } from "hono"; import { nwc } from "npm:@getalby/sdk"; import { logger } from "../src/logger.ts"; import { BASE_URL, DOMAIN } from "./constants.ts"; @@ -14,17 +14,10 @@ function getLnurlMetadata(username: string): string { ]) } -async function computeDescriptionHash(content: string): Promise { - const hashBuffer = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(content)); - return Array.from(new Uint8Array(hashBuffer)) - .map((b) => b.toString(16).padStart(2, "0")) - .join(""); -} - export function createLnurlWellKnownApp(db: DB) { const hono = new Hono(); - hono.get("/:username", async (c: Context) => { + hono.get("/:username", async (c) => { try { const username = c.req.param("username"); @@ -54,7 +47,7 @@ export function createLnurlWellKnownApp(db: DB) { export function createLnurlApp(db: DB) { const hono = new Hono(); - hono.get("/:username/callback", async (c: Context) => { + hono.get("/:username/callback", async (c) => { try { const username = c.req.param("username"); const amount = c.req.query("amount"); @@ -62,7 +55,7 @@ export function createLnurlApp(db: DB) { const payerData = c.req.query("payerdata") ? JSON.parse(c.req.query("payerdata") || "") : null; const nostr = c.req.query("nostr") ? decodeURIComponent(c.req.query("nostr") || "") : null; - logger.debug("LNURLp callback", { username, amount, comment, payerData, nostr }); + logger.debug("LNURLp callback", { username, amount, comment, payer_data: payerData, nostr }); if (!amount) { throw new Error("No amount provided"); @@ -79,9 +72,6 @@ export function createLnurlApp(db: DB) { const description = zapRequest ? zapRequest.content : comment; - const content = zapRequest ? JSON.stringify(nostr) : getLnurlMetadata(username); - const descriptionHash = await computeDescriptionHash(content); - const user = await db.findUser(username); const nwcClient = new nwc.NWCClient({ @@ -96,14 +86,13 @@ export function createLnurlApp(db: DB) { // TODO: payer_data can be improved using nostr worker payer_data: payerData || undefined, nostr: zapRequest || undefined, - }, - description_hash: descriptionHash, + } }); - const invoice = await db.createInvoice(user.id, transaction); + await db.createInvoice(user.id, transaction); return c.json({ - verify: `${BASE_URL}/lnurlp/${username}/verify/${invoice.identifier}`, + verify: `${BASE_URL}/lnurlp/${username}/verify/${transaction.payment_hash}`, routes: [], pr: transaction.invoice, }); @@ -112,14 +101,14 @@ export function createLnurlApp(db: DB) { } }); - hono.get("/:username/verify/:identifier", async (c: Context) => { + hono.get("/:username/verify/:payment_hash", async (c) => { try { const username = c.req.param("username"); - const identifier = c.req.param("identifier"); + const paymentHash = c.req.param("payment_hash"); - logger.debug("LNURLp verify", { username, identifier }); + logger.debug("LNURLp verify", { username, payment_hash: paymentHash }); - const invoice = await db.findInvoice(identifier); + const invoice = await db.findInvoice(paymentHash); return c.json({ settled: !!invoice.settledAt, diff --git a/src/main.ts b/src/main.ts index 8cb87b3..55f14de 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,4 +1,4 @@ -import { Context, Hono } from "hono"; +import { Hono } from "hono"; import { serveStatic } from "hono/deno"; import { secureHeaders } from "hono/secure-headers"; //import { sentry } from "npm:@hono/sentry"; @@ -30,13 +30,13 @@ hono.route("/.well-known/lnurlp", createLnurlWellKnownApp(db)); hono.route("/lnurlp", createLnurlApp(db)); hono.route("/users", createUsersApp(db, nwcPool)); -hono.get("/ping", (c: Context) => { +hono.get("/ping", (c) => { return c.body("OK"); }); hono.use("/favicon.ico", serveStatic({ path: "./favicon.ico" })); -hono.get("/robots.txt", (c: Context) => { +hono.get("/robots.txt", (c) => { return c.body("User-agent: *\nDisallow: /", 200); }); diff --git a/src/nwc/nwcPool.ts b/src/nwc/nwcPool.ts index 80ed409..e4a56c4 100644 --- a/src/nwc/nwcPool.ts +++ b/src/nwc/nwcPool.ts @@ -40,7 +40,7 @@ export class NWCPool { if (notification.notification_type === "payment_received") { const transaction = notification.notification try { - this._db.updateInvoice(userId, transaction) + this._db.markInvoiceSettled(userId, transaction) await this.publishZap(userId, transaction) } catch (error) { logger.error("error processing payment_received notification", { userId, transaction, error }); diff --git a/src/users.ts b/src/users.ts index 80372c2..34e6ea1 100644 --- a/src/users.ts +++ b/src/users.ts @@ -1,4 +1,4 @@ -import { Context, Hono } from "hono"; +import { Hono } from "hono"; import { DOMAIN } from "./constants.ts"; import { DB } from "./db/db.ts"; import { logger } from "./logger.ts"; @@ -7,7 +7,7 @@ import { NWCPool } from "./nwc/nwcPool.ts"; export function createUsersApp(db: DB, nwcPool: NWCPool) { const hono = new Hono(); - hono.post("/", async (c: Context) => { + hono.post("/", async (c) => { logger.debug("create user", {}); const createUserRequest: { connectionSecret: string; username?: string } = @@ -24,7 +24,7 @@ export function createUsersApp(db: DB, nwcPool: NWCPool) { const lightningAddress = user.username + "@" + DOMAIN; - nwcPool.subscribeUser(createUserRequest.connectionSecret, user.username); + nwcPool.subscribeUser(createUserRequest.connectionSecret, user.id); return c.json({ lightningAddress,