From 104d0e2ca2172fae8df55b491257a750a38d5998 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 1 Sep 2025 23:37:24 -0700 Subject: [PATCH 01/23] fix: if replaying, prevent giftcard purchase --- common/src/main/res/values/strings.xml | 1 + .../ui/ctxspend/CTXSpendViewModel.kt | 13 ++++++++- .../ui/ctxspend/PurchaseGiftCardFragment.kt | 28 +++++++++++++++++-- wallet/res/values/strings.xml | 1 - 4 files changed, 38 insertions(+), 5 deletions(-) diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml index 99e24f0d72..b2533b8565 100644 --- a/common/src/main/res/values/strings.xml +++ b/common/src/main/res/values/strings.xml @@ -99,6 +99,7 @@ Payment error Your payment could not be processed by the server, please inquire with the merchant Could not find exchange rate. + Currently payments are not possible because the wallet is not fully synced with the network Receive directly into Dash Wallet diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt index a9c06c25d4..d24d369e97 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt @@ -29,6 +29,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch @@ -73,7 +74,8 @@ class CTXSpendViewModel @Inject constructor( private val analytics: AnalyticsService, private val savedStateHandle: SavedStateHandle, private val exploreDao: MerchantDao, - private val ctxSpendConfig: CTXSpendConfig + private val ctxSpendConfig: CTXSpendConfig, + private val blockchainStateProvider: BlockchainStateProvider ) : ViewModel() { companion object { @@ -128,6 +130,8 @@ class CTXSpendViewModel @Inject constructor( var maxCardPurchaseFiat: Fiat = Fiat.valueOf(Constants.USD_CURRENCY, 0) var openedCTXSpendTermsAndConditions = false + private val _isBlockchainReplaying = MutableStateFlow(false) + val isBlockchainReplaying = _isBlockchainReplaying.asStateFlow() init { exchangeRates @@ -145,6 +149,13 @@ class CTXSpendViewModel @Inject constructor( _balance.observeForever { coin -> savedStateHandle[BALANCE_KEY] = coin?.value } + + blockchainStateProvider.observeState() + .filterNotNull() + .onEach { state -> + _isBlockchainReplaying.value = state.replaying + } + .launchIn(viewModelScope) } suspend fun purchaseGiftCard(): GiftCardResponse { diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt index 160ed08778..c881d7bdae 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt @@ -151,6 +151,10 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi enterAmountFragment?.handleNetworkState(isConnected) } + viewModel.isBlockchainReplaying.observe(viewLifecycleOwner) { + updateView() + } + viewLifecycleOwner.observeOnDestroy { viewModel.resetSelectedDenomination() } @@ -178,6 +182,7 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi binding.enterAmountFragmentPlaceholder.isVisible = true binding.composeContainer.isVisible = false binding.fixedDenomText.isVisible = false + updateView() } private fun setupMerchantDenominations() { @@ -221,13 +226,15 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi val amountFiat = enterAmountViewModel.fiatAmount.value amountFiat?.let { + val isBlockchainReplaying = viewModel.isBlockchainReplaying.value if (!viewModel.withinLimits(amountFiat)) { binding.minValue.text = getString(R.string.purchase_gift_card_min, viewModel.minCardPurchaseFiat.toFormattedString()) binding.maxValue.text = getString(R.string.purchase_gift_card_max, viewModel.maxCardPurchaseFiat.toFormattedString()) - binding.minValue.isVisible = true - binding.maxValue.isVisible = true + // don't show min/max values if blockchain is replaying + binding.minValue.isVisible = !isBlockchainReplaying + binding.maxValue.isVisible = !isBlockchainReplaying binding.discountValue.isVisible = false return } @@ -258,7 +265,8 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi return } - binding.discountValue.isVisible = true + // only show the discount + binding.discountValue.isVisible = !viewModel.isBlockchainReplaying.value val selectedRate = viewModel.usdExchangeRate.value if (selectedRate == null) { @@ -405,4 +413,18 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi ) } } + + // taken from SendCoinsFragment.updateView + private fun updateView() { + val isReplaying = viewModel.isBlockchainReplaying.value + val errorMessage = if (isReplaying) { + getString(R.string.send_coins_fragment_hint_replaying) + } else { + "" + } + + enterAmountFragment?.setError(errorMessage) + enterAmountViewModel.blockContinue = errorMessage.isNotEmpty() || + viewModel.isBlockchainReplaying.value + } } diff --git a/wallet/res/values/strings.xml b/wallet/res/values/strings.xml index c5be0ab420..4a2765594c 100644 --- a/wallet/res/values/strings.xml +++ b/wallet/res/values/strings.xml @@ -61,7 +61,6 @@ A network fee of %s will be paid. A priority fee of %s will be paid. If you care about low fees, use \'priority\' only if you need confirmation as soon as possible. The amount of tiny payments in your wallet doesn\'t add up to a sendable value. - Currently payments are not possible because the wallet is not fully synced with the network Send payment directly to the payee. Your payment was successfully sent directly. Your payment was rejected via direct connection. From 0481815664130ff1dce6fb62bcdd75930404fe4f Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 1 Sep 2025 23:38:30 -0700 Subject: [PATCH 02/23] fix: if replaying, prevent giftcard purchase --- .../ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt index e7470bd574..82df4aed20 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt @@ -112,12 +112,12 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi return@launch } + showLoading() if (authManager.authenticate(requireActivity()) == null) { + hideLoading() return@launch } - showLoading() - val data = try { viewModel.purchaseGiftCard() } catch (ex: CTXSpendException) { From 7849c7d40063477e55bcd8f89c85f49dcf914c5d Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 2 Sep 2025 17:57:25 -0700 Subject: [PATCH 03/23] fix: log purchase errors with crashlytics, other DashSpend fixes --- .../repository/CTXSpendRepository.kt | 4 ++ .../ui/ctxspend/CTXSpendViewModel.kt | 4 ++ .../ctxspend/dialogs/GiftCardDetailsDialog.kt | 5 +- .../dialogs/GiftCardDetailsViewModel.kt | 1 + .../dialogs/PurchaseGiftCardConfirmDialog.kt | 2 + .../wallet/service/BlockchainServiceImpl.kt | 52 +++++++++++++++---- 6 files changed, 56 insertions(+), 12 deletions(-) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt index 2f6ddbd5ec..e255b21c42 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/repository/CTXSpendRepository.kt @@ -69,6 +69,10 @@ class CTXSpendException( } } + override fun toString(): String { + return "CTX error: $message\n $giftCardResponse\n $errorCode: $errorBody" + } + val isLimitError: Boolean get() { val fiatAmount = ((errorMap["fields"] as? Map<*, *>)?.get("fiatAmount") as? List<*>)?.firstOrNull() diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt index d24d369e97..fbca880011 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/CTXSpendViewModel.kt @@ -425,4 +425,8 @@ class CTXSpendViewModel @Inject constructor( suspend fun getMerchantById(merchantId: String): Merchant? = withContext(Dispatchers.IO) { exploreDao.getMerchantById(merchantId) } + + fun logError(ctxSpendException: Throwable, message: String) { + analytics.logError(ctxSpendException, message) + } } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt index 8756717792..a192fcad44 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt @@ -158,7 +158,7 @@ class GiftCardDetailsDialog : OffsetDialogFragment(R.layout.dialog_gift_card_det val error = state.error val shouldShowError = when (state.status) { - "unpaid", "paid" -> state.queries > 5 + "unpaid", "paid" -> state.queries > 10 "rejected" -> true "fulfilled" -> false else -> false @@ -176,6 +176,9 @@ class GiftCardDetailsDialog : OffsetDialogFragment(R.layout.dialog_gift_card_det binding.cardError.isVisible = true binding.cardError.text = message ?: getString(R.string.gift_card_details_error) binding.contactSupport.isVisible = true // force visible, thought it may be visible based on status + if (state.queries == 10) { + ctxSpendViewModel.logError(state.error, "CTX did not deliver the card after 10 tries") + } } else { binding.cardError.isVisible = false binding.contactSupport.isVisible = false diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt index 9d3e73857d..9ae97eab9c 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt @@ -226,6 +226,7 @@ class GiftCardDetailsViewModel @Inject constructor( "rejected" -> { // TODO: handle log.error("CTXSpend returned error: rejected") + analyticsService.logError(CTXSpendException("CTXSpend returned error: rejected", giftCard, ""),"CTX returned error: rejected ${giftCard.merchantName} for ${giftCard.fiatAmount} ${giftCard.fiatCurrency}") _uiState.update { it.copy( error = CTXSpendException( diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt index 82df4aed20..6e478b2a12 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt @@ -146,6 +146,7 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi } } ex.errorCode == 400 && ex.isLimitError -> { + viewModel.logError(ex,"CTX returned error: limits") AdaptiveDialog.create( R.drawable.ic_error, getString(R.string.gift_card_purchase_failed), @@ -169,6 +170,7 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi } } ex.errorCode == 500 -> { + viewModel.logError(ex,"CTX returned error: Error 500") AdaptiveDialog.create( R.drawable.ic_error, getString(R.string.gift_card_purchase_failed), diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt index 0672de46d1..ef96899ad0 100644 --- a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt +++ b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt @@ -249,6 +249,7 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { private var balance = Coin.ZERO private var mixedBalance = Coin.ZERO private var foregroundService = ForegroundService.NONE + private var pendingForegroundNotification: Notification? = null // Risk Analyser for Transactions that is PeerGroup Aware private var riskAnalyzer: AllowLockTimeRiskAnalysis.Analyzer? = null @@ -1207,7 +1208,9 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { log.info(".onStartCommand($intent)") super.onStartCommand(intent, flags, startId) serviceScope.launch { + log.info("onStartCommand waiting for onCreate to complete...") onCreateCompleted.await() // wait until onCreate is finished + log.info("onCreate completed, processing onStartCommand") if (intent != null) { propagateContext() //Restart service as a Foreground Service if it's synchronizing the blockchain @@ -1237,20 +1240,27 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { } else if (BlockchainService.ACTION_BROADCAST_TRANSACTION == action) { val hash = Sha256Hash .wrap(intent.getByteArrayExtra(BlockchainService.ACTION_BROADCAST_TRANSACTION_HASH)) + log.info("broadcast transaction requested for hash: {}", hash) val wallet = application.wallet if (wallet != null) { val tx = wallet.getTransaction(hash) - if (peerGroup != null) { - log.info("broadcasting transaction " + tx!!.hashAsString) - val count = peerGroup!!.numConnectedPeers() - var minimum = peerGroup!!.minBroadcastConnections - // if the number of peers is <= 3, then only require that number of peers to send - // if the number of peers is 0, then require 3 peers (default min connections) - if (count in 1..3) minimum = count - peerGroup!!.broadcastTransaction(tx, minimum, true) + if (tx != null) { + log.info("found transaction {} in wallet", tx.txId) + if (peerGroup != null) { + val count = peerGroup!!.numConnectedPeers() + log.info("broadcasting transaction {} with {} connected peers", tx.txId, count) + var minimum = peerGroup!!.minBroadcastConnections + // if the number of peers is <= 3, then only require that number of peers to send + // if the number of peers is 0, then require 3 peers (default min connections) + if (count in 1..3) minimum = count + peerGroup!!.broadcastTransaction(tx, minimum, true) + log.info("transaction {} broadcast initiated", tx.txId) + } else { + log.warn("peergroup not available, not broadcasting transaction {}", tx.txId) + tx.confidence.setPeerInfo(0, 1) + } } else { - log.info("peergroup not available, not broadcasting transaction {}", tx!!.txId) - tx.confidence.setPeerInfo(0, 1) + log.error("transaction {} not found in wallet", hash) } } else { log.error("wallet is null, cannot broadcast transaction") @@ -1290,13 +1300,33 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { try { startForeground(notification) } catch (e: ForegroundServiceStartNotAllowedException) { - log.info("failed to start in foreground", e) + log.info("failed to start in foreground, try again", e) + // On Android 15+, we'll retry later when the app is in foreground + // For now, continue running as a regular service + scheduleRetryForegroundService(notification) } } else { startForeground(notification) } } + private fun scheduleRetryForegroundService(notification: Notification) { + pendingForegroundNotification = notification + // Schedule a retry after a few seconds to see if the app comes to foreground + handler.postDelayed({ + if (pendingForegroundNotification != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + try { + startForeground(pendingForegroundNotification!!) + pendingForegroundNotification = null + log.info("Successfully started foreground service on retry") + } catch (e: ForegroundServiceStartNotAllowedException) { + log.info("Foreground service start still not allowed, will continue as background service") + pendingForegroundNotification = null + } + } + }, 5000) // Retry after 5 seconds + } + override fun onDestroy() { log.info(".onDestroy()") super.onDestroy() From eac339e3269fbf822564d0ef9035c400ad2a3f03 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 2 Sep 2025 17:57:57 -0700 Subject: [PATCH 04/23] fix: prevent crash from bad password --- .../de/schildbach/wallet/ui/dashpay/PlatformRepo.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt index a9fa1813f6..ea4e6cd041 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt @@ -191,7 +191,7 @@ class PlatformRepo @Inject constructor( null } // Don't bother with DeriveKeyTask here, just call deriveKey - walletApplication.wallet!!.keyCrypter!!.deriveKey(password) + password?.let { walletApplication.wallet!!.keyCrypter!!.deriveKey(it) } } else { null } @@ -1058,15 +1058,14 @@ class PlatformRepo @Inject constructor( val key = decryptedChain.getKey(index) Preconditions.checkState(key.path.last().isHardened) return key - } fun getIdentityFromPublicKeyId(): Identity? { - val encryptionKey = getWalletEncryptionKey() - val firstIdentityKey = getBlockchainIdentityKey(0, encryptionKey) ?: return null - return try { - platform.stateRepository.fetchIdentityFromPubKeyHash(firstIdentityKey.pubKeyHash) + getWalletEncryptionKey()?.let { + val firstIdentityKey = getBlockchainIdentityKey(0, it) ?: return null + platform.stateRepository.fetchIdentityFromPubKeyHash(firstIdentityKey.pubKeyHash) + } } catch (e: MaxRetriesReachedException) { null } catch (e: NoAvailableAddressesForRetryException) { From a8ed983987e2cf99a4ae3be7cd0809ea66d00bcf Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 7 Sep 2025 22:55:06 -0700 Subject: [PATCH 05/23] fix: remove obsolete file --- .../src/de/schildbach/wallet/service/BlockchainServiceImplOld.kt | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 wallet/src/de/schildbach/wallet/service/BlockchainServiceImplOld.kt diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImplOld.kt b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImplOld.kt deleted file mode 100644 index e69de29bb2..0000000000 From 06f39c1f811fca70f550c69969df194824a3fcd8 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 7 Sep 2025 22:56:31 -0700 Subject: [PATCH 06/23] fix: eliminate crash in PeerListFragment --- .../de/schildbach/wallet/ui/PeerListFragment.java | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/ui/PeerListFragment.java b/wallet/src/de/schildbach/wallet/ui/PeerListFragment.java index 576b877ae7..ff34d64456 100644 --- a/wallet/src/de/schildbach/wallet/ui/PeerListFragment.java +++ b/wallet/src/de/schildbach/wallet/ui/PeerListFragment.java @@ -69,6 +69,7 @@ public final class PeerListFragment extends Fragment { private LoaderManager loaderManager; private BlockchainService service; + private boolean serviceBound = false; private ViewAnimator viewGroup; private RecyclerView recyclerView; @@ -97,7 +98,7 @@ public void onAttach(final Activity activity) { public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - activity.bindService(new Intent(activity, BlockchainServiceImpl.class), serviceConnection, + serviceBound = activity.bindService(new Intent(activity, BlockchainServiceImpl.class), serviceConnection, Context.BIND_AUTO_CREATE); } @@ -163,7 +164,14 @@ public void onPause() { @Override public void onDestroy() { - activity.unbindService(serviceConnection); + if (serviceBound) { + try { + activity.unbindService(serviceConnection); + serviceBound = false; + } catch (IllegalArgumentException x) { + log.warn("service not registered: " + serviceConnection); + } + } loaderManager.destroyLoader(ID_REVERSE_DNS_LOADER); @@ -183,6 +191,7 @@ public void onServiceDisconnected(final ComponentName name) { loaderManager.destroyLoader(ID_PEER_LOADER); service = null; + serviceBound = false; } }; From fc5a692079bd6fc65dc4395b8a9d8f91344d2074 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 7 Sep 2025 22:57:09 -0700 Subject: [PATCH 07/23] fix: eliminate crash RequestUserNameViewModel.verify --- .../wallet/ui/username/voting/RequestUserNameViewModel.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt b/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt index fcad88bd95..f31a1f1942 100644 --- a/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/username/voting/RequestUserNameViewModel.kt @@ -343,14 +343,17 @@ class RequestUserNameViewModel @Inject constructor( withContext(Dispatchers.IO) { identityConfig.set(BlockchainIdentityConfig.REQUESTED_USERNAME_LINK, _requestedUserNameLink.value ?: "") identityConfig.get(IDENTITY_ID)?.let { identityId -> + // this may always return null because the request hasn't been added yet. val usernameRequest = usernameRequestDao.getRequest( UsernameRequest.getRequestId( identityId, requestedUserName!! ) ) - usernameRequest!!.link = _requestedUserNameLink.value - usernameRequestDao.update(usernameRequest) + usernameRequest?.let { request -> + request.link = _requestedUserNameLink.value + usernameRequestDao.update(usernameRequest) + } } } _uiState.update { From 3ae7b0ba3a77563063960a433fb8412a8b6bf895 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 7 Sep 2025 22:57:45 -0700 Subject: [PATCH 08/23] fix: remove some debug logs --- .../src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt index 46135f717c..175cec5ef6 100644 --- a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsFragment.kt @@ -32,7 +32,6 @@ import androidx.navigation.fragment.navArgs import dagger.hilt.android.AndroidEntryPoint import de.schildbach.wallet.database.entity.DashPayProfile import de.schildbach.wallet.integration.android.BitcoinIntegration -import de.schildbach.wallet.service.CoinJoinMode import de.schildbach.wallet.ui.dashpay.DashPayViewModel import de.schildbach.wallet.ui.LockScreenActivity import de.schildbach.wallet.ui.transactions.TransactionResultActivity @@ -173,7 +172,7 @@ open class SendCoinsFragment: Fragment(R.layout.send_coins_fragment) { lifecycleScope.launch { authenticateOrConfirm() } } } - private var debug = true + protected open fun updateView() { val isReplaying = viewModel.isBlockchainReplaying.value val dryRunException = viewModel.dryRunException @@ -197,10 +196,7 @@ open class SendCoinsFragment: Fragment(R.layout.send_coins_fragment) { !viewModel.everythingPlausible() || viewModel.dryRunSuccessful.value != true || viewModel.isBlockchainReplaying.value ?: false - if (viewModel.dryRunSuccessful.value != true && debug) { - AnrException(Thread.currentThread()).logProcessMap() - debug = false - } + log.info("enterAmountViewModel.blockContinue = {}, viewModel.dryRunSuccessful.value = {}", enterAmountViewModel.blockContinue, viewModel.dryRunSuccessful.value) enterAmountFragment?.setViewDetails( From 46386fa59ecc6b1acc4c833278a403a20436986a Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 7 Sep 2025 22:58:52 -0700 Subject: [PATCH 09/23] fix: eliminate potential deadlocks in create, destroy BlockchainServiceImpl --- .../wallet/service/BlockchainServiceImpl.kt | 238 +++++++++--------- 1 file changed, 125 insertions(+), 113 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt index ef96899ad0..bb97843cea 100644 --- a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt +++ b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt @@ -694,11 +694,13 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { serviceScope.launch { // make sure that onCreate is finished onCreateCompleted.await() + log.info("acquiring check() mutex") checkMutex.lock() try { checkService() } finally { checkMutex.unlock() + log.info("releasing check() mutex") } } } @@ -1046,123 +1048,131 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { startForegroundAndCatch(createNetworkSyncNotification()) } serviceScope.launch { - cleanupDeferred?.await() - propagateContext() - val wallet = application.wallet - if (wallet == null) { - log.error("onCreate: wallet is null after cleanup, service cannot continue") - return@launch - } - peerConnectivityListener = PeerConnectivityListener() - broadcastPeerState(0) - blockChainFile = - File(getDir("blockstore", MODE_PRIVATE), Constants.Files.BLOCKCHAIN_FILENAME) - val blockChainFileExists = blockChainFile!!.exists() - headerChainFile = File(getDir("blockstore", MODE_PRIVATE), Constants.Files.HEADERS_FILENAME) - mnlistinfoBootStrapStream = loadStream(Constants.Files.MNLIST_BOOTSTRAP_FILENAME) - qrinfoBootStrapStream = loadStream(Constants.Files.QRINFO_BOOTSTRAP_FILENAME) - if (!blockChainFileExists) { - log.info("blockchain does not exist, resetting wallet") - propagateContext() - wallet.reset() - resetMNLists(false) - resetMNListsOnPeerGroupStart = true - } try { - blockStore = SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile) - blockStore?.chainHead // detect corruptions as early as possible - headerStore = SPVBlockStore(Constants.NETWORK_PARAMETERS, headerChainFile) - headerStore?.chainHead // detect corruptions as early as possible - withContext(Dispatchers.Main) { verifyBlockStores() } - val earliestKeyCreationTime = wallet.earliestKeyCreationTime - if (!blockChainFileExists && earliestKeyCreationTime > 0) { - try { - val watch = Stopwatch.createStarted() - var checkpointsInputStream = assets.open(Constants.Files.CHECKPOINTS_FILENAME) - CheckpointManager.checkpoint( - Constants.NETWORK_PARAMETERS, checkpointsInputStream, blockStore, - earliestKeyCreationTime - ) - //the headerStore should be set to the most recent checkpoint - checkpointsInputStream = assets.open(Constants.Files.CHECKPOINTS_FILENAME) - CheckpointManager.checkpoint( - Constants.NETWORK_PARAMETERS, checkpointsInputStream, headerStore, - System.currentTimeMillis() / 1000 - ) - watch.stop() - log.info( - "checkpoints loaded from '{}', took {}", - Constants.Files.CHECKPOINTS_FILENAME, - watch - ) - } catch (x: IOException) { - log.error("problem reading checkpoints, continuing without", x) + log.info("onCreate() serviceScope waiting for cleanup {}", cleanupDeferred?.isActive) + cleanupDeferred?.await() + propagateContext() + val wallet = application.wallet + if (wallet == null) { + log.error("onCreate: wallet is null after cleanup, service cannot continue") + return@launch + } + peerConnectivityListener = PeerConnectivityListener() + broadcastPeerState(0) + blockChainFile = + File(getDir("blockstore", MODE_PRIVATE), Constants.Files.BLOCKCHAIN_FILENAME) + val blockChainFileExists = blockChainFile!!.exists() + headerChainFile = File(getDir("blockstore", MODE_PRIVATE), Constants.Files.HEADERS_FILENAME) + mnlistinfoBootStrapStream = loadStream(Constants.Files.MNLIST_BOOTSTRAP_FILENAME) + qrinfoBootStrapStream = loadStream(Constants.Files.QRINFO_BOOTSTRAP_FILENAME) + if (!blockChainFileExists) { + log.info("blockchain does not exist, resetting wallet") + propagateContext() + wallet.reset() + resetMNLists(false) + resetMNListsOnPeerGroupStart = true + } + try { + blockStore = SPVBlockStore(Constants.NETWORK_PARAMETERS, blockChainFile) + blockStore?.chainHead // detect corruptions as early as possible + headerStore = SPVBlockStore(Constants.NETWORK_PARAMETERS, headerChainFile) + headerStore?.chainHead // detect corruptions as early as possible + withContext(Dispatchers.Main) { verifyBlockStores() } + val earliestKeyCreationTime = wallet.earliestKeyCreationTime + if (!blockChainFileExists && earliestKeyCreationTime > 0) { + try { + val watch = Stopwatch.createStarted() + var checkpointsInputStream = assets.open(Constants.Files.CHECKPOINTS_FILENAME) + CheckpointManager.checkpoint( + Constants.NETWORK_PARAMETERS, checkpointsInputStream, blockStore, + earliestKeyCreationTime + ) + //the headerStore should be set to the most recent checkpoint + checkpointsInputStream = assets.open(Constants.Files.CHECKPOINTS_FILENAME) + CheckpointManager.checkpoint( + Constants.NETWORK_PARAMETERS, checkpointsInputStream, headerStore, + System.currentTimeMillis() / 1000 + ) + watch.stop() + log.info( + "checkpoints loaded from '{}', took {}", + Constants.Files.CHECKPOINTS_FILENAME, + watch + ) + } catch (x: IOException) { + log.error("problem reading checkpoints, continuing without", x) + } } + } catch (x: BlockStoreException) { + blockChainFile!!.delete() + headerChainFile!!.delete() + resetMNLists(false) + val msg = "blockstore cannot be created" + log.error(msg, x) + throw Error(msg, x) + } + try { + blockChain = BlockChain(Constants.NETWORK_PARAMETERS, wallet, blockStore) + headerChain = BlockChain(Constants.NETWORK_PARAMETERS, headerStore) + blockchainStateDataProvider.setBlockChain(blockChain) + } catch (x: BlockStoreException) { + throw Error("blockchain cannot be created", x) + } + // register receivers on the main thread + withContext(Dispatchers.Main) { + val intentFilter = IntentFilter() + intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW) + intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK) + registerReceiver(connectivityReceiver, intentFilter) // implicitly start PeerGroup + connectivityReceiverRegistered = true + log.info("receiver register: connectivityReceiver, {}", connectivityReceiver) + } + wallet.addCoinsReceivedEventListener( + Threading.SAME_THREAD, + walletEventListener + ) + wallet.addCoinsSentEventListener(Threading.SAME_THREAD, walletEventListener) + wallet.addChangeEventListener(Threading.SAME_THREAD, walletEventListener) + config.registerOnSharedPreferenceChangeListener(sharedPrefsChangeListener) + withContext(Dispatchers.Main) { + registerReceiver(tickReceiver, IntentFilter(Intent.ACTION_TIME_TICK)) + tickRecieverRegistered = true + log.info("receiver register: tickReceiver, {}", tickReceiver) + } + peerDiscoveryList.add(dnsDiscovery) + updateAppWidget() + blockchainStateDao.observeState().observe(this@BlockchainServiceImpl) { blockchainState -> + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) + } + apiConfirmationHandler = registerCrowdNodeConfirmedAddressFilter() + coinJoinService.observeMixingState().observe(this@BlockchainServiceImpl) { mixingStatus -> + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) + } + coinJoinService.observeMixingProgress().observe(this@BlockchainServiceImpl) { mixingProgress -> + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) } - } catch (x: BlockStoreException) { - blockChainFile!!.delete() - headerChainFile!!.delete() - resetMNLists(false) - val msg = "blockstore cannot be created" - log.error(msg, x) - throw Error(msg, x) - } - try { - blockChain = BlockChain(Constants.NETWORK_PARAMETERS, wallet, blockStore) - headerChain = BlockChain(Constants.NETWORK_PARAMETERS, headerStore) - blockchainStateDataProvider.setBlockChain(blockChain) - } catch (x: BlockStoreException) { - throw Error("blockchain cannot be created", x) - } - // register receivers on the main thread - withContext(Dispatchers.Main) { - val intentFilter = IntentFilter() - intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_LOW) - intentFilter.addAction(Intent.ACTION_DEVICE_STORAGE_OK) - registerReceiver(connectivityReceiver, intentFilter) // implicitly start PeerGroup - connectivityReceiverRegistered = true - log.info("receiver register: connectivityReceiver, {}", connectivityReceiver) - } - wallet.addCoinsReceivedEventListener( - Threading.SAME_THREAD, - walletEventListener - ) - wallet.addCoinsSentEventListener(Threading.SAME_THREAD, walletEventListener) - wallet.addChangeEventListener(Threading.SAME_THREAD, walletEventListener) - config.registerOnSharedPreferenceChangeListener(sharedPrefsChangeListener) - withContext(Dispatchers.Main) { - registerReceiver(tickReceiver, IntentFilter(Intent.ACTION_TIME_TICK)) - tickRecieverRegistered = true - log.info("receiver register: tickReceiver, {}", tickReceiver) - } - peerDiscoveryList.add(dnsDiscovery) - updateAppWidget() - blockchainStateDao.observeState().observe(this@BlockchainServiceImpl) { blockchainState -> - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) - } - apiConfirmationHandler = registerCrowdNodeConfirmedAddressFilter() - coinJoinService.observeMixingState().observe(this@BlockchainServiceImpl) { mixingStatus -> - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) - } - coinJoinService.observeMixingProgress().observe(this@BlockchainServiceImpl) { mixingProgress -> - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) - } - // we need the total wallet balance for the CoinJoin notification - application.observeTotalBalance().observe(this@BlockchainServiceImpl) { - balance = it - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) - } + // we need the total wallet balance for the CoinJoin notification + application.observeTotalBalance().observe(this@BlockchainServiceImpl) { + balance = it + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) + } - // we need the mixed balance for the CoinJoin notification - application.observeMixedBalance().observe(this@BlockchainServiceImpl) { - mixedBalance = it - handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) - } + // we need the mixed balance for the CoinJoin notification + application.observeMixedBalance().observe(this@BlockchainServiceImpl) { + mixedBalance = it + handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) + } - onCreateCompleted.complete(Unit) // Signal completion of onCreate - log.info(".onCreate() finished") + onCreateCompleted.complete(Unit) // Signal completion of onCreate + log.info(".onCreate() finished") + } finally { + log.error(".onCreate() failed") + if (onCreateCompleted.isActive) { + onCreateCompleted.complete(Unit) + } + } } } @@ -1312,7 +1322,7 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { private fun scheduleRetryForegroundService(notification: Notification) { pendingForegroundNotification = notification - // Schedule a retry after a few seconds to see if the app comes to foreground + // Schedule a retry after a few seconds to see if the app comes to foregrxound handler.postDelayed({ if (pendingForegroundNotification != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { try { @@ -1340,10 +1350,12 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { unregisterReceiver(connectivityReceiver) connectivityReceiverRegistered = false } - cleanupDeferred = CompletableDeferred() serviceScope.launch { try { + log.info("The onCreateCompleted is active: {}", onCreateCompleted.isActive) onCreateCompleted.await() // wait until onCreate is finished + log.info("The check() mutex is locked: {}", checkMutex.isLocked) + cleanupDeferred = CompletableDeferred() checkMutex.lock() WalletApplication.scheduleStartBlockchainService(this@BlockchainServiceImpl) //disconnect feature val wallet = application.wallet From 9ffbcf5ffde8ae56c18fbec10bf752c24259c9e7 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Fri, 12 Sep 2025 15:44:05 -0700 Subject: [PATCH 10/23] fix: fix crash when service is not bound on BlockListFragment --- .../src/de/schildbach/wallet/ui/BlockListFragment.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java b/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java index a56200dc9d..a316dafac9 100644 --- a/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java +++ b/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java @@ -81,6 +81,7 @@ public final class BlockListFragment extends Fragment implements BlockListAdapte private LoaderManager loaderManager; private BlockchainService service; + private boolean serviceIsBound = false; private ViewAnimator viewGroup; private RecyclerView recyclerView; @@ -111,7 +112,7 @@ public void onAttach(final Activity activity) { public void onActivityCreated(final Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - activity.bindService(new Intent(activity, BlockchainServiceImpl.class), serviceConnection, + serviceIsBound = activity.bindService(new Intent(activity, BlockchainServiceImpl.class), serviceConnection, Context.BIND_AUTO_CREATE); } @@ -169,7 +170,10 @@ public void onPause() { @Override public void onDestroy() { - activity.unbindService(serviceConnection); + if (serviceIsBound) { + activity.unbindService(serviceConnection); + serviceIsBound = false; + } super.onDestroy(); } @@ -209,6 +213,7 @@ public void onServiceDisconnected(final ComponentName name) { loaderManager.destroyLoader(ID_BLOCK_LOADER); service = null; + serviceIsBound = false; } }; From af924709e4e645ca710ddeee0ccb2d296ecebd99 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Fri, 12 Sep 2025 15:44:43 -0700 Subject: [PATCH 11/23] fix: use fitsSystemWindows on the Coinbase Result dialog --- .../coinbase/src/main/res/layout/dialog_coinbase_result.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/integrations/coinbase/src/main/res/layout/dialog_coinbase_result.xml b/integrations/coinbase/src/main/res/layout/dialog_coinbase_result.xml index 450116a8e7..3dc924ba4c 100644 --- a/integrations/coinbase/src/main/res/layout/dialog_coinbase_result.xml +++ b/integrations/coinbase/src/main/res/layout/dialog_coinbase_result.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" android:background="@color/background_secondary"> Date: Tue, 16 Sep 2025 07:59:34 -0700 Subject: [PATCH 12/23] style: ktlint --- .../exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt | 2 +- .../ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt | 5 ++++- .../ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt index c881d7bdae..2db244eb80 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt @@ -425,6 +425,6 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi enterAmountFragment?.setError(errorMessage) enterAmountViewModel.blockContinue = errorMessage.isNotEmpty() || - viewModel.isBlockchainReplaying.value + viewModel.isBlockchainReplaying.value } } diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt index 9ae97eab9c..0185a1e7ab 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt @@ -226,7 +226,10 @@ class GiftCardDetailsViewModel @Inject constructor( "rejected" -> { // TODO: handle log.error("CTXSpend returned error: rejected") - analyticsService.logError(CTXSpendException("CTXSpend returned error: rejected", giftCard, ""),"CTX returned error: rejected ${giftCard.merchantName} for ${giftCard.fiatAmount} ${giftCard.fiatCurrency}") + analyticsService.logError( + CTXSpendException("CTXSpend returned error: rejected", giftCard, ""), + "CTX returned error: rejected ${giftCard.merchantName} for ${giftCard.fiatAmount} ${giftCard.fiatCurrency}" + ) _uiState.update { it.copy( error = CTXSpendException( diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt index 6e478b2a12..f03ba6302c 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/PurchaseGiftCardConfirmDialog.kt @@ -146,7 +146,7 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi } } ex.errorCode == 400 && ex.isLimitError -> { - viewModel.logError(ex,"CTX returned error: limits") + viewModel.logError(ex, "CTX returned error: limits") AdaptiveDialog.create( R.drawable.ic_error, getString(R.string.gift_card_purchase_failed), @@ -170,7 +170,7 @@ class PurchaseGiftCardConfirmDialog : OffsetDialogFragment(R.layout.dialog_confi } } ex.errorCode == 500 -> { - viewModel.logError(ex,"CTX returned error: Error 500") + viewModel.logError(ex, "CTX returned error: Error 500") AdaptiveDialog.create( R.drawable.ic_error, getString(R.string.gift_card_purchase_failed), From 2d17c9907fde36c3a5febbd7b5b37262a2dedb1b Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Tue, 16 Sep 2025 08:00:01 -0700 Subject: [PATCH 13/23] fix: fit system windows on Username Registration Fragment --- wallet/res/layout/fragment_username_registration.xml | 1 + 1 file changed, 1 insertion(+) diff --git a/wallet/res/layout/fragment_username_registration.xml b/wallet/res/layout/fragment_username_registration.xml index 1e35a4558a..8442447901 100644 --- a/wallet/res/layout/fragment_username_registration.xml +++ b/wallet/res/layout/fragment_username_registration.xml @@ -4,6 +4,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" app:viewToHideWhenSoftKeyboardIsOpen="@id/header"> Date: Tue, 16 Sep 2025 10:18:32 -0700 Subject: [PATCH 14/23] style: ktlint --- .../ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt index 0185a1e7ab..7ec26de03f 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt @@ -228,7 +228,9 @@ class GiftCardDetailsViewModel @Inject constructor( log.error("CTXSpend returned error: rejected") analyticsService.logError( CTXSpendException("CTXSpend returned error: rejected", giftCard, ""), - "CTX returned error: rejected ${giftCard.merchantName} for ${giftCard.fiatAmount} ${giftCard.fiatCurrency}" + "CTX returned error: rejected ${ + giftCard.merchantName + } for ${giftCard.fiatAmount} ${giftCard.fiatCurrency}" ) _uiState.update { it.copy( From f634237ffda5919181ee44a573aa09e996d3fa7c Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 17 Sep 2025 08:33:49 -0700 Subject: [PATCH 15/23] fix: repair BIP21 support in EnterAmountFragment --- .../common/ui/enter_amount/EnterAmountFragment.kt | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt index fe806fe325..e6035cd7c5 100644 --- a/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt +++ b/common/src/main/java/org/dash/wallet/common/ui/enter_amount/EnterAmountFragment.kt @@ -130,9 +130,13 @@ class EnterAmountFragment : Fragment(R.layout.fragment_enter_amount) { if (args.containsKey(ARG_INITIAL_AMOUNT)) { val initialAmount = args.getSerializable(ARG_INITIAL_AMOUNT) - binding.amountView.input = when { - initialAmount is Coin && dashToFiat -> initialAmount.toPlainString() - initialAmount is Fiat && !dashToFiat -> initialAmount.toPlainString() + when { + initialAmount is Coin && dashToFiat -> { + viewModel._amount.value = initialAmount + } + initialAmount is Fiat && !dashToFiat -> { + viewModel._fiatAmount.value = initialAmount + } else -> throw IllegalArgumentException("dashToFiat argument and type of initialAmount do not match") } } @@ -232,6 +236,7 @@ class EnterAmountFragment : Fragment(R.layout.fragment_enter_amount) { binding.amountView.dashToFiat = currency.title == Constants.DASH_CURRENCY } } + setAmountFromSavedState() binding.maxButton.setOnClickListener { From 39ef5510f7094d4e66cbd6251bb6b1d902592b0f Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 17 Sep 2025 08:40:59 -0700 Subject: [PATCH 16/23] style: ktlint --- .../exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt index 7ec26de03f..87d6bd5a4e 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt @@ -229,7 +229,7 @@ class GiftCardDetailsViewModel @Inject constructor( analyticsService.logError( CTXSpendException("CTXSpend returned error: rejected", giftCard, ""), "CTX returned error: rejected ${ - giftCard.merchantName + giftCard.merchantName } for ${giftCard.fiatAmount} ${giftCard.fiatCurrency}" ) _uiState.update { From a91e41b8a2203723e4f13231042798dcaff872be Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 17 Sep 2025 08:52:59 -0700 Subject: [PATCH 17/23] fix: potential issues with replaying and fixed cards --- .../ui/ctxspend/PurchaseGiftCardFragment.kt | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt index 2db244eb80..67117a132c 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/PurchaseGiftCardFragment.kt @@ -382,6 +382,7 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi @Composable private fun DenominationsBottomContainer() { val merchant = viewModel.giftCardMerchant ?: return + val isReplaying = viewModel.isBlockchainReplaying.collectAsStateWithLifecycle() Box( modifier = Modifier.background( @@ -400,7 +401,7 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi denominations = merchant.denominations, currency = Currency.getInstance(Constants.USD_CURRENCY), selectedDenomination = selectedDenomination.value.toBigDecimal().toInt(), - canContinue = !exceedsBalance(), + canContinue = !exceedsBalance() && !isReplaying.value, onDenominationSelected = { denomination -> val fiat = Fiat.parseFiat(Constants.USD_CURRENCY, denomination.toString()) viewModel.setGiftCardPaymentValue(fiat) @@ -417,14 +418,9 @@ class PurchaseGiftCardFragment : Fragment(R.layout.fragment_purchase_ctxspend_gi // taken from SendCoinsFragment.updateView private fun updateView() { val isReplaying = viewModel.isBlockchainReplaying.value - val errorMessage = if (isReplaying) { - getString(R.string.send_coins_fragment_hint_replaying) - } else { - "" - } - - enterAmountFragment?.setError(errorMessage) - enterAmountViewModel.blockContinue = errorMessage.isNotEmpty() || - viewModel.isBlockchainReplaying.value + enterAmountFragment?.setError( + if (isReplaying) getString(R.string.send_coins_fragment_hint_replaying) else "" + ) + enterAmountViewModel.blockContinue = isReplaying } } From e192b3ebedd39314e6c876abaceca5c90e91a463 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 17 Sep 2025 19:42:19 -0700 Subject: [PATCH 18/23] fix: setReturns restore to show Platform Credits on topup details --- .../wallet/ui/transactions/TransactionResultViewBinder.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wallet/src/de/schildbach/wallet/ui/transactions/TransactionResultViewBinder.kt b/wallet/src/de/schildbach/wallet/ui/transactions/TransactionResultViewBinder.kt index a93e368c70..9245878766 100644 --- a/wallet/src/de/schildbach/wallet/ui/transactions/TransactionResultViewBinder.kt +++ b/wallet/src/de/schildbach/wallet/ui/transactions/TransactionResultViewBinder.kt @@ -161,7 +161,7 @@ class TransactionResultViewBinder( } else { setInputs(inputAddresses, inflater) setOutputs(outputAddresses, inflater) - //setReturns(outputAssetLocks, inflater, false) + setReturns(outputAssetLocks, inflater, false, true) } // For displaying purposes only From 42b8961d1aa575c97bc427d86a79ecf043b1aaaa Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Thu, 18 Sep 2025 21:55:57 -0700 Subject: [PATCH 19/23] fix: improve topup functionality --- .../service/platform/PlatformSyncService.kt | 8 ++- .../service/platform/TopUpRepository.kt | 58 ++++++++++++++++++- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt b/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt index 715becee3e..860ba2c68b 100644 --- a/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt +++ b/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt @@ -1397,9 +1397,13 @@ class PlatformSynchronizationService @Inject constructor( } } + private var hasCheckedTopups = false // only run once private suspend fun checkTopUps() { - platformRepo.getWalletEncryptionKey()?.let { - topUpRepository.checkTopUps(it) + if (!hasCheckedTopups) { + platformRepo.getWalletEncryptionKey()?.let { + topUpRepository.checkTopUps(it) + hasCheckedTopups = true + } } } } diff --git a/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt b/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt index 3e86bc1d68..8c435bb2ff 100644 --- a/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt +++ b/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt @@ -46,7 +46,10 @@ import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine import de.schildbach.wallet.ui.dashpay.CreateIdentityService import kotlinx.coroutines.flow.first +import kotlinx.coroutines.withTimeout +import kotlinx.coroutines.TimeoutCancellationException import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension +import java.util.concurrent.TimeUnit /** * contains topup related functions that are used by [CreateIdentityService] to create @@ -254,13 +257,59 @@ class TopUpRepositoryImpl @Inject constructor( ) ?: addTopUp(topUpTx.txId) Context.propagate(walletDataProvider.wallet!!.context) val confidence = topUpTx.getConfidence(walletDataProvider.wallet!!.context) + log.info("topup tx confidence: {}", confidence) val wasTxSent = confidence.isChainLocked || confidence.isTransactionLocked || + confidence.confidenceType == TransactionConfidence.ConfidenceType.BUILDING || confidence.numBroadcastPeers() > 0 + if (!wasTxSent) { sendTransaction(topUpTx) + } else { + // wait for IX Lock or mining if the TX has been sent + // the transaction was probably sent previously + if (confidence.numBroadcastPeers() > 0 && confidence.confidenceType == TransactionConfidence.ConfidenceType.PENDING) { + try { + withTimeout(TimeUnit.SECONDS.toMillis(30)) { + suspendCoroutine { continuation -> + val listener = object : TransactionConfidence.Listener { + override fun onConfidenceChanged(confidence: TransactionConfidence?, reason: TransactionConfidence.Listener.ChangeReason?) { + when (reason) { + TransactionConfidence.Listener.ChangeReason.IX_TYPE -> { + if (confidence!!.isTransactionLocked || confidence.ixType == TransactionConfidence.IXType.IX_REQUEST) { + log.info("topup: observe ISLock") + confidence.removeEventListener(this) + continuation.resumeWith(Result.success(Unit)) + } + } + TransactionConfidence.Listener.ChangeReason.DEPTH, + TransactionConfidence.Listener.ChangeReason.CHAIN_LOCKED -> { + if (confidence!!.confidenceType == TransactionConfidence.ConfidenceType.BUILDING || confidence.isChainLocked) { + log.info("topup: observe block or chainlock") + confidence.removeEventListener(this) + continuation.resumeWith(Result.success(Unit)) + } + } + else -> { /* ignore */ } + } + } + } + confidence.addEventListener(listener) + } + } + } catch (e: TimeoutCancellationException) { + // Timeout reached, continue with execution + log.info("topup, timeout waiting for islock, continue...") + } + } + } - log.info("topup tx sent: {}", topUpTx.txId) + val status = when (confidence.confidenceType) { + TransactionConfidence.ConfidenceType.BUILDING -> "mined in block ${confidence.appearedAtChainHeight}" + TransactionConfidence.ConfidenceType.PENDING -> "broadcast tx to ${confidence.numBroadcastPeers()} peers" + else -> confidence.toString() + } + log.info("topup tx sent: {}; tx = {}", status, topUpTx.txId) try { platformRepo.blockchainIdentity.topUp( topUpTx, @@ -275,6 +324,7 @@ class TopUpRepositoryImpl @Inject constructor( if (e.message?.let { regex.matches(it) || it.contains("Object already exists: state transition already in chain")} == true) { // the asset lock was already used topUpsDao.insert(topUp.copy(creditedAt = System.currentTimeMillis())) + log.info("topup success, already submitted: {}", topUpTx.txId) } else { throw e } @@ -305,10 +355,14 @@ class TopUpRepositoryImpl @Inject constructor( if (topUp == null) { topUpsDao.insert(TopUp(assetLockTx.txId, identity)) } - topUpIdentity(assetLockTx, platformRepo.getWalletEncryptionKey()!!) + try { + topUpIdentity(assetLockTx, platformRepo.getWalletEncryptionKey()!!) // TopupIdentityOperation(walletApplication) // .create(identity, assetLockTx.txId) // .enqueue() + } catch (e: Exception) { + // swallow + } } } checkedPreviousTopUps = true From 6a41a065b0f21253ff76bbab4453353597e583c4 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Thu, 18 Sep 2025 21:56:15 -0700 Subject: [PATCH 20/23] chore: update dpp to 2.0.1-SNAPSHOT --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 44e98654bb..7d0e1945e6 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { coroutinesVersion = '1.6.4' ok_http_version = '4.9.1' dashjVersion = '21.1.12-SNAPSHOT' - dppVersion = "2.0.0" + dppVersion = "2.0.1-SNAPSHOT" hiltVersion = '2.53' hiltCompilerVersion = '1.2.0' hiltWorkVersion = '1.2.0' From 9f8f0be798bf70ef2e8b5d7a9fc417fe74ed21d5 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sat, 20 Sep 2025 11:07:07 -0700 Subject: [PATCH 21/23] fix: eliminate crash if service is not bound --- wallet/src/de/schildbach/wallet/ui/BlockListFragment.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java b/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java index a316dafac9..6adaeef0b5 100644 --- a/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java +++ b/wallet/src/de/schildbach/wallet/ui/BlockListFragment.java @@ -171,7 +171,11 @@ public void onPause() { @Override public void onDestroy() { if (serviceIsBound) { - activity.unbindService(serviceConnection); + try { + activity.unbindService(serviceConnection); + } catch (IllegalArgumentException e) { + log.warn("Service not registered when unbinding", e); + } serviceIsBound = false; } From ec6c1cdaecee1af18518d8acd59c1ecb0c3a60aa Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 22 Sep 2025 22:19:43 -0700 Subject: [PATCH 22/23] fix: coderabbitai fixes --- .../schildbach/wallet/service/BlockchainServiceImpl.kt | 6 +++--- .../wallet/service/platform/TopUpRepository.kt | 10 +++++++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt index bb97843cea..df9f4c2c12 100644 --- a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt +++ b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.kt @@ -1165,10 +1165,10 @@ class BlockchainServiceImpl : LifecycleService(), BlockchainService { handleBlockchainStateNotification(blockchainState, mixingStatus, mixingProgress) } - onCreateCompleted.complete(Unit) // Signal completion of onCreate + onCreateCompleted.complete(Unit) log.info(".onCreate() finished") - } finally { - log.error(".onCreate() failed") + } catch (t: Throwable) { + log.error(".onCreate() failed", t) if (onCreateCompleted.isActive) { onCreateCompleted.complete(Unit) } diff --git a/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt b/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt index 8c435bb2ff..d7ec1694ed 100644 --- a/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt +++ b/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt @@ -44,6 +44,7 @@ import org.slf4j.LoggerFactory import javax.inject.Inject import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.suspendCancellableCoroutine import de.schildbach.wallet.ui.dashpay.CreateIdentityService import kotlinx.coroutines.flow.first import kotlinx.coroutines.withTimeout @@ -271,7 +272,7 @@ class TopUpRepositoryImpl @Inject constructor( if (confidence.numBroadcastPeers() > 0 && confidence.confidenceType == TransactionConfidence.ConfidenceType.PENDING) { try { withTimeout(TimeUnit.SECONDS.toMillis(30)) { - suspendCoroutine { continuation -> + suspendCancellableCoroutine { continuation -> val listener = object : TransactionConfidence.Listener { override fun onConfidenceChanged(confidence: TransactionConfidence?, reason: TransactionConfidence.Listener.ChangeReason?) { when (reason) { @@ -294,6 +295,13 @@ class TopUpRepositoryImpl @Inject constructor( } } } + + // Register cancellation handler to clean up listener + continuation.invokeOnCancellation { + confidence.removeEventListener(listener) + log.info("topup: listener removed due to cancellation") + } + confidence.addEventListener(listener) } } From 6d6f3aa88d465d9872bd9bb9d0af28a002155d41 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 22 Sep 2025 22:24:59 -0700 Subject: [PATCH 23/23] fix: explore bugs --- .../exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt | 2 +- .../dash/wallet/features/exploredash/ui/explore/ItemDetails.kt | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt index a192fcad44..1214f2fc1b 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/ctxspend/dialogs/GiftCardDetailsDialog.kt @@ -158,7 +158,7 @@ class GiftCardDetailsDialog : OffsetDialogFragment(R.layout.dialog_gift_card_det val error = state.error val shouldShowError = when (state.status) { - "unpaid", "paid" -> state.queries > 10 + "unpaid", "paid" -> state.queries >= 20 "rejected" -> true "fulfilled" -> false else -> false diff --git a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/explore/ItemDetails.kt b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/explore/ItemDetails.kt index 1cd741e11d..badebcf81c 100644 --- a/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/explore/ItemDetails.kt +++ b/features/exploredash/src/main/java/org/dash/wallet/features/exploredash/ui/explore/ItemDetails.kt @@ -216,6 +216,7 @@ class ItemDetails(context: Context, attrs: AttributeSet) : LinearLayout(context, payBtn.setOnClickListener { onSendDashClicked?.invoke(true) } payBtn.isEnabled = merchant.active ?: true temporaryUnavailableText.isVisible = merchant.active == false + countryAvailabilityText.isVisible = false } else if (merchant.source!!.lowercase() == ServiceName.CTXSpend.lowercase()) { payBtn.isVisible = true payBtnTxt.text = context.getText(R.string.explore_buy_gift_card) @@ -223,6 +224,7 @@ class ItemDetails(context: Context, attrs: AttributeSet) : LinearLayout(context, payBtn.setOnClickListener { onBuyGiftCardButtonClicked?.invoke() } payBtn.isEnabled = merchant.active ?: true temporaryUnavailableText.isVisible = merchant.active == false + countryAvailabilityText.isVisible = true } showAllBtn.setOnClickListener { onShowAllLocationsClicked?.invoke() }