aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/migrate/+page.server.ts
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-03 10:44:46 -0700
committerAdam Soltys <[email protected]>2026-08-03 10:44:46 -0700
commit2cc244c19632e9f9f3083e6edf71649a366ba719 (patch)
treedcd499e6e590908e642e814858d6bab70e874066 /src/routes/(app)/migrate/+page.server.ts
parent7b2335636e28f43eb8d48c5d9fa3a3945421cf34 (diff)
downloadcoinos-ui-2cc244c19632e9f9f3083e6edf71649a366ba719.tar.gz
coinos-ui-2cc244c19632e9f9f3083e6edf71649a366ba719.zip
migrate: run the sweep server-side, and send the whole balance
Two fixes from the first live test: The POST went out from the browser, where $lib/utils' base is "" — so it hit the SvelteKit page route and came back "POST method not allowed". Payments in this app all run server-side with auth(cookies), because that's where the session token is usable; /migrate is a form action now like the rest of the send flows. And the routing reserve is gone: the ASP's node has two direct channels to ln.coinos.io, so a sweep is one hop and costs nothing. It sends the full balance, only retrying 0.5% lighter if a payment actually comes back short. Co-Authored-By: Claude Fable 5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Wvj4aoeMZwA7VUGdJP9Swi
Diffstat (limited to 'src/routes/(app)/migrate/+page.server.ts')
-rw-r--r--src/routes/(app)/migrate/+page.server.ts67
1 files changed, 67 insertions, 0 deletions
diff --git a/src/routes/(app)/migrate/+page.server.ts b/src/routes/(app)/migrate/+page.server.ts
index 4529c14c..dc3beb02 100644
--- a/src/routes/(app)/migrate/+page.server.ts
+++ b/src/routes/(app)/migrate/+page.server.ts
@@ -1,5 +1,25 @@
+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)}`);
@@ -8,3 +28,50 @@ export async function load({ parent, url }) {
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;
+ }
+ // Releasing the username is what lets them keep it on v3.
+ await post(
+ "/user",
+ { username: `${username}_v2`, ...(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 };
+ }
+ },
+};