UNPKG

@cashu/cashu-ts

Version:

cashu library for communicating with a cashu mint

1,693 lines 81.3 kB
import { Buffer as G } from "buffer"; import { verifyDLEQProof_reblind as Gt } from "./crypto/client/NUT12.es.js"; import { pointFromHex as ot, hashToCurve as Et } from "./crypto/common.es.js"; import { hexToBytes as Q, bytesToHex as z } from "@noble/curves/abstract/utils"; import { sha256 as Vt } from "@noble/hashes/sha256"; import { signP2PKProofs as St } from "./crypto/client/NUT11.es.js"; import { signMintQuote as Jt } from "./crypto/client/NUT20.es.js"; import { constructProofFromPromise as Xt, serializeProof as Yt, blindMessage as ht } from "./crypto/client.es.js"; import { hexToBytes as Pt, bytesToHex as X, randomBytes as It } from "@noble/hashes/utils"; import { deriveSecret as Zt, deriveBlindingFactor as te } from "./crypto/client/NUT09.es.js"; function ee(n) { return G.from(n).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); } function Rt(n) { return G.from(n, "base64"); } function xt(n) { const t = JSON.stringify(n); return re(G.from(t).toString("base64")); } function se(n) { const t = G.from(ne(n), "base64").toString(); return JSON.parse(t); } function ne(n) { return n.replace(/-/g, "+").replace(/_/g, "/").split("=")[0]; } function re(n) { return n.replace(/\+/g, "-").replace(/\//g, "_").split("=")[0]; } function oe(n) { return typeof n == "number" || typeof n == "string"; } function yt(n) { const t = []; return gt(n, t), new Uint8Array(t); } function gt(n, t) { if (n === null) t.push(246); else if (n === void 0) t.push(247); else if (typeof n == "boolean") t.push(n ? 245 : 244); else if (typeof n == "number") Ut(n, t); else if (typeof n == "string") Ft(n, t); else if (Array.isArray(n)) ae(n, t); else if (n instanceof Uint8Array) ie(n, t); else if ( // Defensive: POJO only (null/array handled above) typeof n == "object" && n !== null && !Array.isArray(n) ) ce(n, t); else throw new Error("Unsupported type"); } function Ut(n, t) { if (n < 24) t.push(n); else if (n < 256) t.push(24, n); else if (n < 65536) t.push(25, n >> 8, n & 255); else if (n < 4294967296) t.push(26, n >> 24, n >> 16 & 255, n >> 8 & 255, n & 255); else throw new Error("Unsupported integer size"); } function ie(n, t) { const e = n.length; if (e < 24) t.push(64 + e); else if (e < 256) t.push(88, e); else if (e < 65536) t.push(89, e >> 8 & 255, e & 255); else if (e < 4294967296) t.push( 90, e >> 24 & 255, e >> 16 & 255, e >> 8 & 255, e & 255 ); else throw new Error("Byte string too long to encode"); for (let s = 0; s < n.length; s++) t.push(n[s]); } function Ft(n, t) { const e = new TextEncoder().encode(n), s = e.length; if (s < 24) t.push(96 + s); else if (s < 256) t.push(120, s); else if (s < 65536) t.push(121, s >> 8 & 255, s & 255); else if (s < 4294967296) t.push( 122, s >> 24 & 255, s >> 16 & 255, s >> 8 & 255, s & 255 ); else throw new Error("String too long to encode"); for (let r = 0; r < e.length; r++) t.push(e[r]); } function ae(n, t) { const e = n.length; if (e < 24) t.push(128 | e); else if (e < 256) t.push(152, e); else if (e < 65536) t.push(153, e >> 8, e & 255); else throw new Error("Unsupported array length"); for (const s of n) gt(s, t); } function ce(n, t) { const e = Object.keys(n); Ut(e.length, t), t[t.length - 1] |= 160; for (const s of e) Ft(s, t), gt(n[s], t); } function wt(n) { const t = new DataView(n.buffer, n.byteOffset, n.byteLength); return it(t, 0).value; } function it(n, t) { if (t >= n.byteLength) throw new Error("Unexpected end of data"); const e = n.getUint8(t++), s = e >> 5, r = e & 31; switch (s) { case 0: return ue(n, t, r); case 1: return he(n, t, r); case 2: return le(n, t, r); case 3: return de(n, t, r); case 4: return fe(n, t, r); case 5: return pe(n, t, r); case 7: return ye(n, t, r); default: throw new Error(`Unsupported major type: ${s}`); } } function V(n, t, e) { if (e < 24) return { value: e, offset: t }; if (e === 24) return { value: n.getUint8(t++), offset: t }; if (e === 25) { const s = n.getUint16(t, !1); return t += 2, { value: s, offset: t }; } if (e === 26) { const s = n.getUint32(t, !1); return t += 4, { value: s, offset: t }; } if (e === 27) { const s = n.getUint32(t, !1), r = n.getUint32(t + 4, !1); return t += 8, { value: s * 2 ** 32 + r, offset: t }; } throw new Error(`Unsupported length: ${e}`); } function ue(n, t, e) { const { value: s, offset: r } = V(n, t, e); return { value: s, offset: r }; } function he(n, t, e) { const { value: s, offset: r } = V(n, t, e); return { value: -1 - s, offset: r }; } function le(n, t, e) { const { value: s, offset: r } = V(n, t, e); if (r + s > n.byteLength) throw new Error("Byte string length exceeds data length"); return { value: new Uint8Array(n.buffer, n.byteOffset + r, s), offset: r + s }; } function de(n, t, e) { const { value: s, offset: r } = V(n, t, e); if (r + s > n.byteLength) throw new Error("String length exceeds data length"); const o = new Uint8Array(n.buffer, n.byteOffset + r, s); return { value: new TextDecoder().decode(o), offset: r + s }; } function fe(n, t, e) { const { value: s, offset: r } = V(n, t, e), o = []; let a = r; for (let i = 0; i < s; i++) { const c = it(n, a); o.push(c.value), a = c.offset; } return { value: o, offset: a }; } function pe(n, t, e) { const { value: s, offset: r } = V(n, t, e), o = {}; let a = r; for (let i = 0; i < s; i++) { const c = it(n, a); if (!oe(c.value)) throw new Error("Invalid key type"); const h = it(n, c.offset); o[c.value] = h.value, a = h.offset; } return { value: o, offset: a }; } function me(n) { const t = (n & 31744) >> 10, e = n & 1023, s = n & 32768 ? -1 : 1; return t === 0 ? s * 2 ** -14 * (e / 1024) : t === 31 ? e ? NaN : s * (1 / 0) : s * 2 ** (t - 15) * (1 + e / 1024); } function ye(n, t, e) { if (e < 24) switch (e) { case 20: return { value: !1, offset: t }; case 21: return { value: !0, offset: t }; case 22: return { value: null, offset: t }; case 23: return { value: void 0, offset: t }; default: throw new Error(`Unknown simple value: ${e}`); } if (e === 24) return { value: n.getUint8(t++), offset: t }; if (e === 25) { const s = me(n.getUint16(t, !1)); return t += 2, { value: s, offset: t }; } if (e === 26) { const s = n.getFloat32(t, !1); return t += 4, { value: s, offset: t }; } if (e === 27) { const s = n.getFloat64(t, !1); return t += 8, { value: s, offset: t }; } throw new Error(`Unknown simple or float value: ${e}`); } class kt { constructor(t, e, s, r, o, a, i = !1, c) { this.transport = t, this.id = e, this.amount = s, this.unit = r, this.mints = o, this.description = a, this.singleUse = i, this.nut10 = c; } toRawRequest() { const t = {}; return this.transport && (t.t = this.transport.map((e) => ({ t: e.type, a: e.target, g: e.tags }))), this.id && (t.i = this.id), this.amount && (t.a = this.amount), this.unit && (t.u = this.unit), this.mints && (t.m = this.mints), this.description && (t.d = this.description), this.singleUse && (t.s = this.singleUse), this.nut10 && (t.nut10 = { k: this.nut10.kind, d: this.nut10.data, t: this.nut10.tags }), t; } toEncodedRequest() { const t = this.toRawRequest(), e = yt(t); return "creqA" + G.from(e).toString("base64"); } getTransport(t) { return this.transport?.find((e) => e.type === t); } static fromRawRequest(t) { const e = t.t ? t.t.map((r) => ({ type: r.t, target: r.a, tags: r.g })) : void 0, s = t.nut10 ? { kind: t.nut10.k, data: t.nut10.d, tags: t.nut10.t } : void 0; return new kt( e, t.i, t.a, t.u, t.m, t.d, t.s, s ); } static fromEncodedRequest(t) { if (!t.startsWith("creq")) throw new Error("unsupported pr: invalid prefix"); if (t[4] !== "A") throw new Error("unsupported pr version"); const s = t.slice(5), r = Rt(s), o = wt(r); return this.fromRawRequest(o); } } const ge = "A", we = "cashu"; function R(n, t, e, s) { if (e) { const o = Tt(e); if (o > n) throw new Error(`Split is greater than total amount: ${o} > ${n}`); if (e.some((a) => !Nt(a, t))) throw new Error("Provided amount preferences do not match the amounts of the mint keyset."); n = n - Tt(e); } else e = []; return Ot(t, "desc").forEach((o) => { const a = Math.floor(n / o); for (let i = 0; i < a; ++i) e?.push(o); n %= o; }), e.sort((o, a) => o - a); } function Mt(n, t, e, s) { const r = [], o = n.map((h) => h.amount); Ot(e, "asc").forEach((h) => { const u = o.filter((f) => f === h).length, l = Math.max(s - u, 0); for (let f = 0; f < l && !(r.reduce((d, g) => d + g, 0) + h > t); ++f) r.push(h); }); const i = t - r.reduce((h, u) => h + u, 0); return i && R(i, e).forEach((u) => { r.push(u); }), r.sort((h, u) => h - u); } function Ot(n, t = "desc") { return t == "desc" ? Object.keys(n).map((e) => parseInt(e)).sort((e, s) => s - e) : Object.keys(n).map((e) => parseInt(e)).sort((e, s) => e - s); } function Nt(n, t) { return n in t; } function ke(n) { return Bt(z(n)); } function Bt(n) { return BigInt(`0x${n}`); } function be(n) { return n.toString(16).padStart(64, "0"); } function vt(n) { return /^[a-f0-9]*$/i.test(n); } function Lt(n) { return Array.isArray(n) ? n.some((t) => !vt(t.id)) : vt(n.id); } function _e(n, t) { t && (n.proofs = at(n.proofs)); const e = { token: [{ mint: n.mint, proofs: n.proofs }] }; return n.unit && (e.unit = n.unit), n.memo && (e.memo = n.memo), we + ge + xt(e); } function Xe(n, t) { if (Lt(n.proofs) || t?.version === 3) { if (t?.version === 4) throw new Error("can not encode to v4 token if proofs contain non-hex keyset id"); return _e(n, t?.removeDleq); } return Ae(n, t?.removeDleq); } function Ae(n, t) { if (t && (n.proofs = at(n.proofs)), n.proofs.forEach((c) => { if (c.dleq && c.dleq.r == null) throw new Error("Missing blinding factor in included DLEQ proof"); }), Lt(n.proofs)) throw new Error("can not encode to v4 token if proofs contain non-hex keyset id"); const s = Ct(n), r = yt(s), o = "cashu", a = "B", i = ee(r); return o + a + i; } function Ct(n) { const t = {}, e = n.mint; for (let r = 0; r < n.proofs.length; r++) { const o = n.proofs[r]; t[o.id] ? t[o.id].push(o) : t[o.id] = [o]; } const s = { m: e, u: n.unit || "sat", t: Object.keys(t).map( (r) => ({ i: Q(r), p: t[r].map( (o) => ({ a: o.amount, s: o.secret, c: Q(o.C), ...o.dleq && { d: { e: Q(o.dleq.e), s: Q(o.dleq.s), r: Q(o.dleq.r ?? "00") } }, ...o.witness && { w: JSON.stringify(o.witness) } }) ) }) ) }; return n.memo && (s.d = n.memo), s; } function Qt(n) { const t = []; n.t.forEach( (s) => s.p.forEach((r) => { t.push({ secret: r.s, C: z(r.c), amount: r.a, id: z(s.i), ...r.d && { dleq: { r: z(r.d.r), s: z(r.d.s), e: z(r.d.e) } }, ...r.w && { witness: r.w } }); }) ); const e = { mint: n.m, proofs: t, unit: n.u || "sat" }; return n.d && (e.memo = n.d), e; } function Ee(n) { return ["web+cashu://", "cashu://", "cashu:", "cashu"].forEach((e) => { n.startsWith(e) && (n = n.slice(e.length)); }), Se(n); } function Se(n) { const t = n.slice(0, 1), e = n.slice(1); if (t === "A") { const s = se(e); if (s.token.length > 1) throw new Error("Multi entry token are not supported"); const r = s.token[0], o = { mint: r.mint, proofs: r.proofs, unit: s.unit || "sat" }; return s.memo && (o.memo = s.memo), o; } else if (t === "B") { const s = Rt(e), r = wt(s); return Qt(r); } throw new Error("Token version is not supported"); } function qt(n) { return Pe(n.keys) === n.id; } function Pe(n) { const t = Object.entries(n).sort((r, o) => +r[0] - +o[0]).map(([, r]) => Q(r)).reduce((r, o) => Ie(r, o), new Uint8Array()), e = Vt(t); return "00" + G.from(e).toString("hex").slice(0, 14); } function Ie(n, t) { const e = new Uint8Array(n.length + t.length); return e.set(n), e.set(t, n.length), e; } function F(n) { return typeof n == "object"; } function T(...n) { return n.map((t) => t.replace(/(^\/+|\/+$)/g, "")).join("/"); } function Wt(n) { return n.replace(/\/$/, ""); } function W(n) { return n.reduce((t, e) => t + e.amount, 0); } function Ye(n) { return kt.fromEncodedRequest(n); } class Me { get value() { return this._value; } set value(t) { this._value = t; } get next() { return this._next; } set next(t) { this._next = t; } constructor(t) { this._value = t, this._next = null; } } class ve { get first() { return this._first; } set first(t) { this._first = t; } get last() { return this._last; } set last(t) { this._last = t; } get size() { return this._size; } set size(t) { this._size = t; } constructor() { this._first = null, this._last = null, this._size = 0; } enqueue(t) { const e = new Me(t); return this._size === 0 || !this._last ? (this._first = e, this._last = e) : (this._last.next = e, this._last = e), this._size++, !0; } dequeue() { if (this._size === 0 || !this._first) return null; const t = this._first; return this._first = t.next, t.next = null, this._size--, t.value; } } function at(n) { return n.map((t) => { const e = { ...t }; return delete e.dleq, e; }); } function jt(n, t) { if (n.dleq == null) return !1; const e = { e: Q(n.dleq.e), s: Q(n.dleq.s), r: Bt(n.dleq.r ?? "00") }; if (!Nt(n.amount, t.keys)) throw new Error(`undefined key for amount ${n.amount}`); const s = t.keys[n.amount]; return !!Gt( new TextEncoder().encode(n.secret), e, ot(n.C), ot(s) ); } function qe(...n) { const t = n.reduce((r, o) => r + o.length, 0), e = new Uint8Array(t); let s = 0; for (let r = 0; r < n.length; r++) e.set(n[r], s), s = s + n[r].length; return e; } function Ze(n) { const t = new TextEncoder(), e = Ct(n), s = yt(e), r = t.encode("craw"), o = t.encode("B"); return qe(r, o, s); } function ts(n) { const t = new TextDecoder(), e = t.decode(n.slice(0, 4)), s = t.decode(new Uint8Array([n[4]])); if (e !== "craw" || s !== "B") throw new Error("not a valid binary token"); const r = n.slice(5), o = wt(r); return Qt(o); } function Tt(n) { return n.reduce((t, e) => t + e, 0); } let ct; typeof WebSocket < "u" && (ct = WebSocket); function es(n) { ct = n; } function Te() { if (ct === void 0) throw new Error("WebSocket implementation not initialized"); return ct; } const M = { FATAL: "FATAL", ERROR: "ERROR", WARN: "WARN", INFO: "INFO", DEBUG: "DEBUG", TRACE: "TRACE" }, O = { fatal() { }, error() { }, warn() { }, info() { }, debug() { }, trace() { }, log() { } }, Z = class Z { constructor(t = M.INFO) { this.minLevel = t; } logToConsole(t, e, s) { if (Z.SEVERITY[t] > Z.SEVERITY[this.minLevel]) return; const r = `[${t}] `; let o = e; const a = /* @__PURE__ */ new Set(); if (s) { const i = Object.fromEntries( Object.entries(s).map(([u, l]) => [ u, l instanceof Error ? { message: l.message, stack: l.stack } : l ]) ); o = e.replace(/\{(\w+)\}/g, (u, l) => { if (l in i && i[l] !== void 0) { a.add(l); const f = i[l]; return typeof f == "string" ? f : typeof f == "number" || typeof f == "boolean" ? f.toString() : f == null ? "" : JSON.stringify(f); } return u; }); const c = Object.fromEntries( Object.entries(i).filter(([u]) => !a.has(u)) ), h = this.getConsoleMethod(t); Object.keys(c).length > 0 ? h(r + o, c) : h(r + o); } else this.getConsoleMethod(t)(r + o); } // Note: NOT static as test suite needs to spy on the output getConsoleMethod(t) { switch (t) { case M.FATAL: case M.ERROR: return console.error; case M.WARN: return console.warn; case M.INFO: return console.info; case M.DEBUG: return console.debug; case M.TRACE: return console.trace; default: return console.log; } } // Interface methods fatal(t, e) { this.logToConsole(M.FATAL, t, e); } error(t, e) { this.logToConsole(M.ERROR, t, e); } warn(t, e) { this.logToConsole(M.WARN, t, e); } info(t, e) { this.logToConsole(M.INFO, t, e); } debug(t, e) { this.logToConsole(M.DEBUG, t, e); } trace(t, e) { this.logToConsole(M.TRACE, t, e); } log(t, e, s) { this.logToConsole(t, e, s); } }; Z.SEVERITY = { [M.FATAL]: 0, [M.ERROR]: 1, [M.WARN]: 2, [M.INFO]: 3, [M.DEBUG]: 4, [M.TRACE]: 5 }; let Kt = Z; function Ke() { const n = Date.now(); return { elapsed: () => Date.now() - n }; } class H { constructor() { this.connectionMap = /* @__PURE__ */ new Map(); } static getInstance() { return H.instance || (H.instance = new H()), H.instance; } getConnection(t, e) { if (this.connectionMap.has(t)) return this.connectionMap.get(t); const s = new De(t, e); return this.connectionMap.set(t, s), s; } } class De { constructor(t, e) { this.subListeners = {}, this.rpcListeners = {}, this.rpcId = 0, this.onCloseCallbacks = [], this._WS = Te(), this.url = new URL(t), this.messageQueue = new ve(), this._logger = e ?? O; } connect() { return this.connectionPromise || (this.connectionPromise = new Promise((t, e) => { try { this.ws = new this._WS(this.url.toString()), this.onCloseCallbacks = []; } catch (s) { e(s instanceof Error ? s : new Error(String(s))); return; } this.ws.onopen = () => { t(); }, this.ws.onerror = () => { e(new Error("Failed to open WebSocket")); }, this.ws.onmessage = (s) => { this.messageQueue.enqueue(s.data), this.handlingInterval || (this.handlingInterval = setInterval( this.handleNextMessage.bind(this), 0 )); }, this.ws.onclose = (s) => { this.connectionPromise = void 0, this.onCloseCallbacks.forEach((r) => r(s)); }; })), this.connectionPromise; } sendRequest(t, e) { if (this.ws?.readyState !== 1) { if (t === "unsubscribe") return; throw this._logger.error("Attempted sendRequest, but socket was not open"), new Error("Socket not open"); } const s = this.rpcId; this.rpcId++; const r = JSON.stringify({ jsonrpc: "2.0", method: t, params: e, id: s }); this.ws?.send(r); } /** * @deprecated Use cancelSubscription for JSONRPC compliance. */ closeSubscription(t) { this.ws?.send(JSON.stringify(["CLOSE", t])); } addSubListener(t, e) { (this.subListeners[t] = this.subListeners[t] || []).push( e ); } addRpcListener(t, e, s) { this.rpcListeners[s] = { callback: t, errorCallback: e }; } removeRpcListener(t) { delete this.rpcListeners[t]; } removeListener(t, e) { if (this.subListeners[t]) { if (this.subListeners[t].length === 1) { delete this.subListeners[t]; return; } this.subListeners[t] = this.subListeners[t].filter( (s) => s !== e ); } } async ensureConnection() { this.ws?.readyState !== 1 && await this.connect(); } handleNextMessage() { if (this.messageQueue.size === 0) { clearInterval(this.handlingInterval), this.handlingInterval = void 0; return; } const t = this.messageQueue.dequeue(); let e; try { if (e = JSON.parse(t), "result" in e && e.id != null) this.rpcListeners[e.id] && (this.rpcListeners[e.id].callback(), this.removeRpcListener(e.id)); else if ("error" in e && e.id != null) this.rpcListeners[e.id] && (this.rpcListeners[e.id].errorCallback(new Error(e.error.message)), this.removeRpcListener(e.id)); else if ("method" in e && !("id" in e)) { const s = e.params?.subId; if (!s) return; if (this.subListeners[s]?.length > 0) { const r = e; this.subListeners[s].forEach((o) => o(r.params?.payload)); } } } catch (s) { this._logger.error("Error doing handleNextMessage", { e: s }); return; } } createSubscription(t, e, s) { if (this.ws?.readyState !== 1) throw this._logger.error("Attempted createSubscription, but socket was not open"), new Error("Socket is not open"); const r = (Math.random() + 1).toString(36).substring(7); return this.addRpcListener( () => { this.addSubListener(r, e); }, s, this.rpcId ), this.sendRequest("subscribe", { ...t, subId: r }), this.rpcId++, r; } /** * Cancels a subscription, sending an unsubscribe request and handling responses. * * @param subId The subscription ID to cancel. * @param callback The original payload callback to remove. * @param errorCallback Optional callback for unsubscribe errors (defaults to logging). */ cancelSubscription(t, e, s) { this.removeListener(t, e), this.addRpcListener( () => { this._logger.info("Unsubscribed {subId}", { subId: t }); }, s || ((r) => this._logger.error("Unsubscribe failed", { e: r })), this.rpcId ), this.sendRequest("unsubscribe", { subId: t }); } get activeSubscriptions() { return Object.keys(this.subListeners); } close() { this.ws && this.ws?.close(); } onClose(t) { this.onCloseCallbacks.push(t); } } const ss = { UNSPENT: "UNSPENT", PENDING: "PENDING", SPENT: "SPENT" }, tt = { UNPAID: "UNPAID", PENDING: "PENDING", PAID: "PAID" }, pt = { UNPAID: "UNPAID", PAID: "PAID", ISSUED: "ISSUED" }; var Re = /* @__PURE__ */ ((n) => (n.POST = "post", n.NOSTR = "nostr", n))(Re || {}); class et extends Error { constructor(t, e) { super(t), this.status = e, this.name = "HttpResponseError", Object.setPrototypeOf(this, et.prototype); } } class bt extends Error { constructor(t) { super(t), this.name = "NetworkError", Object.setPrototypeOf(this, bt.prototype); } } class _t extends et { constructor(t, e) { super(e || "Unknown mint operation error", 400), this.code = t, this.name = "MintOperationError", Object.setPrototypeOf(this, _t.prototype); } } let $t = {}, zt = O; function ns(n) { $t = n; } function xe(n) { zt = n; } async function Ue({ endpoint: n, requestBody: t, headers: e, ...s }) { const r = t ? JSON.stringify(t) : void 0, o = { Accept: "application/json, text/plain, */*", ...r ? { "Content-Type": "application/json" } : void 0, ...e }; let a; try { a = await fetch(n, { body: r, headers: o, ...s }); } catch (i) { throw new bt(i instanceof Error ? i.message : "Network request failed"); } if (!a.ok) { let i; try { i = await a.json(); } catch { i = { error: "bad response" }; } if (a.status === 400 && "code" in i && typeof i.code == "number" && "detail" in i && typeof i.detail == "string") throw new _t(i.code, i.detail); let c = "HTTP request failed"; throw "error" in i && typeof i.error == "string" ? c = i.error : "detail" in i && typeof i.detail == "string" && (c = i.detail), new et(c, a.status); } try { return await a.json(); } catch (i) { throw zt.error("Failed to parse HTTP response", { err: i }), new et("bad response", a.status); } } async function K(n) { return await Ue({ ...n, ...$t }); } function lt(n, t) { return n.state || (t.warn( "Field 'state' not found in MeltQuoteResponse. Update NUT-05 of mint: https://github.com/cashubtc/nuts/pull/136)" ), typeof n.paid == "boolean" && (n.state = n.paid ? tt.PAID : tt.UNPAID)), n; } function Dt(n, t) { return n.state || (t.warn( "Field 'state' not found in MintQuoteResponse. Update NUT-04 of mint: https://github.com/cashubtc/nuts/pull/141)" ), typeof n.paid == "boolean" && (n.state = n.paid ? pt.PAID : pt.UNPAID)), n; } function Fe(n, t) { return Array.isArray(n?.contact) && n?.contact.length > 0 && (n.contact = n.contact.map((e) => Array.isArray(e) && e.length === 2 && typeof e[0] == "string" && typeof e[1] == "string" ? (t.warn( "Mint returned deprecated 'contact' field: Update NUT-06: https://github.com/cashubtc/nuts/pull/117" ), { method: e[0], info: e[1] }) : e)), n; } class mt { constructor(t) { this._mintInfo = t, t.nuts[22] && (this._protectedEnpoints = { cache: {}, apiReturn: t.nuts[22].protected_endpoints.map((e) => ({ method: e.method, regex: new RegExp(e.path) })) }); } isSupported(t) { switch (t) { case 4: case 5: return this.checkMintMelt(t); case 7: case 8: case 9: case 10: case 11: case 12: case 14: case 20: return this.checkGenericNut(t); case 17: return this.checkNut17(); case 15: return this.checkNut15(); default: throw new Error("nut is not supported by cashu-ts"); } } requiresBlindAuthToken(t) { if (!this._protectedEnpoints) return !1; if (typeof this._protectedEnpoints.cache[t] == "boolean") return this._protectedEnpoints.cache[t]; const e = this._protectedEnpoints.apiReturn.some((s) => s.regex.test(t)); return this._protectedEnpoints.cache[t] = e, e; } checkGenericNut(t) { return this._mintInfo.nuts[t]?.supported ? { supported: !0 } : { supported: !1 }; } checkMintMelt(t) { const e = this._mintInfo.nuts[t]; return e && e.methods.length > 0 && !e.disabled ? { disabled: !1, params: e.methods } : { disabled: !0, params: e.methods }; } checkNut17() { return this._mintInfo.nuts[17] && this._mintInfo.nuts[17].supported.length > 0 ? { supported: !0, params: this._mintInfo.nuts[17].supported } : { supported: !1 }; } checkNut15() { return this._mintInfo.nuts[15] && this._mintInfo.nuts[15].methods.length > 0 ? { supported: !0, params: this._mintInfo.nuts[15].methods } : { supported: !1 }; } get contact() { return this._mintInfo.contact; } get description() { return this._mintInfo.description; } get description_long() { return this._mintInfo.description_long; } get name() { return this._mintInfo.name; } get pubkey() { return this._mintInfo.pubkey; } get nuts() { return this._mintInfo.nuts; } get version() { return this._mintInfo.version; } get motd() { return this._mintInfo.motd; } } class D { /** * @param _mintUrl Requires mint URL to create this object. * @param _customRequest If passed, use custom request implementation for network communication * with the mint. * @param [authTokenGetter] A function that is called by the CashuMint instance to obtain a NUT-22 * BlindedAuthToken (e.g. from a database or localstorage) */ constructor(t, e, s, r) { this._mintUrl = t, this._customRequest = e, this._checkNut22 = !1, this._mintUrl = Wt(t), this._customRequest = e, s && (this._checkNut22 = !0, this._authTokenGetter = s), this._logger = r?.logger ?? O, xe(this._logger); } //TODO: v3 - refactor CashuMint to take two or less args. get mintUrl() { return this._mintUrl; } /** * Fetches mints info at the /info endpoint. * * @param mintUrl * @param customRequest */ static async getInfo(t, e, s) { const r = s ?? O, a = await (e || K)({ endpoint: T(t, "/v1/info") }); return Fe(a, r); } /** * Fetches mints info at the /info endpoint. */ async getInfo() { return D.getInfo(this._mintUrl, this._customRequest, this._logger); } async getLazyMintInfo() { if (this._mintInfo) return this._mintInfo; const t = await D.getInfo(this._mintUrl, this._customRequest); return this._mintInfo = new mt(t), this._mintInfo; } /** * Performs a swap operation with ecash inputs and outputs. * * @param mintUrl * @param swapPayload Payload containing inputs and outputs. * @param customRequest * @returns Signed outputs. */ static async swap(t, e, s, r) { const o = s || K, a = r ? { "Blind-auth": r } : {}, i = await o({ endpoint: T(t, "/v1/swap"), method: "POST", requestBody: e, headers: a }); if (!F(i) || !Array.isArray(i?.signatures)) throw new Error(i.detail ?? "bad response"); return i; } /** * Performs a swap operation with ecash inputs and outputs. * * @param swapPayload Payload containing inputs and outputs. * @returns Signed outputs. */ async swap(t) { const e = await this.handleBlindAuth("/v1/swap"); return D.swap(this._mintUrl, t, this._customRequest, e); } /** * Requests a new mint quote from the mint. * * @param mintUrl * @param mintQuotePayload Payload for creating a new mint quote. * @param customRequest * @returns The mint will create and return a new mint quote containing a payment request for the * specified amount and unit. */ static async createMintQuote(t, e, s, r, o) { const a = o ?? O, i = s || K, c = r ? { "Blind-auth": r } : {}, h = await i({ endpoint: T(t, "/v1/mint/quote/bolt11"), method: "POST", requestBody: e, headers: c }); return Dt(h, a); } /** * Requests a new mint quote from the mint. * * @param mintQuotePayload Payload for creating a new mint quote. * @returns The mint will create and return a new mint quote containing a payment request for the * specified amount and unit. */ async createMintQuote(t) { const e = await this.handleBlindAuth("/v1/mint/quote/bolt11"); return D.createMintQuote( this._mintUrl, t, this._customRequest, e ); } /** * Gets an existing mint quote from the mint. * * @param mintUrl * @param quote Quote ID. * @param customRequest * @returns The mint will create and return a Lightning invoice for the specified amount. */ static async checkMintQuote(t, e, s, r, o) { const a = o ?? O, i = s || K, c = r ? { "Blind-auth": r } : {}, h = await i({ endpoint: T(t, "/v1/mint/quote/bolt11", e), method: "GET", headers: c }); return Dt(h, a); } /** * Gets an existing mint quote from the mint. * * @param quote Quote ID. * @returns The mint will create and return a Lightning invoice for the specified amount. */ async checkMintQuote(t) { const e = await this.handleBlindAuth(`/v1/mint/quote/bolt11/${t}`); return D.checkMintQuote(this._mintUrl, t, this._customRequest, e); } /** * Mints new tokens by requesting blind signatures on the provided outputs. * * @param mintUrl * @param mintPayload Payload containing the outputs to get blind signatures on. * @param customRequest * @returns Serialized blinded signatures. */ static async mint(t, e, s, r) { const o = s || K, a = r ? { "Blind-auth": r } : {}, i = await o({ endpoint: T(t, "/v1/mint/bolt11"), method: "POST", requestBody: e, headers: a }); if (!F(i) || !Array.isArray(i?.signatures)) throw new Error("bad response"); return i; } /** * Mints new tokens by requesting blind signatures on the provided outputs. * * @param mintPayload Payload containing the outputs to get blind signatures on. * @returns Serialized blinded signatures. */ async mint(t) { const e = await this.handleBlindAuth("/v1/mint/bolt11"); return D.mint(this._mintUrl, t, this._customRequest, e); } /** * Requests a new melt quote from the mint. * * @param mintUrl * @param MeltQuotePayload * @returns */ static async createMeltQuote(t, e, s, r, o) { const a = o ?? O, i = s || K, c = r ? { "Blind-auth": r } : {}, h = await i({ endpoint: T(t, "/v1/melt/quote/bolt11"), method: "POST", requestBody: e, headers: c }), u = lt(h, a); if (!F(u) || typeof u?.amount != "number" || typeof u?.fee_reserve != "number" || typeof u?.quote != "string") throw new Error("bad response"); return u; } /** * Requests a new melt quote from the mint. * * @param MeltQuotePayload * @returns */ async createMeltQuote(t) { const e = await this.handleBlindAuth("/v1/melt/quote/bolt11"); return D.createMeltQuote( this._mintUrl, t, this._customRequest, e ); } /** * Gets an existing melt quote. * * @param mintUrl * @param quote Quote ID. * @returns */ static async checkMeltQuote(t, e, s, r, o) { const a = o ?? O, i = s || K, c = r ? { "Blind-auth": r } : {}, h = await i({ endpoint: T(t, "/v1/melt/quote/bolt11", e), method: "GET", headers: c }), u = lt(h, a); if (!F(u) || typeof u?.amount != "number" || typeof u?.fee_reserve != "number" || typeof u?.quote != "string" || typeof u?.state != "string" || !Object.values(tt).includes(u.state)) throw new Error("bad response"); return u; } /** * Gets an existing melt quote. * * @param quote Quote ID. * @returns */ async checkMeltQuote(t) { const e = await this.handleBlindAuth(`/v1/melt/quote/bolt11/${t}`); return D.checkMeltQuote(this._mintUrl, t, this._customRequest, e); } /** * Requests the mint to pay for a Bolt11 payment request by providing ecash as inputs to be spent. * The inputs contain the amount and the fee_reserves for a Lightning payment. The payload can * also contain blank outputs in order to receive back overpaid Lightning fees. * * @param mintUrl * @param meltPayload * @param customRequest * @returns */ static async melt(t, e, s, r, o) { const a = o ?? O, i = s || K, c = r ? { "Blind-auth": r } : {}, h = await i({ endpoint: T(t, "/v1/melt/bolt11"), method: "POST", requestBody: e, headers: c }), u = lt(h, a); if (!F(u) || typeof u?.state != "string" || !Object.values(tt).includes(u.state)) throw new Error("bad response"); return u; } /** * Ask mint to perform a melt operation. This pays a lightning invoice and destroys tokens * matching its amount + fees. * * @param meltPayload * @returns */ async melt(t) { const e = await this.handleBlindAuth("/v1/melt/bolt11"); return D.melt(this._mintUrl, t, this._customRequest, e); } /** * Checks if specific proofs have already been redeemed. * * @param mintUrl * @param checkPayload * @param customRequest * @returns Redeemed and unredeemed ordered list of booleans. */ static async check(t, e, s) { const o = await (s || K)({ endpoint: T(t, "/v1/checkstate"), method: "POST", requestBody: e }); if (!F(o) || !Array.isArray(o?.states)) throw new Error("bad response"); return o; } /** * Get the mints public keys. * * @param mintUrl * @param keysetId Optional param to get the keys for a specific keyset. If not specified, the * keys from all active keysets are fetched. * @param customRequest * @returns */ static async getKeys(t, e, s) { e && (e = e.replace(/\//g, "_").replace(/\+/g, "-")); const o = await (s || K)({ endpoint: e ? T(t, "/v1/keys", e) : T(t, "/v1/keys") }); if (!F(o) || !Array.isArray(o.keysets)) throw new Error("bad response"); return o; } /** * Get the mints public keys. * * @param keysetId Optional param to get the keys for a specific keyset. If not specified, the * keys from all active keysets are fetched. * @returns The mints public keys. */ async getKeys(t, e) { return await D.getKeys( e || this._mintUrl, t, this._customRequest ); } /** * Get the mints keysets in no specific order. * * @param mintUrl * @param customRequest * @returns All the mints past and current keysets. */ static async getKeySets(t, e) { return (e || K)({ endpoint: T(t, "/v1/keysets") }); } /** * Get the mints keysets in no specific order. * * @returns All the mints past and current keysets. */ async getKeySets() { return D.getKeySets(this._mintUrl, this._customRequest); } /** * Checks if specific proofs have already been redeemed. * * @param checkPayload * @returns Redeemed and unredeemed ordered list of booleans. */ async check(t) { return D.check(this._mintUrl, t, this._customRequest); } static async restore(t, e, s) { const o = await (s || K)({ endpoint: T(t, "/v1/restore"), method: "POST", requestBody: e }); if (!F(o) || !Array.isArray(o?.outputs) || !Array.isArray(o?.signatures)) throw new Error("bad response"); return o; } async restore(t) { return D.restore(this._mintUrl, t, this._customRequest); } /** * Tries to establish a websocket connection with the websocket mint url according to NUT-17. */ async connectWebSocket() { if (this.ws) await this.ws.ensureConnection(); else { const t = new URL(this._mintUrl), e = "v1/ws"; t.pathname && (t.pathname.endsWith("/") ? t.pathname += e : t.pathname += "/" + e), this.ws = H.getInstance().getConnection( `${t.protocol === "https:" ? "wss" : "ws"}://${t.host}${t.pathname}` ); try { await this.ws.connect(); } catch (s) { throw this._logger.error("Failed to connect to WebSocket...", { e: s }), new Error("Failed to connect to WebSocket..."); } } } /** * Closes a websocket connection. */ disconnectWebSocket() { this.ws && this.ws.close(); } get webSocketConnection() { return this.ws; } async handleBlindAuth(t) { if (!this._checkNut22) return; if ((await this.getLazyMintInfo()).requiresBlindAuthToken(t)) { if (!this._authTokenGetter) throw new Error("Can not call a protected endpoint without authProofGetter"); return this._authTokenGetter(); } } } class dt { constructor(t, e, s) { this.amount = t, this.B_ = e, this.id = s; } getSerializedBlindedMessage() { return { amount: this.amount, B_: this.B_.toHex(!0), id: this.id }; } } function ft(n) { return typeof n == "function"; } class N { constructor(t, e, s) { this.secret = s, this.blindingFactor = e, this.blindedMessage = t; } toProof(t, e) { let s; t.dleq && (s = { s: Pt(t.dleq.s), e: Pt(t.dleq.e), r: this.blindingFactor }); const r = { id: t.id, amount: t.amount, C_: ot(t.C_) }, o = ot(e.keys[t.amount]), a = Xt(r, this.blindingFactor, this.secret, o); return { ...Yt(a), ...s && { dleq: { s: X(s.s), e: X(s.e), r: be(s.r ?? BigInt(0)) } } }; } static createP2PKData(t, e, s, r) { return R(e, s.keys, r).map((a) => this.createSingleP2PKData(t, a, s.id)); } static createSingleP2PKData(t, e, s) { const r = Array.isArray(t.pubkey) ? t.pubkey : [t.pubkey], o = Math.max(1, Math.min(t.requiredSignatures || 1, r.length)), a = Math.max( 1, Math.min(t.requiredRefundSignatures || 1, t.refundKeys ? t.refundKeys.length : 1) ), i = [ "P2PK", { nonce: X(It(32)), data: r[0], // Primary key tags: [] } ]; t.locktime && i[1].tags.push(["locktime", String(t.locktime)]), r.length > 1 && (i[1].tags.push(["pubkeys", ...r.slice(1)]), o > 1 && i[1].tags.push(["n_sigs", String(o)])), t.refundKeys && (i[1].tags.push(["refund", ...t.refundKeys]), a > 1 && i[1].tags.push(["n_sigs_refund", String(a)])); const c = JSON.stringify(i), h = new TextEncoder().encode(c), { r: u, B_: l } = ht(h); return new N( new dt(e, l, s).getSerializedBlindedMessage(), u, h ); } static createRandomData(t, e, s) { return R(t, e.keys, s).map((o) => this.createSingleRandomData(o, e.id)); } static createSingleRandomData(t, e) { const s = X(It(32)), r = new TextEncoder().encode(s), { r: o, B_: a } = ht(r); return new N( new dt(t, a, e).getSerializedBlindedMessage(), o, r ); } static createDeterministicData(t, e, s, r, o) { return R(t, r.keys, o).map( (i, c) => this.createSingleDeterministicData(i, e, s + c, r.id) ); } static createSingleDeterministicData(t, e, s, r) { const o = Zt(e, r, s), a = X(o), i = new TextEncoder().encode(a), c = ke(te(e, r, s)), { r: h, B_: u } = ht(i, c); return new N( new dt(t, u, r).getSerializedBlindedMessage(), h, i ); } } const Oe = 3, Ne = "sat"; class rs { /** * @param mint Cashu mint instance is used to make api calls. * @param options.unit Optionally set unit (default is 'sat') * @param options.keys Public keys from the mint (will be fetched from mint if not provided) * @param options.keysets Keysets from the mint (will be fetched from mint if not provided) * @param options.mintInfo Mint info from the mint (will be fetched from mint if not provided) * @param options.denominationTarget Target number proofs per denomination (default: see @constant * DEFAULT_DENOMINATION_TARGET) * @param options.bip39seed BIP39 seed for deterministic secrets. * @param options.keepFactory A function that will be used by all parts of the library that * produce proofs to be kept (change, etc.). This can lead to poor performance, in which case * the seed should be directly provided. */ constructor(t, e) { this._keys = /* @__PURE__ */ new Map(), this._keysets = [], this._seed = void 0, this._unit = Ne, this._mintInfo = void 0, this._denominationTarget = Oe, this.mint = t, this._logger = e?.logger ?? O; let s = []; if (e?.keys && !Array.isArray(e.keys) ? s = [e.keys] : e?.keys && Array.isArray(e?.keys) && (s = e?.keys), s && s.forEach((r) => this._keys.set(r.id, r)), e?.unit && (this._unit = e?.unit), e?.keysets && (this._keysets = e.keysets), e?.mintInfo && (this._mintInfo = new mt(e.mintInfo)), e?.denominationTarget && (this._denominationTarget = e.denominationTarget), e?.bip39seed) { if (e.bip39seed instanceof Uint8Array) { this._seed = e.bip39seed; return; } throw new Error("bip39seed must be a valid UInt8Array"); } e?.keepFactory && (this._keepFactory = e.keepFactory); } get unit() { return this._unit; } get keys() { return this._keys; } get keysetId() { if (!this._keysetId) throw new Error("No keysetId set"); return this._keysetId; } set keysetId(t) { this._keysetId = t; } get keysets() { return this._keysets; } get mintInfo() { if (!this._mintInfo) throw new Error("Mint info not loaded"); return this._mintInfo; } /** * Get information about the mint. * * @returns Mint info. */ async getMintInfo() { const t = await this.mint.getInfo(); return this._mintInfo = new mt(t), this._mintInfo; } /** * Get stored information about the mint or request it if not loaded. * * @returns Mint info. */ async lazyGetMintInfo() { return this._mintInfo ? this._mintInfo : await this.getMintInfo(); } /** * Load mint information, keysets and keys. This function can be called if no keysets are passed * in the constructor. */ async loadMint() { await this.getMintInfo(), await this.getKeySets(), await this.getKeys(); } /** * Choose a keyset to activate based on the lowest input fee. * * Note: this function will filter out deprecated base64 keysets. * * @param keysets Keysets to choose from. * @returns Active keyset. */ getActiveKeyset(t) { let e = t.filter((r) => r.active && r.unit === this._unit); e = e.filter((r) => r.id.startsWith("00")); const s = e.sort( (r, o) => (r.input_fee_ppk ?? 0) - (o.input_fee_ppk ?? 0) )[0]; if (!s) throw new Error("No active keyset found"); return s; } /** * Get keysets from the mint with the unit of the wallet. * * @returns Keysets with wallet's unit. */ async getKeySets() { const e = (await this.mint.getKeySets()).keysets.filter((s) => s.unit === this._unit); return this._keysets = e, this._keysets; } /** * Get all active keys from the mint and set the keyset with the lowest fees as the active wallet * keyset. * * @returns Keyset. */ async getAllKeys() { const t = await this.mint.getKeys(); return t.keysets.forEach((e) => { if (!qt(e)) throw new Error(`Couldn't verify keyset ID ${e.id}`); }), this._keys = new Map(t.keysets.map((e) => [e.id, e])), this.keysetId = this.getActiveKeyset(this._keysets).id, t.keysets; } /** * Get public keys from the mint. If keys were already fetched, it will return those. * * If `keysetId` is set, it will fetch and return that specific keyset. Otherwise, we select an * active keyset with the unit of the wallet. * * @param keysetId Optional keysetId to get keys for. * @param forceRefresh? If set to true, it will force refresh the keyset from the mint. * @returns Keyset. */ async getKeys(t, e) { if ((!(this._keysets.length > 0) || e) && await this.getKeySets(), t || (t = this.getActiveKeyset(this._keysets).id), !this._keysets.find((s) => s.id === t) && (await this.getKeySets(), !this._keysets.find((s) => s.id === t))) throw new Error(`could not initialize keys. No keyset with id '${t}' found`); if (!this._keys.get(t)) { const s = await this.mint.getKeys(t); if (!qt(s.keysets[0])) throw new Error(`Couldn't verify keyset ID ${s.keysets[0].id}`); this._keys.set(t, s.keysets[0]); } return this.keysetId = t, this._keys.get(t); } /** * Receive an encoded or raw Cashu token (only supports single tokens. It will only process the * first token in the token array) * * @param {string | Token} token - Cashu token, either as string or decoded. * @param {ReceiveOptions} [options] - Optional configuration for token processing. * @returns New token with newly created proofs, token entries that had errors. */ async receive(t, e) { const { requireDleq: s, keysetId: r, outputAmounts: o, counter: a, pubkey: i, privkey: c, outputData: h, p2pk: u } = e || {}; typeof t == "string" && (t = Ee(t)); const l = await this.getKeys(r); if (s && t.proofs.some((P) => !jt(P, l))) throw new Error("Token contains proofs with invalid DLEQ"); const f = W(t.proofs) - this.getFeesForProofs(t.proofs); let d; h ? d = { send: h } : this._keepFactory && (d = { send: this._keepFactory }); const g = this.createSwapPayload( f, t.proofs, l, o, a, i, c, d, u ), { signatures: S } = await this.mint.swap(g.payload), b = g.outputData.map((P, k) => P.toProof(S[k], l)), A = []; return g.sortedIndices.forEach((P, k) => { A[P] = b[k]; }), A; } /** * Send proofs of a given amount, by providing at least the required amount of proofs. * * @param amount Amount to send. * @param proofs Array of proofs (accumulated amount of proofs must be >= than amount) * @param {SendOptions} [options] - Optional parameters for configuring the send operation. * @returns {SendResponse} */ async send(t, e, s) { const { offline: r, includeFees: o, includeDleq: a, keysetId: i, outputAmounts: c, pubkey: h, privkey: u, outputData: l } = s || {}; if (a && (e = e.filter((S) => S.dleq != null)), W(e) < t) throw new Error("Not enough funds available to send"); const { keep: f, send: d } = this.selectProofsToSend( e, t, s?.includeFees ), g = o ? this.getFeesForProofs(d) : 0; if (!r && (W(d) != t + g || // if the exact amount cannot be selected c || h || u || i || l)) { const S = await this.swap(t, e, s), { keep: b, send: A } = S, P = S.serialized; return { keep: b, send: A, serialized: P }; } if (W(d) < t + g) throw new Error("Not enough funds available to send"); return { keep: f, send: d }; } /** * Selects proofs to send based on amount and fee inclusion. * * @remarks * Uses an adapted Randomized Greedy with Local Improvement (RGLI) algorithm, which has a time * complexity O(n log n) and space complexity O(n). * @param proofs Array of Proof objects available to select from. * @param amountToSend The target amount to send. * @param includeFees Optional boolean to include fees; Default: false. * @returns SendResponse containing proofs to keep and proofs to send. * @see https://crypto.ethz.ch/publications/files/Przyda02.pdf */ selectProofsToSend(t, e, s = !1) { const u = Ke(); let l = null, f = 1 / 0, d = 0, g = 0; const S = (m, p) => m - (s ? Math.ceil(p / 1e3) : 0), b = (m) => { const p = [...m]; for (let w