@@ -100,6 +100,7 @@ class TransferViewModel @Inject constructor(
100100 fun setTransferEffect (effect : TransferEffect ) = viewModelScope.launch { transferEffects.emit(effect) }
101101 var maxLspFee = 0uL
102102 private var hwTransferSignJob: Job ? = null
103+ private var hwFeeEstimateJob: Job ? = null
103104 private var pendingHwFundingBroadcast: PendingHwFundingBroadcast ? = null
104105 private var activeHwTransferDeviceId: String? = null
105106
@@ -193,11 +194,14 @@ class TransferViewModel @Inject constructor(
193194 spendingBalanceSats = oldOrder.clientBalanceSat,
194195 receivingBalanceSats = receivingAmountSats.toULong(),
195196 ).getOrThrow()
197+ hwFeeEstimateJob?.cancel()
198+ hwFeeEstimateJob = null
196199 _spendingUiState .update {
197200 it.copy(
198201 order = newOrder,
199202 defaultOrder = oldOrder,
200203 isAdvanced = true ,
204+ hwMiningFeeSats = 0uL ,
201205 )
202206 }
203207 setTransferEffect(TransferEffect .OnOrderCreated )
@@ -393,12 +397,15 @@ class TransferViewModel @Inject constructor(
393397 private suspend fun onOrderCreated (order : IBtOrder ) {
394398 settingsStore.update { it.copy(lightningSetupStep = 0 ) }
395399 pendingHwFundingBroadcast = null
400+ hwFeeEstimateJob?.cancel()
401+ hwFeeEstimateJob = null
396402 _spendingUiState .update {
397403 it.copy(
398404 order = order,
399405 isAdvanced = false ,
400406 defaultOrder = null ,
401407 hasPendingHwBroadcast = false ,
408+ hwMiningFeeSats = 0uL ,
402409 )
403410 }
404411 setTransferEffect(TransferEffect .OnOrderCreated )
@@ -497,18 +504,23 @@ class TransferViewModel @Inject constructor(
497504
498505 fun onUseDefaultLspBalanceClick () {
499506 val defaultOrder = _spendingUiState .value.defaultOrder
507+ hwFeeEstimateJob?.cancel()
508+ hwFeeEstimateJob = null
500509 _spendingUiState .update {
501510 it.copy(
502511 order = defaultOrder,
503512 defaultOrder = null ,
504513 isAdvanced = false ,
514+ hwMiningFeeSats = 0uL ,
505515 )
506516 }
507517 }
508518
509519 fun resetSpendingState () {
510520 hwTransferSignJob?.cancel()
511521 hwTransferSignJob = null
522+ hwFeeEstimateJob?.cancel()
523+ hwFeeEstimateJob = null
512524 pendingHwFundingBroadcast = null
513525 activeHwTransferDeviceId = null
514526 _spendingUiState .update { TransferToSpendingUiState () }
@@ -520,6 +532,8 @@ class TransferViewModel @Inject constructor(
520532 val deviceId = activeHwTransferDeviceId
521533 hwTransferSignJob?.cancel()
522534 hwTransferSignJob = null
535+ hwFeeEstimateJob?.cancel()
536+ hwFeeEstimateJob = null
523537 _spendingUiState .update { it.copy(isSigning = false ) }
524538 if (deviceId != null ) {
525539 viewModelScope.launch {
@@ -564,6 +578,41 @@ class TransferViewModel @Inject constructor(
564578 hwWalletRepo.warmUpKnownDevice(deviceId)
565579 }
566580
581+ /* * Best-effort offline mining-fee estimate for the Sign screen (xpub compose, no device session). */
582+ fun updateHwFundingFeeEstimate (order : IBtOrder , deviceId : String ) {
583+ hwFeeEstimateJob?.cancel()
584+ hwFeeEstimateJob = viewModelScope.launch {
585+ if (_spendingUiState .value.hasPendingHwBroadcast) return @launch
586+ val address = order.payment?.onchain?.address.orEmpty()
587+ if (address.isEmpty()) return @launch
588+ val orderId = order.id
589+
590+ runSuspendCatching {
591+ val satsPerVByte = hwFundingSatsPerVByte()
592+ hwWalletRepo.composeFundingTransaction(
593+ deviceId = deviceId,
594+ address = address,
595+ sats = order.feeSat,
596+ satsPerVByte = satsPerVByte,
597+ ).getOrThrow().miningFeeSats
598+ }.onSuccess { miningFeeSats ->
599+ _spendingUiState .update { state ->
600+ val activeOrderId = state.order?.id
601+ if ((activeOrderId != null && activeOrderId != orderId) || state.hasPendingHwBroadcast) {
602+ state
603+ } else {
604+ state.copy(hwMiningFeeSats = miningFeeSats)
605+ }
606+ }
607+ }.onFailure {
608+ Logger .debug(
609+ " Skipped offline hardware funding fee estimate for '$deviceId '" ,
610+ context = TAG ,
611+ )
612+ }
613+ }
614+ }
615+
567616 fun onTransferToSpendingHwConfirm (order : IBtOrder , deviceId : String ) {
568617 if (hwTransferSignJob?.isActive == true ) return
569618
@@ -614,6 +663,9 @@ class TransferViewModel @Inject constructor(
614663 val signedTx = pendingHwFundingBroadcast
615664 ?.takeIf { it.matches(order, deviceId, address) }
616665 ?.signedTx
666+ ?.also { pending ->
667+ _spendingUiState .update { state -> state.copy(hwMiningFeeSats = pending.miningFeeSats) }
668+ }
617669 ? : prepareSignedHardwareFunding(order, deviceId, address).also {
618670 pendingHwFundingBroadcast = PendingHwFundingBroadcast (
619671 orderId = order.id,
@@ -622,7 +674,12 @@ class TransferViewModel @Inject constructor(
622674 amountSats = order.feeSat,
623675 signedTx = it,
624676 )
625- _spendingUiState .update { state -> state.copy(hasPendingHwBroadcast = true ) }
677+ _spendingUiState .update { state ->
678+ state.copy(
679+ hasPendingHwBroadcast = true ,
680+ hwMiningFeeSats = it.miningFeeSats,
681+ )
682+ }
626683 }
627684 broadcastHardwareFunding(signedTx)
628685 }
@@ -645,6 +702,7 @@ class TransferViewModel @Inject constructor(
645702 sats = order.feeSat,
646703 satsPerVByte = satsPerVByte,
647704 )
705+ _spendingUiState .update { it.copy(hwMiningFeeSats = funding.miningFeeSats) }
648706 return signHardwareFunding(deviceId, funding)
649707 }
650708
@@ -1100,6 +1158,7 @@ data class TransferToSpendingUiState(
11001158 val isLoading : Boolean = false ,
11011159 val isSigning : Boolean = false ,
11021160 val hasPendingHwBroadcast : Boolean = false ,
1161+ val hwMiningFeeSats : ULong = 0uL ,
11031162 val receivingAmount : Long = 0 ,
11041163 val feeEstimate : Long? = null ,
11051164)
0 commit comments