aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--lib/ecash.ts83
-rw-r--r--routes/info.ts9
2 files changed, 67 insertions, 25 deletions
diff --git a/lib/ecash.ts b/lib/ecash.ts
index 9ce8b4e5..b1d8b7fb 100644
--- a/lib/ecash.ts
+++ b/lib/ecash.ts
@@ -1,6 +1,7 @@
import config from "$config";
-import { g, s } from "$lib/db";
+import { db, g } from "$lib/db";
import { lnb } from "$lib/ln";
+import { warn } from "$lib/logging";
import { safeGot } from "$lib/safe-fetch";
import { fail, wait } from "$lib/utils";
import {
@@ -11,13 +12,39 @@ import {
PaymentRequestTransportType,
getDecodedToken,
getEncodedToken,
- getEncodedTokenV4,
} from "@cashu/cashu-ts";
const { URL } = process.env;
const m = new CashuMint(config.mintUrl);
-const w = new CashuWallet(m);
+
+// The mint issues NUT-02 v1 keyset ids (`01…`, 33 bytes). cashu-ts 2.9.0 only
+// derives v0 ids, so CashuWallet.getKeys() rejects every keyset with "Couldn't
+// verify keyset ID" and receive()/send() can never run — ecash claims have been
+// failing since the July keyset cutover. (4.x derives v1 ids, but not the way
+// this nutshell build does, so upgrading wouldn't help either.) config.mintUrl
+// is our own mint, so skip the derivation check: CashuMint.getKeys() returns
+// the keys unverified, and a wallet constructed with them preloaded uses them
+// as-is. Re-fetched periodically so a keyset rotation is picked up.
+const KEYS_TTL = 10 * 60 * 1000;
+let cached: { w: CashuWallet; keysets: any[]; at: number } | undefined;
+const wallet = async () => {
+ if (cached && Date.now() - cached.at < KEYS_TTL) return cached;
+ const [{ keysets: keys }, { keysets }] = await Promise.all([
+ m.getKeys(),
+ m.getKeySets(),
+ ]);
+ const w = new CashuWallet(m, { keys, keysets });
+ cached = { w, keysets, at: Date.now() };
+ return cached;
+};
+
+// 2.9.0 writes v1 keyset ids into V4 tokens in their 8-byte short form, and
+// refuses to decode a short id unless it's handed the mint's keysets to expand
+// it against — so every token we encode (the house pool included) has to be
+// decoded through here rather than with a bare getDecodedToken().
+const decode = async (token) =>
+ getDecodedToken(token, (await wallet()).keysets);
const enc = (proofs) =>
getEncodedToken({
@@ -25,6 +52,29 @@ const enc = (proofs) =>
proofs,
});
+// The house wallet is a single encoded token under the `cash` key. g() JSON-
+// parses whatever is stored there, and the key has been found holding a bare
+// integer — getDecodedToken(number) then threw "n.startsWith is not a function"
+// on every claim. Only trust the value when it decodes; otherwise start from
+// no proofs and let the next write replace it.
+export const pool = async () => {
+ const v = await g("cash");
+ if (typeof v === "string") {
+ try {
+ return (await decode(v)).proofs;
+ } catch (e: any) {
+ warn("cash pool token unreadable, treating as empty:", e.message);
+ }
+ } else {
+ warn("cash pool key is not a token, treating as empty:", typeof v);
+ }
+ return [];
+};
+
+// s() is fire-and-forget; a dropped write here would silently strand the
+// proofs we just swapped in, so await the set directly.
+const setPool = (proofs) => db.set("cash", JSON.stringify(enc(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
@@ -68,37 +118,37 @@ export async function get(id) {
}
export async function claim(token) {
- const { mint } = getDecodedToken(token);
+ const { mint } = await decode(token);
if (await ext(mint)) fail("Unable to receive from other mints");
+ const { w } = await wallet();
return withCashLock(async () => {
- const { proofs: current } = getDecodedToken(await g("cash"));
+ const current = await pool();
const rcvd = await w.receive(token);
- await s("cash", enc([...current, ...rcvd]));
+ await setPool([...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 });
+ const { w } = await wallet();
return withCashLock(async () => {
- const { proofs } = getDecodedToken(await g("cash"));
+ const proofs = await pool();
const { send, keep } = await w.send(amount, proofs);
const rcvd = await w.receive(enc(send));
- const change = enc(keep);
- await s("cash", change);
+ await setPool(keep);
return enc(rcvd);
});
}
export async function check(token) {
- const { mint, proofs } = getDecodedToken(token);
+ const { mint, proofs } = await decode(token);
const total = proofs.reduce((a, b) => a + b.amount, 0);
const external = await ext(mint);
+ const { w } = await wallet();
let spent = 0;
for (const [i, p] of (await w.checkProofsStates(proofs)).entries()) {
if (p.state === "SPENT") spent += proofs[i].amount;
@@ -110,6 +160,7 @@ export async function check(token) {
export async function init(amount = 100000) {
try {
await new Promise((r) => setTimeout(r, 2000));
+ const { w } = await wallet();
const { quote, request } = await w.createMintQuote(amount);
await lnb.pay(request);
@@ -119,13 +170,7 @@ export async function init(amount = 100000) {
});
const proofs = await w.mintProofs(amount, quote);
-
- const cash = getEncodedTokenV4({
- mint: config.mintUrl,
- proofs,
- });
-
- await s("cash", cash);
+ await setPool(proofs);
} catch (e) {}
}
diff --git a/routes/info.ts b/routes/info.ts
index a0ac7693..aae130bf 100644
--- a/routes/info.ts
+++ b/routes/info.ts
@@ -1,8 +1,8 @@
import { archive } from "$lib/db";
-import { db, g } from "$lib/db";
+import { db } from "$lib/db";
import { getHealthStatus } from "$lib/health";
import ln from "$lib/ln";
-import { getDecodedToken } from "@cashu/cashu-ts";
+import { pool } from "$lib/ecash";
export default {
async health(_, res) {
@@ -28,10 +28,7 @@ export default {
);
const lnwallet = parseInt(funds.outputs.reduce((a, b) => a + b.value, 0));
- const cash = getDecodedToken(await g("cash")).proofs.reduce(
- (a, b) => a + b.amount,
- 0,
- );
+ const cash = (await pool()).reduce((a, b) => a + b.amount, 0);
const info = {
cash,