From 2e4d9328bbc480e374e654114afea14127303fd8 Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Mon, 11 May 2026 22:33:22 +0200 Subject: [PATCH] Upgrade to cashu-ts v4 --- package.json | 4 +- ...2+2.0.1.patch => @scure+bip32+2.2.0.patch} | 28 +++---- src/models/Mint.ts | 19 ++--- src/models/ProofsStore.ts | 1 + src/models/WalletStore.ts | 81 ++++--------------- src/screens/ExportBackupScreen.tsx | 4 +- src/screens/MintInfoScreen.tsx | 12 ++- src/screens/NfcPayScreen.tsx | 12 +-- src/screens/ReceiveScreen.tsx | 3 +- src/screens/SendScreen.tsx | 22 ++--- src/screens/TransferScreen.tsx | 6 +- src/screens/Wallet/QRCode.tsx | 4 +- src/services/cashu/cashuUtils.ts | 51 ++++++++++-- src/services/wallet/receiveTask.ts | 5 +- src/services/wallet/revertTask.ts | 9 +-- src/services/wallet/sendTask.ts | 7 +- src/services/wallet/transferTask.ts | 20 ++--- src/services/walletService.ts | 51 +++++------- tsconfig.json | 2 +- yarn.lock | 62 ++++++++------ 20 files changed, 200 insertions(+), 203 deletions(-) rename patches/{@scure+bip32+2.0.1.patch => @scure+bip32+2.2.0.patch} (53%) diff --git a/package.json b/package.json index f432bb60..acea68b9 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "machine-translate": "npx @inlang/cli machine translate --project minibits.inlang" }, "dependencies": { - "@cashu/cashu-ts": "^3.6.1", + "@cashu/cashu-ts": "^4.2.1", "@fortawesome/fontawesome-svg-core": "^7.1.0", "@fortawesome/free-brands-svg-icons": "^7.1.0", "@fortawesome/free-regular-svg-icons": "^7.1.0", @@ -41,7 +41,7 @@ "@react-navigation/native": "^7.1.18", "@react-navigation/native-stack": "^7.3.27", "@react-navigation/stack": "^7.4.9", - "@scure/bip32": "^2.0.1", + "@scure/bip32": "^2.2.0", "@scure/bip39": "2.0.1", "@sentry/react-native": "^7.7.0", "date-fns": "^4.1.0", diff --git a/patches/@scure+bip32+2.0.1.patch b/patches/@scure+bip32+2.2.0.patch similarity index 53% rename from patches/@scure+bip32+2.0.1.patch rename to patches/@scure+bip32+2.2.0.patch index 3e909386..5e637a9f 100644 --- a/patches/@scure+bip32+2.0.1.patch +++ b/patches/@scure+bip32+2.2.0.patch @@ -1,17 +1,17 @@ diff --git a/node_modules/@scure/bip32/index.js b/node_modules/@scure/bip32/index.js -index 935277b..361161a 100644 +index 8e49612..d54da85 100644 --- a/node_modules/@scure/bip32/index.js +++ b/node_modules/@scure/bip32/index.js -@@ -23,6 +23,7 @@ import { ripemd160 } from '@noble/hashes/legacy.js'; +@@ -28,6 +28,7 @@ import { ripemd160 } from '@noble/hashes/legacy.js'; import { sha256, sha512 } from '@noble/hashes/sha2.js'; import { abytes, concatBytes, createView } from '@noble/hashes/utils.js'; import { createBase58check } from '@scure/base'; +import quickCrypto from 'react-native-quick-crypto'; - const Point = secp.Point; - const { Fn } = Point; - const base58check = createBase58check(sha256); -@@ -87,7 +88,8 @@ export class HDKey { - throw new Error('HDKey: seed length must be between 128 and 512 bits; 256 bits is advised, got ' + + const Point = /* @__PURE__ */ (() => secp.Point)(); + const Fn = /* @__PURE__ */ (() => Point.Fn)(); + const base58check = /* @__PURE__ */ createBase58check(sha256); +@@ -102,7 +103,8 @@ export class HDKey { + throw new RangeError('HDKey: seed length must be between 128 and 512 bits; 256 bits is advised, got ' + seed.length); } - const I = hmac(sha512, MASTER_SECRET, seed); @@ -20,13 +20,13 @@ index 935277b..361161a 100644 const privateKey = I.slice(0, 32); const chainCode = I.slice(32); return new HDKey({ versions, chainCode, privateKey }); -@@ -206,7 +208,8 @@ export class HDKey { +@@ -225,7 +227,8 @@ export class HDKey { // Normal child: serP(point(kpar)) || ser32(index) data = concatBytes(this._publicKey, data); } -- const I = hmac(sha512, this.chainCode, data); -+ //const I = hmac(sha512, this.chainCode, data); -+ const I = new Uint8Array(quickCrypto.createHmac('sha512', this.chainCode).update(data).digest()); - const childTweak = I.slice(0, 32); - const chainCode = I.slice(32); - if (!secp.utils.isValidSecretKey(childTweak)) { +- const out = _I || hmac(sha512, this.chainCode, data); ++ //const out = _I || hmac(sha512, this.chainCode, data); ++ const out = _I || new Uint8Array(quickCrypto.createHmac('sha512', this.chainCode).update(data).digest()); + abytes(out, 64); + const childTweak = out.slice(0, 32); + const chainCode = out.slice(32); diff --git a/src/models/Mint.ts b/src/models/Mint.ts index 631232ca..ae699962 100644 --- a/src/models/Mint.ts +++ b/src/models/Mint.ts @@ -15,13 +15,13 @@ import { MintUnit, MintUnits } from '../services/wallet/currency' import { getRootStore } from './helpers/getRootStore' import { generateId } from '../utils/utils' import { Proof } from './Proof' -import { CashuProof, CashuUtils } from '../services/cashu/cashuUtils' +import { CashuProof, CashuUtils, StoredMeltPreview } from '../services/cashu/cashuUtils' -// Helper function to serialize MeltPreview and convert BigInt to string -function serializeMeltPreview(meltPreview: MeltPreview): any { - return JSON.parse(JSON.stringify(meltPreview, (_key, value) => - typeof value === 'bigint' ? value.toString() : value - )) +function serializeMeltPreview(meltPreview: MeltPreview): StoredMeltPreview { + return { + keysetId: meltPreview.keysetId, + outputData: CashuUtils.serializeOutputData(meltPreview.outputData), + } } export type MintBalance = { @@ -60,7 +60,7 @@ const InFlightRequestModel = types.model('InFlightRequest', { const MeltCounterValueModel = types.model('MeltCounterValue', { transactionId: types.number, counterAtMelt: types.number, // the counter value when melt started (kept for backward compatibility) - meltPreview: types.maybe(types.frozen()), // v3.x MeltPreview object for recovery + meltPreview: types.maybe(types.frozen()), createdAt: types.optional(types.Date, () => new Date()), // optional: when it was added }) @@ -155,13 +155,10 @@ export const MintProofsCounterModel = types return self.meltCounterValues.get(key)!.counterAtMelt } - // Serialize MeltPreview to handle BigInt values - const serializableMeltPreview = meltPreview ? serializeMeltPreview(meltPreview) : undefined - self.meltCounterValues.set(key, { transactionId, counterAtMelt: self.counter, - meltPreview: serializableMeltPreview as any, + meltPreview: meltPreview ? serializeMeltPreview(meltPreview) : undefined, createdAt: new Date(), }) diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index e4de5a6a..5d27c10a 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -205,6 +205,7 @@ import { } else { proofNode = ProofModel.create({ ...proof, + amount: Number(proof.amount), mintUrl, tId, unit, diff --git a/src/models/WalletStore.ts b/src/models/WalletStore.ts index 9cc6f0a8..aad02e56 100644 --- a/src/models/WalletStore.ts +++ b/src/models/WalletStore.ts @@ -2,6 +2,7 @@ import {Instance, SnapshotOut, types, flow, getRoot, getSnapshot} from 'mobx-sta import { Mint as CashuMint, Wallet as CashuWallet, + KeyChain as CashuKeyChain, MeltQuoteBolt11Response, setGlobalRequestOptions, type MintKeys, @@ -337,13 +338,17 @@ export const WalletStoreModel = types const newSeedWallet = new CashuWallet(cashuMint, { unit, - keys: mintInstance.keys, - keysets: mintInstance.keysets, - mintInfo: mintInstance.mintInfo, keysetId: walletKeys.id, bip39seed: seed }) + if (mintInstance.mintInfo && mintInstance.keysets?.length && mintInstance.keys?.length) { + const keychainCache = CashuKeyChain.mintToCacheDTO(mintUrl, [...mintInstance.keysets], [...mintInstance.keys]) + newSeedWallet.loadMintFromCache(mintInstance.mintInfo, keychainCache) + } else { + yield newSeedWallet.loadMint() + } + self.seedWallets.push(newSeedWallet) log.trace('[WalletStore.getWallet]', 'Returning NEW cashuWallet instance with seed', {mintUrl}) @@ -363,13 +368,16 @@ export const WalletStoreModel = types const newWallet = new CashuWallet(cashuMint, { unit, - keys: mintInstance.keys, - keysets: mintInstance.keysets, - mintInfo: mintInstance.mintInfo, keysetId: walletKeys.id, - bip39seed: undefined }) - + + if (mintInstance.mintInfo && mintInstance.keysets?.length && mintInstance.keys?.length) { + const keychainCache = CashuKeyChain.mintToCacheDTO(mintUrl, [...mintInstance.keysets], [...mintInstance.keys]) + newWallet.loadMintFromCache(mintInstance.mintInfo, keychainCache) + } else { + yield newWallet.loadMint() + } + self.wallets.push(newWallet) log.trace('[WalletStore.getWallet]', 'Returning NEW cashuWallet instance', {mintUrl}) @@ -1032,63 +1040,6 @@ export const WalletStoreModel = types ) } }), - recoverMeltQuoteChange: flow(function* recoverMeltQuoteChange( - mintUrl: string, - transaction: Transaction - ) { - try { - const mintInstance = self.getMintModelInstance(mintUrl) - const transactionId = transaction.id - - if(!mintInstance || !transaction.keysetId) { - throw new AppError(Err.VALIDATION_ERROR, 'Missing mint instance or keysetId', {mintUrl, transactionId}) - } - - const currentCounter = mintInstance.getProofsCounterByKeysetId!(transaction.keysetId) - const meltCounterValue = currentCounter.getMeltCounterValue(transaction.id) - - if(!meltCounterValue || !meltCounterValue.meltPreview) { - throw new AppError(Err.VALIDATION_ERROR, 'Change already claimed - melt data not available for this transaction', {mintUrl, transactionId}) - } - - const meltPreview: MeltPreview = meltCounterValue.meltPreview - - if(!meltPreview) { - throw new AppError(Err.VALIDATION_ERROR, 'MeltPreview not found - this transaction may be from an older version', {mintUrl, transactionId}) - } - - const cashuWallet: CashuWallet = yield self.getWallet( - mintUrl, - transaction.unit, - { - withSeed: true, - keysetId: transaction.keysetId - } - ) - - // Use completeMelt with the stored MeltPreview to recover change - const {change}: MeltProofsResponse = yield cashuWallet.completeMelt(meltPreview) - - currentCounter.removeMeltCounterValue(transactionId) - - log.info('[recoverMeltQuoteChange]', {change}) - - return change - - } catch (e: any) { - let message = 'The mint could not return change from a melt quote.' - if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; - throw new AppError( - Err.MINT_ERROR, - message, - { - message: e.message, - caller: 'recoverMeltQuoteChange', - mintUrl, - } - ) - } - }), restore: flow(function* restore( mintUrl: string, seed: Uint8Array, diff --git a/src/screens/ExportBackupScreen.tsx b/src/screens/ExportBackupScreen.tsx index c12910e6..18c1a642 100644 --- a/src/screens/ExportBackupScreen.tsx +++ b/src/screens/ExportBackupScreen.tsx @@ -36,7 +36,7 @@ import { ContactsStoreSnapshot } from '../models/ContactsStore' import { MintsStoreSnapshot } from '../models/MintsStore' import { ResultModalInfo } from './Wallet/ResultModalInfo' import { verticalScale } from '@gocodingnow/rn-size-matters' -import { Token, getEncodedToken } from '@cashu/cashu-ts' +import { Token, getEncodedToken, normalizeProofAmounts } from '@cashu/cashu-ts' import { StaticScreenProps, useNavigation } from '@react-navigation/native' const OPTIMIZE_FROM_PROOFS_COUNT = 10 @@ -262,7 +262,7 @@ export const ExportBackupScreen = function ExportBackup({ route }: Props) { const tokenByKeysetId: Token = { mint, - proofs: proofsToExport, + proofs: normalizeProofAmounts(proofsToExport), unit: proofsByKeysetId[0].unit } diff --git a/src/screens/MintInfoScreen.tsx b/src/screens/MintInfoScreen.tsx index 4f464f5d..32f559f1 100644 --- a/src/screens/MintInfoScreen.tsx +++ b/src/screens/MintInfoScreen.tsx @@ -429,7 +429,7 @@ function NutsCard(props: {info: GetInfoResponse}) { continue; } if ('disabled' in info && info.disabled === false) { // detailed - supportedNutsDetailed.push([nut, info]) + supportedNutsDetailed.push([nut, info as unknown as DetailedNutInfo]) } else if ('supported' in info && typeof info.supported !== 'undefined') { // simple nutsSimple.push([nut, !!info.supported]) } else { @@ -567,14 +567,20 @@ function getMintLimits(info: GetInfoResponse) { console.log('runs') for (const method of info.nuts['4'].methods) { if ((typeof method.min_amount !== 'undefined' || typeof method.max_amount !== 'undefined') && method.unit === 'sat') { - mintSats = { min: method.min_amount, max: method.max_amount } + mintSats = { + min: method.min_amount !== undefined ? Number(method.min_amount) : undefined, + max: method.max_amount !== undefined ? Number(method.max_amount) : undefined, + } break; } } for (const method of info.nuts['5'].methods) { if ((typeof method.min_amount !== 'undefined' || typeof method.max_amount !== 'undefined') && method.unit === 'sat') { - meltSats = { min: method.min_amount, max: method.max_amount } + meltSats = { + min: method.min_amount !== undefined ? Number(method.min_amount) : undefined, + max: method.max_amount !== undefined ? Number(method.max_amount) : undefined, + } break; } } diff --git a/src/screens/NfcPayScreen.tsx b/src/screens/NfcPayScreen.tsx index e82c0e0d..307265b6 100644 --- a/src/screens/NfcPayScreen.tsx +++ b/src/screens/NfcPayScreen.tsx @@ -4,7 +4,7 @@ import { View, TextStyle, } from 'react-native' -import { PaymentRequest as CashuPaymentRequest, MeltQuoteBolt11Response, MeltQuoteResponse, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts' +import { PaymentRequest as CashuPaymentRequest, MeltQuoteBolt11Response, PaymentRequestTransportType, decodePaymentRequest, getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts' import NfcManager, { Ndef, NfcEvents } from 'react-native-nfc-manager' import { colors, spacing, typography, useThemeColor } from '../theme' import EventEmitter from '../utils/eventEmitter' @@ -400,7 +400,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { log.trace('[handlePaymentRequest] decoded Cashu payment request', { pr }) // Validate basics - if (!pr.amount || pr.amount <= 0) { + if (!pr.amount || Number(pr.amount) <= 0) { throw new AppError(Err.VALIDATION_ERROR, 'Payment request has no valid amount') } @@ -420,7 +420,7 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { // setEncodedCashuPaymentRequest(encoded) if (pr.description) setMemo(pr.description) - const requiredAmount = pr.amount + const requiredAmount = Number(pr.amount) const eligibleBalances = await getEligibleMintBalancesForCashu(pr, requiredAmount, unit) if (eligibleBalances.length === 0) { @@ -678,14 +678,14 @@ export const NfcPayScreen = observer(function NfcPayScreen({ route }: Props) { setInvoice(decoded) setInvoiceExpiry(expiresAt) setMeltQuote(quote) - setAmountToPay(formatDisplayAmount(quote.amount, unitRef.current)) + setAmountToPay(formatDisplayAmount(quote.amount.toNumber(), unitRef.current)) if (description) setMemo(description) - + setNfcInfo('Paying Lightning invoice...') const result = await WalletTask.transferQueueAwaitable( balanceToUse, - quote.amount, + quote.amount.toNumber(), unitRef.current, quote, description ?? '', diff --git a/src/screens/ReceiveScreen.tsx b/src/screens/ReceiveScreen.tsx index e9ca37f3..7b4a1f91 100644 --- a/src/screens/ReceiveScreen.tsx +++ b/src/screens/ReceiveScreen.tsx @@ -136,7 +136,8 @@ export const ReceiveScreen = observer(function ReceiveScreen({ route }: Props) { // keysetsV2 support const tokenInfo = getTokenMetadata(encoded) - const {amount, unit, memo, mint: mintUrl} = tokenInfo + const {amount: rawAmount, unit, memo, mint: mintUrl} = tokenInfo + const amount = Number(rawAmount) if(!unit) { throw new AppError(Err.VALIDATION_ERROR, translate("decodedMissingCurrencyUnit", { unit: CurrencyCode.SAT })) diff --git a/src/screens/SendScreen.tsx b/src/screens/SendScreen.tsx index 7dbc4714..0ad58a5d 100644 --- a/src/screens/SendScreen.tsx +++ b/src/screens/SendScreen.tsx @@ -618,8 +618,10 @@ export const SendScreen = observer(function SendScreen({ route }: Props) { setIsCashuPrWithDesc(true) } - if (pr.amount) { - setAmountToSend(`${numbro(pr.amount / getCurrency(unitRef.current).precision).format({ + const prAmount = pr.amount ? Number(pr.amount) : undefined + + if (prAmount) { + setAmountToSend(`${numbro(prAmount / getCurrency(unitRef.current).precision).format({ thousandSeparated: true, mantissa: getCurrency(unitRef.current).mantissa, })}`) @@ -646,22 +648,22 @@ export const SendScreen = observer(function SendScreen({ route }: Props) { withEnoughBalance = availableBalances.filter(balance => { const unitBalance = balance.balances[unitRef.current] - if(!pr.amount) return balance - if(pr.amount > 0 && unitBalance && unitBalance >= pr.amount) return balance + if(!prAmount) return balance + if(prAmount > 0 && unitBalance && unitBalance >= prAmount) return balance return null }) - if (pr.amount && withEnoughBalance.length === 0) { - dispatch({ type: 'SET_INFO', message: `Not enough balance to pay this payment request. Required: ${pr.amount} ${unitRef.current}.`}) - //infoMessage(`Not enough balance to pay this payment request. Required: ${pr.amount} ${unitRef.current}.`) + if (prAmount && withEnoughBalance.length === 0) { + dispatch({ type: 'SET_INFO', message: `Not enough balance to pay this payment request. Required: ${prAmount} ${unitRef.current}.`}) + //infoMessage(`Not enough balance to pay this payment request. Required: ${prAmount} ${unitRef.current}.`) return - //throw new AppError(Err.VALIDATION_ERROR, `Not enough balance to pay this payment request. Required: ${pr.amount} ${unitRef.current}.`) + //throw new AppError(Err.VALIDATION_ERROR, `Not enough balance to pay this payment request. Required: ${prAmount} ${unitRef.current}.`) } log.trace('[handlePaymentRequest] available mint balances for this payment request', {availableBalances, withEnoughBalance}) } else { - withEnoughBalance = (pr.amount && pr.amount > 0) - ? proofsStore.getMintBalancesWithEnoughBalance(pr.amount, unitRef.current) + withEnoughBalance = (prAmount && prAmount > 0) + ? proofsStore.getMintBalancesWithEnoughBalance(prAmount, unitRef.current) : proofsStore.getMintBalancesWithUnit(unitRef.current) } diff --git a/src/screens/TransferScreen.tsx b/src/screens/TransferScreen.tsx index e6db9d3a..b484fb68 100644 --- a/src/screens/TransferScreen.tsx +++ b/src/screens/TransferScreen.tsx @@ -514,14 +514,14 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props) meltQuoteRef.current = quote // Format amount for display - const displayAmount = numbro(quote.amount / getCurrency(unitRef.current).precision).format({ + const displayAmount = numbro(quote.amount.toNumber() / getCurrency(unitRef.current).precision).format({ thousandSeparated: true, mantissa: getCurrency(unitRef.current).mantissa, }) setAmountToTransfer(displayAmount) // Check total required balance (amount + fee reserve) - const totalRequired = quote.amount + quote.fee_reserve + const totalRequired = quote.amount.toNumber() + quote.fee_reserve.toNumber() const availableBalances = proofsStore.getMintBalancesWithEnoughBalance(totalRequired, unitRef.current) if (availableBalances.length === 0) { @@ -934,7 +934,7 @@ export const TransferScreen = observer(function TransferScreen({ route }: Props) {encodedInvoice && (meltQuote?.fee_reserve || finalFee) ? ( ) : ( diff --git a/src/screens/Wallet/QRCode.tsx b/src/screens/Wallet/QRCode.tsx index 403e4e6c..c4d2daec 100644 --- a/src/screens/Wallet/QRCode.tsx +++ b/src/screens/Wallet/QRCode.tsx @@ -121,7 +121,7 @@ export const QRCodeBlock = function (props: { setKeysetFormat('hex') } else { try { - const encodedV3 = getEncodedToken(decoded, {version: 3}) + const encodedV3 = getEncodedToken(decoded) setEncodedV3Token(encodedV3) } catch (e: any) { handleQrError(e) @@ -195,7 +195,7 @@ export const QRCodeBlock = function (props: { } else if(type === 'EncodedV4Token') { if(!decodedToken) return - const encodedV3 = getEncodedToken(decodedToken, {version: 3}) + const encodedV3 = getEncodedToken(decodedToken) setEncodedV3Token(encodedV3) } } catch (e: any) { diff --git a/src/services/cashu/cashuUtils.ts b/src/services/cashu/cashuUtils.ts index 6ef3abb9..d2de0ff0 100644 --- a/src/services/cashu/cashuUtils.ts +++ b/src/services/cashu/cashuUtils.ts @@ -1,14 +1,19 @@ import {Mint} from '../../models/Mint' +import { + Amount, + OutputData, +} from '@cashu/cashu-ts' import type { - Token, - Proof as CashuProof, + Token, + ProofLike as CashuProof, PaymentRequest as CashuPaymentRequest, PaymentRequestPayload, TokenMetadata, + OutputDataLike, } from '@cashu/cashu-ts' -import { bytesToHex } from '@noble/hashes/utils' +import { bytesToHex, hexToBytes } from '@noble/hashes/utils' import AppError, {Err} from '../../utils/AppError' -import { getDecodedToken, getTokenMetadata } from '@cashu/cashu-ts' +import { getTokenMetadata } from '@cashu/cashu-ts' import {Proof} from '../../models/Proof' import { log } from '../logService' import { decodePaymentRequest } from '@cashu/cashu-ts' @@ -29,7 +34,7 @@ const isObj = function(v: unknown): v is object { * Sum the amounts of an array of proofs */ const sumProofs = function(proofs: CashuProof[]): number { - return proofs.reduce((acc: number, proof: CashuProof) => acc + proof.amount, 0) + return proofs.reduce((acc: number, proof: CashuProof) => acc + Number(proof.amount), 0) } const CASHU_URI_PREFIXES = [ @@ -407,6 +412,38 @@ const isTokenP2PKLocked = function (token: Token | TokenMetadata): boolean { +export interface StoredMeltPreview { + keysetId: string + outputData: SerializedOutputData[] +} + +export interface SerializedOutputData { + blindedMessage: { amount: string | number; id: string; B_: string } + blindingFactor: string // hex + secret: string // hex + ephemeralE?: string +} + +const serializeOutputData = (outputData: OutputDataLike[]): SerializedOutputData[] => + outputData.map(od => ({ + blindedMessage: { + amount: od.blindedMessage.amount.toString(), + id: od.blindedMessage.id, + B_: od.blindedMessage.B_, + }, + blindingFactor: od.blindingFactor.toString(16), + secret: bytesToHex(od.secret), + ephemeralE: od.ephemeralE, + })) + +const deserializeOutputData = (serialized: SerializedOutputData[]): OutputData[] => + serialized.map(od => new OutputData( + { amount: Amount.from(od.blindedMessage.amount), id: od.blindedMessage.id, B_: od.blindedMessage.B_ }, + BigInt('0x' + od.blindingFactor), + hexToBytes(od.secret), + od.ephemeralE, + )) + export const CashuUtils = { findEncodedCashuToken, findEncodedCashuPaymentRequest, @@ -427,7 +464,9 @@ export const CashuUtils = { isTokenP2PKLocked, isCollidingKeysetId, isObj, - sumProofs + sumProofs, + serializeOutputData, + deserializeOutputData, } diff --git a/src/services/wallet/receiveTask.ts b/src/services/wallet/receiveTask.ts index 7d879a4a..e07f8655 100644 --- a/src/services/wallet/receiveTask.ts +++ b/src/services/wallet/receiveTask.ts @@ -11,8 +11,7 @@ import AppError, {Err} from '../../utils/AppError' import { TransactionTaskResult } from '../walletService' import { WalletUtils } from './utils' import { MintUnit, formatCurrency, getCurrency } from './currency' -import { PaymentRequestPayload, Token, getDecodedToken } from '@cashu/cashu-ts' -import { getEncodedToken } from '@cashu/cashu-ts' +import { PaymentRequestPayload, Token, getDecodedToken, getEncodedToken, normalizeProofAmounts } from '@cashu/cashu-ts' const { mintsStore, @@ -605,7 +604,7 @@ export const receiveSync = async function ( // store swapped proofs as encoded token in tx data const outputToken = getEncodedToken({ mint: mintToReceive, - proofs: receivedProofs, + proofs: normalizeProofAmounts(receivedProofs), unit, memo, }) diff --git a/src/services/wallet/revertTask.ts b/src/services/wallet/revertTask.ts index a091b73c..afd03e60 100644 --- a/src/services/wallet/revertTask.ts +++ b/src/services/wallet/revertTask.ts @@ -9,8 +9,7 @@ import AppError, {Err} from '../../utils/AppError' import { TransactionTaskResult } from '../walletService' import { WalletUtils } from './utils' import { MintUnit } from './currency' -import { Token } from '@cashu/cashu-ts' -import { getEncodedToken, getKeepAmounts } from '@cashu/cashu-ts' +import { Token, getEncodedToken, normalizeProofAmounts } from '@cashu/cashu-ts' const { mintsStore, @@ -52,7 +51,7 @@ try { // This will invalidate originally sent proofs effectively reverting the transaction. const encodedToken: Token = { mint: mintInstance.mintUrl, - proofs: pendingProofs, + proofs: normalizeProofAmounts(pendingProofs), unit } @@ -69,8 +68,8 @@ try { // store freshed proofs as encoded token in tx data const outputToken = getEncodedToken({ mint: mintInstance.mintUrl, - proofs: receivedProofs, - unit + proofs: normalizeProofAmounts(receivedProofs), + unit }) // Remove original pending proofs diff --git a/src/services/wallet/sendTask.ts b/src/services/wallet/sendTask.ts index 1dc3ee2f..94c13024 100644 --- a/src/services/wallet/sendTask.ts +++ b/src/services/wallet/sendTask.ts @@ -15,6 +15,7 @@ import { MintKeyset, ProofState, getEncodedToken, + normalizeProofAmounts, } from '@cashu/cashu-ts' import { MAX_SWAP_INPUT_SIZE, TransactionTaskResult, WalletTask } from '../walletService' import { Mint, MintBalance } from '../../models/Mint' @@ -44,7 +45,7 @@ const _monitorSentProofs = async (params: { try { log.trace('[send] Subscribing to proofStateUpdates for sent proof', {secret: proofsToSend[0]}) const unsub = await wsWallet.on.proofStateUpdates( - [proofsToSend[0]], + normalizeProofAmounts([proofsToSend[0]]), async (proofState: ProofState) => { log.trace(`Websocket: proof state updated: ${proofState.state} with secret: ${proofsToSend[0].secret}`) if (proofState.state == CheckStateEnum.SPENT) { @@ -146,7 +147,7 @@ export const sendTask = async function ( const outputToken = getEncodedToken({ mint: mintUrl, - proofs: proofsToSend, + proofs: normalizeProofAmounts(proofsToSend), unit, memo, }) @@ -340,7 +341,7 @@ export const sendFromMintSync = async function ( if(isSwapNeeded) { // Calculate feeReserve from mint fee rate const walletInstance = await walletStore.getWallet(mintUrl, unit, {withSeed: true}) as CashuWallet - swapFeeReserve = walletInstance.getFeesForProofs(proofsToSendFrom) + swapFeeReserve = walletInstance.getFeesForProofs(proofsToSendFrom).toNumber() const amountWithFees = amountToSend + swapFeeReserve if (totalAmountFromMint < amountWithFees) { diff --git a/src/services/wallet/transferTask.ts b/src/services/wallet/transferTask.ts index 662a5662..86967f19 100644 --- a/src/services/wallet/transferTask.ts +++ b/src/services/wallet/transferTask.ts @@ -1,6 +1,6 @@ import {CashuUtils} from '../cashu/cashuUtils' import AppError, {Err} from '../../utils/AppError' -import {Mint as CashuMint, Wallet as CashuWallet, MeltProofsResponse, MeltQuoteBolt11Response, MeltQuoteResponse, MeltQuoteState, getEncodedToken} from '@cashu/cashu-ts' +import {Mint as CashuMint, Wallet as CashuWallet, MeltProofsResponse, MeltQuoteBolt11Response, MeltQuoteState, getEncodedToken, normalizeProofAmounts} from '@cashu/cashu-ts' import {rootStoreInstance} from '../../models' import { TransactionTaskResult, WalletTask } from '../walletService' import { MintBalance } from '../../models/Mint' @@ -116,7 +116,7 @@ export const transferTask = async function ( const newTransaction = { type: TransactionType.TRANSFER, amount: amountToTransfer, - fee: meltQuote.fee_reserve, + fee: meltQuote.fee_reserve.toNumber(), unit, data: JSON.stringify(transactionData), memo, @@ -137,7 +137,7 @@ export const transferTask = async function ( // Replace individual setters with a single update transaction.update({ paymentId: paymentHash, quote: meltQuote.quote }) - if (amountToTransfer + meltQuote.fee_reserve > mintBalanceToTransferFrom.balances[unit]!) { + if (amountToTransfer + meltQuote.fee_reserve.toNumber() > mintBalanceToTransferFrom.balances[unit]!) { throw new AppError( Err.VALIDATION_ERROR, 'Mint balance is insufficient to cover the amount to transfer with the expected Lightning fees.', @@ -165,15 +165,15 @@ export const transferTask = async function ( const totalAmountFromMint = CashuUtils.getProofsAmount(proofsFromMint) proofsToMeltFrom = CashuUtils.getProofsToSend( - amountToTransfer + meltQuote.fee_reserve, + amountToTransfer + meltQuote.fee_reserve.toNumber(), proofsFromMint ) proofsToMeltFromAmount = CashuUtils.getProofsAmount(proofsToMeltFrom) const walletInstance = await walletStore.getWallet(mintUrl, unit, {withSeed: true}) - meltFeeReserve = walletInstance.getFeesForProofs(proofsToMeltFrom) - const amountWithFees = amountToTransfer + meltQuote.fee_reserve + meltFeeReserve + meltFeeReserve = walletInstance.getFeesForProofs(proofsToMeltFrom).toNumber() + const amountWithFees = amountToTransfer + meltQuote.fee_reserve.toNumber() + meltFeeReserve if (totalAmountFromMint < amountWithFees) { throw new AppError( @@ -217,15 +217,15 @@ export const transferTask = async function ( transactionData.push({ status: TransactionStatus.PREPARED, proofsToMeltFromAmount, - lightningFeeReserve: meltQuote.fee_reserve, + lightningFeeReserve: meltQuote.fee_reserve.toNumber(), meltFeeReserve, createdAt: new Date(), }) const inputToken = getEncodedToken({ mint: mintUrl, - proofs: proofsToMeltFrom, - unit + proofs: normalizeProofAmounts(proofsToMeltFrom), + unit }) transaction.update({ @@ -383,7 +383,7 @@ export const transferTask = async function ( taskFunction: TRANSFER_TASK, mintUrl, transaction, - message: 'Lightning payment is in progress. Check your transaction history for the result.', + message: 'Lightning payment is in progress...', meltQuote: meltResponse.quote, nwcEvent, } as TransactionTaskResult diff --git a/src/services/walletService.ts b/src/services/walletService.ts index c5f5edaa..46f34d41 100644 --- a/src/services/walletService.ts +++ b/src/services/walletService.ts @@ -15,7 +15,7 @@ import {CashuProof, CashuUtils} from './cashu/cashuUtils' import {LightningUtils} from './lightning/lightningUtils' import AppError, {Err} from '../utils/AppError' import {MintBalance, MintStatus} from '../models/Mint' -import {MeltQuoteBaseResponse, MeltQuoteBolt11Response, MeltQuoteResponse, MeltQuoteState, MintQuoteState, OutputData, PaymentRequestPayload, Token, TokenMetadata, getDecodedToken, getEncodedToken, getTokenMetadata} from '@cashu/cashu-ts' +import {MeltQuoteBaseResponse, MeltQuoteBolt11Response, MeltQuoteState, MintQuoteState, PaymentRequestPayload, Token, TokenMetadata, getDecodedToken, getEncodedToken, getTokenMetadata, normalizeProofAmounts} from '@cashu/cashu-ts' import {Mint} from '../models/Mint' import {pollerExists, stopPolling} from '../utils/poller' import { NostrClient, NostrEvent, NostrProfile } from './nostrService' @@ -95,7 +95,7 @@ type WalletTaskService = { mintBalanceToTransferFrom: MintBalance, amountToTransfer: number, unit: MintUnit, - meltQuote: MeltQuoteResponse, + meltQuote: MeltQuoteBolt11Response, memo: string, invoiceExpiry: Date, encodedInvoice: string, @@ -149,7 +149,7 @@ type WalletTaskService = { }) => Promise<{recoveredAmount: number}> recoverMeltQuoteChange: (params: { mintUrl: string, - meltQuote: string | MeltQuoteResponse + meltQuote: string | MeltQuoteBolt11Response }) => Promise<{recoveredAmount: number}> handlePendingMeltTask: (params: { mintUrl: string @@ -301,7 +301,8 @@ const receiveQueueAwaitable = function ( ): Promise { const now = Date.now() - const {amount, memo, unit} = tokenMetadata + const {amount: rawAmount, memo, unit} = tokenMetadata + const amount = Number(rawAmount) const proofsCount = tokenMetadata.incompleteProofs.length const taskId = proofsCount > MAX_SWAP_INPUT_SIZE @@ -338,7 +339,7 @@ const receiveQueueAwaitable = function ( taskId, async () => { try { - const token = getDecodedToken(encodedToken, mint.keysetIds) + const token = getDecodedToken(encodedToken, mint.keysetIds ?? []) return proofsCount > MAX_SWAP_INPUT_SIZE ? await receiveBatchTask(token, amount, memo || '', encodedToken) : await receiveTask(token, amount, memo || '', encodedToken) @@ -443,7 +444,8 @@ const receiveOfflinePrepareQueueAwaitable = function ( ): Promise { const now = Date.now() const taskId = `receiveOfflinePrepareTask-${now}` - const { mint: mintUrl, amount, memo, unit } = tokenMetadata + const { mint: mintUrl, amount: rawAmount, memo, unit } = tokenMetadata + const amount = Number(rawAmount) return new Promise((resolve, reject) => { let resolved = false @@ -1677,7 +1679,7 @@ const handleInFlightByMintTask = async (mint: Mint): Promise = }) - const outputToken = getEncodedToken({ mint: mintUrl, proofs, unit }) + const outputToken = getEncodedToken({ mint: mintUrl, proofs: normalizeProofAmounts(proofs), unit }) const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance txData.push({ status: TransactionStatus.COMPLETED, receivedAmount, swapFeePaid, createdAt: new Date() }) @@ -1718,7 +1720,7 @@ const handleInFlightByMintTask = async (mint: Mint): Promise = proofsStore.addOrUpdate(returnedProofs, { mintUrl, tId: tx.id, unit, isPending: false, isSpent: false }) proofsStore.addOrUpdate(proofsToSend, { mintUrl, tId: tx.id, unit, isPending: true, isSpent: false }) - const outputToken = getEncodedToken({ mint: mintUrl, proofs: proofsToSend, unit }) + const outputToken = getEncodedToken({ mint: mintUrl, proofs: normalizeProofAmounts(proofsToSend), unit }) const balanceAfter = proofsStore.getUnitBalance(unit)?.unitBalance txData.push({ status: TransactionStatus.PENDING, createdAt: new Date() }) @@ -2269,15 +2271,7 @@ const recoverMintQuote = async ( const cashuWallet = await walletStore.getWallet(mintUrl, unit, { withSeed: true, keysetId: meltPreview.keysetId }) const keyset = cashuWallet.getKeyset(meltPreview.keysetId) - const reconstructedOutputData = meltPreview.outputData.map((od: any) => { - const blindingFactor = BigInt(od.blindingFactor) - const secretValues: number[] = typeof od.secret === 'object' && !ArrayBuffer.isView(od.secret) - ? Object.values(od.secret as Record) - : Array.from(od.secret as Uint8Array) - const secret = new Uint8Array(secretValues) - return new OutputData(od.blindedMessage, blindingFactor, secret) - }) - + const reconstructedOutputData = CashuUtils.deserializeOutputData(meltPreview.outputData) const recoveredChange = change.map((sig, i) => reconstructedOutputData[i].toProof(sig, keyset)) currentCounter.removeMeltCounterValue(tx.id) @@ -2383,19 +2377,11 @@ const handlePendingMeltTask = async (params: { { withSeed: true, keysetId: meltPreview.keysetId }) const keyset = cashuWallet.getKeyset(meltPreview.keysetId) - // Reconstruct OutputData instances: serializeMeltPreview converts bigint→string - // and Uint8Array→plain-object, so we must restore the types before calling toProof. - const reconstructedOutputData = meltPreview.outputData.map((od: any) => { - const blindingFactor = BigInt(od.blindingFactor) - const secretValues: number[] = typeof od.secret === 'object' && !ArrayBuffer.isView(od.secret) - ? Object.values(od.secret as Record) - : Array.from(od.secret as Uint8Array) - const secret = new Uint8Array(secretValues) - return new OutputData(od.blindedMessage, blindingFactor, secret) - }) - + const reconstructedOutputData = CashuUtils.deserializeOutputData(meltPreview.outputData) const change = quote.change.map((sig, i) => reconstructedOutputData[i].toProof(sig, keyset)) + log.trace('[handlePendingMelt] Change unblinded', { transactionId, quoteId, change }) + currentCounter.removeMeltCounterValue(transactionId) if (change.length > 0) { @@ -2410,7 +2396,7 @@ const handlePendingMeltTask = async (params: { } } } catch (e: any) { - log.error('[handlePendingMelt] change recovery failed, completing without change', e.message) + log.error('[handlePendingMelt] change recovery failed, completing without change', {message: e.message}) } const txData: TransactionData[] = transaction.data ? JSON.parse(transaction.data) : [] @@ -2540,7 +2526,7 @@ const handleClaimTask = async function (params: { const result: TransactionTaskResult = await receiveTask( decoded, - tokenInfo.amount, + Number(tokenInfo.amount), tokenInfo.memo || 'Received to Lightning address', encodedToken, ) @@ -2838,7 +2824,7 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro // keysetsV2 support const tokenInfo = getTokenMetadata(incoming.encoded) - const amountToReceive = tokenInfo.amount + const amountToReceive = Number(tokenInfo.amount) const memo = tokenInfo.memo || 'Received over Nostr' const {unit, mint: mintUrl} = tokenInfo @@ -3041,7 +3027,8 @@ const handleReceivedEventTask = async function (encryptedEvent: NostrEvent): Pro } // create draft transaction - const {amount, unit, description, id, mints} = decoded + const {amount: rawAmount, unit, description, id, mints} = decoded + const amount = Number(rawAmount) const availableBalances: MintBalance[] = [] diff --git a/tsconfig.json b/tsconfig.json index 08217d25..a1b7f244 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "exclude": ["**/node_modules", "**/Pods"], "compilerOptions": { "jsx": "react", - "typeRoots": ["./src/utils/envtypes"], + "typeRoots": ["./src/utils/envtypes", "./node_modules/@types"], "lib": ["ES2022"], "paths": { "nostr-tools/*": ["./node_modules/nostr-tools/lib/types/*"] diff --git a/yarn.lock b/yarn.lock index eb7201aa..f0902ddd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2869,15 +2869,15 @@ __metadata: languageName: node linkType: hard -"@cashu/cashu-ts@npm:^3.6.1": - version: 3.6.1 - resolution: "@cashu/cashu-ts@npm:3.6.1" +"@cashu/cashu-ts@npm:^4.2.1": + version: 4.2.1 + resolution: "@cashu/cashu-ts@npm:4.2.1" dependencies: - "@noble/curves": ^2.0.1 - "@noble/hashes": ^2.0.1 - "@scure/base": ^2.0.0 - "@scure/bip32": ^2.0.1 - checksum: 07018c68fa9b5bd8affde081a6f5c6fb0ba67e1e6e1a3872527547461aea57a3aeb6bb6dace5dad430464b17f53503ad11d6a56b985a43cea3a8a18e8e86838b + "@noble/curves": ^2.2.0 + "@noble/hashes": ^2.2.0 + "@scure/base": ^2.2.0 + "@scure/bip32": ^2.2.0 + checksum: a950f38686fcab220a61ea7b83c08f14f8af0ed5b54a18b38f8a370fc3e63897a6c1f266d8e7e9e269f05413d8b5f9d58d319bd1b194b2e2830cb7983169c931 languageName: node linkType: hard @@ -5433,12 +5433,12 @@ __metadata: languageName: node linkType: hard -"@noble/curves@npm:2.0.1, @noble/curves@npm:^2.0.1": - version: 2.0.1 - resolution: "@noble/curves@npm:2.0.1" +"@noble/curves@npm:2.2.0, @noble/curves@npm:^2.2.0": + version: 2.2.0 + resolution: "@noble/curves@npm:2.2.0" dependencies: - "@noble/hashes": 2.0.1 - checksum: b6844f350d629bb6de55d3a123bc148403901c8b8eed77d2132a389d080d553242bcf1e06913bf51f51e395849ec70b9a8e8902c19e562ea3c6538bb0240d92f + "@noble/hashes": 2.2.0 + checksum: 2b6f02c18918578f528791644886f54333c67323f5ccefe10bf250f08760f63786b807390d8d5f8562c661e70dd3559ce8e51e1fe360118c8fd47071c2bd6991 languageName: node linkType: hard @@ -5465,13 +5465,20 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:2.0.1, @noble/hashes@npm:^2.0.1": +"@noble/hashes@npm:2.0.1": version: 2.0.1 resolution: "@noble/hashes@npm:2.0.1" checksum: aea67a671ca464027cd512d07666a804bd99abc343ba98c93ee8463a1c870c5594f8de468279ef3c60f3b23b6330db9f1a92bd23723b8a911ec73bddc9420248 languageName: node linkType: hard +"@noble/hashes@npm:2.2.0, @noble/hashes@npm:^2.2.0": + version: 2.2.0 + resolution: "@noble/hashes@npm:2.2.0" + checksum: a8745fb5da57a73ddd3dd6d5ad45d156ea664be51ead5344ca1528f8e56ad0ba0720ec8adff3bda37c50c72a86a33a57332ec02c3fb8d2a2612f9c3e5e7079ab + languageName: node + linkType: hard + "@noble/hashes@npm:^1.1.5": version: 1.8.0 resolution: "@noble/hashes@npm:1.8.0" @@ -6405,13 +6412,20 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:2.0.0, @scure/base@npm:^2.0.0": +"@scure/base@npm:2.0.0": version: 2.0.0 resolution: "@scure/base@npm:2.0.0" checksum: 15d10f154256f52bb3e02b63cdf93b6df6db3814fa4c61a8bb2d339831aec7859609fb715b93c7fc852755f0fe67fb6411664c06159745711bf38af3b10ab93e languageName: node linkType: hard +"@scure/base@npm:2.2.0, @scure/base@npm:^2.2.0": + version: 2.2.0 + resolution: "@scure/base@npm:2.2.0" + checksum: 2dd91f310765366e2dcb5e37709eb35ccd57d3f2f112d36232ca03fb8819c8764708f333ac40935fa719ff2e943bd50e1912ab43d212e44e807100d21f835f13 + languageName: node + linkType: hard + "@scure/base@npm:~1.1.0": version: 1.1.9 resolution: "@scure/base@npm:1.1.9" @@ -6430,14 +6444,14 @@ __metadata: languageName: node linkType: hard -"@scure/bip32@npm:^2.0.1": - version: 2.0.1 - resolution: "@scure/bip32@npm:2.0.1" +"@scure/bip32@npm:^2.2.0": + version: 2.2.0 + resolution: "@scure/bip32@npm:2.2.0" dependencies: - "@noble/curves": 2.0.1 - "@noble/hashes": 2.0.1 - "@scure/base": 2.0.0 - checksum: 5e6c7b455c4a5599673d5fa1b7edc1b3655fe2f7b8388dc19ebe4a89adb9c80c511215d4af624eb774c63c216fd8b7bb8fecf1eb8c3d5e979e279c7542657a42 + "@noble/curves": 2.2.0 + "@noble/hashes": 2.2.0 + "@scure/base": 2.2.0 + checksum: 6cd0ddf603b739d3c24a13618fc246d409bbf0dffec22bc2c3c8e3e64271bb58c7078bea0b6ab9601fcfe881a969ee31e3074d478affe13a439d7527a0b68ecf languageName: node linkType: hard @@ -14606,7 +14620,7 @@ __metadata: "@babel/plugin-proposal-export-namespace-from": ^7.18.9 "@babel/preset-env": ^7.25.3 "@babel/runtime": ^7.25.0 - "@cashu/cashu-ts": ^3.6.1 + "@cashu/cashu-ts": ^4.2.1 "@fortawesome/fontawesome-svg-core": ^7.1.0 "@fortawesome/free-brands-svg-icons": ^7.1.0 "@fortawesome/free-regular-svg-icons": ^7.1.0 @@ -14635,7 +14649,7 @@ __metadata: "@react-navigation/native": ^7.1.18 "@react-navigation/native-stack": ^7.3.27 "@react-navigation/stack": ^7.4.9 - "@scure/bip32": ^2.0.1 + "@scure/bip32": ^2.2.0 "@scure/bip39": 2.0.1 "@sentry/cli": ^2.56.0 "@sentry/react-native": ^7.7.0