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
|
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 { zoneId } = config.cloudflare;
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);
};
|