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,