aboutsummaryrefslogtreecommitdiffstats
path: root/lib/bip353.ts
blob: 2e6e1473cf43c1046e7a55a1625e43c3e2dbe07e (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
import config from "$config";
import { db, g, s } from "$lib/db";
import ln from "$lib/ln";
import { l, warn } from "$lib/logging";
import { v4 } from "uuid";
import { PaymentType } from "$lib/types";

const cfApi = "https://api.cloudflare.com/client/v4";
const cfHeaders = () => ({
  Authorization: `Bearer ${config.cloudflare?.apiToken}`,
  "Content-Type": "application/json",
});

const createDnsRecord = async (username: string, bolt12: string) => {
  const { apiToken, zoneId } = config.cloudflare || {};
  if (!apiToken || !zoneId) return;

  const name = `${username}.user._bitcoin-payment.${config.hostname}`;
  const content = `bitcoin:?lno=${bolt12}`;

  const listRes = await fetch(
    `${cfApi}/zones/${zoneId}/dns_records?type=TXT&name=${name}`,
    { headers: cfHeaders() },
  );
  const listData = (await listRes.json()) as any;

  if (listData.result?.length) {
    const recordId = listData.result[0].id;
    await fetch(`${cfApi}/zones/${zoneId}/dns_records/${recordId}`, {
      method: "PUT",
      headers: cfHeaders(),
      body: JSON.stringify({ type: "TXT", name, content, ttl: 3600 }),
    });
  } else {
    await fetch(`${cfApi}/zones/${zoneId}/dns_records`, {
      method: "POST",
      headers: cfHeaders(),
      body: JSON.stringify({ type: "TXT", name, content, ttl: 3600 }),
    });
  }
};

const deleteDnsRecord = async (username: string) => {
  const { apiToken, zoneId } = config.cloudflare || {};
  if (!apiToken || !zoneId) return;

  const name = `${username}.user._bitcoin-payment.${config.hostname}`;
  const listRes = await fetch(
    `${cfApi}/zones/${zoneId}/dns_records?type=TXT&name=${name}`,
    { headers: cfHeaders() },
  );
  const listData = (await listRes.json()) as any;

  for (const record of listData.result || []) {
    await fetch(`${cfApi}/zones/${zoneId}/dns_records/${record.id}`, {
      method: "DELETE",
      headers: cfHeaders(),
    });
  }
};

export const setupBip353 = async (user) => {
  const existing = await g(`bip353:${user.username}`);
  if (existing) {
    await createDnsRecord(user.username, existing.bolt12);
    return;
  }

  const r = await ln.offer({
    amount: "any",
    label: `bip353 ${user.username}`,
    description: `Pay ${user.username}`,
  });

  await s(`bip353:${user.username}`, {
    offer_id: r.offer_id,
    bolt12: r.bolt12,
  });

  // Template invoice so the lightning listener can route payments
  const id = v4();
  const rates = await g("rates");
  const currency = user.currency || "USD";

  const inv = {
    id,
    aid: user.id,
    amount: 0,
    currency,
    hash: r.bolt12,
    rate: rates?.[currency] || 0,
    memo: "",
    path: null,
    pending: 0,
    received: 0,
    tip: 0,
    type: PaymentType.bolt12,
    uid: user.id,
    created: Date.now(),
  };

  await s(`invoice:${r.offer_id}`, id);
  await s(`invoice:${id}`, inv);

  await createDnsRecord(user.username, r.bolt12);
  l("BIP 353 setup complete for", user.username);
};

export const teardownBip353 = async (user) => {
  const existing = await g(`bip353:${user.username}`);
  if (!existing) return;

  if (existing.offer_id) {
    await ln.disableoffer(existing.offer_id).catch(() => {});
    await db.del(`invoice:${existing.offer_id}`);
  }

  await db.del(`bip353:${user.username}`);
  await deleteDnsRecord(user.username);
  l("BIP 353 teardown complete for", user.username);
};