diff options
| author | Adam Soltys <[email protected]> | 2026-09-14 08:50:31 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-14 08:50:31 -0700 |
| commit | cdda2d34d26bd8fb310867022130e4a59f67d13c (patch) | |
| tree | ac13a2d26bcaf3df5d72e69c5afe03e5778915c2 /lib/ecash.ts | |
| parent | d8a361bdcfaa8cc5b39d9675e74b0e1d2e42be2c (diff) | |
| download | coinos-server-cdda2d34d26bd8fb310867022130e4a59f67d13c.tar.gz coinos-server-cdda2d34d26bd8fb310867022130e4a59f67d13c.zip | |
ecash: make claims work against the v1 keyset mint
cashu-ts 2.9.0 only derives v0 keyset ids, so CashuWallet rejected the
mint's current keyset and receive() could never run. Build the wallet
from keys preloaded off our own mint instead of verifying, decode tokens
with the keysets (2.9.0 shortens v1 ids in the tokens it encodes, and
won't expand them otherwise), and treat an unreadable house-pool value
as empty rather than throwing — the pool key had been found holding a
bare integer, which surfaced as "n.startsWith is not a function" on
every redeem. The pool write is awaited so a dropped set can't strand
freshly swapped proofs.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_0166Ci2XdCair1B9ych7w8dq
Diffstat (limited to 'lib/ecash.ts')
| -rw-r--r-- | lib/ecash.ts | 83 |
1 files changed, 64 insertions, 19 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) {} } |
