From f25080a4aeefbef61906bb8c3faabaf4aebe823c Mon Sep 17 00:00:00 2001 From: minibits-cash Date: Tue, 30 Jul 2024 16:56:58 +0200 Subject: [PATCH] - coin selection for faster and more frequent offline sends Experimental, test-oriented changes: - fees support - binary v4 token format for smaller QR codes --- README.md | 2 +- src/App.tsx | 2 +- src/models/Mint.ts | 49 +- src/models/MintsStore.ts | 95 +++- src/models/ProofsStore.ts | 2 +- src/models/RootStore.ts | 9 + src/models/Token.ts | 15 - src/models/TokenEntry.ts | 24 - src/models/TransactionsStore.ts | 34 +- src/models/WalletStore.ts | 688 +++++++++++++++++++++++++++ src/models/helpers/setupRootStore.ts | 21 +- src/screens/MintInfoScreen.tsx | 7 +- src/screens/MintsScreen.tsx | 8 +- src/screens/ReceiveScreen.tsx | 20 +- src/screens/RemoteBackupScreen.tsx | 11 +- src/screens/RemoteRecoveryScreen.tsx | 15 +- src/screens/TranDetailScreen.tsx | 15 +- src/screens/TransferScreen.tsx | 12 +- src/screens/WalletScreen.tsx | 4 +- src/screens/WelcomeScreen.tsx | 9 +- src/services/cashu/cashuUtils.ts | 13 +- src/services/cashuMintClient.ts | 6 +- src/services/index.ts | 3 +- src/services/keyChain.ts | 3 - src/services/wallet/receiveTask.ts | 39 +- src/services/wallet/sendTask.ts | 23 +- src/services/wallet/topupTask.ts | 11 +- src/services/wallet/transferTask.ts | 18 +- src/services/wallet/utils.ts | 9 +- src/services/walletService.ts | 64 +-- 30 files changed, 957 insertions(+), 274 deletions(-) delete mode 100644 src/models/Token.ts delete mode 100644 src/models/TokenEntry.ts create mode 100644 src/models/WalletStore.ts diff --git a/README.md b/README.md index 5b86d63e..81f7c9de 100644 --- a/README.md +++ b/README.md @@ -144,7 +144,7 @@ Minibits is a bare React Native app written in Typescript. The project structure The code is derived from Ignite template, however with many libraries, notably Expo, stripped down to achieve fast startup times. Performance bottleneck on some Android devices is react-native-keychain. To overcome this, it has been patched not to warm-up on startup, caching for wallet operations is in place and its use to encrypt storage is opt-in. -Wallet state is managed by mobx-state-tree and persisted in fast MMKV storage. Only the basic mobx concepts are in place, whole model could be improved. All critical wallet code is in services/walletService.ts and all ecash state changes are in models/ProofsStore.ts. Wallet communication with the mints is in services/cashuMintClient.ts and uses [cashu-ts](https://github.com/cashubtc/cashu-ts) library. +Wallet state is managed by mobx-state-tree and persisted in fast MMKV storage. Only the basic mobx concepts are in place, whole model could be improved. All critical wallet code is in services/walletService.ts and all ecash state changes are in models/ProofsStore.ts. Wallet communication with the mints is in model/Wallet.ts and uses [cashu-ts](https://github.com/cashubtc/cashu-ts) library. Crypto operations are handled by react-native-quick-crypto, that is fast and does not require awful javascript shims. Transaction history and ecash backup is stored in sqlite, with fast react-native-quick-sqlite driver that enables to run lighter queries synchronously. diff --git a/src/App.tsx b/src/App.tsx index c42d772e..429a47cd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -21,7 +21,7 @@ import { } from '@gocodingnow/rn-size-matters' import {AppNavigator} from './navigation' import {useInitialRootStore, useStores} from './models' -import {Database, MinibitsClient} from './services' +import {Database} from './services' import {ErrorBoundary} from './screens/ErrorScreen/ErrorBoundary' import Config from './config' import {log} from './services' diff --git a/src/models/Mint.ts b/src/models/Mint.ts index c26c7646..ea0cbbd8 100644 --- a/src/models/Mint.ts +++ b/src/models/Mint.ts @@ -1,27 +1,20 @@ -import {cast, flow, getSnapshot, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree' +import {cast, flow, getRoot, getSnapshot, Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree' import {withSetPropAction} from './helpers/withSetPropAction' -import type { - CashuWallet, +import type { GetInfoResponse, MintKeys as CashuMintKeys, MintKeyset as CashuMintKeyset } from '@cashu/cashu-ts' import {colors, getRandomIconColor} from '../theme' -import { log, MintClient } from '../services' +import { log } from '../services' import AppError, { Err } from '../utils/AppError' import { MintUnit, MintUnits } from '../services/wallet/currency' import { getRootStore } from './helpers/getRootStore' import { generateId } from '../utils/utils' -import { Proof } from './Proof' +import { ProofV3 } from '../services/cashu/cashuUtils' -// used as a helper type across app -/* export type Balance = { - balance: number - unit: MintUnit -}*/ - export type MintBalance = { mintUrl: string balances: { @@ -53,15 +46,15 @@ export type MintProofsCounter = { 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) - })*/ +export const MintProofsCounter = 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 */ @@ -74,16 +67,7 @@ export const MintModel = types units: types.array(types.frozen()), keysets: types.array(types.frozen()), keys: types.array(types.frozen()), - proofsCounters: types.array( - 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) - }) - ), + proofsCounters: types.array(MintProofsCounter), color: types.optional(types.string, colors.palette.iconBlue200), status: types.optional(types.frozen(), MintStatus.ONLINE), createdAt: types.optional(types.Date, new Date()), @@ -334,7 +318,8 @@ export const MintModel = types let shortname = self.mintUrl.substring(lastSlashIndex + 1).slice(0, 25) try { - const info: GetInfoResponse = yield MintClient.getMintInfo(self.mintUrl) + const {walletStore} = getRoot(self).nonPersistedStores + const info: GetInfoResponse = yield walletStore.getMintInfo(self.mintUrl) if(info.name.length > 0) { shortname = info.name @@ -421,7 +406,7 @@ export const MintModel = types self.proofsCounters = cast(self.proofsCounters) }, - getMintFeeReserve(proofs: Proof[]): number { + getMintFeeReserve(proofs: ProofV3[]): number { // Find the corresponding keyset for each proof and sum the input fees const totalInputFees = proofs.reduce((sum, proof) => { const keyset = self.keysets.find(k => k.id === proof.id) diff --git a/src/models/MintsStore.ts b/src/models/MintsStore.ts index 1b55a07f..7ec7c9d3 100644 --- a/src/models/MintsStore.ts +++ b/src/models/MintsStore.ts @@ -6,18 +6,22 @@ import { isStateTreeNode, detach, flow, + getRoot, + getSnapshot, + getParent, } from 'mobx-state-tree' import {withSetPropAction} from './helpers/withSetPropAction' - import {MintModel, Mint, MintBalance} from './Mint' + import {MintModel, Mint, MintProofsCounter} from './Mint' import {log} from '../services/logService' - import { MintClient } from '../services' import AppError, { Err } from '../utils/AppError' import type { MintKeys as CashuMintKeys, MintKeyset as CashuMintKeyset } from '@cashu/cashu-ts' -import { MintUnit, MintUnits } from '../services/wallet/currency' +import { MintUnit } from '../services/wallet/currency' + + export type MintsByHostname = { hostname: string @@ -29,10 +33,22 @@ export type MintsByUnit = { mints: Mint[] } +export type CounterBackup = { + mintUrl: string + proofCounters: MintProofsCounter +} + +// Define the CounterBackup model +const CounterBackupModel = types.model('CounterBackup', { + mintUrl: types.string, + counters: types.array(MintProofsCounter) +}) + export const MintsStoreModel = types .model('MintsStore', { mints: types.array(MintModel), - blockedMintUrls: types.array(types.string), + blockedMintUrls: types.array(types.string), + counterBackups: types.array(CounterBackupModel) }) .views(self => ({ findByUrl: (mintUrl: string | URL) => { @@ -41,15 +57,62 @@ export const MintsStoreModel = types }, })) .actions(withSetPropAction) + .actions(self => ({ + addOrUpdateCounterBackup(mintToRemove: Mint) { + try { + const existingIndex = self.counterBackups.findIndex( + (backup) => backup.mintUrl === mintToRemove.mintUrl + ) + + const newCounterBackup = CounterBackupModel.create({ + mintUrl: mintToRemove.mintUrl, + counters: getSnapshot(mintToRemove.proofsCounters!) + }) + + if (existingIndex !== -1) { + // Replace existing backup + self.counterBackups[existingIndex] = newCounterBackup + } else { + // Add new backup + self.counterBackups.push(newCounterBackup) + } + } catch (e: any) { + throw new AppError(Err.STORAGE_ERROR, e.message) + } + }, + updateMintCountersFromBackup(newMint: Mint) { + const backup = self.counterBackups.find( + (backup) => backup.mintUrl === newMint.mintUrl + ) + + if (backup) { + newMint.proofsCounters!.forEach((proofsCounter) => { + const backupCounter = backup.counters.find( + (counter) => counter.keyset === proofsCounter.keyset + ) + + if (backupCounter) { + newMint.increaseProofsCounter!(proofsCounter.keyset, backupCounter.counter) + } + }) + } + }, + })) .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 keysets: CashuMintKeyset[] = yield MintClient.getMintKeysets(mintUrl) - const keys: CashuMintKeys[] = yield MintClient.getMintKeys(mintUrl) + log.trace('[addMint] start') + + // create default wallet instance then download and cache up to date mint keys in that instance + const {walletStore} = getRoot(self).nonPersistedStores + + const keysets: CashuMintKeyset[] = yield walletStore.getMintKeysets(mintUrl) + const keys: CashuMintKeys[] = yield walletStore.getMintKeys(mintUrl) + + log.trace('[addMint]', {keysets}) if(!keysets || keysets.length === 0 || !keys || keys.length === 0) { throw new AppError(Err.VALIDATION_ERROR, 'Mint has no keysets and is not operational.', {mintUrl}) @@ -73,8 +136,12 @@ export const MintsStoreModel = types mintInstance.initKeys(key) } + log.trace('[addMint] updateMintCountersFromBackup') + + self.updateMintCountersFromBackup(mintInstance) + mintInstance.setHostname() - yield mintInstance.setShortname() + yield mintInstance.setShortname() self.mints.push(mintInstance) }), @@ -85,8 +152,9 @@ export const MintsStoreModel = types throw new AppError(Err.VALIDATION_ERROR, 'Could not find mint to update', {mintUrl}) } // refresh up to date mint keys - const keysets: CashuMintKeyset[] = yield MintClient.getMintKeysets(mintUrl) - const keys: CashuMintKeys[] = yield MintClient.getMintKeys(mintUrl) + const {walletStore} = getRoot(self).nonPersistedStores + const keysets: CashuMintKeyset[] = yield walletStore.getMintKeysets(mintUrl) + const keys: CashuMintKeys[] = yield walletStore.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}) @@ -106,7 +174,7 @@ export const MintsStoreModel = types } mintInstance.initKeys(key) - } + } yield mintInstance.setShortname() @@ -126,6 +194,7 @@ export const MintsStoreModel = types } if (mintInstance) { + self.addOrUpdateCounterBackup(mintInstance) detach(mintInstance) destroy(mintInstance) log.info('[removeMint]', 'Mint removed from MintsStore') @@ -142,7 +211,7 @@ export const MintsStoreModel = types unblockMint(blockedMint: Mint) { self.blockedMintUrls.remove(blockedMint.mintUrl) log.debug('[unblockMint]', 'Mint unblocked in MintsStore') - } + } })) .views(self => ({ get mintCount() { @@ -155,7 +224,7 @@ export const MintsStoreModel = types const grouped: Record = {} self.mints.forEach((mint: Mint) => { - const {hostname} = mint + const hostname = mint.hostname! if (!grouped[hostname as string]) { grouped[hostname as string] = { diff --git a/src/models/ProofsStore.ts b/src/models/ProofsStore.ts index 228c9f71..650a337a 100644 --- a/src/models/ProofsStore.ts +++ b/src/models/ProofsStore.ts @@ -12,7 +12,7 @@ import {getRootStore} from './helpers/getRootStore' import AppError, {Err} from '../utils/AppError' import {Mint, MintBalance, UnitBalance} from './Mint' import {Database} from '../services' -import { MintUnit, MintUnits } from '../services/wallet/currency' +import { MintUnit } from '../services/wallet/currency' export const ProofsStoreModel = types .model('Proofs', { diff --git a/src/models/RootStore.ts b/src/models/RootStore.ts index 18f9226b..af1986c0 100644 --- a/src/models/RootStore.ts +++ b/src/models/RootStore.ts @@ -7,9 +7,15 @@ import {WalletProfileStoreModel} from './WalletProfileStore' import {PaymentRequestsStoreModel} from './PaymentRequestsStore' import {ProofsStoreModel} from './ProofsStore' import {RelaysStoreModel} from './RelaysStore' +import {WalletStoreModel} from './WalletStore' export const rootStoreModelVersion = 16 // Update this if model changes require migrations defined in setupRootStore.ts +// Ephemeral non-persisted stores +const NonPersistedStoresModel = types.model('NonPersistedStores', { + walletStore: types.optional(WalletStoreModel, {}), + +}); /** * A RootStore model. */ @@ -24,6 +30,7 @@ export const RootStoreModel = types paymentRequestsStore: types.optional(PaymentRequestsStoreModel, {}), proofsStore: types.optional(ProofsStoreModel, {}), relaysStore: types.optional(RelaysStoreModel, {}), + nonPersistedStores: types.optional(NonPersistedStoresModel, {}), version: types.optional(types.number, rootStoreModelVersion), }) .actions(self => ({ @@ -32,6 +39,8 @@ export const RootStoreModel = types }, })) + + /** * The RootStore instance. */ diff --git a/src/models/Token.ts b/src/models/Token.ts deleted file mode 100644 index 5916ea22..00000000 --- a/src/models/Token.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {Instance, types} from 'mobx-state-tree' -import {TokenEntryModel} from './TokenEntry' -import { MintUnit } from '../services' - -/** - * This represents sendable Cashu token V3 - */ - -export const TokenModel = types.model('Token', { - token: types.array(TokenEntryModel), - unit: types.maybe(types.frozen()), - memo: types.maybe(types.string), -}) - -export interface Token extends Instance {} diff --git a/src/models/TokenEntry.ts b/src/models/TokenEntry.ts deleted file mode 100644 index 1720361f..00000000 --- a/src/models/TokenEntry.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {Instance, SnapshotIn, SnapshotOut, types} from 'mobx-state-tree' -import {withSetPropAction} from './helpers/withSetPropAction' -import {ProofModel} from './Proof' -// import type { Proof } from '@cashu/cashu-ts' - -/** - * This represents a Cashu token V3 - */ - -export const TokenEntryModel = types - .model('TokenEntry', { - mint: types.string, - proofs: types.array( - types.safeReference(ProofModel, {acceptsUndefined: false}), - ), - transactionId: types.maybe(types.number), - }) - - -export interface TokenEntry extends Instance {} -export interface TokenEntrySnapshotOut - extends SnapshotOut {} -export interface TokenEntrySnapshotIn - extends SnapshotIn {} diff --git a/src/models/TransactionsStore.ts b/src/models/TransactionsStore.ts index 8468146a..6346342f 100644 --- a/src/models/TransactionsStore.ts +++ b/src/models/TransactionsStore.ts @@ -14,7 +14,7 @@ import { TransactionStatus, TransactionRecord, } from './Transaction' -import {Database, MintClient} from '../services' +import {Database} from '../services' import {log} from '../services/logService' import { getRootStore } from './helpers/getRootStore' import { formatDistance } from 'date-fns' @@ -35,10 +35,6 @@ export type GroupedByTimeAgo = { export const TransactionsStoreModel = types .model('TransactionsStore', { transactions: types.array(TransactionModel), - /*tranUnit: types.maybe(types.frozen()), - tranMint: types.maybe(types.frozen()), - tranOption: types.maybe(types.frozen()), - tranContact: types.maybe(types.frozen())*/ }) .actions(withSetPropAction) .views(self => ({ @@ -98,7 +94,7 @@ export const TransactionsStoreModel = types recentByHostnameGroupedByTimeAgo(mintHostname: string) { const recentByHostname = this.recentByHostname(mintHostname) - return recentByHostname.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => { + return recentByHostname.reduce((groups: GroupedByTimeAgo, transaction: Transaction) => { const timeAgo = formatDistance(transaction.createdAt as Date, new Date(), {addSuffix: true}) if (!groups[timeAgo]) { groups[timeAgo] = [] @@ -156,7 +152,7 @@ export const TransactionsStoreModel = types const transactionsToRemove = self.transactions.filter(transaction => { // Check if the mint property of the transaction does not exist in the mints array - return !mintsStore.allMints.some(mint => mint.mintUrl === transaction.mint); + return !mintsStore.allMints.some((mint: Mint) => mint.mintUrl === transaction.mint); }); self.transactions.replace(self.transactions.filter(t => !transactionsToRemove.some(removed => removed.id === t.id))) @@ -353,30 +349,6 @@ export const TransactionsStoreModel = types self.transactions.clear() log.debug('[removeAllTransactions]', 'Removed all transactions from TransactionsStore') }, - /* setTranUnit(unit: MintUnit) { - self.tranUnit = unit - }, - setTranMint(mint: Mint) { - self.tranMint = mint - }, - setTranOption(option: ReceiveOption | SendOption) { - self.tranOption = option - }, - setTranContact(contact: Contact) { - self.tranContact = contact - }, - unsetTranUnit() { - self.tranUnit = undefined - }, - unsetTranMint(mint: Mint) { - self.tranMint = undefined - }, - unsetTranOption(option: ReceiveOption | SendOption) { - self.tranOption = undefined - }, - unsetTranContact(contact: Contact) { - self.tranContact = undefined - },*/ })) const getHostname = function (mintUrl: string) { diff --git a/src/models/WalletStore.ts b/src/models/WalletStore.ts new file mode 100644 index 00000000..465c2faa --- /dev/null +++ b/src/models/WalletStore.ts @@ -0,0 +1,688 @@ +import {Instance, SnapshotOut, types, flow, getRoot} from 'mobx-state-tree' +import { + AmountPreference, + CashuMint, + CashuWallet, + MeltQuoteResponse, + MeltTokensResponse, + setGlobalRequestOptions, + deriveSeedFromMnemonic, + type MintKeys, + type MintKeyset +} from '@cashu/cashu-ts' +import { JS_BUNDLE_VERSION } from '@env' +import {KeyChain} from '../services' +import {log} from '../services/logService' +import AppError, { Err } from '../utils/AppError' +import { MintUnit } from '../services/wallet/currency' +import { CashuUtils, ProofV3, TokenV3 } from '../services/cashu/cashuUtils' +import { Proof } from './Proof' +import { isObj } from '@cashu/cashu-ts/src/utils' +import { Mint, MintModel } from './Mint' +import { getRootStore } from './helpers/getRootStore' + +/* + Not persisted, in-memory only model of the cashu-ts wallet instances and seed. + It is instantiated on first use so that wallet retrieves fresh mint keysets, then cached, + so that new cashu-ts instances are re-used over app lifecycle. Seed is as well retrieved as needed and cached because retrieval from keychain might be slow. +*/ + +export const WalletStoreModel = types + .model('WalletStore', { + mints: types.array(types.frozen()), + wallets: types.array(types.frozen()), + seedWallets: types.array(types.frozen()), + mnemonicPhrase: types.maybe(types.string), + seedBase64: types.maybe(types.string), + }) + .actions(self => ({ + getMnemonic: flow(function* getMnemonic() { + if (self.mnemonicPhrase) { + log.trace('[getMnemonic]', 'returning cached mnemonic') + return self.mnemonicPhrase + } + + const mnemonic: string | undefined = yield KeyChain.loadMnemonic() + + if (!mnemonic) { + return undefined + } + + self.mnemonicPhrase = mnemonic + return mnemonic + }), + getSeed: flow(function* getSeed() { + if (self.seedBase64) { + log.trace('[getMnemonic]', 'returning cached mnemonic') + return new Uint8Array(Buffer.from(self.seedBase64, 'base64')) + } + + const seed = yield KeyChain.loadSeed() + + if (!seed) { + return undefined + } + + self.seedBase64 = Buffer.from(seed).toString('base64') + return seed + }), + getMintModelInstance(mintUrl: string) { + const mintsStore = getRoot(self).mintsStore + return mintsStore.findByUrl(mintUrl) + } + })) + .actions(self => ({ + getOrCreateMnemonic: flow(function* getOrCreateMnemonic() { + let mnemonic: string | undefined = undefined + + mnemonic = yield self.getMnemonic() // returns cached or saved mnemonic + + if (!mnemonic) { + mnemonic = KeyChain.generateMnemonic() as string + const seed = deriveSeedFromMnemonic(mnemonic) // expensive + + yield KeyChain.saveMnemonic(mnemonic) + yield KeyChain.saveSeed(seed) + + log.trace('[getOrCreateMnemonic]', 'Created and saved new mnemonic and seed') + } + + return mnemonic + }), + getMint: flow(function* getMint(mintUrl: string) { + const mint = self.mints.find(m => m.mintUrl === mintUrl) + + if (mint) { + return mint + } + + setGlobalRequestOptions({ + headers: {'User-Agent': `Minibits/${JS_BUNDLE_VERSION}`} + }) + // create cashu-ts mint instance + const newMint = new CashuMint(mintUrl) + + // get fresh keysets + const {keysets} = yield newMint.getKeySets() + + // get persisted mint model from wallet state + const mintInstance = self.getMintModelInstance(mintUrl) + + // skip checks if this is new mint being added + if(mintInstance) { + const newKeysets = keysets.filter((freshKeyset: MintKeyset) => { + return !mintInstance.keysets.some((keyset: MintKeyset) => keyset.id === freshKeyset.id) + }) + + if(newKeysets.length > 0) { + // if we heve new keysets, get and sync new keys + const {keysets} = yield 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 + self.mints.push(newMint) + + return newMint + }) + })) + .actions(self => ({ + getWallet: flow(function* getWallet( + mintUrl: string, + unit: MintUnit, + options?: { + keysetId?: string + withSeed: boolean + } + ) { + log.trace('[WalletStore.getWallet] start') + // syncs mint model in wallet state and returns cashu-ts mint class instance + const cashuMint = yield self.getMint(mintUrl) + + // get uptodate mint model from wallet state + // const mintsStore = getRootStore(self).mintsStore + const mintInstance = self.getMintModelInstance(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: MintKeys) => 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 active keyset with lowest fees and related keys + const activeKeyset: MintKeyset = mintInstance.keysets + .filter((k: MintKeyset) => k.unit === unit && k.active) + .sort((a: MintKeyset, b: MintKeyset) => (a.input_fee_ppk ?? 0) - (b.input_fee_ppk ?? 0))[0] + + 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: MintKeys) => k.id === activeKeyset.id) + + if(!activeKeys) { + throw new AppError(Err.VALIDATION_ERROR, 'Wallet has not any keys for the selected unit.', { + mintUrl, + unit + }) + } + + walletKeys = activeKeys + } + + if (options && options.withSeed) { + + const seedWallet: CashuWallet | undefined = self.seedWallets.find( + w => w.mint.mintUrl === mintUrl && + w.keys.id === walletKeys.id + ) + + if (seedWallet) { + return seedWallet + } + + let seed: Uint8Array | undefined = undefined + seed = yield self.getSeed() + + const newSeedWallet = new CashuWallet(cashuMint, { + keys: walletKeys, + mnemonicOrSeed: seed + }) + + self.seedWallets.push(newSeedWallet) + + log.trace('[getWallet]', 'Returning NEW cashuWallet instance with seed') + + return newSeedWallet + } + + const wallet: CashuWallet | undefined = self.wallets.find( + w => w.mint.mintUrl === mintUrl && + w.keys.id === walletKeys.id + ) + + if (wallet) { + return wallet + } + + const newWallet = new CashuWallet(cashuMint, { + keys: walletKeys, + mnemonicOrSeed: undefined + }) + + self.wallets.push(newWallet) + + log.trace('[getWallet]', 'Returning NEW cashuWallet instance') + return newWallet + }), + getMintKeysets: flow(function* getMintKeysets(mintUrl: string) { + log.trace('[getMintKeysets] start') + const cashuMint: CashuMint = yield self.getMint(mintUrl) + + try { + const {keysets} = yield cashuMint.getKeySets() // 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 }) + } + }), + getMintKeys: flow(function* getMintKeys(mintUrl: string) { + const cashuMint: CashuMint = yield self.getMint(mintUrl) + + try { + const {keysets} = yield 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 }) + } + }), + resetWallets() { + self.seedWallets.clear() + self.wallets.clear() + } + })) + .actions(self => ({ + receive: flow(function* receive(mintUrl: string, + unit: MintUnit, + decodedToken: TokenV3, + mintFeeReserve: number, + options: { + preference: AmountPreference[], + counter: number + }) { + + try { + const cashuWallet = yield self.getWallet( + mintUrl, + unit, + { + withSeed: true, + }) + + const amountToReceive: number = CashuUtils.getTokenAmounts(decodedToken).totalAmount + + const proofs = yield cashuWallet.receive( + decodedToken, + mintFeeReserve, + { + keysetId: cashuWallet.keys.id, + preference: options.preference, + counter: options.counter, + pubkey: undefined, + privkey: undefined + }) + + const receivedAmount: number = CashuUtils.getProofsAmount(proofs as Proof[]) + const mintFeePaid = amountToReceive - receivedAmount + + return {proofs, mintFeePaid} + } catch (e: any) { + throw new AppError(Err.MINT_ERROR, e.message) + } + }), + send: flow(function* send(mintUrl: string, + amountToSend: number, + mintFeeReserve: number, + unit: MintUnit, + proofsToSendFrom: Proof[], + options: { + preference: AmountPreference[], + counter: number + } ) { + try { + const cashuWallet = yield self.getWallet( + mintUrl, + unit, + { + withSeed: true, + }) + + log.debug('[WalletStore.send] counter', options.counter) + + const {returnChange, send} = yield cashuWallet.send( + amountToSend, + mintFeeReserve, + proofsToSendFrom, + { + keysetId: cashuWallet.keys.id, + preference: options.preference, + counter: options.counter, + pubkey: undefined, + privkey: undefined + } + ) + + log.debug('[WalletStore.send] returnedProofs', returnChange) + log.debug('[WalletStore.send] proofsToSend', send) + + // do some basic validations that proof amounts from mints match + const totalAmountToSendFrom: number = CashuUtils.getProofsAmount(proofsToSendFrom) + const returnedAmount: number = CashuUtils.getProofsAmount(returnChange as Proof[]) + const sendAmount: number = CashuUtils.getProofsAmount(send as Proof[]) + + if (sendAmount !== amountToSend) { + throw new AppError( + Err.VALIDATION_ERROR, + `Amount to be sent provided by mint does not equal requested amount. Original is ${amountToSend}, mint returned ${sendAmount}`, + ) + } + + const mintFeePaid = totalAmountToSendFrom - amountToSend - returnedAmount + + // we normalize naming of returned parameters + return { + returnedProofs: returnChange as Proof[], + proofsToSend: send as Proof[], + mintFeePaid + } + } catch (e: any) { + let message = 'The mint could not return signatures necessary for this transaction' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: e.message, + mintUrl, + caller: 'WalletStore.sendFromMint', + proofsToSendFrom, + } + ) + } + }), + getSpentOrPendingProofsFromMint: flow(function* getSpentOrPendingProofsFromMint( + mintUrl: string, + unit: MintUnit, + proofs: Proof[] + ) { + try { + log.trace('*** [WalletStore.getSpentOrPendingProofsFromMint] start', {mintUrl, unit}) + const cashuWallet = yield self.getWallet(mintUrl, unit, {withSeed: true}) + const spentPendingProofs = yield cashuWallet.checkProofsSpent(proofs) + + log.trace('*** [WalletStore.getSpentOrPendingProofsFromMint]', {mintUrl, spentPendingProofs}) + + return spentPendingProofs as { + spent: ProofV3[] + pending: ProofV3[] + } + + } catch (e: any) { + let message = 'Could not get response from the mint.' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: e.message, + caller: 'Wallet.getSpentOrPendingProofsFromMint', + mintUrl + } + ) + } + }), + createLightningMintQuote: flow(function* createLightningMintQuote( + mintUrl: string, + unit: MintUnit, + amount: number, + ) { + try { + const cashuMint = yield self.getMint(mintUrl) + const { + request: encodedInvoice, + quote: mintQuote, + } = yield cashuMint.createMintQuote({ + unit, + amount + }) + + log.info('[createLightningMintQuote]', {encodedInvoice, mintQuote}) + + return { + encodedInvoice, + mintQuote, + } + } catch (e: any) { + let message = 'The mint could not return an invoice.' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: e.message, + caller: 'createLightningMintQuote', + mintUrl, + } + ) + } + }), + checkLightningMintQuote: flow(function* checkLightningMintQuote( + mintUrl: string, + quote: string, + ) { + try { + const cashuMint = yield self.getMint(mintUrl) + const { + request: encodedInvoice, + quote: mintQuote, + state, + } = yield cashuMint.checkMintQuote( + quote + ) + + log.info('[checkLightningMintQuote]', {encodedInvoice, mintQuote, state}) + + return { + encodedInvoice, + mintQuote, + state + } + } catch (e: any) { + let message = 'The mint could not return the state of a mint quote.' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: e.message, + caller: 'checkLightningMintQuote', + mintUrl, + } + ) + } + }), + mintProofs: flow(function* mintProofs( + mintUrl: string, + amount: number, + unit: MintUnit, + mintQuote: string, + options: { + preference: AmountPreference[], + counter: number + } + ) { + try { + const cashuWallet = yield self.getWallet(mintUrl, unit, {withSeed: true}) // with seed + + const {proofs} = yield cashuWallet.mintTokens( + amount, + mintQuote, + { + keysetId: cashuWallet.keys.id, + preference: options.preference, + counter: options.counter, + pubkey: undefined + } + ) + + + log.info('[mintProofs]', {proofs}) + + return proofs + + } catch (e: any) { + log.info('[mintProofs]', {error: {name: e.name, message: e.message}}) + if(e.message.includes('quote not paid')) { + return { + proofs: [], + } + } + let message = 'The mint returned error on request to mint new ecash.' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: e.message, + caller: 'mintProofs', + mintUrl, + } + ) + } + }), + createLightningMeltQuote: flow(function* createLightningMeltQuote( + mintUrl: string, + unit: MintUnit, + encodedInvoice: string, + ) { + try { + const cashuMint = yield self.getMint(mintUrl) + const lightningQuote: MeltQuoteResponse = yield cashuMint.createMeltQuote({ + unit, + request: encodedInvoice + }) + + log.info('[createLightningMeltQuote]', {mintUrl, unit, encodedInvoice}, {lightningQuote}) + + return lightningQuote + + } catch (e: any) { + let message = 'The mint could not return the lightning quote.' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: e.message, + caller: 'createLightningMeltQuote', + request: {mintUrl, unit, encodedInvoice}, + } + ) + } + }), + payLightningMelt: flow(function* payLightningMelt( + mintUrl: string, + unit: MintUnit, + lightningMeltQuote: MeltQuoteResponse, // invoice is stored by mint by quote + proofsToPayFrom: ProofV3[], // proofAmount >= amount + fee_reserve + options: { + counter: number + } + ) { + try { + const cashuWallet = yield self.getWallet(mintUrl, unit, {withSeed: true}) // with seed + + const {isPaid, preimage, change: feeSavedProofs}: MeltTokensResponse = + yield cashuWallet.meltTokens( + lightningMeltQuote, + proofsToPayFrom, + { + keysetId: cashuWallet.keys.id, + counter: options.counter + } + ) + + log.trace('[payLightningMelt]', {isPaid, preimage, feeSavedProofs}) + // we normalize naming of returned parameters + return { + feeSavedProofs, + isPaid, + preimage, + } + } catch (e: any) { + let message = 'Lightning payment failed.' + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: isObj(e.message) ? JSON.stringify(e.message) : e.message, + caller: 'payLightningMelt', + mintUrl + } + ) + } + }), + restore: flow(function* restore( + mintUrl: string, + seed: Uint8Array, + options: { + indexFrom: number, + indexTo: number, + keysetId: string + } + ) { + try { + const {indexFrom, indexTo, keysetId} = options + // need special wallet instance to pass seed and keysetId directly + const cashuMint = yield self.getMint(mintUrl) + + const seedWallet = new CashuWallet(cashuMint, { + unit: 'sat', // just use default unit as we restore by keyset + mnemonicOrSeed: seed + }) + + const count = Math.abs(indexTo - indexFrom) + + + const {proofs} = yield seedWallet.restore( + indexFrom, + count, + {keysetId} + ) + + + log.info('[restore]', 'Number of recovered proofs', {proofs: proofs.length}) + + return { + proofs: proofs || [] + } + } catch (e: any) { + throw new AppError(Err.MINT_ERROR, isObj(e.message) ? JSON.stringify(e.message) : e.message, {mintUrl}) + } + }), + getMintInfo: flow(function* getMintInfo(mintUrl: string) { + try { + const cashuMint = yield self.getMint(mintUrl) + const info = yield cashuMint.getInfo() + log.trace('[getMintInfo]', {info}) + return info + } catch (e: any) { + let message = 'The mint could not return mint information.'; + if (isOnionMint(mintUrl)) message += TorVPNSetupInstructions; + throw new AppError( + Err.MINT_ERROR, + message, + { + message: e.message, + caller: 'getMintInfo', + mintUrl + } + ) + } + }) + })) + .views(self => ({ + get mnemonic() { + return self.mnemonicPhrase + }, + get seed() { + return self.seedBase64 + }, + })) + + function isOnionMint(mintUrl: string) { + return new URL(mintUrl).hostname.endsWith('.onion') + } + + const TorVPNSetupInstructions = ` + Is your Tor VPN running? + Mints on Tor require a Tor VPN application like Orbot. You can get it on Google Play or Github.` + + + export interface WalletStore extends Instance {} + export interface WalletStoreSnapshot + extends SnapshotOut {} diff --git a/src/models/helpers/setupRootStore.ts b/src/models/helpers/setupRootStore.ts index 65e29899..01b62aa5 100644 --- a/src/models/helpers/setupRootStore.ts +++ b/src/models/helpers/setupRootStore.ts @@ -17,15 +17,13 @@ import { import * as Sentry from '@sentry/react-native' import RNExitApp from 'react-native-exit-app' import type {RootStore} from '../RootStore' -import {KeyChain, MinibitsClient, MintClient, MMKVStorage, NostrClient} from '../../services' +import {KeyChain, MinibitsClient, MMKVStorage, NostrClient} from '../../services' import {Database} from '../../services' import { log } from '../../services/logService' import { rootStoreModelVersion } from '../RootStore' import AppError, { Err } from '../../utils/AppError' -import { MINIBITS_NIP05_DOMAIN, MINIBITS_RELAY_URL } from '@env' import { LogLevel } from '../../services/log/logTypes' import { MintStatus } from '../Mint' -import { generateId } from '../../utils/utils' /** * The key we'll be saving our state as within storage. @@ -80,7 +78,10 @@ export async function setupRootStore(rootStore: RootStore) { } // load the last known state from storage - restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {} + restoredState = MMKVStorage.load(ROOT_STORAGE_KEY) || {} + + // log.trace('[setupRootStore]', {restoredState}) + applySnapshot(rootStore, restoredState) } catch (e: any) { @@ -93,9 +94,15 @@ export async function setupRootStore(rootStore: RootStore) { } // track changes & save to storage - _disposer = onSnapshot(rootStore, snapshot => - MMKVStorage.save(ROOT_STORAGE_KEY, snapshot), - ) + _disposer = onSnapshot(rootStore, snapshot => { + // log.trace('[setupRootStore] snapshot listener', {snapshot}) + const persistedSnapshot = { + ...snapshot, + nonPersistedStores: undefined // Exclude non-persisted stores + } + + MMKVStorage.save(ROOT_STORAGE_KEY, persistedSnapshot) + }) // run migrations if needed, needs to be after onSnapshot to be persisted try { diff --git a/src/screens/MintInfoScreen.tsx b/src/screens/MintInfoScreen.tsx index 6bd8bf8f..f2c9e0ef 100644 --- a/src/screens/MintInfoScreen.tsx +++ b/src/screens/MintInfoScreen.tsx @@ -26,7 +26,7 @@ import { translate } from '../i18n' import { useStores } from '../models' import { Mint, MintStatus } from '../models/Mint' import { SettingsStackScreenProps } from '../navigation' -import { MintClient, log } from '../services' +import { log } from '../services' import { colors, spacing, typography, useThemeColor } from '../theme' import useColorScheme from '../theme/useThemeColor' import AppError, { Err } from '../utils/AppError' @@ -361,7 +361,8 @@ export const MintInfoScreen: FC> = observer }, }) - const { mintsStore } = useStores() + const { mintsStore, nonPersistedStores } = useStores() + const { walletStore } = nonPersistedStores const [isLoading, setIsLoading] = useState(false) const [mintInfo, setMintInfo] = useState() @@ -384,7 +385,7 @@ export const MintInfoScreen: FC> = observer const mint = mintsStore.findByUrl(route.params.mintUrl) if (mint) { - const info: GetInfoResponse = await MintClient.getMintInfo(mint.mintUrl) + const info: GetInfoResponse = await walletStore.getMintInfo(mint.mintUrl) mint.setStatus(MintStatus.ONLINE) if(info.name && info.name !== mint.shortname) { await mint.setShortname() diff --git a/src/screens/MintsScreen.tsx b/src/screens/MintsScreen.tsx index 0e9be2d5..2415e235 100644 --- a/src/screens/MintsScreen.tsx +++ b/src/screens/MintsScreen.tsx @@ -28,9 +28,8 @@ import AppError, { Err } from '../utils/AppError' import {translate} from '../i18n' import {MintListItem} from './Mints/MintListItem' import { SvgXml } from 'react-native-svg' -import { getSnapshot, isStateTreeNode } from 'mobx-state-tree' +import { isStateTreeNode } from 'mobx-state-tree' import { MintKeyset } from '@cashu/cashu-ts' -import { MintClient } from '../services' @@ -40,7 +39,8 @@ export const MintsScreen: FC> = observer(funct onLeftPress: () => navigation.goBack(), }) - const {mintsStore, proofsStore, userSettingsStore} = useStores() + const {mintsStore, proofsStore, nonPersistedStores} = useStores() + const {walletStore} = nonPersistedStores const mintInputRef = useRef(null) const [mintUrl, setMintUrl] = useState('') @@ -142,7 +142,7 @@ export const MintsScreen: FC> = observer(funct toggleAddMintModal() // close setIsLoading(true) - const keysets: MintKeyset[] = await MintClient.getMintKeysets(mintUrl) + const keysets: MintKeyset[] = await walletStore.getMintKeysets(mintUrl) const matchingKeyset = keysets.find(keyset => selectedMint.keysets?.some(k => k.id === keyset.id)) if(!matchingKeyset) { diff --git a/src/screens/ReceiveScreen.tsx b/src/screens/ReceiveScreen.tsx index 0c3c998e..8e42b96b 100644 --- a/src/screens/ReceiveScreen.tsx +++ b/src/screens/ReceiveScreen.tsx @@ -17,7 +17,6 @@ import { Text, } from '../components' import {Mint} from '../models/Mint' -import {Token} from '../models/Token' import {Transaction, TransactionStatus} from '../models/Transaction' import {useStores} from '../models' import {TransactionTaskResult, WalletTask} from '../services' @@ -25,7 +24,7 @@ import {log} from '../services/logService' import AppError, { Err } from '../utils/AppError' import EventEmitter from '../utils/eventEmitter' -import {CashuUtils} from '../services/cashu/cashuUtils' +import {CashuUtils, TokenV3} from '../services/cashu/cashuUtils' import {ResultModalInfo} from './Wallet/ResultModalInfo' import {MintListItem} from './Mints/MintListItem' import useIsInternetReachable from '../utils/useIsInternetReachable' @@ -42,7 +41,7 @@ export const ReceiveScreen: FC> = observer( const isInternetReachable = useIsInternetReachable() const {mintsStore} = useStores() - const [token, setToken] = useState() + const [token, setToken] = useState() const [encodedToken, setEncodedToken] = useState() const [amountToReceive, setAmountToReceive] = useState('0') const [unit, setUnit] = useState('sat') @@ -146,7 +145,7 @@ export const ReceiveScreen: FC> = observer( try { navigation.setParams({encodedToken: undefined}) - const decoded: Token = CashuUtils.decodeToken(encoded) + const decoded = CashuUtils.decodeToken(encoded) const tokenAmounts = CashuUtils.getTokenAmounts(decoded) log.trace('decoded token', {decoded}) @@ -154,17 +153,14 @@ export const ReceiveScreen: FC> = observer( if(!decoded.unit) { setInfo(translate("decodedMissingCurrencyUnit", { unit: CurrencyCode.SATS })) + decoded.unit = CurrencyCode.SATS } - const currency = getCurrency(decoded.unit) + const currency = getCurrency(decoded.unit as MintUnit) setToken(decoded) setAmountToReceive(numbro(tokenAmounts.totalAmount / currency.precision).format({thousandSeparated: true, mantissa: currency.mantissa})) - - if(decoded.unit) { - log.trace('Token unit', decoded.unit) - setUnit(decoded.unit) - } + setUnit(decoded.unit as MintUnit) if (decoded.memo && decoded.memo.length > 0) { setMemo(decoded.memo as string) @@ -182,7 +178,7 @@ export const ReceiveScreen: FC> = observer( const amountToReceiveInt = round(toNumber(amountToReceive) * getCurrency(unit).precision, 0) WalletTask.receive( - token as Token, + token as TokenV3, amountToReceiveInt, memo, encodedToken as string, @@ -197,7 +193,7 @@ export const ReceiveScreen: FC> = observer( const amountToReceiveInt = round(toNumber(amountToReceive) * getCurrency(unit).precision, 0) WalletTask.receiveOfflinePrepare( - token as Token, + token as TokenV3, amountToReceiveInt, memo, encodedToken as string, diff --git a/src/screens/RemoteBackupScreen.tsx b/src/screens/RemoteBackupScreen.tsx index 37c8a3b2..20543d27 100644 --- a/src/screens/RemoteBackupScreen.tsx +++ b/src/screens/RemoteBackupScreen.tsx @@ -20,10 +20,10 @@ import { } from '../components' import {useHeader} from '../utils/useHeader' import AppError, { Err } from '../utils/AppError' -import { KeyChain, log, MintClient } from '../services' import { scale } from '@gocodingnow/rn-size-matters' import Clipboard from '@react-native-clipboard/clipboard' import { translate } from '../i18n' +import { useStores } from '../models' export const RemoteBackupScreen: FC> = observer(function RemoteBackupScreen(_props) { @@ -35,7 +35,8 @@ export const RemoteBackupScreen: FC> = }, }) - // const {userSettingsStore} = useStores() + const {nonPersistedStores} = useStores() + const {walletStore} = nonPersistedStores const [info, setInfo] = useState('') const [mnemonic, setMnemonic] = useState() @@ -50,12 +51,12 @@ export const RemoteBackupScreen: FC> = setIsLoading(true) let mnemonic: string | undefined = undefined - mnemonic = await MintClient.getMnemonic() + mnemonic = await walletStore.getMnemonic() if(!mnemonic) { // wallets upgraded from 0.1.4 with no generated seed - mnemonic = await MintClient.getOrCreateMnemonic() // expensive, derives seed - MintClient.resetCachedWallets() // force all cached wallet instances to be recreated with seed + mnemonic = await walletStore.getOrCreateMnemonic() // expensive, derives seed + walletStore.resetWallets() // force all cached wallet instances to be recreated with seed setIsNewMnemonic(true) } diff --git a/src/screens/RemoteRecoveryScreen.tsx b/src/screens/RemoteRecoveryScreen.tsx index 5cf71aa8..c7527647 100644 --- a/src/screens/RemoteRecoveryScreen.tsx +++ b/src/screens/RemoteRecoveryScreen.tsx @@ -21,7 +21,7 @@ import { } from '../components' import {useHeader} from '../utils/useHeader' import AppError, { Err } from '../utils/AppError' -import { KeyChain, log, MinibitsClient, MintClient, NostrClient } from '../services' +import { KeyChain, log, MinibitsClient, NostrClient } from '../services' import Clipboard from '@react-native-clipboard/clipboard' import { useStores } from '../models' import { MintListItem } from './Mints/MintListItem' @@ -58,7 +58,8 @@ export const RemoteRecoveryScreen: FC> = o }, }) - const {mintsStore, proofsStore, userSettingsStore, transactionsStore, walletProfileStore} = useStores() + const {mintsStore, proofsStore, userSettingsStore, transactionsStore, walletProfileStore, nonPersistedStores} = useStores() + const {walletStore} = nonPersistedStores const mnemonicInputRef = useRef(null) const indexInputRef = useRef(null) @@ -91,7 +92,7 @@ export const RemoteRecoveryScreen: FC> = o const getMnemonic = async () => { try { setIsLoading(true) - const existing = await MintClient.getMnemonic() + const existing = await walletStore.getMnemonic() if(existing) { setMnemonicExists(true) @@ -174,7 +175,7 @@ export const RemoteRecoveryScreen: FC> = o const onMintSelect = async function (mint: Mint) { try { setSelectedMintUrl(mint.mintUrl) - const allActiveKeysets = await MintClient.getMintKeysets(mint.mintUrl) // mint api call + const allActiveKeysets = await walletStore.getMintKeysets(mint.mintUrl) // mint api call // Default is the last one as it seems to be the newest one const lastKeyset = allActiveKeysets.slice(-1)[0] @@ -240,7 +241,7 @@ export const RemoteRecoveryScreen: FC> = o setStatusMessage(translate("recovery.restoringFromParam", { hostname: recoveredMint.hostname })) log.info('[restore]', `Restoring from ${recoveredMint.hostname}...`) - const { proofs } = await MintClient.restore( + const { proofs } = await walletStore.restore( recoveredMint.mintUrl, seed as Uint8Array, { @@ -257,7 +258,7 @@ export const RemoteRecoveryScreen: FC> = o // need to move counter by whole interval to avoid duplicate _B!!! recoveredMint.increaseProofsCounter(selectedKeyset.id as string, Math.abs(endIndex - startIndex)) - const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint( + const {spent, pending} = await walletStore.getSpentOrPendingProofsFromMint( recoveredMint.mintUrl, selectedKeyset.unit as MintUnit, proofs as Proof[], @@ -270,7 +271,7 @@ export const RemoteRecoveryScreen: FC> = o const spentAmount = CashuUtils.getProofsAmount(spent as Proof[]) alreadySpentAmount += spentAmount - const unspent = proofs.filter(proof => !spent.includes(proof)) + const unspent = proofs.filter((proof: Proof) => !spent.includes(proof)) if(unspent && unspent.length > 0) { diff --git a/src/screens/TranDetailScreen.tsx b/src/screens/TranDetailScreen.tsx index 605202e6..bdfb0cfc 100644 --- a/src/screens/TranDetailScreen.tsx +++ b/src/screens/TranDetailScreen.tsx @@ -10,7 +10,7 @@ import { import Clipboard from '@react-native-clipboard/clipboard' import JSONTree from 'react-native-json-tree' import {colors, spacing, useThemeColor} from '../theme' -import {TransactionsStackScreenProps, WalletStackScreenProps} from '../navigation' +import {TransactionsStackScreenProps} from '../navigation' import EventEmitter from '../utils/eventEmitter' import { Button, @@ -25,7 +25,6 @@ import { Loading, Header, } from '../components' -import {useHeader} from '../utils/useHeader' import {useStores} from '../models' import {translate, TxKeyPath} from '../i18n' import { @@ -41,19 +40,15 @@ import {BackupProof, Proof} from '../models/Proof' import useColorScheme from '../theme/useThemeColor' import useIsInternetReachable from '../utils/useIsInternetReachable' import { ResultModalInfo } from './Wallet/ResultModalInfo' -import QRCode from 'react-native-qrcode-svg' import { getDecodedToken, Token as CashuToken } from '@cashu/cashu-ts' -import { CashuUtils } from '../services/cashu/cashuUtils' +import { CashuUtils, TokenV3 } from '../services/cashu/cashuUtils' import { MintStatus } from '../models/Mint' import { moderateVerticalScale } from '@gocodingnow/rn-size-matters' import { CurrencySign } from './Wallet/CurrencySign' import { MintUnit, formatCurrency, getCurrency } from "../services/wallet/currency" -import { Token } from '../models/Token' import { PaymentRequest } from '../models/PaymentRequest' import { pollerExists } from '../utils/poller' -import { StackActions, useFocusEffect } from '@react-navigation/native' -import { CurrencyAmount } from './Wallet/CurrencyAmount' -import { toNumber } from '../utils/number' +import { useFocusEffect } from '@react-navigation/native' import { QRCodeBlock } from './Wallet/QRCode' type ProofsByStatus = { @@ -500,12 +495,12 @@ const ReceiveInfoBlock = function (props: { try { const tokenToRetry: CashuToken = getDecodedToken(encodedTokenToRetry) - const amountToReceive = CashuUtils.getTokenAmounts(tokenToRetry as Token).totalAmount + const amountToReceive = CashuUtils.getTokenAmounts(tokenToRetry).totalAmount const memo = tokenToRetry.memo || '' setIsReceiveTaskSentToQueue(true) WalletTask.receive( - tokenToRetry as Token, + tokenToRetry, amountToReceive, memo, encodedTokenToRetry diff --git a/src/screens/TransferScreen.tsx b/src/screens/TransferScreen.tsx index a9a3fd01..4a28cc5d 100644 --- a/src/screens/TransferScreen.tsx +++ b/src/screens/TransferScreen.tsx @@ -25,25 +25,22 @@ import { BottomModal, Text, } from '../components' -import {Mint} from '../models/Mint' import {Transaction, TransactionStatus} from '../models/Transaction' import {useStores} from '../models' -import {MintClient, TransactionTaskResult, WalletTask} from '../services' +import {TransactionTaskResult, WalletTask} from '../services' import EventEmitter from '../utils/eventEmitter' import {log} from '../services/logService' import AppError, {Err} from '../utils/AppError' import {MintBalance} from '../models/Mint' -import {MintListItem} from './Mints/MintListItem' import {ResultModalInfo} from './Wallet/ResultModalInfo' import {addSeconds} from 'date-fns' import { PaymentRequestStatus } from '../models/PaymentRequest' -import { infoMessage } from '../utils/utils' import { DecodedLightningInvoice, LightningUtils } from '../services/lightning/lightningUtils' import { SendOption } from './SendOptionsScreen' import { round, roundDown, roundUp, toNumber } from '../utils/number' import { LnurlClient, LNURLPayParams } from '../services/lnurlService' import { moderateVerticalScale } from '@gocodingnow/rn-size-matters' -import { Currencies, CurrencyCode, MintUnit, getCurrency } from "../services/wallet/currency" +import { CurrencyCode, MintUnit, getCurrency } from "../services/wallet/currency" import { FeeBadge } from './Wallet/FeeBadge' import { MeltQuoteResponse } from '@cashu/cashu-ts' import { MintHeader } from './Mints/MintHeader' @@ -68,7 +65,8 @@ export const TransferScreen: FC> = observer( const amountInputRef = useRef(null) const lnurlCommentInputRef = useRef(null) - const {proofsStore, mintsStore, paymentRequestsStore, transactionsStore} = useStores() + const {proofsStore, mintsStore, paymentRequestsStore, transactionsStore, nonPersistedStores} = useStores() + const {walletStore} = nonPersistedStores const isInternetReachable = useIsInternetReachable() @@ -290,7 +288,7 @@ useEffect(() => { } setIsLoading(true) - const quote = await MintClient.createLightningMeltQuote( + const quote = await walletStore.createLightningMeltQuote( mintBalanceToTransferFrom.mintUrl, unit, encodedInvoice, diff --git a/src/screens/WalletScreen.tsx b/src/screens/WalletScreen.tsx index cd0b6193..3c76d0f5 100644 --- a/src/screens/WalletScreen.tsx +++ b/src/screens/WalletScreen.tsx @@ -88,7 +88,7 @@ export const WalletScreen: FC = observer( const pagerRef = useRef(null) const appState = useRef(AppState.currentState) const isInternetReachable = useIsInternetReachable() - const groupedMints = mintsStore.groupedByUnit + const groupedMints: MintsByUnit[] = mintsStore.groupedByUnit const [currentUnit, setCurrentUnit] = useState(groupedMints.length > 0 ? groupedMints[0].unit : 'sat') const [info, setInfo] = useState('') @@ -162,7 +162,7 @@ export const WalletScreen: FC = observer( WalletTask.receiveEventsFromRelays().catch(e => false) // log.trace('[getInitialData]', 'walletProfile', walletProfileStore) const preferredUnit: MintUnit = userSettingsStore.preferredUnit - const pageIndex = groupedMints.findIndex(m => m.unit === preferredUnit) + const pageIndex = groupedMints.findIndex((m) => m.unit === preferredUnit) pagerRef.current && pagerRef.current.setPage(pageIndex) } diff --git a/src/screens/WelcomeScreen.tsx b/src/screens/WelcomeScreen.tsx index 2cbaad59..cebc9f08 100644 --- a/src/screens/WelcomeScreen.tsx +++ b/src/screens/WelcomeScreen.tsx @@ -29,8 +29,8 @@ import { } from '../components' import {TxKeyPath, translate} from '../i18n' import AppError from '../utils/AppError' -import { MintClient } from '../services' -7 + + const AnimatedPagerView = Animated.createAnimatedComponent(PagerView) const PAGES = [ @@ -81,7 +81,8 @@ export const WelcomeScreen: FC> = //StatusBarProps: {barStyle: 'dark-content'}, }) - const {userSettingsStore, relaysStore, walletProfileStore} = useStores() + const {userSettingsStore, relaysStore, walletProfileStore, nonPersistedStores} = useStores() + const {walletStore} = nonPersistedStores const [error, setError] = useState() const [isLoading, setIsLoading] = useState(false) @@ -90,7 +91,7 @@ export const WelcomeScreen: FC> = try { // do not overwrite if one was set during recovery setIsLoading(true) - const mnemonic = await MintClient.getOrCreateMnemonic() + const mnemonic = await walletStore.getOrCreateMnemonic() // move new profile creation to the app start so that device token can register if(!walletProfileStore.pubkey || !walletProfileStore.picture) { diff --git a/src/services/cashu/cashuUtils.ts b/src/services/cashu/cashuUtils.ts index cc07696e..75cad8d4 100644 --- a/src/services/cashu/cashuUtils.ts +++ b/src/services/cashu/cashuUtils.ts @@ -1,19 +1,24 @@ import {Mint} from '../../models/Mint' import { - AmountPreference, - getDecodedToken, + AmountPreference, getEncodedToken, } from '@cashu/cashu-ts' import cbor from '@exodus/borc' -import type {Token as TokenV3, TokenEntry as TokenEntryV3, Proof as ProofV3} from '@cashu/cashu-ts' +import type { + Token as TokenV3, + TokenEntry as + TokenEntryV3, + Proof as ProofV3 +} from '@cashu/cashu-ts' import AppError, {Err} from '../../utils/AppError' import {} from '@cashu/cashu-ts' import {Proof} from '../../models/Proof' import { log } from '../logService' -// import { encodeCBOR } from '@cashu/cashu-ts/src/cbor' import { encodeBase64ToJson, encodeBase64toUint8, encodeUint8toBase64 } from '@cashu/cashu-ts/src/base64' import { bytesToHex, hexToBytes } from '@noble/curves/abstract/utils'; +export type {TokenV3, TokenEntryV3, ProofV3} + interface ProofV4 { a: number; s: string; diff --git a/src/services/cashuMintClient.ts b/src/services/cashuMintClient.ts index e45f3834..eeaae057 100644 --- a/src/services/cashuMintClient.ts +++ b/src/services/cashuMintClient.ts @@ -12,7 +12,7 @@ import { } from '@cashu/cashu-ts' import {rootStoreInstance} from '../models' import { KeyChain } from '../services' -import {CashuUtils} from './cashu/cashuUtils' +import {CashuUtils, TokenV3} from './cashu/cashuUtils' import AppError, {Err} from '../utils/AppError' import {log} from './logService' import { @@ -24,7 +24,7 @@ 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' -import { Token } from '../models/Token' + let _mints: CashuMint[] = [] let _wallets: CashuWallet[] = [] // used where seed is not required (perf) @@ -303,7 +303,7 @@ const receive = async function ( withSeed: true, }) - const amountToReceive = CashuUtils.getTokenAmounts(decodedToken as Token).totalAmount + const amountToReceive = CashuUtils.getTokenAmounts(decodedToken as TokenV3).totalAmount const proofs = await cashuWallet.receive( decodedToken, diff --git a/src/services/index.ts b/src/services/index.ts index 8d0de487..5cab78f7 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -1,4 +1,3 @@ -export * from "./cashuMintClient" export * from "./mmkvStorage" export * from "./sqlite" export * from "./keyChain" @@ -8,4 +7,4 @@ export * from "./nostrService" export * from "./logService" export * from "./torService" export * from "./syncQueueService" -// export * from "./restoreService" + diff --git a/src/services/keyChain.ts b/src/services/keyChain.ts index 2fef9cdb..339e5a20 100644 --- a/src/services/keyChain.ts +++ b/src/services/keyChain.ts @@ -7,9 +7,6 @@ import {btoa, fromByteArray} from 'react-native-quick-base64' import {rootStoreInstance} from '../models' import {log} from './logService' -const { - userSettingsStore -} = rootStoreInstance export enum KeyChainServiceName { MMKV = 'app.minibits.mmkv', diff --git a/src/services/wallet/receiveTask.ts b/src/services/wallet/receiveTask.ts index 27ee6623..3b456749 100644 --- a/src/services/wallet/receiveTask.ts +++ b/src/services/wallet/receiveTask.ts @@ -1,5 +1,4 @@ import {log} from '../logService' -import {MintClient} from '../cashuMintClient' import { Transaction, TransactionData, @@ -8,14 +7,8 @@ import { TransactionType, } from '../../models/Transaction' import {rootStoreInstance} from '../../models' -import {CashuUtils} from '../cashu/cashuUtils' +import {CashuUtils, ProofV3, TokenEntryV3, TokenV3} from '../cashu/cashuUtils' import AppError, {Err} from '../../utils/AppError' -import {Token} from '../../models/Token' -import { - TokenEntry, - type Token as CashuToken, - type Proof as CashuProof -} from '@cashu/cashu-ts' import { getDefaultAmountPreference } from '@cashu/cashu-ts/src/utils' import { TransactionTaskResult } from '../walletService' import { WalletUtils } from './utils' @@ -26,15 +19,19 @@ const { mintsStore, proofsStore, transactionsStore, + nonPersistedStores } = rootStoreInstance +const {walletStore} = nonPersistedStores + + // function names to pass to task results const RECEIVE = 'receiveTask' const RECEIVE_OFFLINE_PREPARE = 'receiveOfflinePrepareTask' const RECEIVE_OFFLINE_COMPLETE = 'receiveOfflineCompleteTask' export const receiveTask = async function ( - token: Token, + token: TokenV3, amountToReceive: number, memo: string, encodedToken: string, @@ -42,10 +39,10 @@ export const receiveTask = async function ( const transactionData: TransactionData[] = [] let transactionId: number = 0 let mintToReceive: string | undefined = undefined - const unit = token.unit || 'sat' + const unit = token.unit as MintUnit || 'sat' try { - const tokenMints: string[] = CashuUtils.getMintsFromToken(token as Token) + const tokenMints: string[] = CashuUtils.getMintsFromToken(token) log.trace('[receiveTask]', 'receiveToken tokenMints', tokenMints) if (tokenMints.length === 0) { @@ -126,8 +123,8 @@ export const receiveTask = async function ( // is not received our recovery index counts for sigs the mint has already issued const amountPreferences = getDefaultAmountPreference(amountToReceive) const countOfInFlightProofs = CashuUtils.getAmountPreferencesCount(amountPreferences) - const tokenEntries: Array = token.token - const proofsToReceive = tokenEntries[0].proofs as Proof[] + const tokenEntries: TokenEntryV3[] = token.token + const proofsToReceive = tokenEntries[0].proofs as ProofV3[] const mintFeeReserve = mintInstance.getMintFeeReserve(proofsToReceive) log.trace('[receiveTask]', 'proofsCounter initial state', {proofsCounter: mintInstance.getProofsCounterByUnit?.(unit)}) @@ -140,7 +137,7 @@ export const receiveTask = async function ( // get locked counter values const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)! - const receivedResult = await MintClient.receive( + const receivedResult = await walletStore.receive( mintToReceive, unit as MintUnit, token, @@ -272,7 +269,7 @@ export const receiveTask = async function ( export const receiveOfflinePrepareTask = async function ( - token: Token, + token: TokenV3, amountToReceive: number, memo: string, encodedToken: string, @@ -280,10 +277,10 @@ export const receiveOfflinePrepareTask = async function ( const transactionData: TransactionData[] = [] let transactionId: number = 0 let mintToReceive = '' - const unit = token.unit || 'sat' + const unit = token.unit as MintUnit || 'sat' try { - const tokenMints: string[] = CashuUtils.getMintsFromToken(token as Token) + const tokenMints: string[] = CashuUtils.getMintsFromToken(token) log.trace('[receiveOfflinePrepareTask]', 'receiveToken tokenMints', tokenMints) if (tokenMints.length === 0) { @@ -409,7 +406,7 @@ export const receiveOfflineCompleteTask = async function ( } const token = CashuUtils.decodeToken(encodedToken) - const tokenMints: string[] = CashuUtils.getMintsFromToken(token as Token) + const tokenMints: string[] = CashuUtils.getMintsFromToken(token as TokenV3) const unit = token.unit || 'sat' if(unit !== transaction.unit) { @@ -452,7 +449,7 @@ export const receiveOfflineCompleteTask = async function ( // Increase the proofs counter before the mint call so that in case the response // is not received our recovery index counts for sigs the mint has already issued (prevents duplicate b_b bug) - const tokenEntries: Array = token.token + const tokenEntries: TokenEntryV3[] = token.token const proofsToReceive = tokenEntries[0].proofs as Proof[] const mintFeeReserve = mintInstance.getMintFeeReserve(proofsToReceive) // We will receive amount less fees paid for a swap for fresh ecash @@ -468,7 +465,7 @@ export const receiveOfflineCompleteTask = async function ( // get locked counter values const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)! - const receivedResult = await MintClient.receive( + const receivedResult = await walletStore.receive( mintToReceive, unit as MintUnit, token, @@ -486,7 +483,7 @@ export const receiveOfflineCompleteTask = async function ( mintInstance.decreaseProofsCounter(lockedProofsCounter.keyset, countOfInFlightProofs) // store swapped proofs as encoded token in tx data - const tokenEntryToSend: TokenEntry = { + const tokenEntryToSend: TokenEntryV3 = { mint: mintToReceive, proofs: receivedProofs, } diff --git a/src/services/wallet/sendTask.ts b/src/services/wallet/sendTask.ts index 0c00681f..33ea1d6a 100644 --- a/src/services/wallet/sendTask.ts +++ b/src/services/wallet/sendTask.ts @@ -1,5 +1,4 @@ import {log} from '../logService' -import {MintClient} from '../cashuMintClient' import { Transaction, TransactionData, @@ -8,12 +7,10 @@ import { TransactionType, } from '../../models/Transaction' import {rootStoreInstance} from '../../models' -import {CashuUtils} from '../cashu/cashuUtils' +import {CashuUtils, ProofV3, TokenEntryV3} from '../cashu/cashuUtils' import AppError, {Err} from '../../utils/AppError' -import { - type Token as CashuToken, - type TokenEntry as CashuTokenEntry, - type Proof as CashuProof, +import { + MintKeyset, } from '@cashu/cashu-ts' import { getDefaultAmountPreference, isObj } from '@cashu/cashu-ts/src/utils' import { TransactionTaskResult, WalletTask } from '../walletService' @@ -28,9 +25,11 @@ import { boolean } from 'mobx-state-tree/dist/internal' const { mintsStore, proofsStore, - transactionsStore, + transactionsStore, + nonPersistedStores } = rootStoreInstance +const {walletStore} = nonPersistedStores export const sendTask = async function ( mintBalanceToSendFrom: MintBalance, @@ -108,7 +107,7 @@ export const sendTask = async function ( } const encodedTokenToSend = CashuUtils.encodeToken({ - token: [tokenEntryToSend as CashuTokenEntry], + token: [tokenEntryToSend as TokenEntryV3], unit, memo, }) @@ -270,8 +269,8 @@ export const sendFromMint = async function ( // Prioritize send from inactive keysets let proofsToSendFrom: Proof[] = [] - const inactiveKeysetIds = mintInstance?.keysets.filter(k => k.active === false).map(k => k.id) - const activeKeysetIds = mintInstance?.keysets.filter(k => k.active === true).map(k => k.id) + const inactiveKeysetIds = mintInstance?.keysets.filter((k: MintKeyset) => k.active === false).map((k: MintKeyset) => k.id) + const activeKeysetIds = mintInstance?.keysets.filter((k: MintKeyset) => k.active === true).map((k: MintKeyset) => k.id) log.trace('[sendFromMint]', {inactiveKeysetIds, activeKeysetIds}) @@ -382,7 +381,7 @@ export const sendFromMint = async function ( // if split to required denominations was necessary, this gets it done with the mint and we get the return - const sendResult = await MintClient.send( + const sendResult = await walletStore.send( mintUrl, amountToSend, mintFeeReserve, @@ -454,7 +453,7 @@ export const sendFromMint = async function ( // We return cleaned proofs to be encoded as a sendable token + fees return { - proofs: cleanedProofsToSend as CashuProof[], + proofs: cleanedProofsToSend as ProofV3[], mintFeeReserve, mintFeePaid } diff --git a/src/services/wallet/topupTask.ts b/src/services/wallet/topupTask.ts index 695ff770..bf8460ea 100644 --- a/src/services/wallet/topupTask.ts +++ b/src/services/wallet/topupTask.ts @@ -1,4 +1,3 @@ -import AppError, {Err} from '../../utils/AppError' import {rootStoreInstance} from '../../models' import { TransactionTaskResult, WalletTask } from '../walletService' import { MintBalance } from '../../models/Mint' @@ -6,19 +5,21 @@ import { poller } from '../../utils/poller' import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../../models/Transaction' import { log } from '../logService' import { Contact } from '../../models/Contact' -import { MintClient } from '../cashuMintClient' import { LightningUtils } from '../lightning/lightningUtils' import { getSnapshot, isStateTreeNode } from 'mobx-state-tree' import { PaymentRequest, PaymentRequestStatus, PaymentRequestType } from '../../models/PaymentRequest' import { WalletUtils } from './utils' -import { MintUnit, MintUnits } from './currency' +import { MintUnit } from './currency' const { transactionsStore, walletProfileStore, - paymentRequestsStore + paymentRequestsStore, + nonPersistedStores } = rootStoreInstance +const {walletStore} = nonPersistedStores + const TOPUP = 'topupTask' export const topupTask = async function ( @@ -59,7 +60,7 @@ export const topupTask = async function ( const storedTransaction: TransactionRecord = await transactionsStore.addTransaction(newTransaction) transactionId = storedTransaction.id as number - const {encodedInvoice, mintQuote} = await MintClient.createLightningMintQuote(mintUrl, unit, amountToTopup) + const {encodedInvoice, mintQuote} = await walletStore.createLightningMintQuote(mintUrl, unit, amountToTopup) const decodedInvoice = LightningUtils.decodeInvoice(encodedInvoice) const {amount, payment_hash, expiry, timestamp} = LightningUtils.getInvoiceData(decodedInvoice) diff --git a/src/services/wallet/transferTask.ts b/src/services/wallet/transferTask.ts index b7003dab..ea08f647 100644 --- a/src/services/wallet/transferTask.ts +++ b/src/services/wallet/transferTask.ts @@ -1,13 +1,12 @@ -import {CashuUtils} from '../cashu/cashuUtils' +import {CashuUtils, ProofV3} from '../cashu/cashuUtils' import AppError, {Err} from '../../utils/AppError' -import {MeltQuoteResponse, type Proof as CashuProof} from '@cashu/cashu-ts' +import {MeltQuoteResponse} from '@cashu/cashu-ts' import {rootStoreInstance} from '../../models' import { TransactionTaskResult, WalletTask } from '../walletService' import { MintBalance } from '../../models/Mint' import { Proof } from '../../models/Proof' import { Transaction, TransactionData, TransactionRecord, TransactionStatus, TransactionType } from '../../models/Transaction' import { log } from '../logService' -import { MintClient } from '../cashuMintClient' import { WalletUtils } from './utils' import {isBefore} from 'date-fns' import { sendFromMint } from './sendTask' @@ -16,9 +15,12 @@ import { MintUnit, formatCurrency, getCurrency } from './currency' const { transactionsStore, mintsStore, - proofsStore + proofsStore, + nonPersistedStores, } = rootStoreInstance +const {walletStore} = nonPersistedStores + const TRANSFER = 'transferTask' export const transferTask = async function ( @@ -51,7 +53,7 @@ export const transferTask = async function ( ] let transactionId: number = 0 - let proofsToPay: CashuProof[] = [] + let proofsToPay: Proof[] = [] try { if (amountToTransfer + meltQuote.fee_reserve > mintBalanceToTransferFrom.balances[unit]!) { @@ -129,7 +131,7 @@ export const transferTask = async function ( // get locked counter values const lockedProofsCounter = mintInstance.getProofsCounterByUnit?.(unit)! - const {isPaid, preimage, feeSavedProofs} = await MintClient.payLightningMelt( + const {isPaid, preimage, feeSavedProofs} = await walletStore.payLightningMelt( mintUrl, unit, meltQuote, @@ -316,7 +318,7 @@ export const transferTask = async function ( const _moveProofsFromPending = async function ( - proofsToMove: CashuProof[], + proofsToMove: ProofV3[], mintUrl: string, unit: MintUnit, transactionId: number, @@ -339,7 +341,7 @@ const _moveProofsFromPending = async function ( // Check with the mint if the proofs are not marked as pending. This happens when lightning payment fails // 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( + const {pending: pendingByMint} = await walletStore.getSpentOrPendingProofsFromMint( mintUrl, unit, proofsToMove as Proof[] diff --git a/src/services/wallet/utils.ts b/src/services/wallet/utils.ts index 1a52e879..d659052e 100644 --- a/src/services/wallet/utils.ts +++ b/src/services/wallet/utils.ts @@ -1,14 +1,9 @@ -import { - type Token as CashuToken, - type TokenEntry as CashuTokenEntry, - type Proof as CashuProof, -} from '@cashu/cashu-ts' import {Proof} from '../../models/Proof' import {rootStoreInstance} from '../../models' import AppError from "../../utils/AppError" import { log } from '../logService' import { isStateTreeNode } from 'mobx-state-tree' -import { CashuUtils } from '../cashu/cashuUtils' +import { CashuUtils, ProofV3 } from '../cashu/cashuUtils' import { Mint, MintProofsCounter } from '../../models/Mint' import { delay } from '../../utils/utils' import { MintUnit } from './currency' @@ -79,7 +74,7 @@ const lockAndSetInFlight = async function ( const addCashuProofs = function ( mintUrl: string, - proofsToAdd: CashuProof[] | Proof[], + proofsToAdd: ProofV3[] | Proof[], options: { unit: MintUnit, transactionId: number, diff --git a/src/services/walletService.ts b/src/services/walletService.ts index 9d096278..ca2978f3 100644 --- a/src/services/walletService.ts +++ b/src/services/walletService.ts @@ -1,19 +1,17 @@ import {isBefore} from 'date-fns' import {getSnapshot} from 'mobx-state-tree' import {log} from './logService' -import {MintClient} from './cashuMintClient' import {Proof} from '../models/Proof' import { Transaction, TransactionStatus } from '../models/Transaction' import {rootStoreInstance} from '../models' -import {CashuUtils} from './cashu/cashuUtils' +import {CashuUtils, TokenV3} from './cashu/cashuUtils' import {LightningUtils} from './lightning/lightningUtils' import AppError, {Err} from '../utils/AppError' import {MintBalance, MintStatus} from '../models/Mint' -import {Token} from '../models/Token' -import {MeltQuoteResponse, MintQuoteState, type Proof as CashuProof} from '@cashu/cashu-ts' +import {CashuMint, MeltQuoteResponse, MintQuoteState, type Proof as CashuProof} from '@cashu/cashu-ts' import {Mint} from '../models/Mint' import {pollerExists, stopPolling} from '../utils/poller' import EventEmitter from '../utils/eventEmitter' @@ -22,7 +20,7 @@ import { MINIBITS_NIP05_DOMAIN, MINIBITS_SERVER_API_HOST, MINIBIT_SERVER_NOSTR_P import { PaymentRequest, PaymentRequestStatus, PaymentRequestType } from '../models/PaymentRequest' import { IncomingDataType, IncomingParser } from './incomingParser' import { Contact } from '../models/Contact' -import { getDecodedToken, getDefaultAmountPreference } from '@cashu/cashu-ts/src/utils' +import { getDefaultAmountPreference } from '@cashu/cashu-ts/src/utils' import { SyncQueue } from './syncQueueService' import { receiveTask, receiveOfflinePrepareTask, receiveOfflineCompleteTask} from './wallet/receiveTask' import { sendTask } from './wallet/sendTask' @@ -30,7 +28,7 @@ import { topupTask } from './wallet/topupTask' import { transferTask } from './wallet/transferTask' import { WalletUtils } from './wallet/utils' import { NotificationService } from './notificationService' -import { CurrencyCode, MintUnit, formatCurrency, getCurrency } from './wallet/currency' +import { MintUnit, formatCurrency, getCurrency } from './wallet/currency' import { MinibitsClient } from './minibitsService' @@ -60,13 +58,13 @@ type WalletTaskService = { encodedInvoice: string, ) => Promise receive: ( - token: Token, + token: TokenV3, amountToReceive: number, memo: string, encodedToken: string, ) => Promise receiveOfflinePrepare: ( - token: Token, + token: TokenV3, amountToReceive: number, memo: string, encodedToken: string, @@ -111,7 +109,7 @@ export type ReceivedEventResult = { memo?: string, picture?: string paymentRequest?: PaymentRequest - token?: Token + token?: TokenV3 } @@ -123,8 +121,11 @@ const { paymentRequestsStore, contactsStore, relaysStore, + nonPersistedStores, } = rootStoreInstance +const { walletStore } = nonPersistedStores + const transfer = async function ( mintBalanceToTransferFrom: MintBalance, @@ -153,7 +154,7 @@ const transfer = async function ( const receive = async function ( - token: Token, + token: TokenV3, amountToReceive: number, memo: string, encodedToken: string, @@ -173,7 +174,7 @@ const receive = async function ( const receiveOfflinePrepare = async function ( - token: Token, + token: TokenV3, amountToReceive: number, memo: string, encodedToken: string, @@ -325,6 +326,7 @@ const handleSpentByMint = async function ( * @isPending whether to work on proofs in spendable or pending state by the wallet * @returns WalletTaskResult */ + const _handleSpentByMintTask = async function ( options: { mintUrl: string, @@ -363,7 +365,7 @@ const _handleSpentByMintTask = async function ( const { spent: spentProofs, pending: pendingProofs - } = await MintClient.getSpentOrPendingProofsFromMint( + } = await walletStore.getSpentOrPendingProofsFromMint( mintUrl, mint && mint.units ? mint.units[0] : 'sat', proofsFromMint @@ -405,7 +407,7 @@ const _handleSpentByMintTask = async function ( log.trace('[_handleSpentByMintTask]', 'Starting sweep of spent pendingByMint proofs') for (const spent of spentProofs) { - const secret = proofsStore.pendingByMintSecrets.find((s => s === spent.secret)) + const secret = proofsStore.pendingByMintSecrets.find((s: string) => s === spent.secret) if(secret) { proofsStore.removeFromPendingByMint(spent as Proof) } @@ -439,7 +441,7 @@ const _handleSpentByMintTask = async function ( // Check if remaining pendingByMint secrets are still pending with the mint. // If not, move related wallet's pending proofs back to spendable as the payment failed // and proofs did not come as spent (those are handled above). - const remainingSecrets = getSnapshot(proofsStore.pendingByMintSecrets) + const remainingSecrets: string[] = getSnapshot(proofsStore.pendingByMintSecrets) log.trace('[_handleSpentByMintTask]', 'Remaining pendingByMintSecrets', remainingSecrets) @@ -454,7 +456,7 @@ const _handleSpentByMintTask = async function ( // only move if it is from current mint if(proofToMove && proofToMove.mintUrl === mintUrl) { - const stillPendingByMint = pendingProofs.find((p => p.secret === secret)) + const stillPendingByMint = pendingProofs.find(p => p.secret === secret) if(!stillPendingByMint) { // move to spendable if it is not pending by mint anymore @@ -542,16 +544,12 @@ const handleInFlight = async function (): Promise { return } - const seed = await MintClient.getSeed() - - if(!seed) {return} - for (const mint of mintsStore.allMints) { const now = new Date().getTime() SyncQueue.addTask( `_handleInFlightByMintTask-${now}`, - async () => await _handleInFlightByMintTask(mint, seed) + async () => await _handleInFlightByMintTask(mint) ) } @@ -561,7 +559,7 @@ const handleInFlight = async function (): Promise { /* * Recover proofs that were issued by mint, but wallet failed to receive them if split did not complete. */ -const _handleInFlightByMintTask = async function (mint: Mint, seed: Uint8Array): Promise { +const _handleInFlightByMintTask = async function (mint: Mint): Promise { const mintUrl = mint.mintUrl const proofsCounter = mint.findInFlightProofsCounter?.() @@ -579,10 +577,16 @@ const _handleInFlightByMintTask = async function (mint: Mint, seed: Uint8Array): } try { + const seed = await walletStore.getSeed() + if(!seed) { + throw new Error('Missing seed') + } + log.info('[_handleInFlightByMintTask]', `Restoring from ${mint.hostname}...`) - log.debug('[_handleInFlightByMintTask]', proofsCounter) + log.debug('[_handleInFlightByMintTask]', proofsCounter) + - const { proofs } = await MintClient.restore( + const { proofs } = await walletStore.restore( mint.mintUrl, seed as Uint8Array, { @@ -604,7 +608,7 @@ const _handleInFlightByMintTask = async function (mint: Mint, seed: Uint8Array): } as WalletTaskResult } - const {spent, pending} = await MintClient.getSpentOrPendingProofsFromMint( + const {spent, pending} = await walletStore.getSpentOrPendingProofsFromMint( mint.mintUrl, mint.units ? mint.units[0] : 'sat', proofs as Proof[] @@ -616,7 +620,7 @@ const _handleInFlightByMintTask = async function (mint: Mint, seed: Uint8Array): const spentAmount = CashuUtils.getProofsAmount(spent as Proof[]) const pendingAmount = CashuUtils.getProofsAmount(pending as Proof[]) - const unspent = proofs.filter(proof => !spent.includes(proof)) + const unspent = proofs.filter((proof: Proof) => !spent.includes(proof)) const unspentCount = unspent.length const unspentAmount = CashuUtils.getProofsAmount(unspent as Proof[]) @@ -770,7 +774,7 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment log.trace('[_handlePendingTopupTask]', 'countOfInFlightProofs', countOfInFlightProofs) // check is quote has been paid - const { state, mintQuote: quote } = await MintClient.checkLightningMintQuote(mint!, mintQuote) + const { state, mintQuote: quote } = await walletStore.checkLightningMintQuote(mint!, mintQuote) if (quote !== mintQuote) { throw new AppError(Err.VALIDATION_ERROR, 'Returned quote is different then the one requested', {mintUrl: mint, quote, mintQuote}) @@ -825,7 +829,7 @@ const _handlePendingTopupTask = async function (params: {paymentRequest: Payment let proofs: CashuProof[] = [] - proofs = (await MintClient.mintProofs( + proofs = (await walletStore.mintProofs( mint as string, amount, unit, @@ -979,7 +983,7 @@ const _handleClaimTask = async function (params: {claimedInvoice: {token: string log.debug('[_handleClaimTask] decrypted token', {encodedToken}) - const decoded: Token = CashuUtils.decodeToken(encodedToken) + const decoded = CashuUtils.decodeToken(encodedToken) const amountToReceive = CashuUtils.getTokenAmounts(decoded).totalAmount const memo = decoded.memo || 'Received to Lightning address' @@ -1175,12 +1179,12 @@ const _handleReceivedEventTask = async function (event: NostrEvent): Promise