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
|
import { createHash } from "node:crypto";
import { db, g } from "$lib/db";
import { parseEntry } from "$lib/invoices";
import { bail, fail } from "$lib/utils";
// Merchants pre-load payment preimages here, then have buyers create invoices
// with usePreimage: true — each such invoice consumes the next queued preimage
// so its payment hash is sha256(preimage). Once paid, the preimage is exposed
// on the invoice record, letting a static storefront sell decryption secrets
// with no backend of its own. An optional per-batch price — sats, or a fiat
// amount converted at invoice-creation time — is enforced at invoice creation;
// creation is unauthenticated, so without it a buyer could name their own
// price for the secret.
const MAX_QUEUE = 1000;
const key = (uid) => `${uid}:preimages`;
const sha256 = (hex) =>
createHash("sha256").update(Buffer.from(hex, "hex")).digest("hex");
export default {
async add(req, res) {
try {
const { preimages, price = null, fiat = null } = req.body;
let { currency = null } = req.body;
if (!Array.isArray(preimages) || !preimages.length)
fail("preimages array required");
if (
!preimages.every(
(p) => typeof p === "string" && /^[0-9a-f]{64}$/i.test(p),
)
)
fail("preimages must be 64-character hex strings");
if (price !== null && fiat !== null)
fail("specify price (sats) or fiat, not both");
if (price !== null && (!Number.isInteger(price) || price <= 0))
fail("price must be a positive integer amount of sats");
if (fiat !== null) {
if (typeof fiat !== "number" || !(fiat > 0))
fail("fiat must be a positive number");
currency ||= req.user.currency;
const rates = await g("rates");
if (!rates?.[currency]) fail(`unsupported currency ${currency}`);
} else {
currency = null;
}
const k = key(req.user.id);
const existing = await db.lRange(k, 0, -1);
const seen = new Set(existing.map((e) => parseEntry(e).preimage));
const fresh = [];
for (let p of preimages) {
p = p.toLowerCase();
if (seen.has(p)) continue;
// A preimage that already settled (or backs a live invoice) can't be
// credited again — reject it rather than let a buyer pay for nothing.
if (
(await db.exists(`credited:${p}`)) ||
(await db.exists(`preimage:${sha256(p)}`))
)
fail("preimage already used");
seen.add(p);
fresh.push(p);
}
if (existing.length + fresh.length > MAX_QUEUE)
fail(`queue limited to ${MAX_QUEUE} preimages`);
if (fresh.length)
await db.rPush(
k,
fresh.map((preimage) =>
JSON.stringify({ preimage, price, fiat, currency }),
),
);
res.send({
queued: existing.length + fresh.length,
added: fresh.map((preimage) => ({
preimage,
hash: sha256(preimage),
price,
fiat,
currency,
})),
});
} catch (e) {
bail(res, e.message);
}
},
async list(req, res) {
try {
const entries = await db.lRange(key(req.user.id), 0, -1);
res.send({
queued: entries.length,
preimages: entries.map((e) => {
const { preimage, price, fiat, currency } = parseEntry(e);
return { preimage, hash: sha256(preimage), price, fiat, currency };
}),
});
} catch (e) {
bail(res, e.message);
}
},
async clear(req, res) {
try {
await db.del(key(req.user.id));
res.send({ queued: 0 });
} catch (e) {
bail(res, e.message);
}
},
};
|