Upgrade to cashu-ts v4

This commit is contained in:
minibits-cash
2026-05-11 22:33:22 +02:00
parent 86f4525bf4
commit 2e4d9328bb
20 changed files with 200 additions and 203 deletions

View File

@@ -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",

View File

@@ -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);

View File

@@ -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<MeltPreview>()), // v3.x MeltPreview object for recovery
meltPreview: types.maybe(types.frozen<StoredMeltPreview>()),
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(),
})

View File

@@ -205,6 +205,7 @@ import {
} else {
proofNode = ProofModel.create({
...proof,
amount: Number(proof.amount),
mintUrl,
tId,
unit,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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;
}
}

View File

@@ -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 ?? '',

View File

@@ -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 }))

View File

@@ -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)
}

View File

@@ -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) ? (
<FeeBadge
currencyCode={getCurrency(unitRef.current).code}
estimatedFee={meltQuote?.fee_reserve || 0}
estimatedFee={meltQuote?.fee_reserve.toNumber() ?? 0}
finalFee={finalFee}
/>
) : (

View File

@@ -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) {

View File

@@ -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,
}

View File

@@ -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,
})

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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<TransactionTaskResult> {
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<TransactionTaskResult> {
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<TransactionTaskResult>((resolve, reject) => {
let resolved = false
@@ -1677,7 +1679,7 @@ const handleInFlightByMintTask = async (mint: Mint): Promise<WalletTaskResult> =
})
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<WalletTaskResult> =
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<string, number>)
: 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<string, number>)
: 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[] = []

View File

@@ -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/*"]

View File

@@ -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