From 8ba371ffb0e2ddf8663889f408c3d16c7c2c3480 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Sun, 7 Jan 2024 21:05:07 -0800 Subject: [PATCH] fix(dashpay): address various coinjoin issues (#1242) * fix: set account to 0 * fix: return change in credit funding tx's * chore: cleanup CoinJoinLevelViewModel * fix: attempt to improve updateBalance * fix(coinjoin): fix a few problems from refactoring dashj * build(dashpay): fix firebase app distribution --- .github/workflows/dashpay.yml | 2 +- .../schildbach/wallet/WalletApplication.java | 14 +++--- .../wallet/service/CoinJoinService.kt | 46 +++++++++++++++---- .../ui/coinjoin/CoinJoinLevelViewModel.kt | 4 +- .../wallet/ui/dashpay/PlatformRepo.kt | 11 ++++- 5 files changed, 55 insertions(+), 22 deletions(-) diff --git a/.github/workflows/dashpay.yml b/.github/workflows/dashpay.yml index de06754b05..35cd822f17 100644 --- a/.github/workflows/dashpay.yml +++ b/.github/workflows/dashpay.yml @@ -94,4 +94,4 @@ jobs: - name: Staging Build and Firebase Distribution if: github.event_name == 'push' - run: bundle exec fastlane build_distribute flavor:"staging" type:"release" storepass:"${{ secrets.SIGNING_STORE_PASS }}" versioncode:"${{ env.build_number }}" comment:"Up to date Dash Wallet TestNet build" appid:"1:1039839682638:android:3202b16d460a1a88" testgroup:"qa" SUPPORT_EMAIL:"${{ secrets.INTERNAL_SUPPORT_EMAIL }}" + run: bundle exec fastlane build_distribute flavor:"staging" type:"release" storepass:"${{ secrets.SIGNING_STORE_PASS }}" versioncode:"${{ env.build_number }}" comment:"Up to date Dash Wallet TestNet build" appid:"1:1039839682638:android:bbcfa8c9939ee993ea631f" testgroup:"qa" diff --git a/wallet/src/de/schildbach/wallet/WalletApplication.java b/wallet/src/de/schildbach/wallet/WalletApplication.java index ecb1fcb0f4..a11c05cd49 100644 --- a/wallet/src/de/schildbach/wallet/WalletApplication.java +++ b/wallet/src/de/schildbach/wallet/WalletApplication.java @@ -391,23 +391,23 @@ public void setWallet(Wallet newWallet) throws GeneralSecurityException, IOExcep authKeyTypes ); - authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY); - authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING); - authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_TOPUP); - authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING); + authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY); + authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_FUNDING); + authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.BLOCKCHAIN_IDENTITY_TOPUP); + authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.INVITATION_FUNDING); authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.MASTERNODE_OWNER); authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.MASTERNODE_VOTING); authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.MASTERNODE_OPERATOR); authenticationGroupExtension.freshKey(AuthenticationKeyChain.KeyChainType.MASTERNODE_PLATFORM_OPERATOR); - authenticationGroupExtension.setWallet(wallet); + authenticationGroupExtension.setWallet(wallet); + } + } } WalletEx walletEx = (WalletEx) wallet; if (walletEx.getCoinJoin() != null) { // this wallet is not encrypted yet walletEx.initializeCoinJoin(null, 0); - } - } } } diff --git a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt index c32e5da60a..8a6f2f6ace 100644 --- a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt +++ b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt @@ -205,12 +205,28 @@ class CoinJoinMixingService @Inject constructor( log.info("coinjoin: mixed balance: ${walletEx.coinJoinBalance.toFriendlyString()}") val anonBalance = walletEx.getAnonymizableBalance(false, false) log.info("coinjoin: anonymizable balance {}", anonBalance.toFriendlyString()) - + val hasPartiallyMixedCoins = (walletEx.denominatedBalance - walletEx.coinJoinBalance).isGreaterThan(Coin.ZERO) val hasAnonymizableBalance = anonBalance.isGreaterThan(CoinJoin.getSmallestDenomination()) - log.info("coinjoin: mixing can occur: $hasAnonymizableBalance") + + val canDenominate = if (this::clientManager.isInitialized) { + clientManager.doAutomaticDenominating(true) + } else { + // if the client manager is not running, just say canDenominate is true + // The first round of execution will determine if mixing can happen + log.info("coinjoin: client manager is not running, canDemoninate=true") + true + } + + val hasBalanceLeftToMix = when { + hasPartiallyMixedCoins -> true + hasAnonymizableBalance && canDenominate -> true + else -> false + } + + log.info("coinjoin: mixing can occur: $hasBalanceLeftToMix = balance: (${anonBalance.isGreaterThan(CoinJoin.getSmallestDenomination())} && canDenominate: $canDenominate) || partially-mixed: $hasPartiallyMixedCoins") updateState( config.getMode(), - hasAnonymizableBalance, + hasBalanceLeftToMix, networkStatus, isSynced, blockchainStateProvider.getBlockChain() @@ -226,12 +242,12 @@ class CoinJoinMixingService @Inject constructor( ) { updateMutex.lock() log.info( - "coinjoin-updateState: ${this.mode}, ${this.hasAnonymizableBalance}, ${this.networkStatus}, ${this.isSynced} ${blockChain != null}" + "coinjoin-old-state: ${this.mode}, ${this.hasAnonymizableBalance}, ${this.networkStatus}, synced: ${this.isSynced} ${blockChain != null}" ) try { setBlockchain(blockChain) log.info( - "coinjoin-updateState: $mode, $hasAnonymizableBalance, $networkStatus, $isSynced, ${blockChain != null}" + "coinjoin-new-state: $mode, $hasAnonymizableBalance, $networkStatus, synced: $isSynced, ${blockChain != null}" ) this.networkStatus = networkStatus this.hasAnonymizableBalance = hasAnonymizableBalance @@ -413,7 +429,10 @@ class CoinJoinMixingService @Inject constructor( MixingCompleteListener { _, statusList -> statusList?.let { for (status in it) { - if (status != PoolStatus.FINISHED && status != PoolStatus.ERR_NOT_ENOUGH_FUNDS && status != PoolStatus.ERR_NO_INPUTS) { + if (status != PoolStatus.FINISHED && + status != PoolStatus.ERR_NOT_ENOUGH_FUNDS && + status != PoolStatus.ERR_NO_INPUTS + ) { coroutineScope.launch { updateMixingState(MixingStatus.ERROR) } exception = Exception("Mixing stopped before completion ${status.name}") return@let @@ -451,6 +470,7 @@ class CoinJoinMixingService @Inject constructor( setRequestKeyParameter(requestKeyParameter) setRequestDecryptedKey(requestDecryptedKey) + start() } } @@ -464,6 +484,7 @@ class CoinJoinMixingService @Inject constructor( // run this on a different thread? val asyncStart = coroutineScope.async(Dispatchers.IO) { Context.propagate(walletDataProvider.wallet!!.context) + coinJoinManager?.initMasternodeGroup(blockChain) clientManager.doAutomaticDenominating() } val result = asyncStart.await() @@ -475,11 +496,10 @@ class CoinJoinMixingService @Inject constructor( } private fun stopMixing() { - log.info("Mixing process will stop...") if (coinJoinManager == null || !this::clientManager.isInitialized) { return } - + log.info("Mixing process will stop...") encryptionKey = null // if mixing is not complete, then tell the future we didn't finish yet @@ -489,6 +509,8 @@ class CoinJoinMixingService @Inject constructor( // remove all listeners mixingCompleteListeners.forEach { coinJoinManager?.removeMixingCompleteListener(it) } sessionCompleteListeners.forEach { coinJoinManager?.removeSessionCompleteListener(it) } + clientManager.stopMixing() + coinJoinManager?.stop() } private fun setBlockchain(blockChain: AbstractBlockChain?) { @@ -525,7 +547,13 @@ class CoinJoinMixingService @Inject constructor( val anonymizableBalance = wallet.getAnonymizableBalance(false, false) if (mixedBalance != Coin.ZERO && anonymizableBalance != Coin.ZERO) { val progress = mixedBalance.value * 100.0 / (mixedBalance.value + anonymizableBalance.value) - log.info("coinjoin: progress {}", progress) + log.info( + "coinjoin: progress {} = 100*{}/({} + {})", + progress, + mixedBalance.value, + mixedBalance.value, + anonymizableBalance.value + ) _progressFlow.emit(progress) } } diff --git a/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt b/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt index 58dce1e5c7..035dd91443 100644 --- a/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt @@ -22,7 +22,6 @@ import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.schildbach.wallet.data.CoinJoinConfig import de.schildbach.wallet.service.CoinJoinMode -import de.schildbach.wallet.service.CoinJoinService import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.filterNotNull @@ -35,7 +34,6 @@ import javax.inject.Inject @HiltViewModel open class CoinJoinLevelViewModel @Inject constructor( private val analytics: AnalyticsService, - private val coinJoinService: CoinJoinService, private val coinJoinConfig: CoinJoinConfig, private var networkState: NetworkStateInt ) : ViewModel() { @@ -43,7 +41,7 @@ open class CoinJoinLevelViewModel @Inject constructor( val isMixing: Boolean get() = _mixingMode.value != CoinJoinMode.NONE - val _mixingMode = MutableStateFlow(CoinJoinMode.NONE) + private val _mixingMode = MutableStateFlow(CoinJoinMode.NONE) val mixingMode: Flow get() = _mixingMode diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt index f88394d089..f66ec8b1a6 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt @@ -38,6 +38,7 @@ import de.schildbach.wallet.livedata.SeriousError import de.schildbach.wallet.livedata.SeriousErrorListener import de.schildbach.wallet.livedata.Status import de.schildbach.wallet.security.SecurityGuard +import de.schildbach.wallet.service.CoinJoinMode import de.schildbach.wallet.service.platform.PlatformService import io.grpc.StatusRuntimeException import kotlinx.coroutines.* @@ -84,7 +85,8 @@ class PlatformRepo @Inject constructor( val walletApplication: WalletApplication, val blockchainIdentityDataDao: BlockchainIdentityConfig, val appDatabase: AppDatabase, - val platform: PlatformService + val platform: PlatformService, + val coinJoinConfig: CoinJoinConfig ) { @EntryPoint @@ -1019,7 +1021,12 @@ class PlatformRepo @Inject constructor( // dashj Context does not work with coroutines well, so we need to call Context.propogate // in each suspend method that uses the dashj Context Context.propagate(walletApplication.wallet!!.context) - val cftx = blockchainIdentity.createInviteFundingTransaction(Constants.DASH_PAY_FEE, keyParameter, useCoinJoin = false, returnChange = true) + val cftx = blockchainIdentity.createInviteFundingTransaction( + Constants.DASH_PAY_FEE, + keyParameter, + useCoinJoin = coinJoinConfig.getMode() != CoinJoinMode.NONE, + returnChange = true + ) val invitation = Invitation(cftx.creditBurnIdentityIdentifier.toStringBase58(), cftx.txId, System.currentTimeMillis()) // update database