mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-08-12 09:23:20 +00:00
Fix spent proofs might remain in wallet after a lightning payment, fix pending tx count
This commit is contained in:
BIN
Binary file not shown.
+1
-1
@@ -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
@@ -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)
|
||||
|
||||
@@ -304,7 +304,7 @@ export const TransactionsStoreModel = types
|
||||
transactionsMap: prunedTransactionsMap
|
||||
}
|
||||
|
||||
console.log('[postProcessSnapshot]', {prunedSnapshot})
|
||||
// console.log('[postProcessSnapshot]', {prunedSnapshot})
|
||||
|
||||
return prunedSnapshot
|
||||
})
|
||||
|
||||
@@ -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})
|
||||
})
|
||||
|
||||
@@ -267,7 +267,7 @@ export const TranHistoryScreen: FC<TransactionsStackScreenProps<'TranHistory'>>
|
||||
/>
|
||||
<ListItem
|
||||
text={translate("tranHistory.pendingParam", {
|
||||
param: transactionsStore.pendingHistoryCount
|
||||
param: pendingDbCount
|
||||
})}
|
||||
LeftComponent={
|
||||
<Icon
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user