mirror of
https://github.com/minibits-cash/minibits_wallet.git
synced 2026-07-22 07:48:28 +00:00
Optimize ecash denominations per mint
This commit is contained in:
4
.vscode/settings.json
vendored
4
.vscode/settings.json
vendored
@@ -2,5 +2,7 @@
|
||||
"[javascriptreact][typescriptreact][javascript][typescript]": {
|
||||
"editor.insertSpaces": true,
|
||||
"editor.tabSize": 4
|
||||
}
|
||||
},
|
||||
"claudeCode.disableLoginPrompt": true,
|
||||
"claudeCode.selectedModel": "claude-opus-4-7"
|
||||
}
|
||||
3
index.js
3
index.js
@@ -70,7 +70,8 @@ notifee.registerForegroundService(async (notification) => {
|
||||
if(notification.data.task === SWAP_DENOMINATION_TASK) {
|
||||
log.debug(`[registerForegroundService] Submitting task ${SWAP_DENOMINATION_TASK} to the queue.`)
|
||||
|
||||
WalletTask.swapByDenominationQueue(notification.data.data)
|
||||
const payload = notification.data.data || {}
|
||||
WalletTask.swapByDenominationQueue(payload.denomination, payload.mintUrl)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const MintBalanceSelector = observer(function (props: {
|
||||
selectedMintBalance?: MintBalance
|
||||
unit: MintUnit
|
||||
title: string
|
||||
confirmTitle: string
|
||||
confirmTitle?: string
|
||||
confirmIcon?: IconTypes
|
||||
secondaryConfirmTitle?: string
|
||||
secondaryConfirmIcon?: IconTypes
|
||||
@@ -23,8 +23,8 @@ export const MintBalanceSelector = observer(function (props: {
|
||||
collapsible?: boolean
|
||||
onMintBalanceSelect: any
|
||||
onSecondaryMintBalanceSelect?: any
|
||||
onCancel: any
|
||||
onMintBalanceConfirm: any
|
||||
onCancel?: any
|
||||
onMintBalanceConfirm?: any
|
||||
}) {
|
||||
|
||||
const collapsible = props.collapsible === false ? false : true // default true
|
||||
@@ -120,45 +120,51 @@ export const MintBalanceSelector = observer(function (props: {
|
||||
</>
|
||||
}
|
||||
/>
|
||||
<View style={$bottomContainer}>
|
||||
{!isKeyboardVisible && (
|
||||
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
|
||||
|
||||
{props.secondaryConfirmTitle && (
|
||||
<Button
|
||||
text={props.secondaryConfirmTitle}
|
||||
LeftAccessory={() =>
|
||||
(props.secondaryConfirmIcon && (
|
||||
<Icon icon={props.secondaryConfirmIcon} color={secondaryIconColor} size={spacing.medium}/>
|
||||
))
|
||||
}
|
||||
preset="secondary"
|
||||
onPress={props.onSecondaryMintBalanceSelect}
|
||||
style={{marginRight: spacing.medium}}
|
||||
/>
|
||||
)}
|
||||
<Button
|
||||
text={props.confirmTitle}
|
||||
LeftAccessory={() =>
|
||||
<Icon icon={props.confirmIcon ? props.confirmIcon : 'faCheckCircle'} color="white" size={spacing.medium}/>
|
||||
}
|
||||
onPress={props.onMintBalanceConfirm}
|
||||
style={{marginRight: spacing.medium}}
|
||||
/>
|
||||
{(props.confirmTitle || props.secondaryConfirmTitle || props.onCancel) && (
|
||||
<View style={$bottomContainer}>
|
||||
{!isKeyboardVisible && (
|
||||
<View style={[$buttonContainer, {marginTop: spacing.large}]}>
|
||||
|
||||
<Button
|
||||
preset="secondary"
|
||||
LeftAccessory={() =>
|
||||
(props.cancelIcon && (
|
||||
<Icon icon={props.cancelIcon} color={secondaryIconColor} size={spacing.medium}/>
|
||||
))
|
||||
}
|
||||
tx={'commonCancel'}
|
||||
onPress={props.onCancel}
|
||||
/>
|
||||
{props.secondaryConfirmTitle && props.onSecondaryMintBalanceSelect && (
|
||||
<Button
|
||||
text={props.secondaryConfirmTitle}
|
||||
LeftAccessory={() =>
|
||||
(props.secondaryConfirmIcon && (
|
||||
<Icon icon={props.secondaryConfirmIcon} color={secondaryIconColor} size={spacing.medium}/>
|
||||
))
|
||||
}
|
||||
preset="secondary"
|
||||
onPress={props.onSecondaryMintBalanceSelect}
|
||||
style={{marginRight: spacing.medium}}
|
||||
/>
|
||||
)}
|
||||
{props.confirmTitle && props.onMintBalanceConfirm && (
|
||||
<Button
|
||||
text={props.confirmTitle}
|
||||
LeftAccessory={() =>
|
||||
<Icon icon={props.confirmIcon ? props.confirmIcon : 'faCheckCircle'} color="white" size={spacing.medium}/>
|
||||
}
|
||||
onPress={props.onMintBalanceConfirm}
|
||||
style={{marginRight: spacing.medium}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{props.onCancel && (
|
||||
<Button
|
||||
preset="secondary"
|
||||
LeftAccessory={() =>
|
||||
(props.cancelIcon && (
|
||||
<Icon icon={props.cancelIcon} color={secondaryIconColor} size={spacing.medium}/>
|
||||
))
|
||||
}
|
||||
tx={'commonCancel'}
|
||||
onPress={props.onCancel}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, {useState, useEffect, useLayoutEffect} from 'react'
|
||||
import React, {useState, useEffect, useLayoutEffect, useMemo} from 'react'
|
||||
import {
|
||||
TextStyle,
|
||||
ViewStyle,
|
||||
@@ -31,6 +31,9 @@ import {TransactionStatus} from '../models/Transaction'
|
||||
import {ResultModalInfo} from './Wallet/ResultModalInfo'
|
||||
import {verticalScale} from '@gocodingnow/rn-size-matters'
|
||||
import {StaticScreenProps, useNavigation} from '@react-navigation/native'
|
||||
import {MintBalanceSelector} from './Mints/MintBalanceSelector'
|
||||
import {MintBalance} from '../models/Mint'
|
||||
import {MintUnit} from '../services/wallet/currency'
|
||||
|
||||
const OPTIMIZE_DENOMINATION_THRESHOLD = 5
|
||||
|
||||
@@ -57,8 +60,20 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
|
||||
})
|
||||
}, [])
|
||||
|
||||
// For now this screen only deals with the 'sat' unit, as denominations are unit-bound.
|
||||
const unit: MintUnit = 'sat'
|
||||
|
||||
const mintBalances = useMemo<MintBalance[]>(
|
||||
() => proofsStore.getMintBalancesWithUnit(unit),
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[proofsStore.balances],
|
||||
)
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [error, setError] = useState<AppError | undefined>()
|
||||
const [selectedMintBalance, setSelectedMintBalance] = useState<MintBalance | undefined>(
|
||||
() => proofsStore.getMintBalanceWithMaxBalance(unit) ?? mintBalances[0],
|
||||
)
|
||||
const [denominationCounts, setDenominationCounts] = useState<{denomination: number; count: number}[]>([])
|
||||
const [activeDenomination, setActiveDenomination] = useState<number | null>(null)
|
||||
const [isResultModalVisible, setIsResultModalVisible] = useState(false)
|
||||
@@ -68,9 +83,10 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
|
||||
>()
|
||||
|
||||
useEffect(() => {
|
||||
const counts = buildDenominationCounts()
|
||||
const counts = buildDenominationCounts(selectedMintBalance?.mintUrl)
|
||||
setDenominationCounts(counts)
|
||||
}, [])
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [selectedMintBalance?.mintUrl])
|
||||
|
||||
useEffect(() => {
|
||||
const handleResult = async (result: WalletTaskResult) => {
|
||||
@@ -85,8 +101,8 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
|
||||
})
|
||||
setIsResultModalVisible(true)
|
||||
|
||||
// Refresh denomination counts after optimization
|
||||
const counts = buildDenominationCounts()
|
||||
// Refresh denomination counts after optimization for the selected mint
|
||||
const counts = buildDenominationCounts(selectedMintBalance?.mintUrl)
|
||||
setDenominationCounts(counts)
|
||||
setActiveDenomination(null)
|
||||
}
|
||||
@@ -98,14 +114,15 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
|
||||
return () => {
|
||||
EventEmitter.off(`ev_${SWAP_DENOMINATION_TASK}_result`, handleResult)
|
||||
}
|
||||
}, [activeDenomination])
|
||||
}, [activeDenomination, selectedMintBalance?.mintUrl])
|
||||
|
||||
const buildDenominationCounts = () => {
|
||||
const allProofs = Array.from(proofsStore.proofs.values()).filter(
|
||||
p => p.state === 'UNSPENT',
|
||||
)
|
||||
const buildDenominationCounts = (mintUrl?: string) => {
|
||||
if (!mintUrl) {
|
||||
return []
|
||||
}
|
||||
const mintProofs = proofsStore.getByMint(mintUrl, {state: 'UNSPENT', unit})
|
||||
const countMap: Record<number, number> = {}
|
||||
for (const proof of allProofs) {
|
||||
for (const proof of mintProofs) {
|
||||
countMap[proof.amount] = (countMap[proof.amount] ?? 0) + 1
|
||||
}
|
||||
return Object.entries(countMap)
|
||||
@@ -113,7 +130,15 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
|
||||
.sort((a, b) => a.denomination - b.denomination)
|
||||
}
|
||||
|
||||
const onMintBalanceSelect = (balance: MintBalance) => {
|
||||
setSelectedMintBalance(balance)
|
||||
}
|
||||
|
||||
const optimizeDenomination = async (denomination: number) => {
|
||||
if (!selectedMintBalance) {
|
||||
return
|
||||
}
|
||||
|
||||
const enabled = await NotificationService.areNotificationsEnabled()
|
||||
|
||||
if (!enabled) {
|
||||
@@ -128,10 +153,13 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
|
||||
if (Platform.OS === 'android') {
|
||||
await NotificationService.createTaskNotification(
|
||||
`Optimizing denomination ${denomination}...`,
|
||||
{task: SWAP_DENOMINATION_TASK, data: denomination},
|
||||
{
|
||||
task: SWAP_DENOMINATION_TASK,
|
||||
data: {denomination, mintUrl: selectedMintBalance.mintUrl},
|
||||
},
|
||||
)
|
||||
} else {
|
||||
WalletTask.swapByDenominationQueue(denomination)
|
||||
WalletTask.swapByDenominationQueue(denomination, selectedMintBalance.mintUrl)
|
||||
}
|
||||
} catch (e: any) {
|
||||
handleError(e)
|
||||
@@ -171,22 +199,35 @@ export const OptimizeEcashScreen = function OptimizeEcash(_props: Props) {
|
||||
title="Optimize ecash"
|
||||
scrollY={scrollY}
|
||||
/>
|
||||
<Card
|
||||
content={`Only denominations with more than ${OPTIMIZE_DENOMINATION_THRESHOLD} proofs can be optimized - by swapping them with the mint for lower number of ecash notes with higher amounts.`}
|
||||
style={[$card, {marginTop: -spacing.extraLarge * 1.5}]}
|
||||
<View style={[$selectorContainer, {marginTop: -spacing.extraLarge * 1.5}]}>
|
||||
{mintBalances.length > 0 ? (
|
||||
<MintBalanceSelector
|
||||
mintBalances={mintBalances}
|
||||
selectedMintBalance={selectedMintBalance}
|
||||
unit={unit}
|
||||
title="Select mint to optimize"
|
||||
onMintBalanceSelect={onMintBalanceSelect}
|
||||
/>
|
||||
) : (
|
||||
<Card content="No mints with balance to optimize." style={$card} />
|
||||
)}
|
||||
</View>
|
||||
<Text
|
||||
text={`Only denominations with more than ${OPTIMIZE_DENOMINATION_THRESHOLD} proofs can be optimized - by swapping them with the mint for lower number of ecash notes with higher amounts.`}
|
||||
preset="formHelper"
|
||||
style={[$hintText, {color: hint}]}
|
||||
/>
|
||||
<Card
|
||||
ContentComponent={
|
||||
<>
|
||||
{denominationCounts.length === 0 ? (
|
||||
<ListItem text="No ecash proofs found." />
|
||||
<ListItem text="No ecash proofs for the selected mint." />
|
||||
) : (
|
||||
denominationCounts.map(({denomination, count}, index) => (
|
||||
<ListItem
|
||||
key={denomination}
|
||||
text={`${denomination}`}
|
||||
leftIcon='faMoneyBill1'
|
||||
//subText={`Count: ${count}`}
|
||||
RightComponent={
|
||||
<View style={$rightContainer}>
|
||||
<Text preset="formHelper" text={`${count}x`} style={{color: hint, marginRight: spacing.small}} />
|
||||
@@ -284,6 +325,15 @@ const $card: ViewStyle = {
|
||||
marginHorizontal: spacing.small,
|
||||
}
|
||||
|
||||
const $selectorContainer: ViewStyle = {
|
||||
marginHorizontal: spacing.small,
|
||||
}
|
||||
|
||||
const $hintText: TextStyle = {
|
||||
marginHorizontal: spacing.medium,
|
||||
marginBottom: spacing.small,
|
||||
}
|
||||
|
||||
const $buttonContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignSelf: 'center',
|
||||
@@ -296,8 +346,3 @@ const $rightContainer: ViewStyle = {
|
||||
flexDirection: 'row',
|
||||
alignItems: 'center',
|
||||
}
|
||||
|
||||
const $hintContainer: ViewStyle = {
|
||||
marginHorizontal: spacing.medium,
|
||||
marginBottom: spacing.small,
|
||||
}
|
||||
|
||||
@@ -594,10 +594,14 @@ const swapAllTask = async function (): Promise<WalletTaskResult> {
|
||||
}
|
||||
|
||||
/*
|
||||
* swapByDenominationTask swaps only proofs with the given denomination (amount) across all mints.
|
||||
* swapByDenominationTask swaps only proofs with the given denomination (amount)
|
||||
* for a single mint identified by mintUrl.
|
||||
*/
|
||||
const swapByDenominationTask = async function (denomination: number): Promise<WalletTaskResult> {
|
||||
log.trace('[swapByDenominationTask] start', {denomination})
|
||||
const swapByDenominationTask = async function (
|
||||
denomination: number,
|
||||
mintUrl: string,
|
||||
): Promise<WalletTaskResult> {
|
||||
log.trace('[swapByDenominationTask] start', {denomination, mintUrl})
|
||||
|
||||
if (mintsStore.mintCount === 0) {
|
||||
return {
|
||||
@@ -606,67 +610,44 @@ const swapByDenominationTask = async function (denomination: number): Promise<Wa
|
||||
}
|
||||
}
|
||||
|
||||
const mint = mintsStore.findByUrl(mintUrl)
|
||||
|
||||
if (!mint) {
|
||||
return {
|
||||
taskFunction: SWAP_DENOMINATION_TASK,
|
||||
message: `Mint ${mintUrl} not found.`,
|
||||
}
|
||||
}
|
||||
|
||||
let initialProofsCount = 0
|
||||
let finalProofsCount = 0
|
||||
const errors: string[] = []
|
||||
const maxBatchSize = MAX_SWAP_INPUT_SIZE
|
||||
|
||||
for (const mint of mintsStore.allMints) {
|
||||
for (const unit of mint.units) {
|
||||
const allProofs = proofsStore.getByMint(mint.mintUrl, {state: 'UNSPENT', unit, ascending: true})
|
||||
const proofsToOptimize = allProofs.filter(p => p.amount === denomination)
|
||||
for (const unit of mint.units) {
|
||||
const allProofs = proofsStore.getByMint(mint.mintUrl, {state: 'UNSPENT', unit, ascending: true})
|
||||
const proofsToOptimize = allProofs.filter(p => p.amount === denomination)
|
||||
|
||||
if (proofsToOptimize.length === 0) {
|
||||
continue
|
||||
}
|
||||
if (proofsToOptimize.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
initialProofsCount += proofsToOptimize.length
|
||||
const mintBalance = mint.balances
|
||||
initialProofsCount += proofsToOptimize.length
|
||||
const mintBalance = mint.balances
|
||||
|
||||
if (proofsToOptimize.length > maxBatchSize) {
|
||||
let index = 0
|
||||
for (let i = 0; i < proofsToOptimize.length; i += maxBatchSize) {
|
||||
index++
|
||||
const batch = proofsToOptimize.slice(i, i + maxBatchSize)
|
||||
const batchAmount = CashuUtils.getProofsAmount(batch)
|
||||
|
||||
const sendResult = await sendTask(
|
||||
mintBalance!,
|
||||
batchAmount,
|
||||
unit,
|
||||
`Optimize denomination ${denomination} #${index}`,
|
||||
batch,
|
||||
)
|
||||
|
||||
const encodedTokenToReceive: string = sendResult.transaction?.outputToken ?? ''
|
||||
const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
|
||||
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
|
||||
|
||||
const receiveResult = await receiveBatchTask(
|
||||
tokenToReceive,
|
||||
tokenAmount,
|
||||
tokenToReceive.memo as string,
|
||||
encodedTokenToReceive,
|
||||
)
|
||||
|
||||
await syncStateWithMintTask({proofsToSync: batch, mintUrl: mint.mintUrl, proofState: 'PENDING'})
|
||||
|
||||
if (receiveResult.receivedProofsCount && receiveResult.receivedProofsCount > 0) {
|
||||
finalProofsCount += receiveResult.receivedProofsCount
|
||||
}
|
||||
if (receiveResult.error) {
|
||||
errors.push(receiveResult.error.message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const proofsAmount = CashuUtils.getProofsAmount(proofsToOptimize)
|
||||
if (proofsToOptimize.length > maxBatchSize) {
|
||||
let index = 0
|
||||
for (let i = 0; i < proofsToOptimize.length; i += maxBatchSize) {
|
||||
index++
|
||||
const batch = proofsToOptimize.slice(i, i + maxBatchSize)
|
||||
const batchAmount = CashuUtils.getProofsAmount(batch)
|
||||
|
||||
const sendResult = await sendTask(
|
||||
mintBalance!,
|
||||
proofsAmount,
|
||||
batchAmount,
|
||||
unit,
|
||||
`Optimize denomination ${denomination}`,
|
||||
proofsToOptimize,
|
||||
`Optimize denomination ${denomination} #${index}`,
|
||||
batch,
|
||||
)
|
||||
|
||||
const encodedTokenToReceive: string = sendResult.transaction?.outputToken ?? ''
|
||||
@@ -680,7 +661,7 @@ const swapByDenominationTask = async function (denomination: number): Promise<Wa
|
||||
encodedTokenToReceive,
|
||||
)
|
||||
|
||||
await syncStateWithMintTask({proofsToSync: proofsToOptimize, mintUrl: mint.mintUrl, proofState: 'PENDING'})
|
||||
await syncStateWithMintTask({proofsToSync: batch, mintUrl: mint.mintUrl, proofState: 'PENDING'})
|
||||
|
||||
if (receiveResult.receivedProofsCount && receiveResult.receivedProofsCount > 0) {
|
||||
finalProofsCount += receiveResult.receivedProofsCount
|
||||
@@ -689,6 +670,36 @@ const swapByDenominationTask = async function (denomination: number): Promise<Wa
|
||||
errors.push(receiveResult.error.message)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const proofsAmount = CashuUtils.getProofsAmount(proofsToOptimize)
|
||||
|
||||
const sendResult = await sendTask(
|
||||
mintBalance!,
|
||||
proofsAmount,
|
||||
unit,
|
||||
`Optimize denomination ${denomination}`,
|
||||
proofsToOptimize,
|
||||
)
|
||||
|
||||
const encodedTokenToReceive: string = sendResult.transaction?.outputToken ?? ''
|
||||
const tokenToReceive = getDecodedToken(encodedTokenToReceive, mint.keysetIds)
|
||||
const tokenAmount = CashuUtils.getProofsAmount(tokenToReceive.proofs)
|
||||
|
||||
const receiveResult = await receiveBatchTask(
|
||||
tokenToReceive,
|
||||
tokenAmount,
|
||||
tokenToReceive.memo as string,
|
||||
encodedTokenToReceive,
|
||||
)
|
||||
|
||||
await syncStateWithMintTask({proofsToSync: proofsToOptimize, mintUrl: mint.mintUrl, proofState: 'PENDING'})
|
||||
|
||||
if (receiveResult.receivedProofsCount && receiveResult.receivedProofsCount > 0) {
|
||||
finalProofsCount += receiveResult.receivedProofsCount
|
||||
}
|
||||
if (receiveResult.error) {
|
||||
errors.push(receiveResult.error.message)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -709,11 +720,14 @@ const swapAllQueue = async function (): Promise<void> {
|
||||
)
|
||||
}
|
||||
|
||||
const swapByDenominationQueue = async function (denomination: number): Promise<void> {
|
||||
const swapByDenominationQueue = async function (
|
||||
denomination: number,
|
||||
mintUrl: string,
|
||||
): Promise<void> {
|
||||
const now = new Date().getTime()
|
||||
return SyncQueue.addPrioritizedTask(
|
||||
`swapDenominationTask-${denomination}-${now}`,
|
||||
async () => await swapByDenominationTask(denomination),
|
||||
async () => await swapByDenominationTask(denomination, mintUrl),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ type WalletTaskService = {
|
||||
draftTransactionId?: number,
|
||||
) => Promise<TransactionTaskResult>
|
||||
swapAllQueue: () => Promise<void>
|
||||
swapByDenominationQueue: (denomination: number) => Promise<void>
|
||||
swapByDenominationQueue: (denomination: number, mintUrl: string) => Promise<void>
|
||||
topupQueueAwaitable: (
|
||||
mintBalanceToTopup: MintBalance,
|
||||
amountToTopup: number,
|
||||
|
||||
Reference in New Issue
Block a user