diff --git a/wallet/androidTest/de/schildbach/wallet/database/DatabaseMigrationTest.kt b/wallet/androidTest/de/schildbach/wallet/database/DatabaseMigrationTest.kt index 7a57460d5a..91c14446f2 100644 --- a/wallet/androidTest/de/schildbach/wallet/database/DatabaseMigrationTest.kt +++ b/wallet/androidTest/de/schildbach/wallet/database/DatabaseMigrationTest.kt @@ -58,7 +58,11 @@ open class DatabaseMigrationTest { private val service = ServiceName.CrowdNode } - private val migrations = arrayOf(AppDatabaseMigrations.migration11To17, AppDatabaseMigrations.migration17To18) + private val migrations = arrayOf( + AppDatabaseMigrations.migration11To12, + AppDatabaseMigrations.migration12To17, + AppDatabaseMigrations.migration17To18 + ) @Rule @JvmField diff --git a/wallet/res/values/strings.xml b/wallet/res/values/strings.xml index 198d7ea1d1..6e9e07caf7 100644 --- a/wallet/res/values/strings.xml +++ b/wallet/res/values/strings.xml @@ -506,8 +506,9 @@ Multiple hours Start Mixing Stop Mixing - Mixing · %1$s of %2$s + Mixing · %1$s of %2$s (%3$s) + Finished · %1$s of %2$s Are you sure you want to change the privacy level? Are you sure you want to stop mixing? - Any funds that have been mixed will be combined with your un mixed funds + Any funds that have been mixed will be combined with your unmixed funds diff --git a/wallet/schnapps/res/values/values.xml b/wallet/schnapps/res/values/values.xml index fa860071bb..cb1c1427c4 100644 --- a/wallet/schnapps/res/values/values.xml +++ b/wallet/schnapps/res/values/values.xml @@ -18,7 +18,7 @@ µtDASH, no decimal places - https://insight.bintang.networks.dash.org:3002/insight + https://insight.ouzo.networks.dash.org:3002/insight Insight Devnet Block Explorer diff --git a/wallet/src/de/schildbach/wallet/data/CoinJoinConfig.kt b/wallet/src/de/schildbach/wallet/data/CoinJoinConfig.kt index a890c97cf7..a8a0938941 100644 --- a/wallet/src/de/schildbach/wallet/data/CoinJoinConfig.kt +++ b/wallet/src/de/schildbach/wallet/data/CoinJoinConfig.kt @@ -21,6 +21,11 @@ import android.content.Context import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey +import androidx.datastore.preferences.core.stringPreferencesKey +import de.schildbach.wallet.service.CoinJoinMode +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.BaseConfig import javax.inject.Inject @@ -34,10 +39,23 @@ open class CoinJoinConfig @Inject constructor( ) : BaseConfig(context, PREFERENCES_NAME, walletDataProvider) { companion object { const val PREFERENCES_NAME = "coinjoin" + val COINJOIN_MODE = stringPreferencesKey("coinjoin_mode") val COINJOIN_ROUNDS = intPreferencesKey("coinjoin_rounds") val COINJOIN_SESSIONS = intPreferencesKey("coinjoin_sessions") val COINJOIN_MULTISESSION = booleanPreferencesKey("coinjoin_multisession") val COINJOIN_AMOUNT = longPreferencesKey("coinjoin_amount") val FIRST_TIME_INFO_SHOWN = booleanPreferencesKey("first_time_info_shown") } + + fun observeMode(): Flow { + return observe(COINJOIN_MODE).filterNotNull().map { mode -> CoinJoinMode.valueOf(mode!!) } + } + + suspend fun getMode(): CoinJoinMode { + return get(COINJOIN_MODE).let { CoinJoinMode.valueOf(it!!) } + } + + suspend fun setMode(mode: CoinJoinMode) { + set(COINJOIN_MODE, mode.toString()) + } } diff --git a/wallet/src/de/schildbach/wallet/database/entity/BlockchainIdentityData.kt b/wallet/src/de/schildbach/wallet/database/entity/BlockchainIdentityData.kt index fb8c98ab29..b0ef914b35 100644 --- a/wallet/src/de/schildbach/wallet/database/entity/BlockchainIdentityData.kt +++ b/wallet/src/de/schildbach/wallet/database/entity/BlockchainIdentityData.kt @@ -100,7 +100,6 @@ data class BlockchainIdentityData(var creationState: CreationState = CreationSta enum class CreationState { NONE, // this should always be the first value UPGRADING_WALLET, - MIXING_FUNDS, CREDIT_FUNDING_TX_CREATING, CREDIT_FUNDING_TX_SENDING, CREDIT_FUNDING_TX_SENT, diff --git a/wallet/src/de/schildbach/wallet/di/AppModule.kt b/wallet/src/de/schildbach/wallet/di/AppModule.kt index c70a94c81d..99333765e9 100644 --- a/wallet/src/de/schildbach/wallet/di/AppModule.kt +++ b/wallet/src/de/schildbach/wallet/di/AppModule.kt @@ -29,6 +29,7 @@ import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.CoinJoinConfig import de.schildbach.wallet.payments.ConfirmTransactionLauncher import de.schildbach.wallet.payments.SendCoinsTaskRunner import de.schildbach.wallet.security.SecurityFunctions @@ -99,9 +100,10 @@ abstract class AppModule { walletData: WalletDataProvider, walletApplication: WalletApplication, securityFunctions: SecurityFunctions, - packageInfoProvider: PackageInfoProvider + packageInfoProvider: PackageInfoProvider, + coinJoinConfig: CoinJoinConfig ): SendPaymentService { - val realService = SendCoinsTaskRunner(walletData, walletApplication, securityFunctions, packageInfoProvider) + val realService = SendCoinsTaskRunner(walletData, walletApplication, securityFunctions, packageInfoProvider, coinJoinConfig) return if (BuildConfig.FLAVOR.lowercase() == "prod") { realService diff --git a/wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt b/wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt index e115a141a3..849627d073 100644 --- a/wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt +++ b/wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt @@ -18,12 +18,17 @@ package de.schildbach.wallet.payments import androidx.annotation.VisibleForTesting import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.CoinJoinConfig import de.schildbach.wallet.data.PaymentIntent import de.schildbach.wallet.payments.parsers.PaymentIntentParser import de.schildbach.wallet.security.SecurityFunctions import de.schildbach.wallet.security.SecurityGuard +import de.schildbach.wallet.service.CoinJoinMode import de.schildbach.wallet.service.PackageInfoProvider import kotlinx.coroutines.* +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import okhttp3.CacheControl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaTypeOrNull @@ -34,7 +39,6 @@ import okio.IOException import org.bitcoin.protocols.payments.Protos import org.bitcoin.protocols.payments.Protos.Payment import org.bitcoinj.coinjoin.CoinJoinCoinSelector -import org.bitcoinj.coinjoin.UnmixedZeroConfCoinSelector import org.bitcoinj.core.* import org.bitcoinj.crypto.IKey import org.bitcoinj.crypto.KeyCrypterException @@ -57,12 +61,25 @@ class SendCoinsTaskRunner @Inject constructor( private val walletData: WalletDataProvider, private val walletApplication: WalletApplication, private val securityFunctions: SecurityFunctions, - private val packageInfoProvider: PackageInfoProvider + private val packageInfoProvider: PackageInfoProvider, + coinJoinConfig: CoinJoinConfig ) : SendPaymentService { companion object { private const val WALLET_EXCEPTION_MESSAGE = "this method can't be used before creating the wallet" private val log = LoggerFactory.getLogger(SendCoinsTaskRunner::class.java) } + private var coinJoinSend = false + private val coroutineScope = CoroutineScope(Dispatchers.IO) + + init { + coinJoinConfig + .observeMode() + .filterNotNull() + .onEach { mode -> + coinJoinSend = mode != CoinJoinMode.NONE + } + .launchIn(coroutineScope) + } @Throws(LeftoverBalanceException::class) override suspend fun sendCoins( @@ -233,12 +250,11 @@ class SendCoinsTaskRunner @Inject constructor( mayEditAmount: Boolean, paymentIntent: PaymentIntent, signInputs: Boolean, - forceEnsureMinRequiredFee: Boolean, - coinJoin: Boolean = false + forceEnsureMinRequiredFee: Boolean ): SendRequest { val wallet = walletData.wallet ?: throw RuntimeException(WALLET_EXCEPTION_MESSAGE) val sendRequest = paymentIntent.toSendRequest() - sendRequest.coinSelector = getCoinSelector(coinJoin) + sendRequest.coinSelector = getCoinSelector() sendRequest.useInstantSend = false sendRequest.feePerKb = Constants.ECONOMIC_FEE sendRequest.ensureMinRequiredFee = forceEnsureMinRequiredFee @@ -256,15 +272,14 @@ class SendCoinsTaskRunner @Inject constructor( amount: Coin, coinSelector: CoinSelector? = null, emptyWallet: Boolean = false, - forceMinFee: Boolean = true, - coinJoin: Boolean = false + forceMinFee: Boolean = true ): SendRequest { return SendRequest.to(address, amount).apply { this.feePerKb = Constants.ECONOMIC_FEE this.ensureMinRequiredFee = forceMinFee this.emptyWallet = emptyWallet - val selector = coinSelector ?: getCoinSelector(coinJoin) + val selector = coinSelector ?: getCoinSelector() this.coinSelector = selector if (selector is ByAddressCoinSelector) { @@ -273,10 +288,12 @@ class SendCoinsTaskRunner @Inject constructor( } } - private fun getCoinSelector(coinJoin: Boolean) = if (coinJoin) { + private fun getCoinSelector() = if (coinJoinSend) { + // mixed only CoinJoinCoinSelector(walletData.wallet) } else { - UnmixedZeroConfCoinSelector(walletData.wallet) + // collect all coins, mixed and unmixed + ZeroConfCoinSelector.get() } @Throws(LeftoverBalanceException::class) diff --git a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java index 834f9dcc32..af76b238e6 100644 --- a/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java +++ b/wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java @@ -81,7 +81,6 @@ import org.bitcoinj.utils.MonetaryFormat; import org.bitcoinj.utils.Threading; import org.bitcoinj.wallet.DefaultRiskAnalysis; -import org.bitcoinj.wallet.Protos; import org.bitcoinj.wallet.Wallet; import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension; import org.dash.wallet.common.Configuration; @@ -165,7 +164,7 @@ public class BlockchainServiceImpl extends LifecycleService implements Blockchai @Inject PackageInfoProvider packageInfoProvider; @Inject ConnectivityManager connectivityManager; @Inject BlockchainStateDataProvider blockchainStateDataProvider; - + @Inject CoinJoinService coinJoinService; // not used in this class, but we need to create it private BlockStore blockStore; private BlockStore headerStore; private File blockChainFile; diff --git a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt index 3bf758938a..1708288864 100644 --- a/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt +++ b/wallet/src/de/schildbach/wallet/service/CoinJoinService.kt @@ -17,20 +17,23 @@ package de.schildbach.wallet.service -import com.google.common.collect.Comparators.max -import de.schildbach.wallet.WalletApplication +import com.google.common.base.Stopwatch import de.schildbach.wallet.data.CoinJoinConfig +import de.schildbach.wallet.ui.dashpay.PlatformRepo import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asCoroutineDispatcher import kotlinx.coroutines.async +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.filterNot import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock +import org.bitcoinj.coinjoin.CoinJoin import org.bitcoinj.coinjoin.CoinJoinClientManager import org.bitcoinj.coinjoin.CoinJoinClientOptions import org.bitcoinj.coinjoin.Denomination @@ -46,66 +49,65 @@ import org.bitcoinj.coinjoin.utils.CoinJoinManager import org.bitcoinj.core.AbstractBlockChain import org.bitcoinj.core.Coin import org.bitcoinj.core.Context +import org.bitcoinj.core.ECKey import org.bitcoinj.core.MasternodeAddress +import org.bitcoinj.utils.ContextPropagatingThreadFactory import org.bitcoinj.utils.Threading import org.bitcoinj.wallet.Wallet import org.bitcoinj.wallet.WalletEx +import org.bouncycastle.crypto.params.KeyParameter import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.NetworkStatus import org.dash.wallet.common.services.BlockchainStateProvider import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit import javax.inject.Inject import javax.inject.Singleton enum class CoinJoinMode { - BASIC, + NONE, INTERMEDIATE, - ADVANCED, + ADVANCED } +/** + * CoinJoin Service + * + * Monitor the status of the CoinJoin Mixing Service + */ interface CoinJoinService { - var mode: CoinJoinMode val mixingStatus: MixingStatus - - fun needsToMix(amount: Coin): Boolean - suspend fun configureMixing( - amount: Coin, - requestKeyParameter: RequestKeyParameter, - requestDecryptedKey: RequestDecryptedKey, - restoreFromConfig: Boolean - ) - - suspend fun prepareAndStartMixing() - suspend fun waitForMixing() - suspend fun waitForMixingWithException() + val mixingProgress: Flow } enum class MixingStatus { - NOT_STARTED, - MIXING, - PAUSED, - FINISHED, - ERROR, + NOT_STARTED, // Mixing has not begun or CoinJoinMode is NONE + MIXING, // Mixing is underway + PAUSED, // Mixing is not finished, but is blocked by network connectivity + FINISHED, // The entire balance has been mixed + ERROR // An error stopped the mixing process } @Singleton class CoinJoinMixingService @Inject constructor( val walletDataProvider: WalletDataProvider, - val walletApplication: WalletApplication, - val blockchainStateProvider: BlockchainStateProvider, - private val config: CoinJoinConfig + blockchainStateProvider: BlockchainStateProvider, + config: CoinJoinConfig, + private val platformRepo: PlatformRepo ) : CoinJoinService { companion object { val log: Logger = LoggerFactory.getLogger(CoinJoinMixingService::class.java) - const val DEFAULT_MULTISESSION = true + const val DEFAULT_MULTISESSION = false // for stability, need to investigate const val DEFAULT_ROUNDS = 1 - const val DEFAULT_SESSIONS = 8 + const val DEFAULT_SESSIONS = 4 + const val DEFAULT_DENOMINATIONS_GOAL = 50 + const val DEFAULT_DENOMINATIONS_HARDCAP = 300 + // these are not for production - val FAST_MIXING_DASHPAY_FEE = Coin.parseCoin("0.15") - val FAST_MIXING_DENOMINATIONS_REMOVE = listOf(Denomination.THOUSANDTH) + val FAST_MIXING_DENOMINATIONS_REMOVE = listOf() // Denomination.THOUSANDTH) } private val coinJoinManager: CoinJoinManager? @@ -115,38 +117,30 @@ class CoinJoinMixingService @Inject constructor( private var mixingCompleteListeners: ArrayList = arrayListOf() private var sessionCompleteListeners: ArrayList = arrayListOf() - override var mode: CoinJoinMode = CoinJoinMode.BASIC + var mode: CoinJoinMode = CoinJoinMode.NONE override var mixingStatus: MixingStatus = MixingStatus.NOT_STARTED private set private val coroutineScope = CoroutineScope( - Executors.newFixedThreadPool(2).asCoroutineDispatcher(), + Executors.newFixedThreadPool(2, ContextPropagatingThreadFactory("coinjoin-pool")).asCoroutineDispatcher() ) + private val uiCoroutineScope = CoroutineScope(Dispatchers.Main) + private var blockChain: AbstractBlockChain? = null private val isBlockChainSet: Boolean get() = blockChain != null private var networkStatus: NetworkStatus = NetworkStatus.UNKNOWN + private var hasAnonymizableBalance: Boolean = false // https://stackoverflow.com/questions/55421710/how-to-suspend-kotlin-coroutine-until-notified - private val mixingMutex = Mutex(locked = true) private val updateMutex = Mutex(locked = false) + private val updateMixingStateMutex = Mutex(locked = false) private var exception: Throwable? = null - override suspend fun waitForMixing() { - log.info("Waiting for mixing to complete by watching lock...") - mixingMutex.withLock {} - log.info("Mixing complete according to released lock") - } - override suspend fun waitForMixingWithException() { - waitForMixing() - exception?.let { - log.error("Mixing error: {}", it.message, it) - throw it - } - } - private fun setMixingComplete() { - mixingMutex.unlock() - } + + override val mixingProgress: Flow + get() = _progressFlow + private val _progressFlow = MutableStateFlow(0.00) init { blockchainStateProvider.observeNetworkStatus() @@ -162,61 +156,128 @@ class CoinJoinMixingService @Inject constructor( updateBlockChain(blockChain) } .launchIn(coroutineScope) + + blockchainStateProvider.observeState() + .filterNotNull() + .distinctUntilChanged() + .onEach { blockChainState -> + log.info("coinjoin: new block: ${blockChainState.bestChainHeight}") + updateBalance(walletDataProvider.getWalletBalance()) + } + .launchIn(coroutineScope) + + walletDataProvider.observeBalance() + .distinctUntilChanged() + .onEach { balance -> + // switch to our context + coroutineScope.launch { + updateBalance(balance) + } + } + .launchIn(uiCoroutineScope) // required for observeBalance + + config.observeMode() + .filterNotNull() + .onEach { + updateMode(it) + } + .launchIn(coroutineScope) } - private suspend fun updateState(mixingStatus: MixingStatus, networkStatus: NetworkStatus, blockChain: AbstractBlockChain?) { + private suspend fun updateBalance(balance: Coin) { + // leave this ui scope + Context.propagate(walletDataProvider.wallet!!.context) + CoinJoinClientOptions.setAmount(balance) + log.info("coinjoin: total balance: ${balance.toFriendlyString()}") + val walletEx = walletDataProvider.wallet as WalletEx + log.info("coinjoin: mixed balance: ${walletEx.coinJoinBalance.toFriendlyString()}") + val anonBalance = walletEx.getAnonymizableBalance(false, false) + log.info("coinjoin: anonymizable balance {}", anonBalance.toFriendlyString()) + + val hasAnonymizableBalance = anonBalance.isGreaterThan(CoinJoin.getSmallestDenomination()) + log.info("coinjoin: mixing can occur: $hasAnonymizableBalance") + updateState(mode, hasAnonymizableBalance, networkStatus, blockChain) + } + + private suspend fun updateState( + mode: CoinJoinMode, + hasAnonymizableBalance: Boolean, + networkStatus: NetworkStatus, + blockChain: AbstractBlockChain? + ) { updateMutex.lock() - log.info("coinjoin-updateState: $mixingStatus, $networkStatus, ${blockChain != null}") + log.info("coinjoin-updateState: ${this.mode}, ${this.hasAnonymizableBalance}, ${this.networkStatus}, ${blockChain != null}") try { setBlockchain(blockChain) - log.info("coinjoin-updateState: $mixingStatus, $networkStatus, ${blockChain != null}") + log.info("coinjoin-updateState: $mode, $hasAnonymizableBalance, $networkStatus, ${blockChain != null}") val previousNetworkStatus = this.networkStatus this.networkStatus = networkStatus this.mixingStatus = mixingStatus + this.hasAnonymizableBalance = hasAnonymizableBalance + this.mode = mode + + if (mode == CoinJoinMode.NONE) { + updateMixingState(MixingStatus.NOT_STARTED) + } else { + configureMixing() + if (hasAnonymizableBalance) { + if (networkStatus == NetworkStatus.CONNECTED && isBlockChainSet) { + updateMixingState(MixingStatus.MIXING) + } else { + updateMixingState(MixingStatus.PAUSED) + } + } else { + updateMixingState(MixingStatus.FINISHED) + } + } + } finally { + updateMutex.unlock() + log.info("updateMutex is unlocked") + } + } + + private suspend fun updateMixingState( + mixingStatus: MixingStatus + ) { + updateMixingStateMutex.lock() + try { + + val previousMixingStatus = this.mixingStatus + this.mixingStatus = mixingStatus + log.info("coinjoin-updateMixingState: $previousMixingStatus -> $mixingStatus") + when { - networkStatus == NetworkStatus.UNKNOWN -> return - mixingStatus == MixingStatus.MIXING && networkStatus == NetworkStatus.CONNECTED && isBlockChainSet -> { + mixingStatus == MixingStatus.MIXING && previousMixingStatus != MixingStatus.MIXING -> { // start mixing prepareMixing() startMixing() } - mixingStatus == MixingStatus.FINISHED -> { + previousMixingStatus == MixingStatus.MIXING && mixingStatus != MixingStatus.MIXING -> { // finish mixing stopMixing() - setMixingComplete() - } - - mixingStatus == MixingStatus.MIXING && previousNetworkStatus == NetworkStatus.CONNECTED && networkStatus == NetworkStatus.NOT_AVAILABLE -> { - // pause mixing - stopMixing() - } - - mixingStatus == MixingStatus.PAUSED && previousNetworkStatus == NetworkStatus.CONNECTING && networkStatus == NetworkStatus.CONNECTED && isBlockChainSet -> { - // resume mixing - prepareMixing() - startMixing() } - - mixingStatus == MixingStatus.ERROR -> setMixingComplete() } - } finally { - updateMutex.unlock() - log.info("updateMutex is unlocked") + updateMixingStateMutex.unlock() } } private suspend fun updateBlockChain(blockChain: AbstractBlockChain) { - updateState(mixingStatus, networkStatus, blockChain) + updateState(mode, hasAnonymizableBalance, networkStatus, blockChain) } private suspend fun updateNetworkStatus(networkStatus: NetworkStatus) { - updateState(mixingStatus, networkStatus, blockChain) + updateState(mode, hasAnonymizableBalance, networkStatus, blockChain) } - private suspend fun updateMixingStatus(mixingStatus: MixingStatus) { - updateState(mixingStatus, networkStatus, blockChain) + private suspend fun updateMode(mode: CoinJoinMode) { + CoinJoinClientOptions.setEnabled(mode != CoinJoinMode.NONE) + if (mode != CoinJoinMode.NONE && this.mode == CoinJoinMode.NONE) { + configureMixing() + updateBalance(walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.AVAILABLE)) + } + updateState(mode, hasAnonymizableBalance, networkStatus, blockChain) } private var mixingProgressTracker: MixingProgressTracker = object : MixingProgressTracker() { @@ -230,7 +291,7 @@ class CoinJoinMixingService @Inject constructor( wallet: WalletEx?, sessionId: Int, denomination: Int, - message: PoolMessage?, + message: PoolMessage? ) { super.onSessionStarted(wallet, sessionId, denomination, message) log.info("Session {} started. {}% mixed", sessionId, progress) @@ -247,83 +308,67 @@ class CoinJoinMixingService @Inject constructor( ) { super.onSessionComplete(wallet, sessionId, denomination, state, message, address, joined) // TODO: _progressFlow.emit(progress) - log.info("Session {} complete. {}% mixed -- {}", sessionId, progress, message) + log.info("Session {} complete. {} % mixed -- {}", sessionId, progress, message) } } - override fun needsToMix(amount: Coin): Boolean { - return walletApplication.wallet?.getBalance(Wallet.BalanceType.COINJOIN_SPENDABLE) - ?.isLessThan(amount) ?: false + var encryptionKey: KeyParameter? = null + private fun encryptionKeyParameter(): KeyParameter { + if (encryptionKey == null) { + encryptionKey = platformRepo.getWalletEncryptionKey() ?: throw IllegalStateException( + "cannot obtain wallet encryption key" + ) + } + return encryptionKey!! } - override suspend fun configureMixing( - amount: Coin, - requestKeyParameter: RequestKeyParameter, - requestDecryptedKey: RequestDecryptedKey, - restoreFromConfig: Boolean - ) { - if (restoreFromConfig) { - // read from data store - val amountToMix = config.get(CoinJoinConfig.COINJOIN_AMOUNT) - val rounds = config.get(CoinJoinConfig.COINJOIN_ROUNDS) - val sessions = config.get(CoinJoinConfig.COINJOIN_SESSIONS) - val isMultiSession = config.get(CoinJoinConfig.COINJOIN_MULTISESSION) - // set client options - CoinJoinClientOptions.setRounds(rounds ?: DEFAULT_ROUNDS) - CoinJoinClientOptions.setSessions(sessions ?: DEFAULT_SESSIONS) - CoinJoinClientOptions.setAmount(amountToMix?.let { Coin.valueOf(amountToMix) } ?: amount) - CoinJoinClientOptions.setMultiSessionEnabled(isMultiSession ?: DEFAULT_MULTISESSION) - } else { - CoinJoinClientOptions.setSessions(DEFAULT_SESSIONS) - CoinJoinClientOptions.setAmount(max(FAST_MIXING_DASHPAY_FEE, amount)) - CoinJoinClientOptions.setMultiSessionEnabled(DEFAULT_MULTISESSION) - - when (mode) { - CoinJoinMode.BASIC -> CoinJoinClientOptions.setAmount(Coin.ZERO) - CoinJoinMode.INTERMEDIATE -> CoinJoinClientOptions.setRounds(DEFAULT_ROUNDS) - CoinJoinMode.ADVANCED -> CoinJoinClientOptions.setRounds(DEFAULT_ROUNDS * 2) - } - // save to data store - config.set(CoinJoinConfig.COINJOIN_AMOUNT, CoinJoinClientOptions.getAmount().value) - config.set(CoinJoinConfig.COINJOIN_ROUNDS, CoinJoinClientOptions.getRounds()) - config.set(CoinJoinConfig.COINJOIN_SESSIONS, CoinJoinClientOptions.getSessions()) - config.set(CoinJoinConfig.COINJOIN_MULTISESSION, CoinJoinClientOptions.isMultiSessionEnabled()) + private fun decryptKey(key: ECKey): ECKey { + val watch = Stopwatch.createStarted() + val decryptedKey = key.decrypt(encryptionKeyParameter()) + log.info("Decrypting key took {}", watch.elapsed(TimeUnit.MILLISECONDS)) + return decryptedKey + } + + private val requestKeyParameter = RequestKeyParameter { encryptionKeyParameter() } + private val requestDecryptedKey = RequestDecryptedKey { decryptKey(it) } + private fun configureMixing() { + configureMixing(walletDataProvider.getWalletBalance()) + } + /** set CoinJoinClientOptions based on CoinJoinMode */ + private fun configureMixing(amount: Coin) { + when (mode) { + CoinJoinMode.NONE -> { + // no options to set + } + CoinJoinMode.INTERMEDIATE -> { + CoinJoinClientOptions.setRounds(DEFAULT_ROUNDS) + (walletDataProvider.wallet as WalletEx).coinJoin.setRounds(DEFAULT_ROUNDS) + } + CoinJoinMode.ADVANCED -> { + CoinJoinClientOptions.setRounds(DEFAULT_ROUNDS * 2) + (walletDataProvider.wallet as WalletEx).coinJoin.setRounds(DEFAULT_ROUNDS * 2) + } } + + CoinJoinClientOptions.setSessions(DEFAULT_SESSIONS) + CoinJoinClientOptions.setMultiSessionEnabled(DEFAULT_MULTISESSION) + CoinJoinClientOptions.setDenomsGoal(DEFAULT_DENOMINATIONS_GOAL) + CoinJoinClientOptions.setDenomsHardCap(DEFAULT_DENOMINATIONS_HARDCAP) + FAST_MIXING_DENOMINATIONS_REMOVE.forEach { CoinJoinClientOptions.removeDenomination(it) } - // TODO: have CoinJoinClientOptions.toString instead do this - log.info("mixing configuration: { rounds: ${CoinJoinClientOptions.getRounds()}, sessions: ${CoinJoinClientOptions.getSessions()}, amount: ${amount.toFriendlyString()}}") - coinJoinManager?.run { - setRequestKeyParameter(requestKeyParameter) - setRequestDecryptedKey(requestDecryptedKey) - } - } - override suspend fun prepareAndStartMixing() { - log.info("coinjoin: prepare and start mixing...") - // do we need to mix? - val wallet = walletDataProvider.wallet!! as WalletEx - Context.propagate(wallet.context) - // the mixed balance must meet the getAmount() requirement and all denominated coins must be mixed - val mixedAmount = wallet.coinJoinBalance - val denominatedAmount = wallet.denominatedBalance - if (mixedAmount.isGreaterThanOrEqualTo(CoinJoinClientOptions.getAmount()) && - mixedAmount.equals(denominatedAmount) - ) { - log.info("coinjoin: mixing is complete $mixedAmount/$denominatedAmount of ${CoinJoinClientOptions.getAmount()}") - setMixingComplete() - } else { - log.info("coinjoin: start the mixing process...") - updateMixingStatus(MixingStatus.MIXING) - } + // TODO: have CoinJoinClientOptions.toString instead do this + log.info( + "mixing configuration: { rounds: ${CoinJoinClientOptions.getRounds()}, sessions: ${CoinJoinClientOptions.getSessions()}, amount: ${amount.toFriendlyString()}, multisession: ${CoinJoinClientOptions.isMultiSessionEnabled()}}" + ) } private suspend fun prepareMixing() { - log.info("Mixing preparation began") + log.info("coinjoin: Mixing preparation began") clear() - CoinJoinClientOptions.setEnabled(true) val wallet = walletDataProvider.wallet!! addMixingCompleteListener(mixingProgressTracker) addSessionCompleteListener(mixingProgressTracker) @@ -331,7 +376,7 @@ class CoinJoinMixingService @Inject constructor( clientManager = CoinJoinClientManager(wallet) coinJoinClientManagers[wallet.description] = clientManager // this allows mixing to wait for the last transaction to be confirmed - clientManager.addContinueMixingOnError(PoolStatus.ERR_NOT_ENOUGH_FUNDS) + //clientManager.addContinueMixingOnError(PoolStatus.ERR_NO_INPUTS) // wait until the masternode sync system fixes itself clientManager.addContinueMixingOnError(PoolStatus.ERR_NO_MASTERNODES_DETECTED) clientManager.setStopOnNothingToDo(true) @@ -341,25 +386,44 @@ class CoinJoinMixingService @Inject constructor( MixingCompleteListener { _, statusList -> statusList?.let { for (status in it) { - if (status != PoolStatus.FINISHED) { - coroutineScope.launch(Dispatchers.IO) { updateMixingStatus(MixingStatus.ERROR) } + 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 } } } } + val sessionCompleteListener = SessionCompleteListener { _, _, _, _, _, _, _ -> + coroutineScope.launch { + updateProgress() + } + } + mixingFinished.addListener({ log.info("Mixing complete.") removeMixingCompleteListener(mixingCompleteListener) + removeSessionCompleteListener(sessionCompleteListener) if (mixingFinished.get()) { - coroutineScope.launch(Dispatchers.IO) { updateMixingStatus(MixingStatus.FINISHED) } + coroutineScope.launch { + updateProgress() + updateMixingState(MixingStatus.FINISHED) + } } else { - coroutineScope.launch(Dispatchers.IO) { updateMixingStatus(MixingStatus.PAUSED) } + coroutineScope.launch { + updateProgress() + updateMixingState(MixingStatus.PAUSED) + } } - }, Threading.SAME_THREAD) + }, Threading.USER_THREAD) + + addMixingCompleteListener(Threading.USER_THREAD, mixingCompleteListener) + addSessionCompleteListener(Threading.USER_THREAD, sessionCompleteListener) + log.info("coinjoin: mixing preparation finished") - addMixingCompleteListener(Threading.SAME_THREAD, mixingCompleteListener) + setRequestKeyParameter(requestKeyParameter) + setRequestDecryptedKey(requestDecryptedKey) } } @@ -376,7 +440,9 @@ class CoinJoinMixingService @Inject constructor( clientManager.doAutomaticDenominating() } val result = asyncStart.await() - log.info("Mixing " + if (result) "started successfully" else "start failed: " + clientManager.statuses + ", will retry") + log.info( + "Mixing " + if (result) "started successfully" else "start failed: " + clientManager.statuses + ", will retry" + ) true } } @@ -387,6 +453,8 @@ class CoinJoinMixingService @Inject constructor( return } + encryptionKey = null + // if mixing is not complete, then tell the future we didn't finish yet if (!clientManager.mixingFinishedFuture.isDone) { clientManager.mixingFinishedFuture.set(false) @@ -395,7 +463,6 @@ class CoinJoinMixingService @Inject constructor( mixingCompleteListeners.forEach { coinJoinManager?.removeMixingCompleteListener(it) } sessionCompleteListeners.forEach { coinJoinManager?.removeSessionCompleteListener(it) } coinJoinManager?.stop() - CoinJoinClientOptions.setEnabled(false) } private fun setBlockchain(blockChain: AbstractBlockChain?) { @@ -407,29 +474,29 @@ class CoinJoinMixingService @Inject constructor( this.blockChain = blockChain } - fun addSessionCompleteListener(sessionCompleteListener: SessionCompleteListener) { + private fun addSessionCompleteListener(sessionCompleteListener: SessionCompleteListener) { sessionCompleteListeners.add(sessionCompleteListener) - coinJoinManager?.addSessionCompleteListener(Threading.SAME_THREAD, sessionCompleteListener) + coinJoinManager?.addSessionCompleteListener(Threading.USER_THREAD, sessionCompleteListener) } - fun addMixingCompleteListener(mixingCompleteListener: MixingCompleteListener) { + private fun addMixingCompleteListener(mixingCompleteListener: MixingCompleteListener) { mixingCompleteListeners.add(mixingCompleteListener) - coinJoinManager?.addMixingCompleteListener(Threading.SAME_THREAD, mixingCompleteListener) + coinJoinManager?.addMixingCompleteListener(Threading.USER_THREAD, mixingCompleteListener) } - fun removeMixingCompleteListener(mixingCompleteListener: MixingCompleteListener) { - coinJoinManager?.removeMixingCompleteListener(mixingCompleteListener) - } - - // TODO: private val _progressFlow = MutableStateFlow(0.00) - // TODO: override suspend fun getMixingProgress(): Flow = _progressFlow - // TODO: suspend fun setProgress(progress: Double) = _progressFlow.emit(progress) - /** clear previous state */ - private suspend fun clear() { + private fun clear() { exception = null - mixingStatus = MixingStatus.NOT_STARTED - if (!mixingMutex.isLocked) - mixingMutex.lock() + } + + private suspend fun updateProgress() { + val wallet = walletDataProvider.wallet as WalletEx + val mixedBalance = wallet.coinJoinBalance + 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) + _progressFlow.emit(progress) + } } } diff --git a/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt b/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt index d865573491..1c0a12189c 100644 --- a/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt +++ b/wallet/src/de/schildbach/wallet/ui/SettingsActivity.kt @@ -23,6 +23,7 @@ import androidx.core.view.isVisible import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint import de.schildbach.wallet.WalletBalanceWidgetProvider +import de.schildbach.wallet.service.CoinJoinMode import de.schildbach.wallet.service.MixingStatus import de.schildbach.wallet.ui.coinjoin.CoinJoinActivity import de.schildbach.wallet.ui.main.MainActivity @@ -106,19 +107,23 @@ class SettingsActivity : LockScreenActivity() { binding.votingDashPaySwitch.setOnCheckedChangeListener { _, isChecked -> viewModel.setVoteDashPay(isChecked) } - } - override fun onResume() { - super.onResume() - - if (viewModel.coinJoinMixingStatus == MixingStatus.MIXING || - viewModel.coinJoinMixingStatus == MixingStatus.PAUSED - ) { - // TODO: Observe progress - binding.coinjoinSubtitleIcon.isVisible = true - binding.coinjoinSubtitle.text = getString(R.string.coinjoin_progress, "0.012", "0.028") - } else { - binding.coinjoinSubtitle.text = getText(R.string.turned_off) + viewModel.coinJoinMixingMode.observe(this) { mode -> + if (mode == CoinJoinMode.NONE) { + binding.coinjoinSubtitle.text = getText(R.string.turned_off) + } else { + // TODO: Observe progress + // TODO: This does not meet the designs + binding.coinjoinSubtitleIcon.isVisible = true + binding.coinjoinSubtitle.text = getString( + if (viewModel.coinJoinMixingStatus == MixingStatus.FINISHED) + R.string.coinjoin_progress_finished + else R.string.coinjoin_progress, + viewModel.mixedBalance, + viewModel.walletBalance, + viewModel.mixingPercent + ) + } } } diff --git a/wallet/src/de/schildbach/wallet/ui/SettingsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/SettingsViewModel.kt index f314076d4d..746007daf9 100644 --- a/wallet/src/de/schildbach/wallet/ui/SettingsViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/SettingsViewModel.kt @@ -4,9 +4,15 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.schildbach.wallet.data.CoinJoinConfig -import de.schildbach.wallet.service.CoinJoinMixingService +import de.schildbach.wallet.service.CoinJoinMode +import de.schildbach.wallet.service.CoinJoinService import de.schildbach.wallet.service.MixingStatus +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch +import org.bitcoinj.utils.MonetaryFormat +import org.bitcoinj.wallet.Wallet +import org.bitcoinj.wallet.WalletEx +import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.data.WalletUIConfig import javax.inject.Inject @@ -14,12 +20,36 @@ import javax.inject.Inject class SettingsViewModel @Inject constructor( private val walletUIConfig: WalletUIConfig, private val coinJoinConfig: CoinJoinConfig, - private val coinJoinService: CoinJoinMixingService + private val coinJoinService: CoinJoinService, + private val walletDataProvider: WalletDataProvider ): ViewModel() { val voteDashPayIsEnabled = walletUIConfig.observe(WalletUIConfig.VOTE_DASH_PAY_ENABLED) + val coinJoinMixingMode: Flow + get() = coinJoinConfig.observeMode() + val coinJoinMixingStatus: MixingStatus get() = coinJoinService.mixingStatus + val walletBalance: String + get() = dashFormat.format(walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.ESTIMATED)).toString() + + val mixedBalance: String + get() = dashFormat.format((walletDataProvider.wallet as WalletEx).coinJoinBalance).toString() + + val mixingPercent: String + get() { + val wallet = walletDataProvider.wallet as WalletEx + val mixedBalance = wallet.coinJoinBalance + val anonymizableBalance = wallet.getAnonymizableBalance(false, false) + return if (mixedBalance.value + anonymizableBalance.value != 0L) { + return "${mixedBalance.value * 100 / (mixedBalance.value + anonymizableBalance.value)}%" + } else { + return "N/A" + } + } + + private val dashFormat = MonetaryFormat.BTC.noCode().minDecimals(2) + fun setVoteDashPay(isEnabled: Boolean) { viewModelScope.launch { walletUIConfig.set(WalletUIConfig.VOTE_DASH_PAY_ENABLED, isEnabled) diff --git a/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelFragment.kt b/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelFragment.kt index a6ac1828dc..3c3ff2486f 100644 --- a/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelFragment.kt @@ -31,12 +31,13 @@ import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.ui.dialogs.AdaptiveDialog import org.dash.wallet.common.ui.setRoundedRippleBackground import org.dash.wallet.common.ui.viewBinding +import org.dash.wallet.common.util.observe @AndroidEntryPoint class CoinJoinLevelFragment : Fragment(R.layout.fragment_coinjoin_level) { private val binding by viewBinding(FragmentCoinjoinLevelBinding::bind) private val viewModel by viewModels() - private var selectedCoinJoinMode = CoinJoinMode.BASIC + private var selectedCoinJoinMode = CoinJoinMode.NONE override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -53,11 +54,11 @@ class CoinJoinLevelFragment : Fragment(R.layout.fragment_coinjoin_level) { lifecycleScope.launch { if (viewModel.isMixing) { if (confirmStopMixing()) { - viewModel.stopMixing() + viewModel.setMode(CoinJoinMode.NONE) requireActivity().finish() } } else { - viewModel.startMixing(selectedCoinJoinMode) + viewModel.setMode(selectedCoinJoinMode) requireActivity().finish() } } @@ -67,7 +68,9 @@ class CoinJoinLevelFragment : Fragment(R.layout.fragment_coinjoin_level) { requireActivity().finish() } - setMode(viewModel.mixingMode) + viewModel.mixingMode.observe(viewLifecycleOwner) { mixingMode -> + setMode(mixingMode) + } if (viewModel.isMixing) { binding.continueBtn.setText(R.string.coinjoin_stop) @@ -113,7 +116,9 @@ class CoinJoinLevelFragment : Fragment(R.layout.fragment_coinjoin_level) { ).show(requireActivity()) { toChange -> if (toChange == true) { setMode(mode) - viewModel.mixingMode = mode + lifecycleScope.launch { + viewModel.setMode(mode) + } requireActivity().finish() } } diff --git a/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt b/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt index c92acf797e..58dce1e5c7 100644 --- a/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/coinjoin/CoinJoinLevelViewModel.kt @@ -18,12 +18,17 @@ package de.schildbach.wallet.ui.coinjoin import androidx.lifecycle.ViewModel +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 de.schildbach.wallet.service.MixingStatus +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import org.dash.wallet.common.services.NetworkStateInt -import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService import javax.inject.Inject @@ -31,36 +36,31 @@ import javax.inject.Inject open class CoinJoinLevelViewModel @Inject constructor( private val analytics: AnalyticsService, private val coinJoinService: CoinJoinService, + private val coinJoinConfig: CoinJoinConfig, private var networkState: NetworkStateInt ) : ViewModel() { val isMixing: Boolean - get() = coinJoinService.mixingStatus == MixingStatus.MIXING || - coinJoinService.mixingStatus == MixingStatus.PAUSED + get() = _mixingMode.value != CoinJoinMode.NONE - var mixingMode: CoinJoinMode - get() = coinJoinService.mode - set(value) { - coinJoinService.mode = value -// coinJoinService.prepareAndStartMixing() TODO restart mixing? - } + val _mixingMode = MutableStateFlow(CoinJoinMode.NONE) - fun isWifiConnected(): Boolean { - return networkState.isWifiConnected() - } + val mixingMode: Flow + get() = _mixingMode - suspend fun startMixing(mode: CoinJoinMode) { - analytics.logEvent( - AnalyticsConstants.CoinJoinPrivacy.USERNAME_PRIVACY_BTN_CONTINUE, - mapOf(AnalyticsConstants.Parameter.VALUE to mode.name) - ) + init { + coinJoinConfig.observeMode() + .filterNotNull() + .onEach { _mixingMode.value = it } + .launchIn(viewModelScope) + } - coinJoinService.mode = mode - coinJoinService.prepareAndStartMixing() // TODO: change the logic if needed + fun isWifiConnected(): Boolean { + return networkState.isWifiConnected() } - suspend fun stopMixing() { -// coinJoinService.stopMixing() // TODO expose stop method? + suspend fun setMode(mode: CoinJoinMode) { + coinJoinConfig.setMode(mode) } fun logEvent(event: String) { diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt index 049fdd5f5d..59362df5f9 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.LifecycleService import dagger.hilt.android.AndroidEntryPoint import de.schildbach.wallet.Constants import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.CoinJoinConfig import de.schildbach.wallet.data.InvitationLinkData import de.schildbach.wallet.database.dao.UserAlertDao import de.schildbach.wallet.database.entity.BlockchainIdentityConfig @@ -17,7 +18,6 @@ import de.schildbach.wallet.database.entity.DashPayProfile import de.schildbach.wallet.security.SecurityFunctions import de.schildbach.wallet.security.SecurityGuard import de.schildbach.wallet.service.CoinJoinMode -import de.schildbach.wallet.service.CoinJoinService import de.schildbach.wallet.service.platform.PlatformSyncService import de.schildbach.wallet.ui.dashpay.work.SendContactRequestOperation import de.schildbach.wallet_test.R @@ -135,7 +135,7 @@ class CreateIdentityService : LifecycleService() { @Inject lateinit var userAlertDao: UserAlertDao @Inject lateinit var blockchainIdentityDataDao: BlockchainIdentityConfig @Inject lateinit var securityFunctions: SecurityFunctions - @Inject lateinit var coinJoinService: CoinJoinService + @Inject lateinit var coinJoinConfig: CoinJoinConfig private lateinit var securityGuard: SecurityGuard private val wakeLock by lazy { @@ -332,19 +332,6 @@ class CreateIdentityService : LifecycleService() { } } - if (blockchainIdentityData.creationState <= CreationState.MIXING_FUNDS) { - platformRepo.updateIdentityCreationState(blockchainIdentityData, CreationState.MIXING_FUNDS) - coinJoinService.configureMixing( - Constants.DASH_PAY_FEE, - { encryptionKey }, - { it.decrypt(encryptionKey) }, - restoreFromConfig = username == null - ) - coinJoinService.prepareAndStartMixing() - - coinJoinService.waitForMixingWithException() - } - if (blockchainIdentityData.creationState <= CreationState.CREDIT_FUNDING_TX_CREATING) { platformRepo.updateIdentityCreationState(blockchainIdentityData, CreationState.CREDIT_FUNDING_TX_CREATING) // @@ -352,7 +339,7 @@ class CreateIdentityService : LifecycleService() { // // check to see if the funding transaction exists if (blockchainIdentity.creditFundingTransaction == null) { - val useCoinJoin = blockchainIdentityData.privacyMode == CoinJoinMode.INTERMEDIATE || blockchainIdentityData.privacyMode == CoinJoinMode.ADVANCED + val useCoinJoin = coinJoinConfig.getMode() != CoinJoinMode.NONE platformRepo.createCreditFundingTransactionAsync(blockchainIdentity, encryptionKey, useCoinJoin) } } diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/HistoryHeaderAdapter.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/HistoryHeaderAdapter.kt index 5ef049396f..44f84fbf4f 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/HistoryHeaderAdapter.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/HistoryHeaderAdapter.kt @@ -132,7 +132,6 @@ class HistoryHeaderAdapter( when (blockchainIdentityData.creationState) { BlockchainIdentityData.CreationState.NONE, BlockchainIdentityData.CreationState.UPGRADING_WALLET, - BlockchainIdentityData.CreationState.MIXING_FUNDS, BlockchainIdentityData.CreationState.CREDIT_FUNDING_TX_CREATING, BlockchainIdentityData.CreationState.CREDIT_FUNDING_TX_SENDING, BlockchainIdentityData.CreationState.CREDIT_FUNDING_TX_SENT, diff --git a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt index 357e445fb7..637f7ed32b 100644 --- a/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/send/SendCoinsViewModel.kt @@ -124,6 +124,7 @@ class SendCoinsViewModel @Inject constructor( } .launchIn(viewModelScope) + // TODO: the coin selector will need to use CoinJoinCoinSelector if CoinJoin is ON walletDataProvider.observeBalance(coinSelector = MaxOutputAmountCoinSelector()) .distinctUntilChanged() .onEach(_maxOutputAmount::postValue) diff --git a/wallet/test/de/schildbach/wallet/util/services/SendCoinsTaskRunnerTest.kt b/wallet/test/de/schildbach/wallet/util/services/SendCoinsTaskRunnerTest.kt index ccb6696b66..5406e0a498 100644 --- a/wallet/test/de/schildbach/wallet/util/services/SendCoinsTaskRunnerTest.kt +++ b/wallet/test/de/schildbach/wallet/util/services/SendCoinsTaskRunnerTest.kt @@ -18,11 +18,16 @@ package de.schildbach.wallet.util.services import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.CoinJoinConfig import de.schildbach.wallet.payments.SendCoinsTaskRunner +import de.schildbach.wallet.service.CoinJoinMode +import io.mockk.coEvery import io.mockk.every import io.mockk.mockk import junit.framework.TestCase.assertTrue import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import org.bitcoinj.core.Address import org.bitcoinj.core.Coin import org.bitcoinj.core.Context @@ -37,10 +42,12 @@ class SendCoinsTaskRunnerTest { @Test fun sendCoins_coinSelectorSet_correctCoinSelector() { val wallet = mockk() + val coinJoinConfig = mockk() every { wallet.context } returns Context(MainNetParams.get()) + coEvery { coinJoinConfig.observeMode() } returns MutableStateFlow(CoinJoinMode.NONE) val application = mockk() - val sendCoinsTaskRunner = SendCoinsTaskRunner(application, mockk(), mockk(), mockk()) + val sendCoinsTaskRunner = SendCoinsTaskRunner(application, mockk(), mockk(), mockk(), coinJoinConfig) val request = sendCoinsTaskRunner.createSendRequest( Address.fromBase58(MainNetParams.get(), "XjBya4EnibUyxubEA8D2Y8KSrBMW1oHq5U"), Coin.COIN, @@ -54,11 +61,13 @@ class SendCoinsTaskRunnerTest { @Test fun sendCoins_nullCoinSelector_zeroConfSelectorByDefault() { val wallet = mockk() + val coinJoinConfig = mockk() every { wallet.context } returns Context(MainNetParams.get()) + coEvery { coinJoinConfig.observeMode() } returns MutableStateFlow(CoinJoinMode.NONE) val application = mockk() every { application.wallet } returns wallet - val sendCoinsTaskRunner = SendCoinsTaskRunner(application, mockk(), mockk(), mockk()) + val sendCoinsTaskRunner = SendCoinsTaskRunner(application, mockk(), mockk(), mockk(), coinJoinConfig) val request = sendCoinsTaskRunner.createSendRequest( Address.fromBase58(MainNetParams.get(), "XjBya4EnibUyxubEA8D2Y8KSrBMW1oHq5U"), Coin.COIN,