Fix spent proofs might remain in wallet after a lightning payment, fix pending tx count

This commit is contained in:
minibits-cash
2024-11-12 15:15:20 +01:00
parent def26b28d7
commit 234a35d2a0
11 changed files with 66 additions and 67 deletions
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.1.9-beta.30",
"version": "0.1.9-beta.31",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
+2 -4
View File
@@ -15,9 +15,7 @@ import { MintUnit, MintUnits } from '../services/wallet/currency'
import { getRootStore } from './helpers/getRootStore'
import { generateId } from '../utils/utils'
import { ProofV3 } from '../services/cashu/cashuUtils'
import { RootStoreModel } from './RootStore'
import { MintsStoreModel } from './MintsStore'
import { WalletStoreModel } from './WalletStore'
import { Proof } from './Proof'
export type MintBalance = {
@@ -367,7 +365,7 @@ export const MintModel = types
self.proofsCounters = cast(self.proofsCounters)
},
getMintFeeReserve(proofs: ProofV3[]): number {
getMintFeeReserve(proofs: ProofV3[] | Proof[]): 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)
+1 -1
View File
@@ -304,7 +304,7 @@ export const TransactionsStoreModel = types
transactionsMap: prunedTransactionsMap
}
console.log('[postProcessSnapshot]', {prunedSnapshot})
// console.log('[postProcessSnapshot]', {prunedSnapshot})
return prunedSnapshot
})
+4 -3
View File
@@ -59,7 +59,7 @@ export async function setupRootStore(rootStore: RootStore) {
const mmkvLoaded = performance.now()
const dataSize = Buffer.byteLength(JSON.stringify(restoredState), 'utf8')
log.trace({restoredState})
// log.trace({restoredState})
log.trace('[setupRootStore]', `Loading ${dataSize.toLocaleString()} bytes of state from MMKV took ${(mmkvLoaded - start).toLocaleString()} ms.`)
applySnapshot(rootStore, restoredState)
@@ -83,11 +83,12 @@ export async function setupRootStore(rootStore: RootStore) {
}
// track changes & save snapshot to the storage not more then once per second
const saveSnapshot = debounce((snapshot) => {
const saveSnapshot = debounce((snapshot) => {
MMKVStorage.save(ROOT_STORAGE_KEY, snapshot)
}, 1000)
_disposer = onSnapshot(rootStore, snapshot => {
_disposer = onSnapshot(rootStore, snapshot => {
// log.trace('[setupRootStore] onSnapshot *** MMKV SHOULD SAVE ***')
saveSnapshot(snapshot)
// log.trace('[setupRootStore] saved', {walletStore: snapshot.walletStore})
})
+1 -1
View File
@@ -267,7 +267,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
/>
<ListItem
text={translate("tranHistory.pendingParam", {
param: transactionsStore.pendingHistoryCount
param: pendingDbCount
})}
LeftComponent={
<Icon
+20 -20
View File
@@ -63,6 +63,8 @@ import { NavigationState, Route, TabBar, TabView } from 'react-native-tab-view'
import { getUnixTime } from 'date-fns/getUnixTime'
const deploymentKey = APP_ENV === Env.PROD ? CODEPUSH_PRODUCTION_DEPLOYMENT_KEY : CODEPUSH_STAGING_DEPLOYMENT_KEY
const PENDING_CHECK_INTERVAL = 30
const CLAIM_CHECK_INTERVAL = 60
interface WalletScreenProps extends WalletStackScreenProps<'Wallet'> {}
@@ -98,8 +100,8 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const [defaultMintUrl, setDefaultMintUrl] = useState<string>(MINIBITS_MINT_URL)
const [error, setError] = useState<AppError | undefined>()
const [isLoading, setIsLoading] = useState<boolean>(false)
const [lastClaimCheck, setLastClaimCheck] = useState<number>(getUnixTime(new Date()))
const [lastPendingCheck, setLastPendingCheck] = useState<number>(getUnixTime(new Date()))
const [lastClaimCheck, setLastClaimCheck] = useState<number>(0)
const [lastPendingCheck, setLastPendingCheck] = useState<number>(0)
const [isMintsModalVisible, setIsMintsModalVisible] = useState<boolean>(false)
const [isUpdateAvailable, setIsUpdateAvailable] = useState<boolean>(false)
const [isUpdateModalVisible, setIsUpdateModalVisible] = useState<boolean>(false)
@@ -165,17 +167,12 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
if(groupedMints.length === 0) {
await addMint()
}
// check lnaddress claims on app start and set timestamp to trigger focus updates
WalletTask.handleClaim().catch(e => setInfo(e.message))
// Auto-recover inflight proofs - do only on startup and before checkPendingReceived to prevent conflicts
// Only once on startup - auto-recover inflight proofs
WalletTask.handleInFlight().catch(e => false)
// Create websocket subscriptions to receive tokens or payment requests by NOSTR DMs
WalletTask.receiveEventsFromRelays().catch(e => false)
// Get exchange rate
if(userSettingsStore.exchangeCurrency) {
walletStore.refreshExchangeRate(userSettingsStore.exchangeCurrency!)
}
// Only once on startup - Create websocket subscriptions to receive tokens or payment requests by NOSTR DMs
WalletTask.receiveEventsFromRelays().catch(e => false)
// Set wallet tab to preferred unit
const preferredUnit: MintUnit = userSettingsStore.preferredUnit
const preferredTabIndex = routes.findIndex(route => route.key === preferredUnit)
@@ -253,16 +250,18 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
}
const nowInSec = getUnixTime(new Date())
// log.trace('[useFocusEffect]', {nowInSec, lastClaimCheck, delay: lastClaimCheck ? nowInSec - lastClaimCheck : undefined})
log.trace('[useFocusEffect] Start', {secsFromLastPending: nowInSec - lastPendingCheck, secsFromLastClaim: nowInSec - lastClaimCheck})
if(lastPendingCheck && nowInSec - lastPendingCheck > 10) {
// On startup and on re-focus if some secs passed
if(nowInSec - lastPendingCheck > PENDING_CHECK_INTERVAL) {
setLastPendingCheck(nowInSec)
WalletTask.syncPendingStateWithMints().catch(e => false)
WalletTask.handlePendingTopups().catch(e => false)
} else {
log.trace('[useFocusEffect] Skipping pending checks...')
}
if(lastClaimCheck && nowInSec - lastClaimCheck > 60) {
if(nowInSec - lastClaimCheck > CLAIM_CHECK_INTERVAL) {
setLastClaimCheck(nowInSec)
WalletTask.handleClaim().catch(e => false)
@@ -276,7 +275,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
}
}, [lastClaimCheck, isInternetReachable, userSettingsStore.exchangeCurrency])
}, [lastPendingCheck, lastClaimCheck, isInternetReachable, userSettingsStore.exchangeCurrency])
)
@@ -303,16 +302,17 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
const nowInSec = getUnixTime(new Date())
// log.trace('[appState]', { nowInSec, lastClaimCheck, delay: lastClaimCheck ? nowInSec - lastClaimCheck : undefined });
log.trace('[handleAppStateChange] Start', {secsFromLastPending: nowInSec - lastPendingCheck, secsFromLastClaim: nowInSec - lastClaimCheck})
if (lastPendingCheck && nowInSec - lastPendingCheck > 10) {
if (nowInSec - lastPendingCheck > PENDING_CHECK_INTERVAL) {
setLastPendingCheck(nowInSec)
WalletTask.syncPendingStateWithMints().catch(e => false)
WalletTask.handlePendingTopups().catch(e => false)
} else {
log.trace('[handleAppStateChange] Skipping pending checks...')
}
if(lastClaimCheck && nowInSec - lastClaimCheck > 60) {
if(nowInSec - lastClaimCheck > CLAIM_CHECK_INTERVAL) {
setLastClaimCheck(nowInSec)
WalletTask.handleClaim().catch(e => false)
@@ -336,7 +336,7 @@ export const WalletScreen: FC<WalletScreenProps> = observer(
return () => {
subscription.remove(); // Ensure cleanup to avoid multiple listeners
};
}, [lastClaimCheck, lastPendingCheck, isInternetReachable, userSettingsStore.exchangeCurrency])
}, [lastPendingCheck, lastClaimCheck, isInternetReachable, userSettingsStore.exchangeCurrency])
const toggleMintsModal = () => {
+1 -1
View File
@@ -212,7 +212,7 @@ const save = function (key: string, value: any): boolean {
const end = performance.now()
// const dataSize = Buffer.byteLength(JSON.stringify(value), 'utf8')
// log.trace(`[mmkvStorage.save] Took ${end - start} ms to save ${dataSize} bytes.`)
// log.trace(`[mmkvStorage.save] *** MMKV REAL SAVE *** Took ${end - start} ms to save ${dataSize} bytes.`)
return true
} catch (e: any) {
+4 -6
View File
@@ -140,7 +140,7 @@ export const sendTask = async function (
log.trace('[send] totalBalance after', balanceAfter)
// Start polling for accepted payment is it is not offline send
// Start polling for accepted payment it is not an offline send
if(selectedProofs.length === 0) {
const proofsToSync = proofsStore.getByMint(mintUrl, {isPending: true})
@@ -214,7 +214,7 @@ export const sendFromMintSync = async function (
)
}
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit}) as Proof[]
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit})
log.debug('[sendFromMintSync]', 'proofsFromMint count', {mintBalance: mintBalance.balances[unit], amountToSend})
@@ -399,7 +399,7 @@ export const sendFromMintSync = async function (
// release lock
lockedProofsCounter.resetInFlight(transactionId)
} else if (returnedAmount === 0) {
} else {
/*
* SWAP is NOT needed, we've found denominations that match exact amount
*
@@ -416,9 +416,7 @@ export const sendFromMintSync = async function (
proofsToSend = [...proofsToSendFrom]
} else {
throw new AppError(Err.VALIDATION_ERROR, 'Amount to keep can not be negative')
}
}
// remove used proofs and move sent proofs to pending
proofsStore.removeProofs(proofsToSendFrom)
+14 -22
View File
@@ -97,7 +97,7 @@ export const transferTask = async function (
}
// calculate fees charged by mint for melt transaction to prepare enough proofs
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit}) as Proof[]
const proofsFromMint = proofsStore.getByMint(mintUrl, {isPending: false, unit})
let proofsToMelt = CashuUtils.getProofsToSend(
amountToTransfer + meltQuote.fee_reserve,
@@ -120,9 +120,14 @@ export const transferTask = async function (
[],
transaction.id,
)
const {
proofs: proofsToPay,
mintFeePaid,
mintFeeReserve,
isSwapNeeded
} = swapResult
proofsToPay = swapResult.proofs
const {mintFeePaid, mintFeeReserve, isSwapNeeded} = swapResult
proofsToPayAmount = CashuUtils.getProofsAmount(proofsToPay)
// TODO in case of swap from inactive keysets, different meltFees might apply than above calculated meltFeeReserve
@@ -135,6 +140,7 @@ export const transferTask = async function (
status: TransactionStatus.PREPARED,
mintFeeReserve,
mintFeePaid,
proofsToPayAmount,
isSwapNeeded,
createdAt: new Date(),
})
@@ -182,28 +188,14 @@ export const transferTask = async function (
lockedProofsCounter.decreaseProofsCounter(countOfInFlightProofs)
// update transaction status and proofs state based on sync with the mint
// proofsToPay are clean ProofV3, not Proof models so we send all pending from state
/* const proofsToSync = proofsStore.getByMint(mintUrl, {isPending: true})
const { completedTransactionIds, transactionStateUpdates } = await WalletTask.syncStateWithMintSync(
{
proofsToSync,
mintUrl,
isPending: true
}
)
if(!completedTransactionIds.includes(transaction.id)) {
// silent
log.warn('[transfer] payLightningMelt call suceeded but proofs were not spent by mint', {transactionStateUpdates})
}*/
// If real fees were less then estimated, cash the returned savings.
if (state === MeltQuoteState.PAID) {
log.trace('[transfer] Invoice PAID', {state, preimage})
// Spend pending proofs that were used to settle the lightning invoice
proofsStore.removeProofs(proofsToPay as Proof[], true, false)
let lightningFeePaid = meltQuote.fee_reserve
if (feeSavedProofs.length) {
if (feeSavedProofs.length > 0) {
const {addedAmount: feeSaved} = WalletUtils.addCashuProofs(
mintUrl,
feeSavedProofs,
+18 -8
View File
@@ -434,7 +434,7 @@ const syncPendingStateWithMints = async function (): Promise<void> {
}
const isPending = true
// const maxBatchSize = MAX_CHECK_INPUT_SIZE
const maxBatchSize = MAX_SYNC_INPUT_SIZE
// group proofs by mint so that we do max one call per mint
for (const mint of mintsStore.allMints) {
@@ -445,8 +445,17 @@ const syncPendingStateWithMints = async function (): Promise<void> {
}
const proofsToSync = proofsStore.getByMint(mint.mintUrl, {isPending})
const totalProofs = proofsToSync.length
syncStateWithMint({ proofsToSync, mintUrl: mint.mintUrl, isPending })
const totalProofsCount = proofsToSync.length
if (totalProofsCount > maxBatchSize) {
for (let i = 0; i < totalProofsCount; i += maxBatchSize) {
const batch = proofsToSync.slice(i, i + maxBatchSize)
syncStateWithMint({ proofsToSync: batch, mintUrl: mint.mintUrl, isPending })
}
} else {
// If the length is less than or equal to 100, run syncStateWithMint with all proofs.
syncStateWithMint({ proofsToSync, mintUrl: mint.mintUrl, isPending });
}
}
}
@@ -473,7 +482,7 @@ const syncSpendableStateWithMints = async function (): Promise<void> {
if (totalProofsCount > maxBatchSize) {
for (let i = 0; i < totalProofsCount; i += maxBatchSize) {
const batch = proofsToSync.slice(i, i + maxBatchSize)
syncStateWithMint({ proofsToSync: batch, mintUrl: mint.mintUrl, isPending });
syncStateWithMint({ proofsToSync: batch, mintUrl: mint.mintUrl, isPending })
}
} else {
// If the length is less than or equal to 100, run syncStateWithMint with all proofs.
@@ -495,7 +504,7 @@ const syncStateWithMint = async function (
}
): Promise<void> {
const {mintUrl, isPending, proofsToSync} = options
log.trace('[syncStateWithMint] start', {mintUrl, isPending})
log.trace('[syncStateWithMint] start', {mintUrl, isPending, proofsToSyncCount: proofsToSync.length})
const now = new Date().getTime()
return SyncQueue.addTask(
@@ -516,7 +525,7 @@ const syncStateWithMintSync = async function (
): Promise<SyncStateTaskResult> {
const {mintUrl, isPending, proofsToSync} = options
log.trace('[syncStateWithMintSync] start', {mintUrl, isPending, proofsToSyncCount: proofsToSync?.length})
log.trace('[syncStateWithMintSync] start', {mintUrl, isPending, proofsToSyncCount: proofsToSync.length})
return await _syncStateWithMintTask({proofsToSync, mintUrl, isPending})
}
@@ -636,7 +645,7 @@ const _syncStateWithMintTask = async function (
if (tx) {
// spent amount does not cover matched tx amount
// means that some spent proofs were used as inputs into the send
// means that some spent proofs were used as inputs into the swap / melt
if(spentByMintTxAmount < tx.amount) {
errorTransactionIds.push(Number(tId))
@@ -679,7 +688,8 @@ const _syncStateWithMintTask = async function (
return {
tId: Number(tId),
updatedStatus: TransactionStatus.ERROR
updatedStatus: TransactionStatus.ERROR,
message: 'Could not find transaction in the database.'
} as TransactionStateUpdate
})