- coin selection for faster and more frequent offline sends

Experimental, test-oriented changes:
- fees support
- binary v4 token format for smaller QR codes
This commit is contained in:
minibits-cash
2024-07-30 16:56:58 +02:00
parent d4873a1f89
commit f25080a4ae
30 changed files with 957 additions and 274 deletions
+1 -1
View File
@@ -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.
+1 -1
View File
@@ -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'
+17 -32
View File
@@ -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<MintUnit>(), '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<MintUnit>(), '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<MintUnit>()),
keysets: types.array(types.frozen<CashuMintKeyset>()),
keys: types.array(types.frozen<CashuMintKeys>()),
proofsCounters: types.array(
types.model('MintProofsCounter', {
keyset: types.string,
unit: types.optional(types.frozen<MintUnit>(), '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>(), 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)
+82 -13
View File
@@ -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<string, MintsByHostname> = {}
self.mints.forEach((mint: Mint) => {
const {hostname} = mint
const hostname = mint.hostname!
if (!grouped[hostname as string]) {
grouped[hostname as string] = {
+1 -1
View File
@@ -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', {
+9
View File
@@ -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.
*/
-15
View File
@@ -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<MintUnit>()),
memo: types.maybe(types.string),
})
export interface Token extends Instance<typeof TokenModel> {}
-24
View File
@@ -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<typeof TokenEntryModel> {}
export interface TokenEntrySnapshotOut
extends SnapshotOut<typeof TokenEntryModel> {}
export interface TokenEntrySnapshotIn
extends SnapshotIn<typeof TokenEntryModel> {}
+3 -31
View File
@@ -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<MintUnit>()),
tranMint: types.maybe(types.frozen<Mint>()),
tranOption: types.maybe(types.frozen<ReceiveOption | SendOption>()),
tranContact: types.maybe(types.frozen<Contact>())*/
})
.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) {
+688
View File
@@ -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<CashuMint>()),
wallets: types.array(types.frozen<CashuWallet>()),
seedWallets: types.array(types.frozen<CashuWallet>()),
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<typeof WalletStoreModel> {}
export interface WalletStoreSnapshot
extends SnapshotOut<typeof WalletStoreModel> {}
+14 -7
View File
@@ -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 {
+4 -3
View File
@@ -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<SettingsStackScreenProps<'MintInfo'>> = observer
},
})
const { mintsStore } = useStores()
const { mintsStore, nonPersistedStores } = useStores()
const { walletStore } = nonPersistedStores
const [isLoading, setIsLoading] = useState(false)
const [mintInfo, setMintInfo] = useState<GetInfoResponse | undefined>()
@@ -384,7 +385,7 @@ export const MintInfoScreen: FC<SettingsStackScreenProps<'MintInfo'>> = 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()
+4 -4
View File
@@ -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<SettingsStackScreenProps<'Mints'>> = observer(funct
onLeftPress: () => navigation.goBack(),
})
const {mintsStore, proofsStore, userSettingsStore} = useStores()
const {mintsStore, proofsStore, nonPersistedStores} = useStores()
const {walletStore} = nonPersistedStores
const mintInputRef = useRef<TextInput>(null)
const [mintUrl, setMintUrl] = useState('')
@@ -142,7 +142,7 @@ export const MintsScreen: FC<SettingsStackScreenProps<'Mints'>> = 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) {
+8 -12
View File
@@ -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<WalletStackScreenProps<'Receive'>> = observer(
const isInternetReachable = useIsInternetReachable()
const {mintsStore} = useStores()
const [token, setToken] = useState<Token | undefined>()
const [token, setToken] = useState<TokenV3 | undefined>()
const [encodedToken, setEncodedToken] = useState<string | undefined>()
const [amountToReceive, setAmountToReceive] = useState<string>('0')
const [unit, setUnit] = useState<MintUnit>('sat')
@@ -146,7 +145,7 @@ export const ReceiveScreen: FC<WalletStackScreenProps<'Receive'>> = 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<WalletStackScreenProps<'Receive'>> = 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<WalletStackScreenProps<'Receive'>> = 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<WalletStackScreenProps<'Receive'>> = observer(
const amountToReceiveInt = round(toNumber(amountToReceive) * getCurrency(unit).precision, 0)
WalletTask.receiveOfflinePrepare(
token as Token,
token as TokenV3,
amountToReceiveInt,
memo,
encodedToken as string,
+6 -5
View File
@@ -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<SettingsStackScreenProps<'RemoteBackup'>> = observer(function RemoteBackupScreen(_props) {
@@ -35,7 +35,8 @@ export const RemoteBackupScreen: FC<SettingsStackScreenProps<'RemoteBackup'>> =
},
})
// const {userSettingsStore} = useStores()
const {nonPersistedStores} = useStores()
const {walletStore} = nonPersistedStores
const [info, setInfo] = useState('')
const [mnemonic, setMnemonic] = useState<string>()
@@ -50,12 +51,12 @@ export const RemoteBackupScreen: FC<SettingsStackScreenProps<'RemoteBackup'>> =
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)
}
+8 -7
View File
@@ -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<AppStackScreenProps<'RemoteRecovery'>> = o
},
})
const {mintsStore, proofsStore, userSettingsStore, transactionsStore, walletProfileStore} = useStores()
const {mintsStore, proofsStore, userSettingsStore, transactionsStore, walletProfileStore, nonPersistedStores} = useStores()
const {walletStore} = nonPersistedStores
const mnemonicInputRef = useRef<TextInput>(null)
const indexInputRef = useRef<TextInput>(null)
@@ -91,7 +92,7 @@ export const RemoteRecoveryScreen: FC<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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<AppStackScreenProps<'RemoteRecovery'>> = 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) {
+5 -10
View File
@@ -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
+5 -7
View File
@@ -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<WalletStackScreenProps<'Transfer'>> = observer(
const amountInputRef = useRef<TextInput>(null)
const lnurlCommentInputRef = useRef<TextInput>(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,
+2 -2
View File
@@ -88,7 +88,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const pagerRef = useRef<PagerView>(null)
const appState = useRef(AppState.currentState)
const isInternetReachable = useIsInternetReachable()
const groupedMints = mintsStore.groupedByUnit
const groupedMints: MintsByUnit[] = mintsStore.groupedByUnit
const [currentUnit, setCurrentUnit] = useState<MintUnit>(groupedMints.length > 0 ? groupedMints[0].unit : 'sat')
const [info, setInfo] = useState<string>('')
@@ -162,7 +162,7 @@ export const WalletScreen: FC<WalletScreenProps> = 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)
}
+5 -4
View File
@@ -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<AppStackScreenProps<'Welcome'>> =
//StatusBarProps: {barStyle: 'dark-content'},
})
const {userSettingsStore, relaysStore, walletProfileStore} = useStores()
const {userSettingsStore, relaysStore, walletProfileStore, nonPersistedStores} = useStores()
const {walletStore} = nonPersistedStores
const [error, setError] = useState<AppError | undefined>()
const [isLoading, setIsLoading] = useState<boolean>(false)
@@ -90,7 +91,7 @@ export const WelcomeScreen: FC<AppStackScreenProps<'Welcome'>> =
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) {
+9 -4
View File
@@ -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;
+3 -3
View File
@@ -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,
+1 -2
View File
@@ -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"
-3
View File
@@ -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',
+18 -21
View File
@@ -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<TokenEntry> = 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<TokenEntry> = 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,
}
+11 -12
View File
@@ -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
}
+6 -5
View File
@@ -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)
+10 -8
View File
@@ -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[]
+2 -7
View File
@@ -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,
+34 -30
View File
@@ -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<void>
receive: (
token: Token,
token: TokenV3,
amountToReceive: number,
memo: string,
encodedToken: string,
) => Promise<void>
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<void> {
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<void> {
/*
* 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<WalletTaskResult> {
const _handleInFlightByMintTask = async function (mint: Mint): Promise<WalletTaskResult> {
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<Wal
//
if(incoming.type === IncomingDataType.CASHU) {
const decoded: Token = CashuUtils.decodeToken(incoming.encoded)
const decoded = CashuUtils.decodeToken(incoming.encoded)
const amountToReceive = CashuUtils.getTokenAmounts(decoded).totalAmount
const memo = decoded.memo || 'Received over Nostr'
const {transaction, receivedAmount} = await receiveTask(
decoded as Token,
decoded,
amountToReceive,
memo,
incoming.encoded as string,