aboutsummaryrefslogtreecommitdiffstats
path: root/src/lib/parse.ts
blob: e7fa6af218349b1aa4738b66c48d854d82ebf126 (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
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import { PUBLIC_DOMAIN } from "$env/static/public";
import { decode } from "$lib/bip21";
import { auth, get, isLiquid, post, sats } from "$lib/utils";
import { redirect } from "@sveltejs/kit";

let validatePromise: Promise<typeof import("bitcoin-address-validation")> | undefined;
const loadValidate = () => (validatePromise ||= import("bitcoin-address-validation"));

export default async (s, host, cookies) => {
  if (!s) return;
  let t = s.trim();
  let amount;
  let invoice;
  let user;
  const aid = cookies.get("aid");

  if (t.startsWith("ark") || t.startsWith("tark")) redirect(303, `/send/ark/${t}${aid ? `?aid=${aid}` : ""}`);

  if (t.startsWith("http")) redirect(303, t);
  if (t.startsWith(host)) redirect(303, `http://${t}`);

  if (t.includes("lightning=")) {
    const url = new URL(t);
    const params = new URLSearchParams(url.search);
    t = params.get("lightning");
  }

  if (t.startsWith("lightning:")) t = t.replace("lightning:", "");
  if (t.includes("@") && t.split("@").pop() === PUBLIC_DOMAIN) t = t.split("@")[0];
  if (t.includes("@") && t.includes(".")) {
    try {
      t = await get(`/encode?address=${t}`);
    } catch {}
  }

  if (t.toLowerCase().startsWith("nostr:")) {
    t = t.split(":")[1];
  }

  if (["note", "nevent"].some((p) => t.startsWith(p))) redirect(303, `/e/${t}`);
  if (["nprofile", "npub"].some((p) => t.startsWith(p))) redirect(303, `/${t}`);

  // bitcoin
  if (t.toLowerCase().startsWith("bitcoin:") || t.toLowerCase().startsWith("liquidnetwork:")) {
    const { address: addr, options } = decode(t);
    amount = options.amount;

    if (options.ark) {
      let r = `/send/ark/${options.ark}`;
      if (amount) r += `/${Math.round(Number(amount) * sats)}`;
      redirect(303, r);
    }

    t = addr;
  }

  const { validate } = await loadValidate();
  if (validate(t) || isLiquid(t) || t.startsWith("bcrt1")) {
    let r = `/send/bitcoin/${t}`;
    if (amount) r += `/${Math.round(amount * sats)}`;
    redirect(303, r);
  }

  // lightning
  if (t.toLowerCase().startsWith("lightning:")) t = t.toLowerCase().replace("lightning:", "");
  if (t.toLowerCase().startsWith("lnurl")) redirect(303, `/ln/${t}`);
  if (t.includes(":")) t = t.split(":")[1];

  if (t.toLowerCase().startsWith("lnr")) {
    const { iid } = await post("/sendinvoice", { invreq: t }, auth(cookies));
    invoice = { id: iid };
  } else if (t.toLowerCase().startsWith("ln")) {
    try {
      const aid = cookies.get("aid");
      invoice = await get(`/invoice/${t}`);
      if (invoice.user.username === "mint") throw new Error("mint payment");
      if (aid && aid !== invoice.aid) throw new Error("cross-account");
    } catch {
      if (t.toLowerCase().startsWith("lno")) {
        const { offer_amount_msat: a } = await get(`/decode/${t}`);
        if (a) ({ invoice: t } = await post("/fetchinvoice", { offer: t }));
      }
      redirect(303, `/send/lightning/${t}`);
    }
  }

  if (invoice) redirect(303, `/invoice/${invoice.id}`);

  if (t.match(/^🥜([\uFE00-\uFE0F]|[\uE0100-\uE01EF]+)$/)) {
    t = (Array.from(t) as string[])
      .slice(1)
      .map((char: string) => {
        const codePoint = char.codePointAt(0)!;

        // Handle Variation Selectors (VS1-VS16): U+FE00 to U+FE0F
        if (codePoint >= 0xfe00 && codePoint <= 0xfe0f) {
          const byteValue = codePoint - 0xfe00; // Maps FE00->0, FE01->1, ..., FE0F->15
          return String.fromCharCode(byteValue);
        }

        // Handle Variation Selectors Supplement (VS17-VS256): U+E0100 to U+E01EF
        if (codePoint >= 0xe0100 && codePoint <= 0xe01ef) {
          const byteValue = codePoint - 0xe0100 + 16; // Maps E0100->16, E0101->17, ..., E01EF->255
          return String.fromCharCode(byteValue);
        }
      })
      .join("");
  }

  if (t.startsWith("cashu")) {
    const { id } = await post("/cash", { token: t });
    redirect(303, `/ecash/${id}`);
  }

  if (t.startsWith("creq")) redirect(303, `/send/ecash/${t}`);

  // user
  try {
    user = await get(`/users/${t.split("/")[0]}`);
    if (user.anon) user = null;
  } catch {}

  if (user) redirect(303, `/pay/${t}`);

  let fund;
  if (t.includes("/fund")) redirect(303, t.substring(t.indexOf("/fund")));

  try {
    fund = await get(`/fund/${t}`);
  } catch {}

  if (fund) redirect(303, `/send/fund/${t}`);
};