aboutsummaryrefslogtreecommitdiffstats
path: root/src/routes/(app)/migrate/+page.svelte
blob: 4493709ba0454753767b146ecbb8efbd2a5dc275 (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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
<script>
  // Migrate a legacy coinos account to coinos v3.
  //
  // v3 sends the user here with ?to=<their new payment address>&back=<url>.
  // They are already signed in on this site, so nothing has to handle their
  // password: we sweep the balance over Lightning to the new wallet and then
  // release the username, which lets v3 claim it on the new registrar.
  import { post, success } from "$lib/utils";
  import { goto } from "$app/navigation";

  let { data } = $props();
  let { user, to, back } = $derived(data);

  let step = $state("intro"); // intro | working | done | error
  let note = $state("");
  let error = $state("");
  let pin = $state("");
  let sent = $state(0);

  let balance = $derived(user?.balance || 0);
  // Lightning routing takes a cut on the way out; leave a little behind so
  // the sweep can't fail for being a few sats over.
  let reserve = $derived(Math.max(10, Math.ceil(balance * 0.005)));
  let amount = $derived(Math.max(0, balance - reserve));
  let newName = $derived(`${user?.username}_v2`);

  // Ask the v3 registrar for an invoice for this amount (LNURL-pay against
  // the destination address). The registrar is CORS-open.
  async function invoiceFor(address, sats) {
    const [name, domain] = String(address).split("@");
    if (!name || !domain) throw new Error("bad destination address");
    const base = "https://names.coinos.io";
    const p = await fetch(`${base}/.well-known/lnurlp/${name}?domain=${domain}`).then((r) => r.json());
    if (!p?.callback) throw new Error(p?.reason || "destination not found");
    const msat = sats * 1000;
    if (msat < p.minSendable || msat > p.maxSendable) throw new Error("amount out of range for the destination");
    const inv = await fetch(`${p.callback}${p.callback.includes("?") ? "&" : "?"}amount=${msat}`).then((r) => r.json());
    if (!inv?.pr) throw new Error(inv?.reason || "could not get an invoice");
    return inv.pr;
  }

  async function migrate() {
    error = "";
    step = "working";
    try {
      if (amount > 0) {
        note = "Getting an invoice from your new wallet…";
        const payreq = await invoiceFor(to, amount);
        note = `Sending ${amount.toLocaleString()} sats…`;
        await post("/payments", { payreq, ...(pin ? { pin } : {}) });
        sent = amount;
      }
      note = "Releasing your username…";
      await post("/user", { username: newName, ...(pin ? { pin } : {}) });
      step = "done";
      success("Migrated");
    } catch (e) {
      error = String(e && e.message ? e.message : e);
      step = "error";
    }
  }

  function goBack() {
    const url = new URL(back || "https://v3.coinos.io/");
    url.searchParams.set("migrated", user.username);
    goto(url.toString(), { replaceState: true });
  }
</script>

<div class="max-w-md mx-auto p-4 space-y-5">
  <h1 class="text-2xl font-bold">Move your account to coinos v3</h1>

  {#if !to}
    <p class="text-red-600">
      This page needs a destination. Start the migration from coinos v3.
    </p>
  {:else if step === "done"}
    <div class="space-y-3">
      <p class="text-xl">✓ All done!</p>
      {#if sent}
        <p>{sent.toLocaleString()} sats are on their way to your new wallet.</p>
      {/if}
      <p>
        Your old username was released, so <strong>{user.username}</strong> can now be
        yours on coinos v3.
      </p>
      <button class="btn btn-primary w-full" onclick={goBack}>Back to coinos v3</button>
    </div>
  {:else if step === "working"}
    <p>{note}</p>
    <progress class="progress w-full"></progress>
  {:else}
    <div class="space-y-3">
      <p>
        This moves your balance to your new wallet and frees up your username so you can
        keep it there. Your old account stays, renamed to
        <strong>{newName}</strong>, and stops receiving payments.
      </p>
      <div class="rounded-xl border p-3 space-y-1">
        <div class="flex justify-between"><span>Balance</span><strong>{balance.toLocaleString()} sats</strong></div>
        <div class="flex justify-between"><span>Sending</span><strong>{amount.toLocaleString()} sats</strong></div>
        <div class="flex justify-between text-sm opacity-60"><span>Left for routing</span><span>{reserve.toLocaleString()} sats</span></div>
        <div class="flex justify-between break-all gap-2"><span>To</span><span>{to}</span></div>
      </div>
      {#if user?.haspin}
        <input class="input input-bordered w-full" type="password" inputmode="numeric"
          placeholder="Your PIN" bind:value={pin} />
      {/if}
      {#if error}<p class="text-red-600">{error}</p>{/if}
      <button class="btn btn-primary w-full" onclick={migrate} disabled={!to}>
        Move {amount.toLocaleString()} sats and release {user.username}
      </button>
      <p class="text-sm opacity-60">
        You can also do nothing — your old account keeps working as it is.
      </p>
    </div>
  {/if}
</div>