Send large ecash tokens in batches, performance improvements for large wallets.

This commit is contained in:
minibits-cash
2024-10-16 16:42:23 +02:00
parent c834342a2c
commit 3affc6ed31
6 changed files with 193 additions and 98 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "minibits_wallet",
"version": "0.1.9-beta.23",
"version": "0.1.9-beta.24",
"private": true,
"scripts": {
"android:clean": "cd android && ./gradlew clean",
+83 -25
View File
@@ -37,6 +37,7 @@ import { LogLevel } from '../services/log/logTypes'
import { getSnapshot } from 'mobx-state-tree'
import { delay } from '../utils/delay'
import RNExitApp from 'react-native-exit-app'
import { TransactionStatus } from '../models/Transaction'
// refresh
@@ -47,7 +48,7 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
onLeftPress: () => navigation.goBack(),
})
const {transactionsStore, userSettingsStore} = useStores()
const {transactionsStore, userSettingsStore, proofsStore} = useStores()
const [isLoading, setIsLoading] = useState(false)
const [rnVersion, setRnVersion] = useState<string>('')
@@ -75,7 +76,7 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
init()
}, [])
// Reset of whole model state and reload from DB
// Reset of transaction model state and reload from DB
const syncTransactionsFromDb = async function () {
setIsLoading(true)
try {
@@ -105,6 +106,48 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
}
const deletePending = async function () {
Alert.alert(
translate("common.confirmAlertTitle"),
"This action can not be undone. Use only in development or testing.",
[
{
text: translate('common.cancel'),
style: 'cancel',
onPress: () => { /* Action canceled */ },
},
{
text: translate('common.confirm'),
onPress: async () => {
try {
setIsLoading(true)
const rows = Database.deleteTransactionsByStatus(TransactionStatus.PENDING)
const pending = proofsStore.allPendingProofs
const pendingCount = proofsStore.pendingProofsCount.valueOf()
if(pendingCount > 0) {
proofsStore.removeProofs(pending, true, false) // remove pending proofs and spend the in db
}
if(rows?.length && rows.length > 0) {
await syncTransactionsFromDb()
}
setIsLoading(false)
setInfo(`Removed ${rows?.length || 0} transactions from the database and ${pendingCount} proofs from the wallet state`)
} catch (e: any) {
handleError(e)
}
},
},
],
)
}
const toggleLogLevelSelector = () =>
setIsLogLevelSelectorVisible(previousState => !previousState)
@@ -180,9 +223,31 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
style={{color: headerTitle}}
/>
</View>
<View style={$contentContainer}>
<View style={$contentContainer}>
<Card
style={$card}
style={[$card]}
HeadingComponent={
<ListItem
tx="developerScreen.info"
subText={`Environment: ${APP_ENV}
Native version: ${NATIVE_VERSION_ANDROID}
JS Bundle version: ${JS_BUNDLE_VERSION}
DB version: ${dbVersion}
State size: ${walletStateSize.toLocaleString()} bytes
React Native: ${rnVersion}
Commit: ${COMMIT}
Sentry id: ${userSettingsStore.userSettings.walletId}
`}
leftIcon='faInfoCircle'
leftIconColor={colors.palette.iconGreen300}
leftIconInverse={true}
RightComponent={<View style={$rightContainer} />}
style={$item}
/>
}
/>
<Card
style={[$card, {marginTop: spacing.medium}]}
HeadingComponent={
<>
<ListItem
@@ -211,30 +276,21 @@ export const DeveloperScreen: FC<SettingsStackScreenProps<'Developer'>> = observ
}
/>
<Card
style={[$card, {marginTop: spacing.medium}]}
label='Danger zone'
labelStyle={{marginTop: spacing.medium}}
style={[$card]}
HeadingComponent={
<>
<ListItem
tx="developerScreen.info"
subText={`Environment: ${APP_ENV}
Native version: ${NATIVE_VERSION_ANDROID}
JS Bundle version: ${JS_BUNDLE_VERSION}
DB version: ${dbVersion}
State size: ${walletStateSize.toLocaleString()} bytes
React Native: ${rnVersion}
Commit: ${COMMIT}
Sentry id: ${userSettingsStore.userSettings.walletId}
`}
leftIcon='faInfoCircle'
leftIconColor={colors.palette.iconGreen300}
leftIconInverse={true}
text="Force delete pending"
subText="Removes ecash from pending state and deletes all pending transactions."
leftIcon='faClock'
leftIconColor={colors.palette.accent400}
leftIconInverse={true}
RightComponent={<View style={$rightContainer} />}
style={$item}
/>
}
/>
<Card
style={[$card, {marginTop: spacing.medium}]}
HeadingComponent={
style={$item}
onPress={deletePending}
/>
<ListItem
tx="developerScreen.reset"
subTx="developerScreen.resetDescription"
@@ -244,7 +300,9 @@ Sentry id: ${userSettingsStore.userSettings.walletId}
RightComponent={<View style={$rightContainer} />}
style={$item}
onPress={factoryReset}
topSeparator
/>
</>
}
/>
</View>
+42 -30
View File
@@ -42,7 +42,7 @@ import { verticalScale } from '@gocodingnow/rn-size-matters'
interface ExportBackupScreenProps extends SettingsStackScreenProps<'ExportBackup'> {}
const OPTIMIZE_FROM_PROOFS_COUNT = 100
const OPTIMIZE_FROM_PROOFS_COUNT = 5
export const ExportBackupScreen: FC<ExportBackupScreenProps> =
function ExportBackup(_props) {
@@ -65,6 +65,7 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
const [isLoading, setIsLoading] = useState(false)
const [isSendAllSentToQueue, setIsSendAllSentToQueue] = useState<boolean>(false)
const [isReceiveBatchSentToQueue, setIsReceiveBatchSentToQueue] = useState<boolean>(false)
const [totalProofsCount, setTotalProofsCount] = useState<number>(0)
const [totalSentProofsCount, setTotalSentProofsCount] = useState<number>(0)
const [totalReceiveErrorCount, setTotalReceiveErrorCount] = useState<number>(0)
const [totalReceiveCompleteCount, setTotalReceiveCompleteCount] = useState<number>(0)
@@ -85,12 +86,14 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
await proofsStore.loadProofsFromDatabase()
const orphaned = proofsStore.allProofs.filter(proof => !proof.mintUrl)
if (orphaned.length > 0) {
setOrphanedProofs(orphaned)
setInfo(`Found ${orphaned.length} orphaned proofs not belonging to any active mint. Those won't be included to the backup, but you can copy them separately.`)
}
// log.trace('[loadProofs]', {refreshedProofs: proofsStore.proofs})
setTotalProofsCount(proofsStore.proofsCount)
setIsLoading(false)
} catch (e: any) {
handleError(e)
@@ -135,7 +138,7 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
}
return () => {
EventEmitter.off('ev_sendTask_result', handleSendAllResult)
EventEmitter.off('ev_sendTask_result', handleSendAllResult)
}
}, [isSendAllSentToQueue])
@@ -151,14 +154,18 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
if (error) {
setTotalReceiveErrorCount(prev => prev + 1)
} else {
setTotalReceiveCompleteCount(prev => prev + 1)
}
setTotalReceiveCompleteCount(prev => prev + 1)
}
}
if(isReceiveBatchSentToQueue) {
EventEmitter.on('ev_receiveTask_result', handleReceiveTaskResult)
}
return () => {
EventEmitter.off('ev_receiveTask_result', handleReceiveTaskResult)
}
}, [isReceiveBatchSentToQueue])
@@ -169,12 +176,12 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
setIsLoading(false)
const currentProofsCount = proofsStore.proofsCount
const currentProofsCount = proofsStore.proofsCount
let message = `Original proofs count: ${totalSentProofsCount}, Optimized proofs count: ${currentProofsCount}`
if (totalReceiveErrorCount > 0) {
message += `, errors: ${totalReceiveErrorCount}`
message += `, Errors: ${totalReceiveErrorCount}`
}
setResultModalInfo({
@@ -185,15 +192,18 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
setIsResultModalVisible(true)
}
if(totalReceiveCompleteCount > 0 || totalReceiveCompleteCount > 0) {
if(totalReceiveCompleteCount > 0 || totalReceiveErrorCount > 0) {
showProofOptimizationResult()
}
}, [totalReceiveErrorCount, totalReceiveCompleteCount])
const toggleResultModal = () =>
setIsResultModalVisible(previousState => !previousState)
const toggleResultModal = () => {
setIsResultModalVisible(previousState => !previousState)
WalletTask.syncPendingStateWithMints()
}
const toggleBackupEcashSwitch = () =>
@@ -221,7 +231,9 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
{
text: translate('common.confirm'),
onPress: async () => {
// Moves all wallet proofs to pending in transactions split by mints and by units and in offline mode
// Moves all wallet proofs to pending in transactions
// split by mints and by units and in offline mode.
// Supports batching in case proofs count is above limit.
setIsLoading(true)
setIsSendAllSentToQueue(true)
WalletTask.sendAll()
@@ -578,27 +590,27 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
<Card
ContentComponent={
<>
{proofsStore.proofsCount + proofsStore.pendingProofsCount > 0 && (
{totalProofsCount > 0 && (
<ListItem
text="Ecash proofs"
subText={`Number of proofs: ${proofsStore.proofsCount + proofsStore.pendingProofsCount}`}
RightComponent={
<View style={$rightContainer}>
{proofsStore.proofsCount > OPTIMIZE_FROM_PROOFS_COUNT && (
<Button
preset='secondary'
onPress={optimizeProofAmountsStart}
textStyle={{lineHeight: verticalScale(16), fontSize: verticalScale(14)}}
style={{minHeight: verticalScale(40), paddingVertical: verticalScale(spacing.tiny)}}
text={'Optimize'}
/>
)}
<Switch
onValueChange={toggleBackupEcashSwitch}
value={isEcashInBackup}
text="Ecash proofs"
subText={`Number of proofs: ${proofsStore.proofsCount}`}
RightComponent={
<View style={$rightContainer}>
{proofsStore.proofsCount > OPTIMIZE_FROM_PROOFS_COUNT && (
<Button
preset='secondary'
onPress={optimizeProofAmountsStart}
textStyle={{lineHeight: verticalScale(16), fontSize: verticalScale(14)}}
style={{minHeight: verticalScale(40), paddingVertical: verticalScale(spacing.tiny)}}
text={'Optimize'}
/>
</View>
}
)}
<Switch
onValueChange={toggleBackupEcashSwitch}
value={isEcashInBackup}
/>
</View>
}
/>
)}
{mintsStore.mintCount > 0 && (
@@ -613,7 +625,7 @@ export const ExportBackupScreen: FC<ExportBackupScreenProps> =
/>
</View>
}
topSeparator={proofsStore.proofsCount + proofsStore.pendingProofsCount > 0 ? true : false}
topSeparator={totalProofsCount > 0 ? true : false}
/>
)}
{contactsStore.count > 0 && (
+1 -1
View File
@@ -1060,7 +1060,7 @@ const getPendingAmount = function () {
}
const deleteTransactionsByStatus = async function (status: TransactionStatus) {
const deleteTransactionsByStatus = function (status: TransactionStatus) {
try {
const query = `
DELETE FROM transactions
+21 -19
View File
@@ -12,7 +12,7 @@ import {
MintKeyset,
} from '@cashu/cashu-ts'
import { getDefaultAmountPreference, isObj } from '@cashu/cashu-ts/src/utils'
import { TransactionTaskResult, WalletTask } from '../walletService'
import { MAX_SWAP_INPUT_SIZE, TransactionTaskResult, WalletTask } from '../walletService'
import { MintBalance, MintProofsCounter } from '../../models/Mint'
import { Proof } from '../../models/Proof'
import { poller } from '../../utils/poller'
@@ -242,7 +242,17 @@ export const sendFromMint = async function (
if(selectedProofsAmount > 0) {
if(amountToSend !== selectedProofsAmount) { // failsafe for some unknown ecash selection UX error
throw new AppError(Err.VALIDATION_ERROR, 'Requested amount to send does not equal sum of ecash denominations provided.')
throw new AppError(
Err.VALIDATION_ERROR,
'Requested amount to send does not equal sum of ecash denominations provided.'
)
}
if(selectedProofs.length > MAX_SWAP_INPUT_SIZE) {
throw new AppError(
Err.VALIDATION_ERROR,
`Number of proofs is above max of ${MAX_SWAP_INPUT_SIZE}. Visit Backup to optimize, then try again.`
)
}
for (const proof of selectedProofs) {
@@ -434,25 +444,17 @@ export const sendFromMint = async function (
lockedProofsCounter.resetInFlight(transactionId)
} else if (returnedAmount === 0) {
/*
* SWAP not needed, all selected proofs will be sent
* In this case we might make sure upfront those are not spent - not used now
*/
/* const syncResult = await WalletTask.syncStateWithMintSync(
{
proofsToSync: proofsToSendFrom,
mintUrl,
isPending: false
}
)
if(syncResult.transactionStateUpdates.find(stateUpdate =>
stateUpdate.spentByMintAmount &&
stateUpdate.spentByMintAmount > 0)) {
throw new AppError(Err.VALIDATION_ERROR, 'Spent ecash has been used as an input for this transaction, try again.')
}*/
log.debug('[sendFromMint] Swap is not necessary, all proofsToSendFrom will be sent.')
// If we selected whole balance, check if it is not above limit acceptable by wallet and mints.
if(proofsToSendFrom.length > MAX_SWAP_INPUT_SIZE) {
throw new AppError(
Err.VALIDATION_ERROR,
`Number of proofs is above max of ${MAX_SWAP_INPUT_SIZE}. Visit Backup to optimize, then try again.`
)
}
proofsToSend = [...proofsToSendFrom]
} else {
+45 -22
View File
@@ -31,7 +31,8 @@ import { MintUnit, formatCurrency, getCurrency } from './wallet/currency'
import { MinibitsClient } from './minibitsService'
export const MAX_SWAP_INPUT_SIZE = 50
export const MAX_SWAP_INPUT_SIZE = 100
export const MAX_SYNC_INPUT_SIZE = 200 // 1000 hard mint limit
type WalletTaskService = {
syncPendingStateWithMints: () => Promise<void>
@@ -339,20 +340,41 @@ const sendAll = async function (): Promise<void> {
// Move all proofs by mint units to pending as in offline mode (do not ask for swap)
for (const mint of mintsStore.allMints) {
// Do not create a pending transaction above mint's spent sync (check) limit as it becomes stuck pending
// As well keep tokens reasonably sized so that a device can keep related transaction in the state / load it from DB
const maxBatchSize = MAX_SWAP_INPUT_SIZE
for (const unit of mint.units) {
const proofsToOptimize = proofsStore.getByMint(mint.mintUrl, { isPending: false, unit })
const proofsAmount = CashuUtils.getProofsAmount(proofsToOptimize)
const mintBalance = mint.balances
const proofsToOptimize = proofsStore.getByMint(mint.mintUrl, { isPending: false, unit })
const totalProofsCount = proofsToOptimize.length
const mintBalance = mint.balances
if (totalProofsCount > maxBatchSize) {
let index = 0
for (let i = 0; i < totalProofsCount; i += maxBatchSize) {
index++
const batch = proofsToOptimize.slice(i, i + maxBatchSize)
const batchAmount = CashuUtils.getProofsAmount(batch)
// Queued WalletTask.send
send(
mintBalance!,
proofsAmount,
unit,
`Optimize proof amounts`,
proofsToOptimize // forces offline mode
)
send(
mintBalance!,
batchAmount,
unit,
`Optimize ecash #${index}`,
batch // forces offline mode
)
}
} else {
// If the length is less than or equal to limit, run with all proofs.
const proofsAmount = CashuUtils.getProofsAmount(proofsToOptimize)
send(
mintBalance!,
proofsAmount,
unit,
`Optimize ecash`,
proofsToOptimize // forces offline mode
)
}
}
}
}
@@ -394,6 +416,7 @@ const syncPendingStateWithMints = async function (): Promise<void> {
}
const isPending = true
// const maxBatchSize = MAX_CHECK_INPUT_SIZE
// group proofs by mint so that we do max one call per mint
for (const mint of mintsStore.allMints) {
@@ -403,8 +426,9 @@ const syncPendingStateWithMints = async function (): Promise<void> {
continue
}
const proofsToSync = proofsStore.getByMint(mint.mintUrl, {isPending})
syncStateWithMint({proofsToSync, mintUrl: mint.mintUrl, isPending}) // isPending = true
const proofsToSync = proofsStore.getByMint(mint.mintUrl, {isPending})
const totalProofs = proofsToSync.length
syncStateWithMint({ proofsToSync, mintUrl: mint.mintUrl, isPending })
}
}
@@ -420,23 +444,22 @@ const syncSpendableStateWithMints = async function (): Promise<void> {
}
const isPending = false
const maxBatchSize = 50
const maxBatchSize = MAX_SYNC_INPUT_SIZE
// group proofs by mint so that we do max one call per mint
// does not depend on unit, process in batches by 100
for (const mint of mintsStore.allMints) {
for (const mint of mintsStore.allMints) {
const proofsToSync = proofsStore.getByMint(mint.mintUrl, { isPending })
const totalProofs = proofsToSync.length
const totalProofsCount = proofsToSync.length
if (totalProofs > maxBatchSize) {
for (let i = 0; i < totalProofs; i += maxBatchSize) {
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: false });
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: false });
syncStateWithMint({ proofsToSync, mintUrl: mint.mintUrl, isPending });
}
}