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
|
import config from "$config";
import { db, g, gf, s } from "$lib/db";
import { generate } from "$lib/invoices";
import { err } from "$lib/logging";
import { bail, fail, fields, getInvoice, getUser } from "$lib/utils";
import rpc from "@coinos/rpc";
export default {
async get(req, res) {
try {
const {
params: { id },
} = req;
if (id === "undefined") fail("invalid id");
const invoice = await getInvoice(id);
if (invoice) {
invoice.secret = undefined;
invoice.user = await getUser(invoice.uid, fields);
invoice.items ||= [];
}
if (invoice) res.send(invoice);
else fail("invoice not found");
} catch (e) {
bail(res, e.message);
}
},
async create(req, res) {
let { body, user } = req;
const { invoice } = body;
if (body.user) user = body.user;
if (req.user.username === user.username) invoice.own = true;
else invoice.own = false;
try {
// A supplied id that already exists is an UPDATE of that record —
// generate() rewrites it wholesale. Only the authenticated owner may do
// that. Without this, an anonymous pay-page session passing
// body.user = merchant could rewrite the amount/memo of a merchant's
// live invoice. generate() separately refuses to move an invoice to a
// different uid.
if (invoice?.id) {
const existing = await getInvoice(invoice.id);
if (existing && existing.uid !== req.user.id) fail("invoice not found");
}
const result = await generate({ invoice, user });
res.send(result);
} catch (e) {
bail(res, e.message);
}
},
async update(req, res) {
try {
const { id } = req.params;
const { body } = req;
const { tip, webhook, secret } = body.invoice;
let invoice = await gf(`invoice:${id}`);
if (!invoice) fail("invoice not found");
const user = await g(`user:${invoice.uid}`);
if (typeof tip !== "undefined") {
const t = Number.parseInt(tip);
if (!Number.isSafeInteger(t) || t < 0 || t > 2_100_000_000_000_000)
fail("Invalid tip");
// Defense-in-depth: never change the tip once the invoice is paid/
// settled. The primary mint guard is on the credit side (it uses the
// tip the sender was actually debited); this stops post-settlement
// tampering. The endpoint stays open by design — payers set tips on
// merchants' invoices before paying — so we do NOT require ownership.
if (invoice.received > 0 || invoice.settled)
fail("Invoice already settled");
invoice.tip = t;
}
if (webhook && secret) {
if (invoice.uid !== req.user?.id) fail("Unauthorized");
invoice.webhook = webhook;
invoice.secret = secret;
}
invoice = await generate({ invoice, user });
await s(`invoice:${id}`, invoice);
res.send(invoice);
} catch (e) {
bail(res, e.message);
}
},
async list(req, res) {
const { id } = req.user;
let invoices = await db.lRange(`${id}:invoices`, 0, -1);
invoices = (await Promise.all(invoices.map((i) => getInvoice(i)))).filter(
Boolean,
);
res.send(invoices);
},
async sign(req, res) {
try {
const { address, message, type = "bitcoin" } = req.body;
// Sign only with an address the CALLER owns. Users have no per-user keys
// in this custodial design — every address belongs to the server hot
// wallet — so without this any authenticated account could have the node
// sign an arbitrary message under any address the wallet controls
// (another user's deposit address, a change address, a cold-storage
// address published as ours), which is a proof-of-control forgery
// primitive. The legitimate flow (coinos.io/sign, for Ocean offers that
// need a signable on-chain address) mints a fresh invoice address for
// the caller and posts that back, so it always satisfies this check.
const invoice = await getInvoice(address);
if (!invoice || (invoice.uid !== req.user.id && invoice.aid !== req.user.id))
fail("unauthorized");
const node = rpc(config[type]);
if (config[type].walletpass)
await node.walletPassphrase(config[type].walletpass, 300);
const signature = await node.signMessage({ address, message });
res.send({ signature });
} catch (e) {
bail(res, e.message);
}
},
};
|