PortalBTC/src/routes/.well-known/lnurlp/[username]/+server.ts

139 lines
3.3 KiB
TypeScript

import { error, json } from "@sveltejs/kit";
import { BASE_DOMAIN, MAX_SATS_RECEIVE, MIN_SATS_RECEIVE } from "$lib/config";
import {
isNpub,
userExists,
userExistsOrNpub,
validateLightningInvoiceAmount,
} from "$lib";
import { getDb } from "$lib/database";
import { nip19 } from "nostr-tools";
interface LNURLPayRequest {
callback: string;
minSendable: number;
maxSendable: number;
metadata: string;
tag: "payRequest";
}
/** @type {import('./$types').RequestHandler} */
export async function GET({ params, url, request }) {
const { username } = params;
const amount = url.searchParams.get("amount");
if (!/^[a-z0-9\-_.]+$/.test(username)) {
throw error(400, "Invalid username format");
}
if (!(await userExistsOrNpub(username))) {
throw error(404, "User not found");
}
if (
amount && !validateLightningInvoiceAmount(amount, { isMillisats: true })
) {
throw error(
400,
`Amount must be between ${MIN_SATS_RECEIVE} and ${MAX_SATS_RECEIVE} millisats`,
);
}
const domain = request.headers.get("host") || new URL(BASE_DOMAIN).host;
const identifier = `${username}@${domain}`;
const response: LNURLPayRequest = {
callback: `https://${domain}/.well-known/lnurl-pay/callback/${
encodeURIComponent(
username,
)
}`,
minSendable: MIN_SATS_RECEIVE * 1000,
maxSendable: MAX_SATS_RECEIVE * 1000,
metadata: JSON.stringify([
["text/plain", `Pay ${username}`],
["text/identifier", identifier],
]),
tag: "payRequest",
};
return json(response);
}
/** @type {import('./$types').RequestHandler} */
export async function POST({ params, url, request }) {
// TODO: creating a username should cost a small amount of sats
// 105k sats for 1 character names
// 63k sats for 2 character names
// 42k sats for 3 character names
// 21k sats for 4 character names
// 10k sats otherwise
const { username } = params;
if (username.length > 32) {
throw error(400, "Username is too long. Maximum length is 32 characters.");
}
if (username.length < 5) {
throw error(
400,
"Username is too short. Minimum length is 5 characters. Shorter usernames are reserved for future use.",
);
}
if (!/^[a-z0-9\-_]+$/.test(username)) {
throw error(
400,
"Invalid username format. Only lowercase letters, numbers, and hyphens are allowed.",
);
}
if (await userExists(username)) {
throw error(400, "User already exists");
}
if (isNpub(username)) {
throw error(
400,
"Username is a valid npub. Please use a username instead.",
);
}
const db = await getDb();
const body = await request.json();
const { npub }: {
npub: `npub1${string}`;
} = body;
if (!npub) {
throw error(400, "Missing npub");
}
try {
const decoded = nip19.decode(npub);
if (decoded.type !== "npub") {
throw error(400, "Invalid npub");
}
} catch {
throw error(400, "Invalid npub");
}
await db.execute(
"INSERT INTO users (username, npub) VALUES (?, ?)",
[username, npub],
);
return json({
success: true,
});
}
export async function OPTIONS() {
return new Response(null, {
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
});
}