From 9624792c1c7dcc42888fa48da53cd018b31193d2 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Fri, 13 Dec 2024 14:59:34 +0530 Subject: [PATCH 1/6] feat: add nip-05 --- drizzle/0002_crazy_sauron.sql | 2 + drizzle/meta/0002_snapshot.json | 209 ++++++++++++++++++++++++++++++++ drizzle/meta/_journal.json | 7 ++ src/db/db.ts | 7 +- src/db/schema.ts | 1 + src/lnurlp.ts | 26 +++- src/main.ts | 4 +- src/users.ts | 54 ++++++--- 8 files changed, 284 insertions(+), 26 deletions(-) create mode 100644 drizzle/0002_crazy_sauron.sql create mode 100644 drizzle/meta/0002_snapshot.json diff --git a/drizzle/0002_crazy_sauron.sql b/drizzle/0002_crazy_sauron.sql new file mode 100644 index 0000000..bca2d38 --- /dev/null +++ b/drizzle/0002_crazy_sauron.sql @@ -0,0 +1,2 @@ +ALTER TABLE "users" ADD COLUMN "nostr_pubkey" text;--> statement-breakpoint +ALTER TABLE "users" ADD CONSTRAINT "users_nostr_pubkey_unique" UNIQUE("nostr_pubkey"); \ No newline at end of file diff --git a/drizzle/meta/0002_snapshot.json b/drizzle/meta/0002_snapshot.json new file mode 100644 index 0000000..a7fd577 --- /dev/null +++ b/drizzle/meta/0002_snapshot.json @@ -0,0 +1,209 @@ +{ + "id": "0158a5e2-5076-4051-8e8d-2d382552344d", + "prevId": "52607bd8-7a1a-4a34-ad27-91b78733e85c", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.invoices": { + "name": "invoices", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "bigint", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payment_request": { + "name": "payment_request", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "payment_hash": { + "name": "payment_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "preimage": { + "name": "preimage", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "settled_at": { + "name": "settled_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_id_idx": { + "name": "user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "user_payment_hash_idx": { + "name": "user_payment_hash_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "payment_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invoices_user_id_users_id_fk": { + "name": "invoices_user_id_users_id_fk", + "tableFrom": "invoices", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "invoices_payment_request_unique": { + "name": "invoices_payment_request_unique", + "nullsNotDistinct": false, + "columns": [ + "payment_request" + ] + }, + "invoices_payment_hash_unique": { + "name": "invoices_payment_hash_unique", + "nullsNotDistinct": false, + "columns": [ + "payment_hash" + ] + } + } + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "connection_secret": { + "name": "connection_secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "nostr_pubkey": { + "name": "nostr_pubkey", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "users_username_unique": { + "name": "users_username_unique", + "nullsNotDistinct": false, + "columns": [ + "username" + ] + }, + "users_nostr_pubkey_unique": { + "name": "users_nostr_pubkey_unique", + "nullsNotDistinct": false, + "columns": [ + "nostr_pubkey" + ] + } + } + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 8c625fe..2be20bb 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1733813329314, "tag": "0001_white_prism", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1734078845730, + "tag": "0002_crazy_sauron", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/db/db.ts b/src/db/db.ts index 520e6fd..3c21857 100644 --- a/src/db/db.ts +++ b/src/db/db.ts @@ -28,7 +28,8 @@ export class DB { async createUser( connectionSecret: string, - username?: string + username?: string, + nostrPubkey?: string ) { const parsed = nwc.NWCClient.parseWalletConnectUrl(connectionSecret); if (!parsed.secret) { @@ -43,7 +44,8 @@ export class DB { const [newUser] = await this._db.insert(users).values({ encryptedConnectionSecret, username, - }).returning({ id: users.id, username: users.username }); + nostrPubkey + }).returning({ id: users.id, username: users.username, nostrPubkey: users.nostrPubkey }); return newUser; } @@ -62,6 +64,7 @@ export class DB { const connectionSecret = await decrypt(result.encryptedConnectionSecret); return { id: result.id, + nostrPubkey: result.nostrPubkey, connectionSecret }; } diff --git a/src/db/schema.ts b/src/db/schema.ts index cea1949..21f8aac 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -4,6 +4,7 @@ export const users = pgTable("users", { id: serial("id").primaryKey(), encryptedConnectionSecret: text("connection_secret").notNull(), username: text("username").unique().notNull(), + nostrPubkey: text("nostr_pubkey").unique(), createdAt: timestamp("created_at").notNull().defaultNow(), }); diff --git a/src/lnurlp.ts b/src/lnurlp.ts index e901114..27aff52 100644 --- a/src/lnurlp.ts +++ b/src/lnurlp.ts @@ -14,10 +14,10 @@ function getLnurlMetadata(username: string): string { ]) } -export function createLnurlWellKnownApp(db: DB) { +export function createWellKnownApp(db: DB) { const hono = new Hono(); - hono.get("/:username", async (c) => { + hono.get("/lnurlp/:username", async (c) => { try { const username = c.req.param("username"); @@ -41,6 +41,28 @@ export function createLnurlWellKnownApp(db: DB) { } }); + hono.get("/nostr.json", async (c) => { + try { + const username = c.req.query("name"); + + logger.debug("NIP05 request", { username }); + + if (!username) { + throw new Error("No username provided"); + } + + const user = await db.findUser(username); + + return c.json({ + names: { + [username]: user.nostrPubkey + } + }); + } catch (error) { + return c.json({ status: "ERROR", reason: "" + error }); + } + }); + return hono; } diff --git a/src/main.ts b/src/main.ts index 55f14de..2e922b7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,7 +4,7 @@ import { secureHeaders } from "hono/secure-headers"; //import { sentry } from "npm:@hono/sentry"; import { PORT } from "./constants.ts"; import { DB, runMigration } from "./db/db.ts"; -import { createLnurlApp, createLnurlWellKnownApp } from "./lnurlp.ts"; +import { createLnurlApp, createWellKnownApp } from "./lnurlp.ts"; import { LOG_LEVEL, logger, loggerMiddleware } from "./logger.ts"; import { NWCPool } from "./nwc/nwcPool.ts"; import { createUsersApp } from "./users.ts"; @@ -26,7 +26,7 @@ hono.use(secureHeaders()); hono.use("*", sentry({ dsn: SENTRY_DSN })); }*/ -hono.route("/.well-known/lnurlp", createLnurlWellKnownApp(db)); +hono.route("/.well-known", createWellKnownApp(db)); hono.route("/lnurlp", createLnurlApp(db)); hono.route("/users", createUsersApp(db, nwcPool)); diff --git a/src/users.ts b/src/users.ts index 34e6ea1..cc88678 100644 --- a/src/users.ts +++ b/src/users.ts @@ -1,4 +1,5 @@ import { Hono } from "hono"; +import postgres from "postgres"; import { DOMAIN } from "./constants.ts"; import { DB } from "./db/db.ts"; import { logger } from "./logger.ts"; @@ -8,27 +9,40 @@ export function createUsersApp(db: DB, nwcPool: NWCPool) { const hono = new Hono(); hono.post("/", async (c) => { - logger.debug("create user", {}); - - const createUserRequest: { connectionSecret: string; username?: string } = - await c.req.json(); - - if (!createUserRequest.connectionSecret) { - return c.text("no connection secret provided", 400); + try { + logger.debug("create user", {}); + + const createUserRequest: { connectionSecret: string; username?: string, nostrPubkey?: string } = + await c.req.json(); + + if (!createUserRequest.connectionSecret) { + return c.text("no connection secret provided", 400); + } + + const user = await db.createUser( + createUserRequest.connectionSecret, + createUserRequest.username, + createUserRequest.nostrPubkey + ); + + const lightningAddress = user.username + "@" + DOMAIN; + + nwcPool.subscribeUser(createUserRequest.connectionSecret, user.id); + + return c.json({ + lightningAddress, + }); + } catch (error) { + let reason = "" + error + if (error instanceof postgres.PostgresError) { + if (error.constraint_name === "users_username_unique") { + reason = "Username has already been taken" + } else if (error.constraint_name === "users_nostr_pubkey_unique") { + reason = "Nostr pubkey has already been taken" + } + } + return c.json({ status: "ERROR", reason }); } - - const user = await db.createUser( - createUserRequest.connectionSecret, - createUserRequest.username - ); - - const lightningAddress = user.username + "@" + DOMAIN; - - nwcPool.subscribeUser(createUserRequest.connectionSecret, user.id); - - return c.json({ - lightningAddress, - }); }); return hono; } From bb22c249d7fd5f0dd8b95fca40ee7a722ac0902c Mon Sep 17 00:00:00 2001 From: im-adithya Date: Tue, 17 Dec 2024 19:34:35 +0530 Subject: [PATCH 2/6] chore: move well-known links to folder --- src/lnurlp.ts | 62 +--------------------------------------- src/main.ts | 6 ++-- src/well-known/index.ts | 7 +++++ src/well-known/lnurlp.ts | 41 ++++++++++++++++++++++++++ src/well-known/nostr.ts | 31 ++++++++++++++++++++ 5 files changed, 84 insertions(+), 63 deletions(-) create mode 100644 src/well-known/index.ts create mode 100644 src/well-known/lnurlp.ts create mode 100644 src/well-known/nostr.ts diff --git a/src/lnurlp.ts b/src/lnurlp.ts index 27aff52..955e4a4 100644 --- a/src/lnurlp.ts +++ b/src/lnurlp.ts @@ -3,68 +3,8 @@ 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 } from "./constants.ts"; import { DB } from "./db/db.ts"; -import "./nwc/nwcPool.ts"; - -function getLnurlMetadata(username: string): string { - return JSON.stringify([ - ["text/identifier", `${username}@${DOMAIN}`], - ["text/plain", `Sats for ${username}`], - ]) -} - -export function createWellKnownApp(db: DB) { - const hono = new Hono(); - - hono.get("/lnurlp/:username", async (c) => { - try { - const username = c.req.param("username"); - - logger.debug("LNURLp request", { username }); - - // check the user exists - await db.findUser(username); - - // TODO: zapper support - - return c.json({ - tag: "payRequest", - commentAllowed: 255, - callback: `${BASE_URL}/lnurlp/${username}/callback`, - minSendable: 1000, - maxSendable: 10000000000, - metadata: getLnurlMetadata(username), - }); - } catch (error) { - return c.json({ status: "ERROR", reason: "" + error }); - } - }); - - hono.get("/nostr.json", async (c) => { - try { - const username = c.req.query("name"); - - logger.debug("NIP05 request", { username }); - - if (!username) { - throw new Error("No username provided"); - } - - const user = await db.findUser(username); - - return c.json({ - names: { - [username]: user.nostrPubkey - } - }); - } catch (error) { - return c.json({ status: "ERROR", reason: "" + error }); - } - }); - - return hono; -} export function createLnurlApp(db: DB) { const hono = new Hono(); diff --git a/src/main.ts b/src/main.ts index 2e922b7..ecd3bb4 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,10 +4,11 @@ import { secureHeaders } from "hono/secure-headers"; //import { sentry } from "npm:@hono/sentry"; import { PORT } from "./constants.ts"; import { DB, runMigration } from "./db/db.ts"; -import { createLnurlApp, createWellKnownApp } from "./lnurlp.ts"; +import { createLnurlApp } from "./lnurlp.ts"; import { LOG_LEVEL, logger, loggerMiddleware } from "./logger.ts"; import { NWCPool } from "./nwc/nwcPool.ts"; import { createUsersApp } from "./users.ts"; +import { createLnurlWellKnownApp, createNostrWellKnownApp } from "./well-known/index.ts"; await runMigration(); @@ -26,7 +27,8 @@ hono.use(secureHeaders()); hono.use("*", sentry({ dsn: SENTRY_DSN })); }*/ -hono.route("/.well-known", createWellKnownApp(db)); +hono.route("/.well-known/lnurlp", createLnurlWellKnownApp(db)); +hono.route("/.well-known/nostr.json", createNostrWellKnownApp(db)); hono.route("/lnurlp", createLnurlApp(db)); hono.route("/users", createUsersApp(db, nwcPool)); diff --git a/src/well-known/index.ts b/src/well-known/index.ts new file mode 100644 index 0000000..535e53c --- /dev/null +++ b/src/well-known/index.ts @@ -0,0 +1,7 @@ +import { createLnurlWellKnownApp } from "./lnurlp.ts"; +import { createNostrWellKnownApp } from "./nostr.ts"; + +export { + createLnurlWellKnownApp, + createNostrWellKnownApp +}; diff --git a/src/well-known/lnurlp.ts b/src/well-known/lnurlp.ts new file mode 100644 index 0000000..c9ca70c --- /dev/null +++ b/src/well-known/lnurlp.ts @@ -0,0 +1,41 @@ +import { Hono } from "hono"; +import { BASE_URL, DOMAIN } from "../constants.ts"; +import { DB } from "../db/db.ts"; +import { logger } from "../logger.ts"; + +function getLnurlMetadata(username: string): string { + return JSON.stringify([ + ["text/identifier", `${username}@${DOMAIN}`], + ["text/plain", `Sats for ${username}`], + ]) +} + +export function createLnurlWellKnownApp(db: DB) { + const hono = new Hono(); + + hono.get("/:username", async (c) => { + try { + const username = c.req.param("username"); + + logger.debug("LNURLp request", { username }); + + // check the user exists + await db.findUser(username); + + // TODO: zapper support + + return c.json({ + tag: "payRequest", + commentAllowed: 255, + callback: `${BASE_URL}/lnurlp/${username}/callback`, + minSendable: 1000, + maxSendable: 10000000000, + metadata: getLnurlMetadata(username), + }); + } catch (error) { + return c.json({ status: "ERROR", reason: "" + error }); + } + }); + + return hono; +} \ No newline at end of file diff --git a/src/well-known/nostr.ts b/src/well-known/nostr.ts new file mode 100644 index 0000000..8f7a11d --- /dev/null +++ b/src/well-known/nostr.ts @@ -0,0 +1,31 @@ +import { Hono } from "hono"; +import { DB } from "../db/db.ts"; +import { logger } from "../logger.ts"; + +export function createNostrWellKnownApp(db: DB) { + const hono = new Hono(); + + hono.get("/", async (c) => { + try { + const username = c.req.query("name"); + + logger.debug("NIP05 request", { username }); + + if (!username) { + throw new Error("No username provided"); + } + + const user = await db.findUser(username); + + return c.json({ + names: { + [username]: user.nostrPubkey + } + }); + } catch (error) { + return c.json({ status: "ERROR", reason: "" + error }); + } + }); + + return hono; +} \ No newline at end of file From e3db3a1e610f86b2789310f0ffc7f211bc48b2b8 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 18 Dec 2024 12:08:24 +0530 Subject: [PATCH 3/6] chore: verify nostr pubkey before user creation --- src/users.ts | 16 +++++++++++++++- src/utils.ts | 4 ++++ 2 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 src/utils.ts diff --git a/src/users.ts b/src/users.ts index cc88678..f6bc2e7 100644 --- a/src/users.ts +++ b/src/users.ts @@ -1,9 +1,11 @@ +import { nip19 } from "@nostr/tools"; import { Hono } from "hono"; import postgres from "postgres"; import { DOMAIN } from "./constants.ts"; import { DB } from "./db/db.ts"; import { logger } from "./logger.ts"; import { NWCPool } from "./nwc/nwcPool.ts"; +import { isValid32ByteHex } from "./utils.ts"; export function createUsersApp(db: DB, nwcPool: NWCPool) { const hono = new Hono(); @@ -19,10 +21,22 @@ export function createUsersApp(db: DB, nwcPool: NWCPool) { return c.text("no connection secret provided", 400); } + let nostrPubkey = createUserRequest.nostrPubkey + + if (nostrPubkey) { + if (nostrPubkey.startsWith("npub")) { + nostrPubkey = nip19.decode(nostrPubkey).data as string + } + + if (!isValid32ByteHex(nostrPubkey)) { + return c.text("invalid nostr pubkey provided", 400); + } + } + const user = await db.createUser( createUserRequest.connectionSecret, createUserRequest.username, - createUserRequest.nostrPubkey + nostrPubkey ); const lightningAddress = user.username + "@" + DOMAIN; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..215cfa5 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,4 @@ +export function isValid32ByteHex(input: string): boolean { + const hexRegex = /^[a-fA-F0-9]{64}$/; + return hexRegex.test(input); +} \ No newline at end of file From aa631a86f9d651808361c38e75403eca4fa437cf Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 18 Dec 2024 12:16:47 +0530 Subject: [PATCH 4/6] chore: add payerdata and nostr info to lnurlp well known --- src/well-known/lnurlp.ts | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/well-known/lnurlp.ts b/src/well-known/lnurlp.ts index c9ca70c..f4f1b3d 100644 --- a/src/well-known/lnurlp.ts +++ b/src/well-known/lnurlp.ts @@ -1,5 +1,5 @@ import { Hono } from "hono"; -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 { logger } from "../logger.ts"; @@ -22,8 +22,6 @@ export function createLnurlWellKnownApp(db: DB) { // check the user exists await db.findUser(username); - // TODO: zapper support - return c.json({ tag: "payRequest", commentAllowed: 255, @@ -31,6 +29,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 }); From 6ac2eb0e65478d289afc8c4334fac81661f71568 Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 18 Dec 2024 12:18:57 +0530 Subject: [PATCH 5/6] fix: do not allow user creation without nostr pubkey --- src/users.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/users.ts b/src/users.ts index f6bc2e7..a761e92 100644 --- a/src/users.ts +++ b/src/users.ts @@ -22,15 +22,16 @@ export function createUsersApp(db: DB, nwcPool: NWCPool) { } let nostrPubkey = createUserRequest.nostrPubkey + if (!nostrPubkey) { + return c.text("no nostr pubkey provided", 400); + } - if (nostrPubkey) { - if (nostrPubkey.startsWith("npub")) { - nostrPubkey = nip19.decode(nostrPubkey).data as string - } - - if (!isValid32ByteHex(nostrPubkey)) { - return c.text("invalid nostr pubkey provided", 400); - } + if (nostrPubkey.startsWith("npub")) { + nostrPubkey = nip19.decode(nostrPubkey).data as string + } + + if (!isValid32ByteHex(nostrPubkey)) { + return c.text("invalid nostr pubkey provided", 400); } const user = await db.createUser( From 11c67a4ec5b9dfbfd13622dec5c45d2236329a7e Mon Sep 17 00:00:00 2001 From: im-adithya Date: Wed, 18 Dec 2024 12:23:21 +0530 Subject: [PATCH 6/6] chore: end file with new line --- src/utils.ts | 2 +- src/well-known/lnurlp.ts | 2 +- src/well-known/nostr.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index 215cfa5..082454a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,4 +1,4 @@ export function isValid32ByteHex(input: string): boolean { const hexRegex = /^[a-fA-F0-9]{64}$/; return hexRegex.test(input); -} \ No newline at end of file +} diff --git a/src/well-known/lnurlp.ts b/src/well-known/lnurlp.ts index f4f1b3d..a1c9c1b 100644 --- a/src/well-known/lnurlp.ts +++ b/src/well-known/lnurlp.ts @@ -51,4 +51,4 @@ export function createLnurlWellKnownApp(db: DB) { }); return hono; -} \ No newline at end of file +} diff --git a/src/well-known/nostr.ts b/src/well-known/nostr.ts index 8f7a11d..6e5a64b 100644 --- a/src/well-known/nostr.ts +++ b/src/well-known/nostr.ts @@ -28,4 +28,4 @@ export function createNostrWellKnownApp(db: DB) { }); return hono; -} \ No newline at end of file +}