Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dashpay): coinjoin entire wallet balance #1222

Merged
merged 11 commits into from
Nov 21, 2023
Merged
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ buildscript {
kotlin_version = '1.8.22'
coroutinesVersion = '1.6.4'
ok_http_version = '4.9.1'
dashjVersion = '19.1-CJ-SNAPSHOT'
dashjVersion = '20.0.0-CJ-SNAPSHOT'
hiltVersion = '2.45'
hiltWorkVersion = '1.0.0'
workRuntimeVersion='2.7.1'
Expand Down
2 changes: 1 addition & 1 deletion wallet/schnapps/res/values/values.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
<item>µtDASH, no decimal places</item>
</string-array>
<string-array name="preferences_block_explorer_values">
<item>https://insight.bintang.networks.dash.org:3002/insight</item>
<item>https://insight.ouzo.networks.dash.org:3002/insight</item>
</string-array>
<string-array name="preferences_block_explorer_labels">
<item>Insight Devnet Block Explorer</item>
Expand Down
4 changes: 2 additions & 2 deletions wallet/src/de/schildbach/wallet/Constants.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import org.bitcoinj.crypto.ChildNumber;
import org.bitcoinj.params.DevNetParams;
import org.bitcoinj.params.MainNetParams;
import org.bitcoinj.params.ScrewDriverDevNetParams;
import org.bitcoinj.params.OuzoDevNetParams;
import org.bitcoinj.params.TestNet3Params;
import org.bitcoinj.utils.MonetaryFormat;
import org.bitcoinj.wallet.DeterministicKeyChain;
Expand Down Expand Up @@ -102,7 +102,7 @@ public final class Constants {
case "devnet": {
// Schnapps Devnet
BIP44_PATH = DeterministicKeyChain.BIP44_ACCOUNT_ZERO_PATH_TESTNET;
NETWORK_PARAMETERS = ScrewDriverDevNetParams.get();
NETWORK_PARAMETERS = OuzoDevNetParams.get();
String devNetName = ((DevNetParams)NETWORK_PARAMETERS).getDevNetName();
devNetName = devNetName.substring(devNetName.indexOf("-") + 1);
DNS_SEED = NETWORK_PARAMETERS.getDnsSeeds();
Expand Down
18 changes: 18 additions & 0 deletions wallet/src/de/schildbach/wallet/data/CoinJoinConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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<CoinJoinMode> {
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())
}
}
6 changes: 4 additions & 2 deletions wallet/src/de/schildbach/wallet/di/AppModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
37 changes: 27 additions & 10 deletions wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)
}
Comment on lines +71 to +82
Copy link
Collaborator Author

@HashEngineering HashEngineering Oct 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first thought was to pass a "send with coinjoin" boolean value in many of these functions. But this would require that many ViewModels in many modules have access to the CoinJoinConfig.

My final idea was to load the CoinJoin mode here and no other modules or view models need to know anything about CoinJoin.


@Throws(LeftoverBalanceException::class)
override suspend fun sendCoins(
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<CoinJoinLevelViewModel>()
private var selectedCoinJoinMode = CoinJoinMode.BASIC
private var selectedCoinJoinMode = CoinJoinMode.NONE

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Expand All @@ -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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the CoinJoinMode will trigger the CoinJoinService to start or stop mixing.

requireActivity().finish()
}
} else {
viewModel.startMixing(selectedCoinJoinMode)
viewModel.setMode(selectedCoinJoinMode)
requireActivity().finish()
}
}
Expand All @@ -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)
Expand Down Expand Up @@ -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()
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,49 +18,51 @@
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

@HiltViewModel
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
// get() = coinJoinService.mixingStatus == MixingStatus.MIXING ||
// coinJoinService.mixingStatus == MixingStatus.PAUSED

var mixingMode: CoinJoinMode
get() = coinJoinService.mode
set(value) {
coinJoinService.mode = value
// coinJoinService.prepareAndStartMixing() TODO restart mixing?
}
val _mixingMode = MutableStateFlow<CoinJoinMode>(CoinJoinMode.NONE)

fun isWifiConnected(): Boolean {
return networkState.isWifiConnected()
}
val mixingMode: Flow<CoinJoinMode>
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) {
Expand Down
Loading