139 lines
3.3 KiB
TypeScript
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",
|
|
},
|
|
});
|
|
}
|