From 104d0e2ca2172fae8df55b491257a750a38d5998 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 1 Sep 2025 23:37:24 -0700 Subject: [PATCH 01/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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/11] 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 88ff5c8d7838989b7855b6157672eaefe43297b2 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 10 Sep 2025 15:37:21 -0700 Subject: [PATCH 10/11] feat: state cards only valid in US --- .../exploredash/ui/ctxspend/CTXSpendViewModel.kt | 3 +-- .../ui/ctxspend/dialogs/GiftCardDetailsViewModel.kt | 2 +- .../src/main/res/layout/item_details_view.xml | 13 +++++++++++++ .../src/main/res/values/strings-explore-dash.xml | 1 + 4 files changed, 16 insertions(+), 3 deletions(-) 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 fbca880011..0798e08379 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 @@ -226,8 +226,7 @@ class CTXSpendViewModel @Inject constructor( merchant.savingsPercentage = this.savingsPercentage merchant.minCardPurchase = this.minimumCardPurchase merchant.maxCardPurchase = this.maximumCardPurchase - // TODO: re-enable fixed denoms - merchant.active = this.enabled || this.denominationType == DenominationType.Fixed + merchant.active = this.enabled merchant.fixedDenomination = this.denominationType == DenominationType.Fixed merchant.denominations = this.denominations.map { it.toInt() } } 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..b790e8c345 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 @@ -191,7 +191,7 @@ class GiftCardDetailsViewModel @Inject constructor( val timeElapsed = (System.currentTimeMillis() - startPurchaseTime).toDouble() / 1000 if (BuildConfig.DEBUG) { log.info( - "event:process_gift_card_purchase: {} ms", + "event:process_gift_card_purchase: {} s", timeElapsed ) } diff --git a/features/exploredash/src/main/res/layout/item_details_view.xml b/features/exploredash/src/main/res/layout/item_details_view.xml index e9cc774763..5435cf8578 100644 --- a/features/exploredash/src/main/res/layout/item_details_view.xml +++ b/features/exploredash/src/main/res/layout/item_details_view.xml @@ -267,6 +267,19 @@ app:layout_constraintBottom_toTopOf="@id/pay_btn" /> + + Create a DashSpend account or log in to the existing one https://ctx.com/gift-card-agreement/ Temporarily unavailable + This card works only in the United States Your session expired It looks like you haven’t used DashSpend in a while. For security reasons, you’ve been logged out.\n\nPlease sign in again to continue exploring where to spend your Dash. \ No newline at end of file From 791d356036688974d0d5efe67a70da05987fb71e Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Mon, 22 Sep 2025 17:50:56 -0700 Subject: [PATCH 11/11] style: ktlint --- .../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 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),