aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/migrate/+page.server.ts
blob: f1964d1bdb7793e0aeefd670c65ce337049fe905 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { auth, post } from "$lib/utils";
import { redirect } from "@sveltejs/kit";

const REGISTRAR = "https://names.coinos.io";

// An invoice from the v3 registrar for this address and amount (LNURL-pay).
async function invoiceFor(address: string, sats: number) {
	const [name, domain] = String(address).split("@");
	if (!name || !domain) throw new Error("bad destination address");
	const p = await fetch(
		`${REGISTRAR}/.well-known/lnurlp/${name}?domain=${encodeURIComponent(domain)}`,
	).then((r) => r.json());
	if (!p?.callback) throw new Error(p?.reason || "that address isn't registered on coinos v3 yet");
	const msat = sats * 1000;
	if (msat < p.minSendable) throw new Error("balance is below the destination's minimum");
	if (msat > p.maxSendable) throw new Error("balance is above the destination's maximum");
	const sep = p.callback.includes("?") ? "&" : "?";
	const inv = await fetch(`${p.callback}${sep}amount=${msat}`).then((r) => r.json());
	if (!inv?.pr) throw new Error(inv?.reason || "could not get an invoice");
	return inv.pr as string;
}

export async function load({ parent, url }) {
	const { user } = await parent();
	if (!user) redirect(307, `/login?redirect=${encodeURIComponent(url.pathname + url.search)}`);
	return {
		to: url.searchParams.get("to") || "",
		back: url.searchParams.get("back") || "",
	};
}

export const actions = {
	default: async ({ cookies, request, url }) => {
		const form = await request.formData();
		const to = String(form.get("to") || url.searchParams.get("to") || "");
		const pin = String(form.get("pin") || "");
		const balance = Number(form.get("balance") || 0);
		const username = String(form.get("username") || "");
		if (!to) return { error: "no destination" };

		let sent = 0;
		try {
			if (balance > 0) {
				// coinos and the ASP peer directly, so a sweep normally costs
				// nothing. Try the whole balance first and only hold a little
				// back if the payment actually comes up short.
				const attempts = [balance, balance - Math.max(1, Math.ceil(balance * 0.005))];
				let lastErr;
				for (const amount of attempts) {
					if (amount <= 0) continue;
					try {
						const payreq = await invoiceFor(to, amount);
						await post("/payments", { payreq, ...(pin ? { pin } : {}) }, auth(cookies));
						sent = amount;
						break;
					} catch (e) {
						lastErr = e;
						const msg = String((e as any)?.message || e);
						// only a shortfall is worth retrying smaller
						if (!/insufficient|balance|not enough|fee/i.test(msg)) throw e;
					}
				}
				if (!sent) throw lastErr;
			}
			// Hand the name over for receiving. The account itself stays exactly
			// as it is — same username, same history, still usable here; the
			// registrar just becomes the answer for incoming payments.
			await post("/user", { migrated: true, ...(pin ? { pin } : {}) }, auth(cookies));
			return { ok: true, sent, released: username };
		} catch (e) {
			const msg = e && typeof e === "object" && "message" in e ? String(e.message) : String(e);
			return { error: msg, sent };
		}
	},
};