Skip to content

Commit

Permalink
feat: store and send metadata during invoice creation
Browse files Browse the repository at this point in the history
  • Loading branch information
im-adithya committed Nov 26, 2024
1 parent ccf1a10 commit e9875b0
Show file tree
Hide file tree
Showing 8 changed files with 4,387 additions and 4,437 deletions.
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"imports": {
"@nostr/tools": "jsr:@nostr/tools@^2.10.3",
"hono": "jsr:@hono/hono@^4.5.5",
"drizzle-orm": "npm:[email protected]",
"drizzle-kit": "npm:[email protected]",
Expand Down
8,588 changes: 4,158 additions & 4,430 deletions deno.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions drizzle/0001_ordinary_silver_sable.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
CREATE TABLE IF NOT EXISTS "invoices" (
"id" serial PRIMARY KEY NOT NULL,
"amount" integer NOT NULL,
"description" text,
"description_hash" text,
"payment_request" text NOT NULL,
"payment_hash" text NOT NULL,
"metadata" jsonb,
"created_at" timestamp DEFAULT now() NOT NULL,
CONSTRAINT "invoices_payment_request_unique" UNIQUE("payment_request"),
CONSTRAINT "invoices_payment_hash_unique" UNIQUE("payment_hash")
);
133 changes: 133 additions & 0 deletions drizzle/meta/0001_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
{
"id": "54f6d0b9-47e0-450e-8c68-e0a3b0ac72ae",
"prevId": "e292703e-d08b-4f8b-a9eb-3937fe872be7",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.invoices": {
"name": "invoices",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "serial",
"primaryKey": true,
"notNull": true
},
"amount": {
"name": "amount",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false
},
"description_hash": {
"name": "description_hash",
"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
},
"metadata": {
"name": "metadata",
"type": "jsonb",
"primaryKey": false,
"notNull": false
},
"created_at": {
"name": "created_at",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"default": "now()"
}
},
"indexes": {},
"foreignKeys": {},
"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
},
"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"
]
}
}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
7 changes: 7 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@
"when": 1728309815221,
"tag": "0000_greedy_phalanx",
"breakpoints": true
},
{
"idx": 1,
"version": "7",
"when": 1732634796419,
"tag": "0001_ordinary_silver_sable",
"breakpoints": true
}
]
}
27 changes: 26 additions & 1 deletion src/db/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { eq } from "drizzle-orm";
import { DATABASE_URL } from "../constants.ts";
import { decrypt, encrypt } from "./aesgcm.ts";
import * as schema from "./schema.ts";
import { users } from "./schema.ts";
import { invoices, users } from "./schema.ts";

export async function runMigration() {
const migrationClient = postgres(DATABASE_URL, { max: 1 });
Expand Down Expand Up @@ -48,6 +48,21 @@ export class DB {
return { username };
}

async createInvoice(
transaction: nwc.Nip47Transaction
): Promise<{ paymentHash: string }> {
await this._db.insert(invoices).values({
amount: transaction.amount,
description: transaction.description,
descriptionHash: transaction.description_hash,
paymentRequest: transaction.invoice,
paymentHash: transaction.payment_hash,
metadata: transaction.metadata,
});

return { paymentHash: transaction.payment_hash };
}

getAllUsers() {
return this._db.query.users.findMany();
}
Expand All @@ -62,4 +77,14 @@ export class DB {
const connectionSecret = await decrypt(result.encryptedConnectionSecret);
return connectionSecret;
}

async findInvoice(paymentHash: string) {
const result = await this._db.query.invoices.findFirst({
where: eq(invoices.paymentHash, paymentHash),
});
if (!result) {
throw new Error("invoice not found");
}
return result;
}
}
13 changes: 12 additions & 1 deletion src/db/schema.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,19 @@
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
import { 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(),
});

export const invoices = pgTable("invoices", {
id: serial("id").primaryKey(),
amount: integer("amount").notNull(),
description: text("description"),
descriptionHash: text("description_hash"),
paymentRequest: text("payment_request").unique().notNull(),
paymentHash: text("payment_hash").unique().notNull(),
metadata: jsonb("metadata"),
createdAt: timestamp("created_at").notNull().defaultNow(),
});
43 changes: 38 additions & 5 deletions src/lnurlp.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
import { validateEvent } from "@nostr/tools";
import { Context, Hono } from "hono";
import { nwc } from "npm:@getalby/sdk";
import { logger } from "../src/logger.ts";
import { BASE_URL, DOMAIN } 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}`],
])
}

async function computeDescriptionHash(content: string): Promise<string> {
const encoder = new TextEncoder();
const buffer = encoder.encode(content);
const hashBuffer = await crypto.subtle.digest("SHA-256", buffer);
return Array.from(new Uint8Array(hashBuffer))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}

export function createLnurlWellKnownApp(db: DB) {
const hono = new Hono();

Expand All @@ -25,7 +42,7 @@ export function createLnurlWellKnownApp(db: DB) {
callback: `${BASE_URL}/lnurlp/${username}/callback`,
minSendable: 1000,
maxSendable: 10000000000,
metadata: `[["text/identifier","${username}@${DOMAIN}"],["text/plain","Sats for ${username}"]]`,
metadata: getLnurlMetadata(username),
});
} catch (error) {
return c.json({ status: "ERROR", reason: "" + error });
Expand All @@ -43,14 +60,21 @@ export function createLnurlApp(db: DB) {
const username = c.req.param("username");
const amount = c.req.query("amount");
const comment = c.req.query("comment") || "";
logger.debug("LNURLp callback", { username, amount, comment });
const payerData = c.req.query("payerdata") ? JSON.parse(c.req.query("payerdata") || "") : null;
const nostr = c.req.query("nostr") ? JSON.parse(decodeURIComponent(c.req.query("nostr") || "")) : null;

// TODO: store data (e.g. for zaps)
logger.debug("LNURLp callback", { username, amount, comment, payerData, nostr });

if (!amount) {
throw new Error("No amount provided");
}

const isZapRequestValid = validateEvent(nostr)
const description = isZapRequestValid ? nostr.content : comment;

const content = isZapRequestValid ? JSON.stringify(nostr) : getLnurlMetadata(username);
const descriptionHash = await computeDescriptionHash(content);

const connectionSecret = await db.findWalletConnectionSecret(username);

const nwcClient = new nwc.NWCClient({
Expand All @@ -59,11 +83,20 @@ export function createLnurlApp(db: DB) {

const transaction = await nwcClient.makeInvoice({
amount: Math.floor(+amount / 1000) * 1000,
description: comment,
description,
metadata: {
comment: comment || undefined,
// TODO: payer_data can be improved using nostr worker
payer_data: payerData || undefined,
nostr: isZapRequestValid ? nostr : undefined,
},
description_hash: descriptionHash,
});

const invoice = await db.createInvoice(transaction);

return c.json({
verify: `${BASE_URL}/lnurlp/${username}/verify/${transaction.payment_hash}`,
verify: `${BASE_URL}/lnurlp/${username}/verify/${invoice.paymentHash}`,
routes: [],
pr: transaction.invoice,
});
Expand Down

0 comments on commit e9875b0

Please sign in to comment.