diff options
| author | Adam Soltys <[email protected]> | 2026-08-03 10:44:46 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-08-03 10:44:46 -0700 |
| commit | 2cc244c19632e9f9f3083e6edf71649a366ba719 (patch) | |
| tree | dcd499e6e590908e642e814858d6bab70e874066 /src/routes/(app)/migrate/+page.server.ts | |
| parent | 7b2335636e28f43eb8d48c5d9fa3a3945421cf34 (diff) | |
| download | coinos-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.ts | 67 |
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 }; + } + }, +}; |
