diff --git a/package.json b/package.json index a2f4e55c..07e9f871 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "postinstall": "patch-package --exclude 'nothing'" }, "dependencies": { - "@cashu/cashu-ts": "minibits-cash/cashu-ts#1.0.0-minibits", + "@cashu/cashu-ts": "minibits-cash/cashu-ts#1.0.0-minibits.2", "@fortawesome/fontawesome-svg-core": "^6.5.2", "@fortawesome/free-brands-svg-icons": "^6.5.2", "@fortawesome/free-regular-svg-icons": "^6.5.2", diff --git a/src/models/Mint.ts b/src/models/Mint.ts index 0e818a10..10d3d0ff 100644 --- a/src/models/Mint.ts +++ b/src/models/Mint.ts @@ -1,6 +1,11 @@ -import {cast, flow, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree' +import {cast, flow, getSnapshot, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree' import {withSetPropAction} from './helpers/withSetPropAction' -import type {CashuWallet, GetInfoResponse, MintKeys, MintKeyset} from '@cashu/cashu-ts' +import type { + CashuWallet, + GetInfoResponse, + MintKeys as CashuMintKeys, + MintKeyset as CashuMintKeyset +} from '@cashu/cashu-ts' import {colors, getRandomIconColor} from '../theme' import { log, MintClient } from '../services' @@ -33,8 +38,6 @@ export type Balances = { unitBalances: UnitBalance[] } - - export enum MintStatus { ONLINE = 'ONLINE', OFFLINE = 'OFFLINE' @@ -43,13 +46,21 @@ export enum MintStatus { export type MintProofsCounter = { keyset: string counter: number - unit: MintUnit - input_fee_ppk: number + unit: MintUnit inFlightFrom?: number // starting counter index for pending split request sent to mint (for recovery from failure to receive proofs) inFlightTo?: number // last counter index for pending split request sent to mint inFlightTid?: number // related tx id } +export const MintProofsCounterModel = types + .model('MintProofsCounter', { + keyset: types.string, + unit: types.optional(types.frozen(), 'sat'), + counter: types.number, + inFlightFrom: types.maybe(types.number), + inFlightTo: types.maybe(types.number), + inFlightTid: types.maybe(types.number) + }) /** * This represents a Cashu mint */ @@ -59,24 +70,61 @@ export const MintModel = types mintUrl: types.string, hostname: types.maybe(types.string), shortname: types.maybe(types.string), - units: types.array(types.frozen()), - proofsCounters: types.array( - types.model('MintProofsCounter', { - keyset: types.string, - unit: types.optional(types.frozen(), 'sat'), - counter: types.number, - input_fee_ppk: types.optional(types.number, 0), - inFlightFrom: types.maybe(types.number), - inFlightTo: types.maybe(types.number), - inFlightTid: types.maybe(types.number) - }) - ), + units: types.array(types.frozen()), + keysets: types.array(types.frozen()), + keys: types.array(types.frozen()), + proofsCounters: types.array(MintProofsCounterModel), color: types.optional(types.string, colors.palette.iconBlue200), status: types.optional(types.frozen(), MintStatus.ONLINE), createdAt: types.optional(types.Date, new Date()), }) .actions(withSetPropAction) // TODO? start to use across app to avoid pure setter methods, e.g. mint.setProp('color', '#ccc') .actions(self => ({ + addKeyset(keyset: CashuMintKeyset) { + const alreadyExists = self.keysets.some(k => k.id === keyset.id) + + if(!alreadyExists) { + self.keysets.push(keyset) + } + + self.keysets = cast(self.keysets) + }, + removeKeyset(keyset: CashuMintKeyset) { + const index = self.keysets.findIndex(k => k.id === keyset.id) + + if(index) { + self.keysets.splice(index, 0) + } + + self.keysets = cast(self.keysets) + }, + setIsActive(freshKeyset: CashuMintKeyset) { + const keyset = self.keysets.find(k => k.id === freshKeyset.id) + + if(keyset) { + keyset.active = freshKeyset.active + } + + self.keysets = cast(self.keysets) + }, + addKeys(keys: CashuMintKeys) { + const alreadyExists = self.keys.some(k => k.id === keys.id) + + if(!alreadyExists) { + self.keys.push(keys) + } + + self.keys = cast(self.keys) + }, + removeKeys(keys: CashuMintKeys) { + const index = self.keys.findIndex(k => k.id === keys.id) + + if(index) { + self.keys.splice(index, 0) + } + + self.keys = cast(self.keys) + }, addUnit(unit: MintUnit) { const alreadyExists = self.units.some(u => u === unit) @@ -95,43 +143,99 @@ export const MintModel = types self.units = cast(self.units) }, + addProofsCounter(counter: MintProofsCounter) { + const alreadyExists = self.proofsCounters.some(p => p.keyset === counter.keyset) + + if(!alreadyExists) { + self.proofsCounters.push(counter) + } + + self.proofsCounters = cast(self.proofsCounters) + }, + removeProofsCounter(counter: MintProofsCounter) { + const index = self.proofsCounters.findIndex(p => p.keyset === counter.keyset) + + if(index) { + self.proofsCounters.splice(index, 0) + } + + self.proofsCounters = cast(self.proofsCounters) + }, getProofsCounter(keysetId: string) { const counter = self.proofsCounters.find(c => c.keyset === keysetId) return counter }, + isUnitSupported(unit: MintUnit): boolean { + return MintUnits.includes(unit) ? true : false + }, + keysetExists(keyset: CashuMintKeyset): boolean { + return self.keysets.some(k => k.id === keyset.id) + }, })) - .actions(self => ({ - createProofsCounter(keyset: MintKeyset) { - // Do not add unit the wallet does not have configured - if (!MintUnits.includes(keyset.unit as MintUnit)) { - throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${keyset.unit}`) - } - - const existing = self.getProofsCounter(keyset.id) - - if(existing) { // update fees if they can change (?) - if (existing.unit !== keyset.unit) { - throw new AppError(Err.VALIDATION_ERROR, `Keyset unit mismatch, got ${keyset.unit}, expected ${existing.unit}`) - } - - existing.input_fee_ppk = keyset.input_fee_ppk || 0 - self.proofsCounters = cast(self.proofsCounters) - return existing - } - + .actions(self => ({ + createProofsCounter(keyset: CashuMintKeyset) { const newCounter: MintProofsCounter = { keyset: keyset.id, - unit: keyset.unit as MintUnit, - input_fee_ppk: keyset.input_fee_ppk || 0, + unit: keyset.unit as MintUnit, counter: 0, } - self.proofsCounters.push(newCounter) - self.addUnit(keyset.unit as MintUnit) - const instance = self.proofsCounters.find(c => c.keyset === keyset.id) as MintProofsCounter + const proofsCounterInstance = MintProofsCounterModel.create(newCounter) + self.addProofsCounter(newCounter) + + log.trace('[ceateProofsCounter]', {newCounter: getSnapshot(proofsCounterInstance)}) + return proofsCounterInstance + } + })) + .actions(self => ({ + initKeyset(keyset: CashuMintKeyset) { + // Do not add unit the wallet does not have configured + if(!self.isUnitSupported(keyset.unit as MintUnit)) { + throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${keyset.unit}`) + } + + const existing = self.keysets.find(k => k.id === keyset.id) - log.trace('[ceateProofsCounter]', {newCounter: instance}) - return instance + if(existing) { + if (existing.unit !== keyset.unit) { + throw new AppError(Err.VALIDATION_ERROR, `Keyset unit mismatch, got ${keyset.unit}, expected ${existing.unit}`) + } + + return existing + } + + if(!keyset.input_fee_ppk) { + keyset.input_fee_ppk = 0 + } + + if(!keyset.unit) { + keyset.unit = 'sat' + } + + self.addKeyset(keyset) + self.addUnit(keyset.unit as MintUnit) + self.createProofsCounter(keyset) + + log.trace('[initKeyset]', {newKeyset: keyset}) + }, + initKeys(key: CashuMintKeys) { + // Do not add unit the wallet does not have configured + if(!self.isUnitSupported(key.unit as MintUnit)) { + throw new AppError(Err.VALIDATION_ERROR, `Unsupported unit provided by the mint: ${key.unit}`) + } + + const existing = self.keys.find(k => k.id === key.id) + + if(existing) { + if (existing.unit !== key.unit) { + throw new AppError(Err.VALIDATION_ERROR, `Keyset unit mismatch, got ${key.unit}, expected ${existing.unit}`) + } + + return existing + } + + self.addKeys(key) + log.trace('[initKeys]', {newKeys: key.id}) }, findInFlightProofsCounter() { const counter = self.proofsCounters.find(c => c.inFlightFrom && c.inFlightTo && c.inFlightTid) @@ -151,32 +255,39 @@ export const MintModel = types }, })) .actions(self => ({ - getProofsCounterByUnit: flow(function* getProofsCounterByUnit(unit: MintUnit) { - try { - // Refresh current keys for this unit from new or existing in-memory wallet instance - const wallet: CashuWallet = yield MintClient.getWallet(self.mintUrl, unit) - const keyset = wallet.keyset - - // Get or create new proofs counter for this keyset - const counter = self.getProofsCounter(keyset.id) - - if(!counter) { - return self.createProofsCounter(keyset) - } - - return counter - } catch(e: any) { - log.warn('[getProofsCounterByUnit] Could not refresh keyset, using mint proofsCounter for unit', {unit}) - - const proofsCounterInstance = self.proofsCounters.find(c => c.unit === unit) - - if(!proofsCounterInstance) { - throw new AppError(Err.NOTFOUND_ERROR, 'Could not get keyset to create new mint proofsCounter for unit', {unit}) - } - - return proofsCounterInstance + refreshKeysets(freshKeysets: CashuMintKeyset[]) { + // add new keyset if not exists + for (const keyset of freshKeysets) { + self.initKeyset(keyset) + self.setIsActive(keyset) } - }), + }, + refreshKeys(freshKeys: CashuMintKeys[]) { + for (const key of freshKeys) { + self.initKeys(key) + } + }, + getProofsCounterByUnit(unit: MintUnit, useActiveKeyset: boolean = true) { + let keyset: CashuMintKeyset | undefined + + if(useActiveKeyset) { + keyset = self.keysets.find(k => k.active === true && k.unit === unit) + } else { + keyset = self.keysets.find(k => k.active === false && k.unit === unit) + } + + if(!keyset) { + throw new AppError(Err.NOTFOUND_ERROR, 'Mint has no keyset for this unit', {unit}) + } + + const counter = self.proofsCounters.find(p => p.keyset === keyset?.id) + + if(!counter) { + return self.createProofsCounter(keyset) + } + + return counter + }, setHostname() { try { self.hostname = new URL(self.mintUrl).hostname @@ -299,10 +410,10 @@ export const MintModel = types self.proofsCounters = cast(self.proofsCounters) }, getFeesForProofs(proofs: Proof[]): number { - // Find the corresponding MintProofsCounter for each proof and sum the input fees + // Find the corresponding keyset for each proof and sum the input fees const totalInputFees = proofs.reduce((sum, proof) => { - const counter = self.proofsCounters.find(pc => pc.keyset === proof.id) - return counter ? sum + counter.input_fee_ppk : sum + const keyset = self.keysets.find(k => k.id === proof.id) + return keyset && keyset.input_fee_ppk ? sum + keyset.input_fee_ppk : sum }, 0) // Calculate the fees @@ -317,15 +428,15 @@ export const MintModel = types const mintBalance: MintBalance | undefined = getRootStore(self).proofsStore.getMintBalance(self.mintUrl) return mintBalance }, - get keysets(): string[] { - return self.proofsCounters.map(c => c.keyset) - } + get keysetIds(): string[] { + return self.keysets.map(k => k.id) + } })) export type Mint = { - mintUrl: string + mintUrl: string } & Partial> export interface MintSnapshotOut extends SnapshotOut {} export interface MintSnapshotIn extends SnapshotIn {} \ No newline at end of file diff --git a/src/models/MintsStore.ts b/src/models/MintsStore.ts index 8e1b9fb6..1b55a07f 100644 --- a/src/models/MintsStore.ts +++ b/src/models/MintsStore.ts @@ -12,157 +12,164 @@ import { import {log} from '../services/logService' import { MintClient } from '../services' import AppError, { Err } from '../utils/AppError' -import { MintKeyset } from '@cashu/cashu-ts' + import type { + MintKeys as CashuMintKeys, + MintKeyset as CashuMintKeyset + } from '@cashu/cashu-ts' import { MintUnit, MintUnits } from '../services/wallet/currency' - export type MintsByHostname = { - hostname: string - mints: Mint[] - } +export type MintsByHostname = { + hostname: string + mints: Mint[] +} - export type MintsByUnit = { +export type MintsByUnit = { unit: MintUnit mints: Mint[] - } - - export const MintsStoreModel = types - .model('MintsStore', { - mints: types.array(MintModel), - blockedMintUrls: types.array(types.string), - }) - .views(self => ({ - findByUrl: (mintUrl: string | URL) => { - const mint = self.mints.find(m => m.mintUrl === mintUrl) - return mint ? mint : undefined - }, - })) - .actions(withSetPropAction) - .actions(self => ({ - addMint: flow(function* addMint(mintUrl: string) { - if(!mintUrl.includes('.onion') && !mintUrl.startsWith('https')) { - throw new AppError(Err.VALIDATION_ERROR, 'Mint URL needs to start with https') - } - - // create default wallet instance then download and cache up to date mint keys in that instance - const allKeysets: MintKeyset[] = yield MintClient.getMintKeysets(mintUrl) - const activeKeysets = allKeysets.filter(keyset => keyset.active === true) +} - if(!activeKeysets || activeKeysets.length === 0) { - throw new AppError(Err.VALIDATION_ERROR, 'Mint has no active keysets and is not operational', {mintUrl}) - } - - const newMint: Mint = { - mintUrl, - } - - const mintInstance = MintModel.create(newMint) - - mintInstance.setHostname() - mintInstance.setRandomColor() +export const MintsStoreModel = types + .model('MintsStore', { + mints: types.array(MintModel), + blockedMintUrls: types.array(types.string), + }) + .views(self => ({ + findByUrl: (mintUrl: string | URL) => { + const mint = self.mints.find(m => m.mintUrl === mintUrl) + return mint ? mint : undefined + }, + })) + .actions(withSetPropAction) + .actions(self => ({ + addMint: flow(function* addMint(mintUrl: string) { + if(!mintUrl.includes('.onion') && !mintUrl.startsWith('https')) { + throw new AppError(Err.VALIDATION_ERROR, 'Mint URL needs to start with https.') + } - for(const keyset of activeKeysets) { - try { - if(keyset.active === true) { - mintInstance.createProofsCounter(keyset) - } - } catch(e: any) { - continue - } + // create default wallet instance then download and cache up to date mint keys in that instance + const keysets: CashuMintKeyset[] = yield MintClient.getMintKeysets(mintUrl) + const keys: CashuMintKeys[] = yield MintClient.getMintKeys(mintUrl) + + if(!keysets || keysets.length === 0 || !keys || keys.length === 0) { + throw new AppError(Err.VALIDATION_ERROR, 'Mint has no keysets and is not operational.', {mintUrl}) + } + + const mintInstance = MintModel.create({mintUrl}) + + for(const keyset of keysets) { + if (!mintInstance.isUnitSupported(keyset.unit as MintUnit)) { + continue } - - yield mintInstance.setShortname() - self.mints.push(mintInstance) - }), - updateMint: flow(function* updateMint(mintUrl: string) { + + mintInstance.initKeyset(keyset) + } + + for(const key of keys) { + if (!mintInstance.isUnitSupported(key.unit as MintUnit)) { + continue + } + + mintInstance.initKeys(key) + } + + mintInstance.setHostname() + yield mintInstance.setShortname() + self.mints.push(mintInstance) + }), + updateMint: flow(function* updateMint(mintUrl: string) { const mintInstance = self.findByUrl(mintUrl) if(!mintInstance) { throw new AppError(Err.VALIDATION_ERROR, 'Could not find mint to update', {mintUrl}) } // refresh up to date mint keys - const allKeysets: MintKeyset[] = yield MintClient.getMintKeysets(mintUrl) + const keysets: CashuMintKeyset[] = yield MintClient.getMintKeysets(mintUrl) + const keys: CashuMintKeys[] = yield MintClient.getMintKeys(mintUrl) - if(!allKeysets || allKeysets.length === 0) { - throw new AppError(Err.VALIDATION_ERROR, 'Mint has no active keysets and is not operational', {mintUrl}) + if(!keysets || keysets.length === 0 || !keys || keys.length === 0) { + throw new AppError(Err.VALIDATION_ERROR, 'Mint has no keysets and is not operational', {mintUrl}) } - for(const keyset of allKeysets) { - if(keyset.active === true) { - // Do not add unit the wallet does not have configured - if (!MintUnits.includes(keyset.unit as MintUnit)) { - log.error(`Unsupported unit provided by the mint: ${keyset.unit}`) - continue - } - - mintInstance.addUnit(keyset.unit as MintUnit) // add supported units by mint if not yet exist - mintInstance.createProofsCounter(keyset) // create proofsCounters or update fees if exists + for(const keyset of keysets) { + if (!mintInstance.isUnitSupported(keyset.unit as MintUnit)) { + continue } + + mintInstance.initKeyset(keyset) + } + + for(const key of keys) { + if (!mintInstance.isUnitSupported(key.unit as MintUnit)) { + continue + } + + mintInstance.initKeys(key) } yield mintInstance.setShortname() - }), - removeMint(mintToBeRemoved: Mint) { - if (self.blockedMintUrls.some(m => m === mintToBeRemoved.mintUrl)) { - self.blockedMintUrls.remove(mintToBeRemoved.mintUrl) - log.debug('[removeMint]', 'Mint removed from blockedMintUrls') - } - - let mintInstance: Mint | undefined - - if (isStateTreeNode(mintToBeRemoved)) { - mintInstance = mintToBeRemoved - } else { - mintInstance = self.findByUrl((mintToBeRemoved as Mint).mintUrl) - } - - if (mintInstance) { - detach(mintInstance) - destroy(mintInstance) - log.info('[removeMint]', 'Mint removed from MintsStore') - } - }, - blockMint(mintToBeBlocked: Mint) { - if(self.blockedMintUrls.some(url => url === mintToBeBlocked.mintUrl)) { - return - } - - self.blockedMintUrls.push(mintToBeBlocked.mintUrl) - log.debug('[blockMint]', 'Mint blocked in MintsStore') - }, - unblockMint(blockedMint: Mint) { - self.blockedMintUrls.remove(blockedMint.mintUrl) - log.debug('[unblockMint]', 'Mint unblocked in MintsStore') - } - })) - .views(self => ({ - get mintCount() { - return self.mints.length - }, - get allMints() { - return self.mints - }, - get groupedByHostname() { - const grouped: Record = {} - - self.mints.forEach((mint: Mint) => { - const {hostname} = mint - - if (!grouped[hostname as string]) { - grouped[hostname as string] = { - hostname, - mints: [], - } - } - - grouped[hostname as string].mints.push(mint) - }) - - return Object.values(grouped) as MintsByHostname[] - }, - get groupedByUnit() { + }), + removeMint(mintToBeRemoved: Mint) { + if (self.blockedMintUrls.some(m => m === mintToBeRemoved.mintUrl)) { + self.blockedMintUrls.remove(mintToBeRemoved.mintUrl) + log.debug('[removeMint]', 'Mint removed from blockedMintUrls') + } + + let mintInstance: Mint | undefined + + if (isStateTreeNode(mintToBeRemoved)) { + mintInstance = mintToBeRemoved + } else { + mintInstance = self.findByUrl((mintToBeRemoved as Mint).mintUrl) + } + + if (mintInstance) { + detach(mintInstance) + destroy(mintInstance) + log.info('[removeMint]', 'Mint removed from MintsStore') + } + }, + blockMint(mintToBeBlocked: Mint) { + if(self.blockedMintUrls.some(url => url === mintToBeBlocked.mintUrl)) { + return + } + + self.blockedMintUrls.push(mintToBeBlocked.mintUrl) + log.debug('[blockMint]', 'Mint blocked in MintsStore') + }, + unblockMint(blockedMint: Mint) { + self.blockedMintUrls.remove(blockedMint.mintUrl) + log.debug('[unblockMint]', 'Mint unblocked in MintsStore') + } + })) + .views(self => ({ + get mintCount() { + return self.mints.length + }, + get allMints() { + return self.mints + }, + get groupedByHostname() { + const grouped: Record = {} + + self.mints.forEach((mint: Mint) => { + const {hostname} = mint + + if (!grouped[hostname as string]) { + grouped[hostname as string] = { + hostname, + mints: [], + } + } + + grouped[hostname as string].mints.push(mint) + }) + + return Object.values(grouped) as MintsByHostname[] + }, + get groupedByUnit() { const groupedByUnit: Record = {} self.mints.forEach(mint => { @@ -175,29 +182,29 @@ import { MintUnit, MintUnits } from '../services/wallet/currency' }) return Object.values(groupedByUnit) as MintsByUnit[] - }, - alreadyExists(mintUrl: string) { - return self.mints.some(m => m.mintUrl === mintUrl) ? true : false - }, - isBlocked(mintUrl: string) { - return self.blockedMintUrls.some(m => m === mintUrl) ? true : false - }, - getBlockedFromList(mintUrls: string[]) { - return mintUrls.filter(mintUrl => - self.blockedMintUrls.some(blockedUrl => blockedUrl === mintUrl), - ) - }, - getMissingMints: (mintUrls: string[]) => { - const missingMints: string[] = [] - for (const url of mintUrls) { - if (!self.mints.find(mint => mint.mintUrl === url)) { - missingMints.push(url) - } - } - return missingMints - }, - })) + }, + alreadyExists(mintUrl: string) { + return self.mints.some(m => m.mintUrl === mintUrl) ? true : false + }, + isBlocked(mintUrl: string) { + return self.blockedMintUrls.some(m => m === mintUrl) ? true : false + }, + getBlockedFromList(mintUrls: string[]) { + return mintUrls.filter(mintUrl => + self.blockedMintUrls.some(blockedUrl => blockedUrl === mintUrl), + ) + }, + getMissingMints: (mintUrls: string[]) => { + const missingMints: string[] = [] + for (const url of mintUrls) { + if (!self.mints.find(mint => mint.mintUrl === url)) { + missingMints.push(url) + } + } + return missingMints + }, +})) - export interface MintsStore extends Instance {} - export interface MintsStoreSnapshot +export interface MintsStore extends Instance {} +export interface MintsStoreSnapshot extends SnapshotOut {} \ No newline at end of file diff --git a/src/models/helpers/setupRootStore.ts b/src/models/helpers/setupRootStore.ts index 35778975..65e29899 100644 --- a/src/models/helpers/setupRootStore.ts +++ b/src/models/helpers/setupRootStore.ts @@ -17,7 +17,7 @@ import { import * as Sentry from '@sentry/react-native' import RNExitApp from 'react-native-exit-app' import type {RootStore} from '../RootStore' -import {KeyChain, MinibitsClient, MMKVStorage, NostrClient} from '../../services' +import {KeyChain, MinibitsClient, MintClient, MMKVStorage, NostrClient} from '../../services' import {Database} from '../../services' import { log } from '../../services/logService' import { rootStoreModelVersion } from '../RootStore' @@ -285,7 +285,6 @@ async function _runMigrations(rootStore: RootStore) { log.warn('[setupRootStore] Migration error', {message: e.name}) } } - } catch (e: any) { throw new AppError( Err.STORAGE_ERROR, diff --git a/src/screens/MintsScreen.tsx b/src/screens/MintsScreen.tsx index f79def01..0e9be2d5 100644 --- a/src/screens/MintsScreen.tsx +++ b/src/screens/MintsScreen.tsx @@ -142,8 +142,8 @@ export const MintsScreen: FC> = observer(funct toggleAddMintModal() // close setIsLoading(true) - const activeKeysets: MintKeyset[] = await MintClient.getMintKeysets(mintUrl) - const matchingKeyset = activeKeysets.find(keyset => selectedMint.keysets?.some(k => k === keyset.id)) + const keysets: MintKeyset[] = await MintClient.getMintKeysets(mintUrl) + const matchingKeyset = keysets.find(keyset => selectedMint.keysets?.some(k => k.id === keyset.id)) if(!matchingKeyset) { throw new AppError(Err.VALIDATION_ERROR, 'No keyset match, provided URL likely points to different mint.') diff --git a/src/screens/RemoteRecoveryScreen.tsx b/src/screens/RemoteRecoveryScreen.tsx index 7846d2b2..405b3780 100644 --- a/src/screens/RemoteRecoveryScreen.tsx +++ b/src/screens/RemoteRecoveryScreen.tsx @@ -261,9 +261,9 @@ export const RemoteRecoveryScreen: FC> = o recoveredMint.increaseProofsCounter(selectedKeyset.id as string, Math.abs(endIndex - startIndex)) const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint( - proofs as Proof[], recoveredMint.mintUrl, selectedKeyset.unit as MintUnit, + proofs as Proof[], ) log.debug('[restore]', `Spent and pending proofs`, {spent: spent.length, pending: pending.length}) diff --git a/src/services/cashuMintClient.ts b/src/services/cashuMintClient.ts index 5502f68b..c2c15633 100644 --- a/src/services/cashuMintClient.ts +++ b/src/services/cashuMintClient.ts @@ -8,6 +8,9 @@ import { deriveKeysetId, setGlobalRequestOptions, type Proof as CashuProof, + type MintKeyset as CashuMintKeyset, + type MintKeys as CashuMintKeys, + MintKeys } from '@cashu/cashu-ts' import {rootStoreInstance} from '../models' import { KeyChain } from '../services' @@ -22,7 +25,7 @@ import { deriveSeedFromMnemonic } from '@cashu/cashu-ts' import { isObj } from '@cashu/cashu-ts/src/utils' import { JS_BUNDLE_VERSION } from '@env' import { MintUnit } from './wallet/currency' - +import { getSnapshot } from 'mobx-state-tree' let _mints: CashuMint[] = [] let _wallets: CashuWallet[] = [] // used where seed is not required (perf) @@ -37,8 +40,8 @@ function isOnionMint(mintUrl: string) { } const TorVPNSetupInstructions = ` Is your Tor VPN running? -Mints on Tor require a Tor VPN like Orbot. You can get it on Google Play or Github (guardianproject/orbot). -Tor can sometimes be slow, so requests might time out. If Orbot is running, try again in a bit.` +Mints on Tor require a Tor VPN application like Orbot. You can get it on Google Play or Github.` + const getOrCreateMnemonic = async function (): Promise { let mnemonic: string | undefined = undefined @@ -93,7 +96,7 @@ const getSeed = async function (): Promise { } -const getMint = function (mintUrl: string): CashuMint { +const getMint = async function (mintUrl: string): Promise { const mint = _mints.find(m => m.mintUrl === mintUrl) @@ -104,8 +107,30 @@ const getMint = function (mintUrl: string): CashuMint { setGlobalRequestOptions({ headers: {'User-Agent': `Minibits/${JS_BUNDLE_VERSION}`} }) - + // create cashu-ts mint instance const newMint = new CashuMint(mintUrl) + // get fresh keysets + const {keysets} = await newMint.getKeySets() + // get mint model from wallet state + const mintInstance = mintsStore.findByUrl(mintUrl) + + if(!mintInstance) { + throw new AppError(Err.NOTFOUND_ERROR, 'Missing mint in the wallet state', {mintUrl}) + } + // check if we've got new keysets not existing in wallet state + const newKeysets = keysets.filter(freshKeyset => { + return !mintInstance.keysets.some(keyset => keyset.id === freshKeyset.id); + }) + + if(newKeysets.length > 0) { + // if we heve new keysets, get and sync new keys + const {keysets} = await newMint.getKeys() + mintInstance.refreshKeys(keysets) + } + + // sync wallet state with fresh keysets, active statuses and keys + mintInstance.refreshKeysets(keysets) + // store cashu-ts mint instance in memory _mints.push(newMint) return newMint @@ -117,13 +142,72 @@ const getWallet = async function ( mintUrl: string, unit: MintUnit, options?: { + keysetId?: string // force specific (inactive) keys to swap from withSeed: boolean } ): Promise { - const cashuMint = getMint(mintUrl) + // syncs mint model in wallet state and returns cashu-ts mint class instance + const cashuMint = await getMint(mintUrl) + + // mint model from wallet state + const mintInstance = mintsStore.findByUrl(mintUrl) + if(!mintInstance) { + throw new AppError(Err.NOTFOUND_ERROR, 'Mint not found in the wallet state.', { + mintUrl + }) + } + + // select keys to be used to find or create new cashu-ts wallet instance + let walletKeys: MintKeys + if(options && options.keysetId) { + + const requestedKeys = mintInstance.keys.find(k => k.id === options.keysetId) + + if(!requestedKeys) { + throw new AppError(Err.NOTFOUND_ERROR, 'Wallet has not keys with provided keyset id.', { + mintUrl, + keysetId: options.keysetId + }) + } + + if(requestedKeys.unit !== unit) { + throw new AppError(Err.VALIDATION_ERROR, 'Wallet unit and keys mismatch.', { + mintUrl, + keysetId: options.keysetId, + unit + }) + } + + walletKeys = requestedKeys + } else { + // if not we find first active keyset and related keys + const activeKeyset = mintInstance.keysets.find(k => k.unit === unit && k.active === true) + + if(!activeKeyset) { + throw new AppError(Err.VALIDATION_ERROR, 'Wallet has not any active keyset for the selected unit.', { + mintUrl, + unit + }) + } + + const activeKeys = mintInstance.keys.find(k => k.id === activeKeyset.id) + + if(!activeKeys) { + throw new AppError(Err.VALIDATION_ERROR, 'Wallet has not any keys for the selected unit.', { + mintUrl, + unit + }) + } + + walletKeys = getSnapshot(activeKeys) + } if (options && options.withSeed) { - const seedWallet = _seedWallets.find(w => w.mint.mintUrl === mintUrl && w.unit === unit) + + const seedWallet = _seedWallets.find( + w => w.mint.mintUrl === mintUrl && + w.keys.id === walletKeys.id + ) if (seedWallet) { return seedWallet @@ -131,62 +215,37 @@ const getWallet = async function ( let seed: Uint8Array | undefined = undefined seed = await getSeed() - - // Handle legacy pre-0.1.5 created wallets - if(!seed) { - const mnemonic = await getOrCreateMnemonic() - seed = await getSeed() - resetCachedWallets() // force all wallet instances to be recreated with seed - } const newSeedWallet = new CashuWallet(cashuMint, { - unit, + keys: walletKeys, mnemonicOrSeed: seed }) - try { - // make sure we have keys for wallet unit cached in wallet instance - const keys = await newSeedWallet.getKeys(undefined, unit) + _seedWallets.push(newSeedWallet) - if(!keys || keys.unit !== unit) { - throw new AppError(Err.VALIDATION_ERROR, `This mint does not currently support unit ${unit}`) - } - - _seedWallets.push(newSeedWallet) - - log.trace('[getWallet]', 'Returning new cashuWallet instance with seed') - return newSeedWallet - } catch (e: any) { - let message = 'Could not connect to the selected mint.' - if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; - throw new AppError(Err.NETWORK_ERROR, message, {message: e.message, caller: 'getWallet'}) - } + log.trace('[getWallet]', 'Returning new cashuWallet instance with seed') + + return newSeedWallet } - const wallet = _wallets.find(w => w.mint.mintUrl === mintUrl && w.unit === unit) + const wallet = _wallets.find( + w => w.mint.mintUrl === mintUrl && + w.keys.id === walletKeys.id + ) if (wallet) { return wallet } const newWallet = new CashuWallet(cashuMint, { - unit, + keys: walletKeys, mnemonicOrSeed: undefined }) - - try { - - await newWallet.getKeys(undefined, unit) - - _wallets.push(newWallet) + + _wallets.push(newWallet) - log.trace('[getWallet]', 'Returning new cashuWallet instance') - return newWallet - } catch (e: any) { - let message = 'Could not connect to the selected mint.' - if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; - throw new AppError(Err.NETWORK_ERROR, message, {message: e.message, caller: 'getWallet'}) - } + log.trace('[getWallet]', 'Returning new cashuWallet instance') + return newWallet } @@ -198,7 +257,7 @@ const resetCachedWallets = function () { const getMintKeysets = async function (mintUrl: string) { - const cashuMint = getMint(mintUrl) + const cashuMint = await getMint(mintUrl) try { const {keysets} = await cashuMint.getKeySets() // all @@ -211,23 +270,46 @@ const getMintKeysets = async function (mintUrl: string) { } -const receiveFromMint = async function ( +const getMintKeys = async function (mintUrl: string) { + const cashuMint = await getMint(mintUrl) + + try { + const {keysets} = await cashuMint.getKeys() // all + return keysets + } catch (e: any) { + let message = 'Could not connect to the selected mint.' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError(Err.CONNECTION_ERROR, message, { message: e.message, mintUrl }) + } +} + + +const receive = async function ( mintUrl: string, unit: MintUnit, decodedToken: CashuToken, - amountPreferences: AmountPreference[], - counter: number + options: { + preference: AmountPreference[], + counter: number + } ) { try { - const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed + const cashuWallet = await getWallet( + mintUrl, + unit, + { + withSeed: true, + }) - const proofs = await cashuWallet.receive(decodedToken, { - keysetId: cashuWallet.keys.id, - preference: amountPreferences, - counter, - pubkey: undefined, - privkey: undefined - }) + const proofs = await cashuWallet.receive( + decodedToken, + { + keysetId: cashuWallet.keys.id, + preference: options.preference, + counter: options.counter, + pubkey: undefined, + privkey: undefined + }) return proofs } catch (e: any) { @@ -237,59 +319,60 @@ const receiveFromMint = async function ( -const sendFromMint = async function ( +const send = async function ( mintUrl: string, - unit: MintUnit, amountToSend: number, + unit: MintUnit, proofsToSendFrom: Proof[], - amountPreferences: AmountPreference[], - counter: number + options: { + preference: AmountPreference[], + counter: number + } ) { try { - const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed + const cashuWallet = await getWallet( + mintUrl, + unit, + { + withSeed: true, + }) - log.debug('[MintClient.sendFromMint] counter', counter) + log.debug('[MintClient.sendFromMint] counter', options.counter) const {returnChange, send} = await cashuWallet.send( amountToSend, proofsToSendFrom, { keysetId: cashuWallet.keys.id, - preference: amountPreferences, - counter, + preference: options.preference, + counter: options.counter, pubkey: undefined, privkey: undefined } ) log.debug('[MintClient.sendFromMint] returnedProofs', returnChange) - log.debug('[MintClient.sendFromMint] sentProofs', send) + log.debug('[MintClient.sendFromMint] proofsToSend', send) // do some basic validations that proof amounts from mints match const totalAmountToSendFrom = CashuUtils.getProofsAmount(proofsToSendFrom) const returnedAmount = CashuUtils.getProofsAmount(returnChange as Proof[]) - const proofsAmount = CashuUtils.getProofsAmount(send as Proof[]) + const sendAmount = CashuUtils.getProofsAmount(send as Proof[]) - if (proofsAmount !== amountToSend) { + if (sendAmount !== amountToSend) { throw new AppError( Err.VALIDATION_ERROR, - `Amount to be sent does not equal requested original amount. Original is ${amountToSend}, mint returned ${proofsAmount}`, + `Amount to be sent provided by mint does not equal requested amount. Original is ${amountToSend}, mint returned ${sendAmount}`, ) } - if (totalAmountToSendFrom !== returnedAmount + proofsAmount) { - throw new AppError( - Err.VALIDATION_ERROR, - `Amount returned by the mint as a change ${returnedAmount} is incorrect, it should be ${ - totalAmountToSendFrom - proofsAmount - }`, - ) - } + const mintFeePaid = totalAmountToSendFrom - amountToSend - returnedAmount // we normalize naming of returned parameters return { returnedProofs: returnChange as Proof[], - proofsToSend: send as Proof[], + proofsToSend: send as Proof[], + mintFeePaid } } catch (e: any) { let message = 'The mint could not return signatures necessary for this transaction' @@ -310,9 +393,9 @@ const sendFromMint = async function ( const getSpentOrPendingProofsFromMint = async function ( - proofs: Proof[], mintUrl: string, unit: MintUnit, + proofs: Proof[], ) { try { @@ -350,7 +433,7 @@ const createLightningMeltQuote = async function ( encodedInvoice: string, ) { try { - const cashuMint = getMint(mintUrl) + const cashuMint = await getMint(mintUrl) const lightningQuote: MeltQuoteResponse = await cashuMint.createMeltQuote({ unit, request: encodedInvoice @@ -382,7 +465,9 @@ const payLightningMelt = async function ( unit: MintUnit, lightningMeltQuote: MeltQuoteResponse, // invoice is stored by mint by quote proofsToPayFrom: CashuProof[], // proofAmount >= amount + fee_reserve - counter: number + options: { + counter: number + } ) { try { const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed @@ -393,7 +478,7 @@ const payLightningMelt = async function ( proofsToPayFrom, { keysetId: cashuWallet.keys.id, - counter + counter: options.counter } ) @@ -427,7 +512,7 @@ const createLightningMintQuote = async function ( amount: number, ) { try { - const cashuMint = getMint(mintUrl) + const cashuMint = await getMint(mintUrl) const { request: encodedInvoice, quote: mintQuote, @@ -463,7 +548,7 @@ const checkLightningMintQuote = async function ( quote: string, ) { try { - const cashuMint = getMint(mintUrl) + const cashuMint = await getMint(mintUrl) const { request: encodedInvoice, quote: mintQuote, @@ -498,11 +583,13 @@ const checkLightningMintQuote = async function ( const mintProofs = async function ( mintUrl: string, - unit: MintUnit, amount: number, + unit: MintUnit, mintQuote: string, - amountPreferences: AmountPreference[], - counter: number + options: { + preference: AmountPreference[], + counter: number + } ) { try { const cashuWallet = await getWallet(mintUrl, unit, {withSeed: true}) // with seed @@ -512,8 +599,8 @@ const mintProofs = async function ( mintQuote, { keysetId: cashuWallet.keys.id, - preference: amountPreferences, - counter, + preference: options.preference, + counter: options.counter, pubkey: undefined } ) @@ -557,7 +644,7 @@ const restore = async function ( try { const {indexFrom, indexTo, keysetId} = options // need special wallet instance to pass seed and keysetId directly - const cashuMint = getMint(mintUrl) + const cashuMint = await getMint(mintUrl) const seedWallet = new CashuWallet(cashuMint, { unit: 'sat', // just use default unit as we restore by keyset @@ -589,7 +676,7 @@ const getMintInfo = async function ( mintUrl: string, ) { try { - const cashuMint = getMint(mintUrl) + const cashuMint = await getMint(mintUrl) const info = await cashuMint.getInfo() log.trace('[getMintInfo]', {info}) return info @@ -616,8 +703,9 @@ export const MintClient = { getSeed, resetCachedWallets, getMintKeysets, - receiveFromMint, - sendFromMint, + getMintKeys, + receive, + send, getSpentOrPendingProofsFromMint, createLightningMintQuote, checkLightningMintQuote, diff --git a/src/services/wallet/receiveTask.ts b/src/services/wallet/receiveTask.ts index e269f674..ff52f2de 100644 --- a/src/services/wallet/receiveTask.ts +++ b/src/services/wallet/receiveTask.ts @@ -17,7 +17,7 @@ import { import { getDefaultAmountPreference, getEncodedToken } from '@cashu/cashu-ts/src/utils' import { TransactionTaskResult } from '../walletService' import { WalletUtils } from './utils' -import { formatCurrency, getCurrency } from './currency' +import { MintUnit, formatCurrency, getCurrency } from './currency' const { mintsStore, @@ -124,7 +124,7 @@ export const receiveTask = async function ( const amountPreferences = getDefaultAmountPreference(amountToReceive) const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences) - log.trace('[receiveTask]', 'proofsCounter initial state', {proofsCounter: await mintInstance.getProofsCounterByUnit?.(unit)}) + log.trace('[receiveTask]', 'proofsCounter initial state', {proofsCounter: mintInstance.getProofsCounterByUnit?.(unit)}) log.trace('[receiveTask]', 'amountPreferences', {amountPreferences, transactionId}) log.trace('[receiveTask]', 'countOfInFlightProofs', {countOfInFlightProofs, transactionId}) @@ -132,14 +132,16 @@ export const receiveTask = async function ( await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId) // get locked counter values - const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit) + const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit) - const receivedProofs = await MintClient.receiveFromMint( + const receivedProofs = await MintClient.receive( mintToReceive, - unit, + unit as MintUnit, token, - amountPreferences, - lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase + { + preference: amountPreferences, + counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase + } ) // If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs @@ -443,15 +445,17 @@ export const receiveOfflineCompleteTask = async function ( await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transaction.id as number) // get locked counter values - const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit) + const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit) - const receivedProofs = await MintClient.receiveFromMint( + const receivedProofs = await MintClient.receive( mintToReceive, - unit, + unit as MintUnit, token, - amountPreferences, - lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase - ) + { + preference: amountPreferences, + counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase + } + ) // If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs) diff --git a/src/services/wallet/sendTask.ts b/src/services/wallet/sendTask.ts index 92a831c3..9a056426 100644 --- a/src/services/wallet/sendTask.ts +++ b/src/services/wallet/sendTask.ts @@ -76,7 +76,7 @@ export const sendTask = async function ( transactionId = storedTransaction.id as number // get ready proofs to send and update proofs and pending proofs storage - const proofsToSend = await sendFromMint( + const {proofs: proofsToSend, mintFeePaid, mintFeeReserve} = await sendFromMint( mintBalanceToSendFrom, amountToSend, unit, @@ -87,7 +87,8 @@ export const sendTask = async function ( // Update transaction status transactionData.push({ status: TransactionStatus.PREPARED, - //proofsToSend, + mintFeeReserve, + mintFeePaid, createdAt: new Date(), }) @@ -115,7 +116,7 @@ export const sendTask = async function ( // Update transaction status transactionData.push({ - status: TransactionStatus.PENDING, + status: TransactionStatus.PENDING, encodedTokenToSend, createdAt: new Date(), }) @@ -153,6 +154,7 @@ export const sendTask = async function ( transaction: pendingTransaction, message: '', encodedTokenToSend, + mintFeePaid } as TransactionTaskResult } catch (e: any) { // Update transaction status if we have any @@ -249,7 +251,11 @@ export const sendFromMint = async function ( }) // We return cleaned proofs to be encoded as a sendable token - return cleanedProofsToSend + return { + proofs: cleanedProofsToSend, + mintFeeReserve: 0, + mintFeePaid: 0 + } } /* @@ -261,28 +267,27 @@ export const sendFromMint = async function ( proofsFromMint, ) - let proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom) - + let proofsToSendFromAmount = CashuUtils.getProofsAmount(proofsToSendFrom) // swap will happen if we could not select proofs equal to amountToSend let returnedAmount = proofsToSendFromAmount - amountToSend - let feesAmount: number = 0 + let mintFeeReserve: number = 0 if(returnedAmount > 0) { - feesAmount = mintInstance.getFeesForProofs(proofsToSendFrom) + mintFeeReserve = mintInstance.getFeesForProofs(proofsToSendFrom) // if we did not selected enough proofs to cover the fees we need some more - if(feesAmount > returnedAmount) { - const missingFeesAmount = feesAmount - returnedAmount + if(mintFeeReserve > returnedAmount) { + const missingFeesAmount = mintFeeReserve - returnedAmount const remainingProofs = proofsStore.getProofsSubset(proofsFromMint, proofsToSendFrom) const remainingProofsAmount = CashuUtils.getProofsAmount(remainingProofs) - log.warn('[sendFromMint]', 'Not enough proofs to cover feeAmount', {feesAmount, returnedAmount}) + log.warn('[sendFromMint]', 'Not enough proofs to cover feeReserve', {mintFeeReserve, returnedAmount}) if(missingFeesAmount > remainingProofsAmount) { throw new AppError( Err.VALIDATION_ERROR, 'There is not enough funds to send this payment with expected fees', - {totalAmountFromMint, amountToSend, feesAmount}, + {totalAmountFromMint, amountToSend, mintFeeReserve}, ) } @@ -297,8 +302,8 @@ export const sendFromMint = async function ( returnedAmount = proofsToSendFromAmount - amountToSend } - // decrease requested returned outputs by fees so mint can charge them - returnedAmount -= feesAmount + // decrease requested returned outputs by fees so that the mint can charge them + returnedAmount -= mintFeeReserve } // Inputs we are about to send @@ -319,19 +324,20 @@ export const sendFromMint = async function ( await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId) // get locked counter values - const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit) + const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit) // if split to required denominations was necessary, this gets it done with the mint and we get the return - const {returnedProofs, proofsToSend} = await MintClient.sendFromMint( + const {returnedProofs, proofsToSend, mintFeePaid} = await MintClient.send( mintUrl, - unit, amountToSend, + unit, proofsToSendFrom, - amountPreferences, - lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase - ) - + { + preference: amountPreferences, + counter: lockedProofsCounter.inFlightFrom as number // MUST be counter value before increase + } + ) // If we've got valid response, decrease proofsCounter and let it be increased back in next step when adding proofs mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs) @@ -339,7 +345,7 @@ export const sendFromMint = async function ( // add proofs returned by the mint after the split if (returnedProofs.length > 0) { log.trace('[sendFromMint] add returned proofs to spendable') - const { addedProofs, addedAmount } = WalletUtils.addCashuProofs( + WalletUtils.addCashuProofs( mintUrl, returnedProofs, { @@ -356,7 +362,7 @@ export const sendFromMint = async function ( // these might be original proofToSendFrom if they matched the exact amount and split was not necessary log.trace('[sendFromMint] add proofsToSend to pending') - const { addedProofs, addedAmount } = WalletUtils.addCashuProofs( + WalletUtils.addCashuProofs( mintUrl, proofsToSend, { @@ -380,8 +386,12 @@ export const sendFromMint = async function ( } }) - // We return cleaned proofs to be encoded as a sendable token - return cleanedProofsToSend + // We return cleaned proofs to be encoded as a sendable token + fees + return { + proofs: cleanedProofsToSend as CashuProof[], + mintFeeReserve, + mintFeePaid + } } catch (e: any) { // release lock mintInstance?.resetInFlight(transactionId) diff --git a/src/services/wallet/transferTask.ts b/src/services/wallet/transferTask.ts index ae977cdf..25997fb0 100644 --- a/src/services/wallet/transferTask.ts +++ b/src/services/wallet/transferTask.ts @@ -55,7 +55,7 @@ export const transferTask = async function ( try { if (amountToTransfer + meltQuote.fee_reserve > mintBalanceToTransferFrom.balances[unit]!) { - throw new AppError(Err.VALIDATION_ERROR, 'Mint balance is insufficient to cover the amount to transfer with expected Lightning fees.') + throw new AppError(Err.VALIDATION_ERROR, 'Mint balance is insufficient to cover the amount to transfer with the expected Lightning fees.') } if(isBefore(invoiceExpiry, new Date())) { @@ -87,7 +87,7 @@ export const transferTask = async function ( transactionId = storedTransaction.id as number // get proofs ready to be paid to the mint - proofsToPay = await sendFromMint( + const {proofs: proofsToPay, mintFeePaid, mintFeeReserve} = await sendFromMint( mintBalanceToTransferFrom, amountToTransfer + meltQuote.fee_reserve, unit, @@ -101,6 +101,8 @@ export const transferTask = async function ( // Update transaction status transactionData.push({ status: TransactionStatus.PREPARED, + mintFeeReserve, + mintFeePaid, createdAt: new Date(), }) @@ -116,14 +118,16 @@ export const transferTask = async function ( await WalletUtils.lockAndSetInFlight(mintInstance, unit, countOfInFlightProofs, transactionId) // get locked counter values - const lockedProofsCounter = await mintInstance.getProofsCounterByUnit?.(unit) + const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit) const {isPaid, preimage, feeSavedProofs} = await MintClient.payLightningMelt( mintUrl, unit, meltQuote, - proofsToPay, - lockedProofsCounter.inFlightFrom as number + proofsToPay, + { + counter: lockedProofsCounter.inFlightFrom as number + } ) mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs) @@ -178,10 +182,9 @@ export const transferTask = async function ( } // If real fees were less then estimated, cash the returned savings. - let finalFee = meltQuote.fee_reserve + let lightningFeePaid = meltQuote.fee_reserve - if (feeSavedProofs.length) { - + if (feeSavedProofs.length) { const {addedAmount: feeSaved} = WalletUtils.addCashuProofs( mintUrl, feeSavedProofs, @@ -193,19 +196,20 @@ export const transferTask = async function ( ) - finalFee = meltQuote.fee_reserve - feeSaved + lightningFeePaid = meltQuote.fee_reserve - feeSaved } // release lock mintInstance.resetInFlight(transactionId) // Save final fee in db - await transactionsStore.updateFee(transactionId, finalFee) + await transactionsStore.updateFee(transactionId, lightningFeePaid + mintFeePaid) // Update transaction status transactionData.push({ status: TransactionStatus.COMPLETED, - finalFee, + lightningFeeReserve: meltQuote.fee_reserve, + lightningFeePaid, preimage, // TODO add to tx details createdAt: new Date(), }) @@ -226,7 +230,8 @@ export const transferTask = async function ( mintUrl, transaction: completedTransaction, message: `Lightning invoice has been successfully paid and settled with your Minibits ecash. Final network fee has been ${formatCurrency(finalFee, getCurrency(unit).code)} ${getCurrency(unit).code}.`, - finalFee, + lightningFeePaid, + mintFeePaid } as TransactionTaskResult } catch (e: any) { // Update transaction status if we have any @@ -324,9 +329,9 @@ const _moveProofsFromPending = async function ( // due to the timeout but mint's node keeps the payment as in-flight (e.g. receiving node holds the invoice) // In this case we need to keep such proofs as pending and not move them back to wallet as in other payment failures. const {pending: pendingByMint} = await MintClient.getSpentOrPendingProofsFromMint( - proofsToMove as Proof[], mintUrl, - unit, + unit, + proofsToMove as Proof[] ) let amountPendingByMint: number = 0 diff --git a/src/services/wallet/utils.ts b/src/services/wallet/utils.ts index 0fbdb37f..5bb48d14 100644 --- a/src/services/wallet/utils.ts +++ b/src/services/wallet/utils.ts @@ -31,7 +31,7 @@ const lockAndSetInFlight = async function ( retryCount: number = 0, ): Promise { - const currentCounter = await mint.getProofsCounterByUnit?.(unit) + const currentCounter = mint.getProofsCounterByUnit?.(unit) log.trace('[lockAndSetInFlight] proofsCounter', {currentCounter}) if(!retryCount) { diff --git a/src/services/walletService.ts b/src/services/walletService.ts index d57ea24f..d8317c35 100644 --- a/src/services/walletService.ts +++ b/src/services/walletService.ts @@ -100,6 +100,8 @@ export interface WalletTaskResult { export interface TransactionTaskResult extends WalletTaskResult { transaction?: Transaction + lightningFeePaid?: number + mintFeePaid?: number } export type ReceivedEventResult = { @@ -362,9 +364,9 @@ const _handleSpentByMintTask = async function ( spent: spentProofs, pending: pendingProofs } = await MintClient.getSpentOrPendingProofsFromMint( - proofsFromMint, mintUrl, - mint && mint.units ? mint.units[0] : 'sat' + mint && mint.units ? mint.units[0] : 'sat', + proofsFromMint ) if(mint) { @@ -603,9 +605,9 @@ const _handleInFlightByMintTask = async function (mint: Mint, seed: Uint8Array): } const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint( - proofs as Proof[], mint.mintUrl, - mint.units ? mint.units[0] : 'sat' + mint.units ? mint.units[0] : 'sat', + proofs as Proof[] ) const spentCount = spent.length @@ -819,17 +821,19 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment ) // get locked counter values - const lockedProofsCounter = await mintInstance.getProofsCounterByUnit(unit) + const lockedProofsCounter = mintInstance.getProofsCounterByUnit(unit) let proofs: CashuProof[] = [] proofs = (await MintClient.mintProofs( mint as string, - unit, amount, - mintQuote as string, - amountPreferences, - lockedProofsCounter.inFlightFrom as number + unit, + mintQuote, + { + preference: amountPreferences, + counter: lockedProofsCounter.inFlightFrom as number + } )) as CashuProof[] mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs) diff --git a/yarn.lock b/yarn.lock index ae407b35..92984c32 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1202,9 +1202,9 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== -"@cashu/cashu-ts@minibits-cash/cashu-ts#1.0.0-minibits": +"@cashu/cashu-ts@minibits-cash/cashu-ts#1.0.0-minibits.2": version "1.0.0-minibits" - resolved "https://codeload.github.com/minibits-cash/cashu-ts/tar.gz/95cd37801f54700bed1b84113a774bd23f36aecb" + resolved "https://codeload.github.com/minibits-cash/cashu-ts/tar.gz/03b26c1db3a72d3d8e975a72f63b0ed06dc1be34" dependencies: "@cashu/crypto" "^0.2.6" "@noble/curves" "^1.3.0" @@ -1214,9 +1214,9 @@ buffer "^6.0.3" "@cashu/crypto@^0.2.6": - version "0.2.6" - resolved "https://registry.yarnpkg.com/@cashu/crypto/-/crypto-0.2.6.tgz#fbc173f1a5c380085f62b5141ae62d309a427426" - integrity sha512-qjytcY26MRntG6nJc9U2tSeDw+BApKQaIch58POjEiTuc7MbIxgR/l/xU5NzXa/nGrSLdNZQwl/o5RQDhc2otw== + version "0.2.7" + resolved "https://registry.yarnpkg.com/@cashu/crypto/-/crypto-0.2.7.tgz#26467fd2f5ecfeabcc74b9c65189a1e8c6ce85ce" + integrity sha512-1aaDfUjiHNXoJqg8nW+341TLWV9W28DsVNXJUKcHL0yAmwLs5+56SSnb8LLDJzPamLVoYL0U0bda91klAzptig== dependencies: "@noble/curves" "^1.3.0" "@noble/hashes" "^1.3.3"