Skip to content

Commit

Permalink
feature(dashpay): display coinjoin mixing info (#1224)
Browse files Browse the repository at this point in the history
* Calculate MAX amount correctly

* feat(coinjoin): update settings progress

* fix: coinjoin transactions to not make fees

* fix: update mixing pane

* fix: return change

* fix: add mixing notification

* tests: update MainViewModelTest

* fix(coinjoin): don't stop the coinjoin manager

* fix(coinjoin): consider sync status as part of update balance

* chore: remove comment and add private
  • Loading branch information
HashEngineering authored Nov 21, 2023
1 parent d911db1 commit e6b6144
Show file tree
Hide file tree
Showing 17 changed files with 324 additions and 60 deletions.
16 changes: 16 additions & 0 deletions wallet/res/drawable/ic_mixing_icon.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="38dp"
android:height="38dp"
android:viewportWidth="38"
android:viewportHeight="38">
<path
android:pathData="M19,0L19,0A19,19 0,0 1,38 19L38,19A19,19 0,0 1,19 38L19,38A19,19 0,0 1,0 19L0,19A19,19 0,0 1,19 0z"
android:fillColor="#008DE4"
android:fillAlpha="0.1"/>
<path
android:pathData="M19,30C17.497,30 16.081,29.712 14.751,29.137C13.428,28.562 12.26,27.768 11.246,26.754C10.232,25.74 9.438,24.572 8.863,23.249C8.288,21.919 8,20.503 8,19C8,17.497 8.288,16.085 8.863,14.762C9.438,13.432 10.229,12.26 11.235,11.246C12.249,10.232 13.417,9.438 14.74,8.863C16.07,8.288 17.487,8 18.989,8C20.492,8 21.908,8.288 23.238,8.863C24.568,9.438 25.74,10.232 26.754,11.246C27.768,12.26 28.562,13.432 29.137,14.762C29.712,16.085 30,17.497 30,19C30,20.503 29.712,21.919 29.137,23.249C28.562,24.572 27.768,25.74 26.754,26.754C25.74,27.768 24.568,28.562 23.238,29.137C21.915,29.712 20.503,30 19,30Z"
android:fillColor="#008DE4"/>
<path
android:pathData="M16.627,16.681C17.152,16.681 17.605,16.491 17.986,16.11C18.375,15.721 18.569,15.265 18.569,14.74C18.569,14.208 18.375,13.755 17.986,13.381C17.605,13 17.152,12.81 16.627,12.81C16.095,12.81 15.639,13 15.258,13.381C14.884,13.755 14.697,14.208 14.697,14.74C14.697,15.265 14.884,15.721 15.258,16.11C15.632,16.491 16.088,16.681 16.627,16.681ZM21.383,16.681C21.915,16.681 22.368,16.491 22.742,16.11C23.123,15.721 23.314,15.265 23.314,14.74C23.314,14.208 23.123,13.755 22.742,13.381C22.368,13 21.915,12.81 21.383,12.81C20.858,12.81 20.402,13 20.014,13.381C19.633,13.755 19.442,14.208 19.442,14.74C19.442,15.265 19.633,15.721 20.014,16.11C20.402,16.491 20.858,16.681 21.383,16.681ZM14.266,20.93C14.79,20.93 15.24,20.74 15.614,20.359C15.995,19.978 16.185,19.525 16.185,19C16.185,18.475 15.995,18.022 15.614,17.641C15.24,17.253 14.79,17.059 14.266,17.059C13.734,17.059 13.274,17.253 12.885,17.641C12.504,18.022 12.314,18.475 12.314,19C12.314,19.525 12.504,19.978 12.885,20.359C13.266,20.74 13.726,20.93 14.266,20.93ZM19.011,20.93C19.536,20.93 19.989,20.74 20.37,20.359C20.751,19.978 20.941,19.525 20.941,19C20.941,18.475 20.751,18.022 20.37,17.641C19.989,17.253 19.536,17.059 19.011,17.059C18.479,17.059 18.022,17.253 17.641,17.641C17.26,18.022 17.07,18.475 17.07,19C17.07,19.525 17.26,19.978 17.641,20.359C18.022,20.74 18.479,20.93 19.011,20.93ZM23.756,20.93C24.281,20.93 24.734,20.74 25.115,20.359C25.503,19.978 25.697,19.525 25.697,19C25.697,18.475 25.503,18.022 25.115,17.641C24.734,17.253 24.281,17.059 23.756,17.059C23.224,17.059 22.767,17.253 22.386,17.641C22.012,18.022 21.826,18.475 21.826,19C21.826,19.525 22.012,19.978 22.386,20.359C22.767,20.74 23.224,20.93 23.756,20.93ZM16.627,25.19C17.152,25.19 17.605,25.003 17.986,24.629C18.375,24.248 18.569,23.792 18.569,23.26C18.569,22.728 18.375,22.271 17.986,21.89C17.605,21.509 17.152,21.318 16.627,21.318C16.095,21.318 15.639,21.509 15.258,21.89C14.884,22.271 14.697,22.728 14.697,23.26C14.697,23.784 14.884,24.237 15.258,24.618C15.632,24.999 16.088,25.19 16.627,25.19ZM21.383,25.19C21.915,25.19 22.368,25.003 22.742,24.629C23.123,24.248 23.314,23.792 23.314,23.26C23.314,22.728 23.123,22.271 22.742,21.89C22.368,21.509 21.915,21.318 21.383,21.318C20.858,21.318 20.402,21.509 20.014,21.89C19.633,22.271 19.442,22.728 19.442,23.26C19.442,23.792 19.633,24.248 20.014,24.629C20.402,25.003 20.858,25.19 21.383,25.19Z"
android:fillColor="#ffffff"/>
</vector>
8 changes: 4 additions & 4 deletions wallet/res/layout/activity_settings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,13 @@
<TextView
android:id="@+id/coinjoin_title"
style="@style/Body2.Medium"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:text="@string/coinjoin"
android:layout_height="27dp"
android:layout_marginStart="22dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/coinjoin_icon"
android:text="@string/coinjoin"
app:layout_constraintBottom_toTopOf="@id/coinjoin_subtitle"
app:layout_constraintStart_toEndOf="@id/coinjoin_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_chainStyle="packed" />

<TextView
Expand Down
10 changes: 10 additions & 0 deletions wallet/res/layout/home_content.xml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="14dp" />

<include
android:id="@+id/mixing_status_pane"
layout="@layout/mixing_status_pane"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
tools:visibility="visible"
android:layout_marginBottom="14dp" />

</LinearLayout>
</LinearLayout>

Expand Down
70 changes: 70 additions & 0 deletions wallet/res/layout/mixing_status_pane.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_height="wrap_content"
android:layout_width="match_parent">

<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/mixing_status_pane2"
android:layout_width="match_parent"
android:layout_height="58dp"
android:background="@drawable/white_background_rounded">

<ImageView
android:id="@+id/mixing_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginHorizontal="15dp"
android:src="@drawable/ic_mixing_icon"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/mixing_mode"
style="@style/Overline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@string/coinjoin_mixing"
app:layout_constraintStart_toEndOf="@id/mixing_icon"
app:layout_constraintTop_toTopOf="@id/mixing_icon" />

<TextView
android:id="@+id/balance"
style="@style/Overline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="3dp"
app:layout_constraintEnd_toStartOf="@id/dash_icon"
app:layout_constraintTop_toTopOf="@id/mixing_mode"
tools:text="0.012 of 0.028 DASH" />

<ImageView
android:id="@+id/dash_icon"
android:layout_width="11dp"
android:layout_height="11dp"
android:layout_gravity="center_vertical"
android:layout_marginTop="3dp"
android:layout_marginEnd="15dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/mixing_mode"
app:srcCompat="@drawable/ic_dash"
app:tint="@color/content_primary" />

<ProgressBar
android:id="@+id/mixing_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginTop="8dp"
android:progress="50"
app:layout_constraintEnd_toEndOf="@id/dash_icon"
app:layout_constraintStart_toEndOf="@id/mixing_icon"
app:layout_constraintTop_toBottomOf="@id/mixing_mode" />

</androidx.constraintlayout.widget.ConstraintLayout>

</RelativeLayout>
6 changes: 4 additions & 2 deletions wallet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -506,8 +506,10 @@
<string name="coinjoin_advanced_time">Multiple hours</string>
<string name="coinjoin_start">Start Mixing</string>
<string name="coinjoin_stop">Stop Mixing</string>
<string name="coinjoin_progress">Mixing · %1$s of %2$s (%3$s)</string>
<string name="coinjoin_progress_finished">Finished · %1$s of %2$s</string>
<string name="coinjoin_mixing">Mixing</string>
<string name="coinjoin_progress">Mixing · %1$s of %2$s</string>
<string name="coinjoin_progress_balance">%1$s of %2$s</string>
<string name="coinjoin_progress_finished">Fully Mixed</string>
<string name="coinjoin_change_level_confirmation">Are you sure you want to change the privacy level?</string>
<string name="coinjoin_stop_mixing_title">Are you sure you want to stop mixing?</string>
<string name="coinjoin_stop_mixing_message">Any funds that have been mixed will be combined with your unmixed funds</string>
Expand Down
4 changes: 2 additions & 2 deletions wallet/src/de/schildbach/wallet/data/CoinJoinConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ open class CoinJoinConfig @Inject constructor(
}

fun observeMode(): Flow<CoinJoinMode> {
return observe(COINJOIN_MODE).filterNotNull().map { mode -> CoinJoinMode.valueOf(mode!!) }
return observe(COINJOIN_MODE).map { mode -> mode?.let { CoinJoinMode.valueOf(mode) } ?: CoinJoinMode.NONE }
}

suspend fun getMode(): CoinJoinMode {
return get(COINJOIN_MODE).let { CoinJoinMode.valueOf(it!!) }
return get(COINJOIN_MODE).let { mode -> mode?.let { CoinJoinMode.valueOf(it) } ?: CoinJoinMode.NONE }
}

suspend fun setMode(mode: CoinJoinMode) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2023 Dash Core Group.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

package de.schildbach.wallet.payments

import org.bitcoinj.coinjoin.CoinJoinCoinSelector
import org.bitcoinj.core.Coin
import org.bitcoinj.core.TransactionOutput
import org.bitcoinj.wallet.CoinSelection
import org.bitcoinj.wallet.Wallet

class MaxOutputAmountCoinJoinCoinSelector(wallet: Wallet): MaxOutputAmountCoinSelector() {

private val coinJoinCoinSelector = CoinJoinCoinSelector(wallet)

override fun select(target: Coin, candidates: MutableList<TransactionOutput>): CoinSelection {
val coinJoinCandidates = coinJoinCoinSelector.select(target, candidates)
return super.select(coinJoinCandidates.valueGathered, coinJoinCandidates.gathered.toMutableList())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import org.bitcoinj.core.VarInt
import org.bitcoinj.wallet.CoinSelection
import org.bitcoinj.wallet.CoinSelector

class MaxOutputAmountCoinSelector: CoinSelector {
open class MaxOutputAmountCoinSelector: CoinSelector {
companion object {
private const val TX_OUTPUT_SIZE = 34 // estimated size for a typical transaction output
private const val TX_INPUT_SIZE = 148 // estimated size for a typical compact pubkey transaction input
Expand Down
12 changes: 11 additions & 1 deletion wallet/src/de/schildbach/wallet/payments/SendCoinsTaskRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package de.schildbach.wallet.payments

import androidx.annotation.VisibleForTesting
import androidx.lifecycle.viewModelScope
import de.schildbach.wallet.WalletApplication
import de.schildbach.wallet.data.CoinJoinConfig
import de.schildbach.wallet.data.PaymentIntent
Expand All @@ -26,6 +27,7 @@ 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.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
Expand Down Expand Up @@ -260,7 +262,7 @@ class SendCoinsTaskRunner @Inject constructor(
sendRequest.ensureMinRequiredFee = forceEnsureMinRequiredFee
sendRequest.signInputs = signInputs

val walletBalance = wallet.getBalance(MaxOutputAmountCoinSelector())
val walletBalance = wallet.getBalance(getMaxOutputCoinSelector())
sendRequest.emptyWallet = mayEditAmount && walletBalance == paymentIntent.amount

return sendRequest
Expand Down Expand Up @@ -296,6 +298,14 @@ class SendCoinsTaskRunner @Inject constructor(
ZeroConfCoinSelector.get()
}

private fun getMaxOutputCoinSelector() = if (coinJoinSend) {
// mixed only
MaxOutputAmountCoinJoinCoinSelector(walletData.wallet!!)
} else {
// collect all coins, mixed and unmixed
MaxOutputAmountCoinSelector()
}

@Throws(LeftoverBalanceException::class)
suspend fun sendCoins(
sendRequest: SendRequest,
Expand Down
55 changes: 50 additions & 5 deletions wallet/src/de/schildbach/wallet/service/BlockchainServiceImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.DefaultRiskAnalysis;
import org.bitcoinj.wallet.Wallet;
import org.bitcoinj.wallet.WalletEx;
import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension;
import org.dash.wallet.common.Configuration;
import org.dash.wallet.common.data.WalletUIConfig;
Expand All @@ -91,6 +92,8 @@
import org.dash.wallet.common.transactions.filters.NotFromAddressTxFilter;
import org.dash.wallet.common.transactions.filters.TransactionFilter;
import org.dash.wallet.common.transactions.TransactionUtils;
import org.dash.wallet.common.util.FlowExtKt;
import org.dash.wallet.common.util.MonetaryExtKt;
import org.dash.wallet.integrations.crowdnode.api.CrowdNodeAPIConfirmationHandler;
import org.dash.wallet.integrations.crowdnode.api.CrowdNodeBlockchainApi;
import org.dash.wallet.integrations.crowdnode.transactions.CrowdNodeDepositReceivedResponse;
Expand All @@ -104,6 +107,7 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
Expand Down Expand Up @@ -213,6 +217,8 @@ public class BlockchainServiceImpl extends LifecycleService implements Blockchai

private Executor executor = Executors.newSingleThreadExecutor();
private int syncPercentage = 0; // 0 to 100%
private MixingStatus mixingStatus = MixingStatus.NOT_STARTED;
private boolean isForegroundService = false;

// Risk Analyser for Transactions that is PeerGroup Aware
AllowLockTimeRiskAnalysis.Analyzer riskAnalyzer;
Expand Down Expand Up @@ -855,7 +861,8 @@ public void onReceive(final Context context, final Intent intent) {
builder.append(", ");
builder.append(entry);
}
log.info("History of transactions/blocks: " + builder);
log.info("History of transactions/blocks: " +
(mixingStatus == MixingStatus.MIXING ? "[mixing] " : "") + builder);

// determine if block and transaction activity is idling
boolean isIdle = false;
Expand All @@ -875,7 +882,7 @@ public void onReceive(final Context context, final Intent intent) {
}

// if idling, shutdown service
if (isIdle) {
if (isIdle && mixingStatus != MixingStatus.MIXING) {
log.info("idling detected, stopping service");
stopSelf();
}
Expand Down Expand Up @@ -1015,8 +1022,33 @@ public void onCreate() {
}

void initViewModel() {
blockchainStateDao.load().observe(this, this::handleBlockchainStateNotification);
blockchainStateDao.load().observe(this, (blockchainState) -> handleBlockchainStateNotification(blockchainState, mixingStatus));
registerCrowdNodeConfirmedAddressFilter();

FlowExtKt.observe(coinJoinService.getMixingState(), this, (mixingStatus, continuation) -> {
handleBlockchainStateNotification(blockchainState, mixingStatus);
return null;
});
}

private Notification createCoinJoinNotification(Coin mixedBalance, Coin totalBalance) {
Intent notificationIntent = OnboardingActivity.createIntent(this);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0,
notificationIntent, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

DecimalFormat decimalFormat = new DecimalFormat("0.000");
final String message = getString(
R.string.coinjoin_progress,
decimalFormat.format(MonetaryExtKt.toBigDecimal(mixedBalance)),
decimalFormat.format(MonetaryExtKt.toBigDecimal(totalBalance))
);

return new NotificationCompat.Builder(this,
Constants.NOTIFICATION_CHANNEL_ID_ONGOING)
.setSmallIcon(R.drawable.ic_dash_d_white)
.setContentTitle(getString(R.string.app_name))
.setContentText(message)
.setContentIntent(pendingIntent).build();
}

private void resetMNLists(boolean requestFreshList) {
Expand Down Expand Up @@ -1102,6 +1134,7 @@ private void startForeground() {
//preventing it from being killed in Android 26 or later
Notification notification = createNetworkSyncNotification(null);
startForeground(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification);
isForegroundService = true;
}

@Override
Expand Down Expand Up @@ -1289,7 +1322,7 @@ private void broadcastPeerState(final int numPeers) {
LocalBroadcastManager.getInstance(this).sendBroadcast(broadcast);
}

private void handleBlockchainStateNotification(BlockchainState blockchainState) {
private void handleBlockchainStateNotification(BlockchainState blockchainState, MixingStatus mixingStatus) {
// send this out for the Network Monitor, other activities observe the database
final Intent broadcast = new Intent(ACTION_BLOCKCHAIN_STATE);
broadcast.setPackage(getPackageName());
Expand All @@ -1299,17 +1332,29 @@ private void handleBlockchainStateNotification(BlockchainState blockchainState)
&& blockchainState.getBestChainDate() != null) {
//Handle Ongoing notification state
boolean syncing = blockchainState.getBestChainDate().getTime() < (Utils.currentTimeMillis() - DateUtils.HOUR_IN_MILLIS); //1 hour
if (!syncing && blockchainState.getBestChainHeight() == config.getBestChainHeightEver()) {
if (!syncing && blockchainState.getBestChainHeight() == config.getBestChainHeightEver() && mixingStatus != MixingStatus.MIXING) {
//Remove ongoing notification if blockchain sync finished
stopForeground(true);
isForegroundService = false;
nm.cancel(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC);
} else if (blockchainState.getReplaying() || syncing) {
//Shows ongoing notification when synchronizing the blockchain
Notification notification = createNetworkSyncNotification(blockchainState);
nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification);
} else if (mixingStatus == MixingStatus.MIXING) {
Notification notification = createCoinJoinNotification(
((WalletEx)application.getWallet()).getCoinJoinBalance(),
application.getWallet().getBalance()
);
if (isForegroundService) {
nm.notify(Constants.NOTIFICATION_ID_BLOCKCHAIN_SYNC, notification);
} else {
startForeground();
}
}
}
this.blockchainState = blockchainState;
this.mixingStatus = mixingStatus;
}

private int percentageSync() {
Expand Down
Loading

0 comments on commit e6b6144

Please sign in to comment.