Upgrade to cashu-ts v4 and drop unused deps.

Adapt amount handling for v4 Amount objects, replace axios/uuid with fetch and crypto.randomUUID, and remove dead helpers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Michilis
2026-07-23 03:46:59 +00:00
co-authored by Cursor
parent bd18271957
commit 449a8dda0d
9 changed files with 696 additions and 1197 deletions
+45 -91
View File
@@ -1,8 +1,25 @@
const { Wallet, Mint, getDecodedToken, CheckStateEnum } = require('@cashu/cashu-ts');
const { Wallet, getDecodedToken, CheckStateEnum } = require('@cashu/cashu-ts');
/**
* Normalize a cashu-ts amount into a plain JavaScript number.
*
* As of cashu-ts v4, amounts returned by the library (proof amounts, melt quote
* `amount`/`fee_reserve`, etc.) are `Amount` class instances rather than plain
* numbers. `Amount` has no `valueOf()`, so arithmetic/comparison against numbers
* silently breaks (string concatenation or NaN). Always funnel library amounts
* through this before doing math with them.
*/
function toNumber(value) {
if (value == null) return 0;
if (typeof value === 'number') return value;
if (typeof value === 'bigint') return Number(value);
if (typeof value.toNumber === 'function') return value.toNumber();
const n = Number(value);
return Number.isFinite(n) ? n : 0;
}
class CashuComponent {
constructor() {
this.mints = new Map();
this.wallets = new Map();
}
@@ -13,26 +30,15 @@ class CashuComponent {
return /^cashu[abAB][a-zA-Z0-9-_]+$/.test(token);
}
/**
* Get token mint URL from decoded token
*/
async getTokenMintUrl(token) {
try {
const decoded = getDecodedToken(token);
if (!decoded) return null;
return decoded.mint || null;
} catch (error) {
console.error('Error getting token mint URL:', error);
return null;
}
}
/**
* Decode token structure (v3/v4 format always has { mint, proofs } at top level)
*/
async decodeTokenStructure(token) {
try {
const decoded = getDecodedToken(token);
// cashu-ts v4 requires a `keysetIds` argument used only to resolve short
// (NUT-16 v2) keyset IDs. Tokens with full v0 keyset IDs — the common case —
// need no mapping, so we pass an empty list. Passing nothing throws.
const decoded = getDecodedToken(token, []);
if (!decoded) throw new Error('Failed to decode token');
if (!decoded.proofs || !Array.isArray(decoded.proofs)) {
throw new Error('Invalid token structure - no proofs found');
@@ -47,14 +53,6 @@ class CashuComponent {
}
}
/**
* Calculate fee according to NUT-05 specification
*/
calculateFee(amount) {
const fee = Math.ceil(amount * 0.02);
return Math.max(1, fee);
}
/**
* Parse and validate a Cashu token
*/
@@ -76,13 +74,13 @@ class CashuComponent {
throw new Error('Invalid token structure - no proofs found');
}
const totalAmount = decoded.proofs.reduce((sum, proof) => sum + (proof.amount || 0), 0);
const totalAmount = decoded.proofs.reduce((sum, proof) => sum + toNumber(proof.amount), 0);
if (totalAmount <= 0) {
throw new Error('Token has no value');
}
const denominations = decoded.proofs.map(proof => proof.amount);
const denominations = decoded.proofs.map(proof => toNumber(proof.amount));
return {
mint: decoded.mint,
@@ -98,30 +96,6 @@ class CashuComponent {
}
}
/**
* Get total amount from a token
*/
async getTotalAmount(token) {
const parsed = await this.parseToken(token);
return parsed.totalAmount;
}
/**
* Get or create a Mint instance
*/
async getMint(mintUrl) {
if (!this.mints.has(mintUrl)) {
try {
const mint = new Mint(mintUrl);
await mint.getInfo();
this.mints.set(mintUrl, mint);
} catch (error) {
throw new Error(`Failed to connect to mint ${mintUrl}: ${error.message}`);
}
}
return this.mints.get(mintUrl);
}
/**
* Get or create a Wallet instance for a specific mint
*/
@@ -148,15 +122,18 @@ class CashuComponent {
const meltQuote = await wallet.createMeltQuoteBolt11(bolt11);
const amount = toNumber(meltQuote.amount);
const feeReserve = toNumber(meltQuote.fee_reserve);
console.log('Melt quote created:', {
amount: meltQuote.amount,
fee_reserve: meltQuote.fee_reserve,
amount,
fee_reserve: feeReserve,
quote: meltQuote.quote
});
return {
amount: meltQuote.amount,
fee_reserve: meltQuote.fee_reserve,
amount,
fee_reserve: feeReserve,
quote: meltQuote.quote
};
} catch (error) {
@@ -176,19 +153,21 @@ class CashuComponent {
const proofs = decoded.proofs;
const meltQuote = await wallet.createMeltQuoteBolt11(bolt11);
const quoteAmount = toNumber(meltQuote.amount);
const quoteFeeReserve = toNumber(meltQuote.fee_reserve);
console.log('Melt quote created:', {
amount: meltQuote.amount,
fee_reserve: meltQuote.fee_reserve,
amount: quoteAmount,
fee_reserve: quoteFeeReserve,
quote: meltQuote.quote
});
console.log('Paying invoice:', bolt11.substring(0, 50) + '...');
const total = meltQuote.amount + meltQuote.fee_reserve;
console.log('Total required:', total, 'sats (amount:', meltQuote.amount, '+ fee:', meltQuote.fee_reserve, ')');
const total = quoteAmount + quoteFeeReserve;
console.log('Total required:', total, 'sats (amount:', quoteAmount, '+ fee:', quoteFeeReserve, ')');
console.log('Available in token:', parsed.totalAmount, 'sats');
if (total > parsed.totalAmount) {
throw new Error(`Insufficient funds. Required: ${total} sats (including ${meltQuote.fee_reserve} sats fee), Available: ${parsed.totalAmount} sats`);
throw new Error(`Insufficient funds. Required: ${total} sats (including ${quoteFeeReserve} sats fee), Available: ${parsed.totalAmount} sats`);
}
console.log('Selecting proofs with includeFees: true for', total, 'sats');
@@ -198,13 +177,11 @@ class CashuComponent {
console.log('Selected', proofsToSend.length, 'proofs for melting');
console.log('Performing melt operation...');
const meltResponse = await wallet.meltProofs(meltQuote, proofsToSend);
console.log('Melt response:', JSON.stringify(meltResponse, null, 2));
const meltResponse = await wallet.meltProofsBolt11(meltQuote, proofsToSend);
const quote = meltResponse.quote || {};
const paymentSuccessful = quote.state === 'PAID' ||
quote.payment_preimage ||
!!quote.payment_preimage ||
meltResponse.paid === true;
if (!paymentSuccessful) {
@@ -212,7 +189,9 @@ class CashuComponent {
}
const preimage = quote.payment_preimage || meltResponse.preimage;
const actualFeeCharged = quote.fee_reserve || meltQuote.fee_reserve;
const actualFeeCharged = quote.fee_reserve != null
? toNumber(quote.fee_reserve)
: quoteFeeReserve;
const actualNetAmount = parsed.totalAmount - actualFeeCharged;
return {
@@ -220,7 +199,7 @@ class CashuComponent {
paid: paymentSuccessful,
preimage,
change: meltResponse.change || [],
amount: meltQuote.amount,
amount: quoteAmount,
fee: actualFeeCharged,
netAmount: actualNetAmount,
quote: meltQuote.quote,
@@ -244,31 +223,6 @@ class CashuComponent {
}
}
/**
* Validate if a token is properly formatted and has valid proofs
*/
async validateToken(token) {
try {
if (!this.isValidTokenFormat(token)) return false;
const parsed = await this.parseToken(token);
return parsed.totalAmount > 0 && parsed.proofs.length > 0;
} catch (error) {
return false;
}
}
/**
* Get mint info for a given mint URL
*/
async getMintInfo(mintUrl) {
try {
const mint = await this.getMint(mintUrl);
return await mint.getInfo();
} catch (error) {
throw new Error(`Failed to get mint info: ${error.message}`);
}
}
/**
* Check if proofs are spendable at the mint using NUT-07 state check
*/