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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
|
import config from "$config";
import { g, s } from "$lib/db";
import { lnb } from "$lib/ln";
import { safeGot } from "$lib/safe-fetch";
import { fail, wait } from "$lib/utils";
import {
CashuMint,
CashuWallet,
MintQuoteState,
PaymentRequest,
PaymentRequestTransportType,
getDecodedToken,
getEncodedToken,
getEncodedTokenV4,
} from "@cashu/cashu-ts";
const { URL } = process.env;
const m = new CashuMint(config.mintUrl);
const w = new CashuWallet(m);
const enc = (proofs) =>
getEncodedToken({
mint: config.mintUrl,
proofs,
});
// Serialize every read-modify-write of the shared `cash` house-wallet token. The
// mint swap sits between reading the current proofs and writing the merged set,
// so two concurrent claim()/mint() calls would each read the same `current` and
// the later write would clobber the earlier — the earlier call's freshly-swapped
// proofs vanish from the stored token while both users stay credited (an
// insolvency leak). This mutex is in-process; if the server ever runs
// multi-process, move to a redis lock (WATCH/MULTI or SET NX).
let cashLock: Promise<void> = Promise.resolve();
const withCashLock = async <T>(fn: () => Promise<T>): Promise<T> => {
const prev = cashLock;
let release: () => void = () => {};
cashLock = new Promise((res) => {
release = res;
});
await prev;
try {
return await fn();
} finally {
release();
}
};
// `mint` comes straight out of an attacker-supplied cashu token, and this is
// the first thing we do with it — so fetching it with cashu-ts's plain fetch
// made every /cash and /ecash entry point an unauthenticated SSRF probe into
// the internal network. Use the same IP-pinning fetcher lnurl resolution uses
// (it rejects loopback/private/link-local/CGNAT/metadata targets and closes the
// DNS-rebinding window by pinning the socket to the IP it validated). NUT-06
// /v1/info is exactly what CashuMint.getInfo() would have called.
const ext = async (mint) => {
const { pubkey: issuerPk } = await safeGot(
`${String(mint).replace(/\/+$/, "")}/v1/info`,
);
const { pubkey: ourPk } = await m.getInfo();
return issuerPk !== ourPk;
};
export async function get(id) {
const token = await g(`cash:${id}`);
return token;
}
export async function claim(token) {
const { mint } = getDecodedToken(token);
if (await ext(mint)) fail("Unable to receive from other mints");
return withCashLock(async () => {
const { proofs: current } = getDecodedToken(await g("cash"));
const rcvd = await w.receive(token);
await s("cash", enc([...current, ...rcvd]));
return rcvd.reduce((a, b) => a + b.amount, 0);
});
}
export async function mint(amount) {
const { keysets } = await m.getKeySets();
const w = new CashuWallet(m, { keysets });
return withCashLock(async () => {
const { proofs } = getDecodedToken(await g("cash"));
const { send, keep } = await w.send(amount, proofs);
const rcvd = await w.receive(enc(send));
const change = enc(keep);
await s("cash", change);
return enc(rcvd);
});
}
export async function check(token) {
const { mint, proofs } = getDecodedToken(token);
const total = proofs.reduce((a, b) => a + b.amount, 0);
const external = await ext(mint);
let spent = 0;
for (const [i, p] of (await w.checkProofsStates(proofs)).entries()) {
if (p.state === "SPENT") spent += proofs[i].amount;
}
return { total, spent, mint, external };
}
export async function init(amount = 100000) {
try {
await new Promise((r) => setTimeout(r, 2000));
const { quote, request } = await w.createMintQuote(amount);
await lnb.pay(request);
await wait(async () => {
const { state } = await w.checkMintQuote(quote);
return state === MintQuoteState.PAID;
});
const proofs = await w.mintProofs(amount, quote);
const cash = getEncodedTokenV4({
mint: config.mintUrl,
proofs,
});
await s("cash", cash);
} catch (e) {}
}
export function request(uuid, amount, memo) {
const target = `${URL}/api/ecash/${uuid}`;
const { POST: type } = PaymentRequestTransportType;
const transport = [{ type, target }];
const unit = "sat";
return new PaymentRequest(
transport,
uuid,
amount,
unit,
[config.mintUrl],
memo,
).toEncodedRequest();
}
|