Skip to content

Commit

Permalink
feat(coinjoin): group mixing transactions on home screen (#1272)
Browse files Browse the repository at this point in the history
* feat: add grouping of CoinJoin transactions

* feat: create multiple groups

* fix: add method to obtain wrappers
  • Loading branch information
HashEngineering authored Apr 2, 2024
1 parent e89bf41 commit 17e6983
Show file tree
Hide file tree
Showing 18 changed files with 331 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension
import org.bitcoinj.wallet.authentication.AuthenticationKeyUsage
import org.dash.wallet.common.services.LeftoverBalanceException
import org.dash.wallet.common.transactions.TransactionWrapper
import org.dash.wallet.common.transactions.TransactionWrapperFactory
import org.dash.wallet.common.transactions.filters.TransactionFilter
import kotlin.jvm.Throws

Expand Down Expand Up @@ -59,7 +60,7 @@ interface WalletDataProvider {

fun getTransactions(vararg filters: TransactionFilter): Collection<Transaction>

fun wrapAllTransactions(vararg wrappers: TransactionWrapper): Collection<TransactionWrapper>
fun wrapAllTransactions(vararg wrappers: TransactionWrapperFactory): Collection<TransactionWrapper>

fun attachOnWalletWipedListener(listener: () -> Unit)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright 2024 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 org.dash.wallet.common.transactions

import org.bitcoinj.core.Transaction

interface TransactionWrapperFactory {
fun tryInclude(tx: Transaction): Pair<Boolean, TransactionWrapper?>
val wrappers: List<TransactionWrapper>
}
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,7 @@ open class CrowdNodeBlockchainApi @Inject constructor(

open fun getFullSignUpTxSet(): FullCrowdNodeSignUpTxSet? {
val wrappedTransactions = walletData.wrapAllTransactions(
FullCrowdNodeSignUpTxSet(params, walletData.transactionBag)
FullCrowdNodeSignUpTxSetFactory(params, walletData.transactionBag)
)
return wrappedTransactions.firstOrNull { it is FullCrowdNodeSignUpTxSet } as? FullCrowdNodeSignUpTxSet
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.dash.wallet.integrations.crowdnode.transactions

import org.bitcoinj.core.NetworkParameters
import org.bitcoinj.core.Transaction
import org.bitcoinj.core.TransactionBag
import org.dash.wallet.common.transactions.TransactionWrapper
import org.dash.wallet.common.transactions.TransactionWrapperFactory

class FullCrowdNodeSignUpTxSetFactory(params: NetworkParameters, transactionBag: TransactionBag) :
TransactionWrapperFactory {
private val wrapper = FullCrowdNodeSignUpTxSet(params, transactionBag)
override val wrappers = listOf(wrapper)

override fun tryInclude(tx: Transaction): Pair<Boolean, TransactionWrapper?> {
return Pair(wrapper.tryInclude(tx), wrapper)
}
}
13 changes: 13 additions & 0 deletions wallet/res/drawable/ic_coinjoin_mixing_group.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="36dp"
android:height="36dp"
android:viewportWidth="36"
android:viewportHeight="36">
<path
android:pathData="M0,18C0,8.059 8.059,0 18,0C27.941,0 36,8.059 36,18C36,27.941 27.941,36 18,36C8.059,36 0,27.941 0,18Z"
android:fillColor="#008DE4"
android:fillAlpha="0.1"/>
<path
android:pathData="M18,28C16.634,28 15.346,27.739 14.137,27.216C12.935,26.693 11.873,25.971 10.951,25.049C10.029,24.128 9.307,23.065 8.784,21.863C8.261,20.654 8,19.366 8,18C8,16.634 8.261,15.35 8.784,14.147C9.307,12.938 10.026,11.873 10.941,10.951C11.863,10.029 12.925,9.307 14.127,8.784C15.337,8.261 16.624,8 17.99,8C19.356,8 20.644,8.261 21.853,8.784C23.062,9.307 24.128,10.029 25.049,10.951C25.971,11.873 26.693,12.938 27.216,14.147C27.739,15.35 28,16.634 28,18C28,19.366 27.739,20.654 27.216,21.863C26.693,23.065 25.971,24.128 25.049,25.049C24.128,25.971 23.062,26.693 21.853,27.216C20.65,27.739 19.366,28 18,28ZM18,26.333C19.157,26.333 20.239,26.118 21.245,25.686C22.252,25.255 23.137,24.66 23.902,23.902C24.667,23.137 25.261,22.252 25.686,21.245C26.118,20.239 26.333,19.157 26.333,18C26.333,16.843 26.118,15.761 25.686,14.755C25.255,13.748 24.657,12.863 23.892,12.098C23.134,11.333 22.248,10.739 21.235,10.314C20.229,9.882 19.147,9.667 17.99,9.667C16.833,9.667 15.752,9.882 14.745,10.314C13.739,10.739 12.856,11.333 12.098,12.098C11.34,12.863 10.745,13.748 10.314,14.755C9.889,15.761 9.676,16.843 9.676,18C9.676,19.157 9.889,20.239 10.314,21.245C10.745,22.252 11.34,23.137 12.098,23.902C12.863,24.66 13.748,25.255 14.755,25.686C15.761,26.118 16.843,26.333 18,26.333ZM15.902,15.941C15.431,15.941 15.029,15.774 14.696,15.441C14.363,15.101 14.196,14.699 14.196,14.235C14.196,13.765 14.363,13.363 14.696,13.029C15.036,12.696 15.438,12.529 15.902,12.529C16.366,12.529 16.765,12.696 17.098,13.029C17.438,13.363 17.608,13.765 17.608,14.235C17.608,14.699 17.438,15.101 17.098,15.441C16.765,15.774 16.366,15.941 15.902,15.941ZM20.088,15.941C19.624,15.941 19.222,15.774 18.882,15.441C18.549,15.101 18.382,14.699 18.382,14.235C18.382,13.765 18.549,13.363 18.882,13.029C19.222,12.696 19.624,12.529 20.088,12.529C20.559,12.529 20.961,12.696 21.294,13.029C21.628,13.363 21.794,13.765 21.794,14.235C21.794,14.699 21.628,15.101 21.294,15.441C20.961,15.774 20.559,15.941 20.088,15.941ZM13.814,19.696C13.337,19.696 12.931,19.529 12.598,19.196C12.265,18.856 12.098,18.454 12.098,17.99C12.098,17.526 12.265,17.124 12.598,16.784C12.938,16.444 13.343,16.274 13.814,16.274C14.271,16.274 14.667,16.444 15,16.784C15.34,17.124 15.51,17.526 15.51,17.99C15.51,18.454 15.34,18.856 15,19.196C14.667,19.529 14.271,19.696 13.814,19.696ZM18,19.696C17.536,19.696 17.134,19.529 16.794,19.196C16.454,18.856 16.284,18.454 16.284,17.99C16.284,17.526 16.454,17.124 16.794,16.784C17.134,16.444 17.536,16.274 18,16.274C18.458,16.274 18.856,16.444 19.196,16.784C19.536,17.124 19.706,17.526 19.706,17.99C19.706,18.454 19.536,18.856 19.196,19.196C18.856,19.529 18.458,19.696 18,19.696ZM22.186,19.696C21.722,19.696 21.32,19.529 20.98,19.196C20.647,18.856 20.48,18.454 20.48,17.99C20.48,17.526 20.647,17.124 20.98,16.784C21.32,16.444 21.722,16.274 22.186,16.274C22.65,16.274 23.049,16.444 23.382,16.784C23.722,17.124 23.892,17.526 23.892,17.99C23.892,18.454 23.722,18.856 23.382,19.196C23.049,19.529 22.65,19.696 22.186,19.696ZM15.902,23.451C15.431,23.451 15.029,23.284 14.696,22.951C14.363,22.611 14.196,22.209 14.196,21.745C14.196,21.281 14.363,20.882 14.696,20.549C15.036,20.209 15.438,20.039 15.902,20.039C16.366,20.039 16.765,20.209 17.098,20.549C17.438,20.882 17.608,21.281 17.608,21.745C17.608,22.209 17.438,22.611 17.098,22.951C16.765,23.284 16.366,23.451 15.902,23.451ZM20.088,23.451C19.624,23.451 19.222,23.284 18.882,22.951C18.549,22.611 18.382,22.209 18.382,21.745C18.382,21.281 18.549,20.882 18.882,20.549C19.222,20.209 19.624,20.039 20.088,20.039C20.559,20.039 20.961,20.209 21.294,20.549C21.628,20.882 21.794,21.281 21.794,21.745C21.794,22.209 21.628,22.611 21.294,22.951C20.961,23.284 20.559,23.451 20.088,23.451Z"
android:fillColor="#008DE4"/>
</vector>
2 changes: 2 additions & 0 deletions wallet/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,8 @@
<string name="coinjoin_description_3">Turning this feature on will result a higher battery usage</string>
<string name="coinjoin_mixing_level_title">Select mixing level</string>
<string name="coinjoin_mixing_level_subtitle">You can change or stop the mixing level at any time</string>
<string name="coinjoin_mixing_transactions">Mixing Transactions</string>
<string name="coinjoin_transaction_group">These are mixing related transactions.</string>
<string name="intermediate">Intermediate</string>
<string name="advanced">Advanced</string>
<string name="intermediate_level_card_decs">Advanced users who have a very high level of technical expertise can determine your transaction history</string>
Expand Down
5 changes: 3 additions & 2 deletions wallet/src/de/schildbach/wallet/WalletApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@
import org.dash.wallet.common.services.LeftoverBalanceException;
import org.dash.wallet.common.services.TransactionMetadataProvider;
import org.dash.wallet.common.services.analytics.AnalyticsService;
import org.dash.wallet.common.transactions.TransactionWrapperFactory;
import org.dash.wallet.common.transactions.filters.TransactionFilter;
import org.dash.wallet.common.transactions.TransactionWrapper;
import org.dash.wallet.features.exploredash.ExploreSyncWorker;
Expand Down Expand Up @@ -1142,11 +1143,11 @@ public Collection<Transaction> getTransactions(@NonNull TransactionFilter... fil

@NonNull
@Override
public Collection<TransactionWrapper> wrapAllTransactions(@NonNull TransactionWrapper... wrappers) {
public Collection<TransactionWrapper> wrapAllTransactions(@NonNull TransactionWrapperFactory... wrapperFactories) {
org.bitcoinj.core.Context.propagate(Constants.CONTEXT);
return TransactionWrapperHelper.INSTANCE.wrapTransactions(
wallet.getTransactions(true),
wrappers
wrapperFactories
);
}

Expand Down
1 change: 0 additions & 1 deletion wallet/src/de/schildbach/wallet/service/CoinJoinService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,6 @@ class CoinJoinMixingService @Inject constructor(
log.info(
"coinjoin-state: $mode, $timeSkew ms, $hasAnonymizableBalance, $networkStatus, synced: ${blockchainState.isSynced()}, ${blockChain != null}"
)
// log.info("coinjoin-Current timeskew: ${getCurrentTimeSkew()}")
this.networkStatus = networkStatus
this.hasAnonymizableBalance = hasAnonymizableBalance
this.blockchainState = blockchainState
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ package de.schildbach.wallet.transactions
import org.bitcoinj.core.Transaction
import org.bitcoinj.core.TransactionBag
import org.dash.wallet.common.transactions.TransactionWrapper
import org.dash.wallet.common.transactions.TransactionWrapperFactory
import org.slf4j.LoggerFactory

object TransactionWrapperHelper {
private val log = LoggerFactory.getLogger(TransactionWrapperHelper::class.java)
fun wrapTransactions(
transactions: Set<Transaction?>,
vararg wrappers: TransactionWrapper
vararg wrapperFactories: TransactionWrapperFactory
): Collection<TransactionWrapper> {
val wrappedTransactions = ArrayList<TransactionWrapper>()

for (transaction in transactions) {
if (transaction == null) {
continue
Expand All @@ -39,14 +41,18 @@ object TransactionWrapperHelper {
override fun getValue(bag: TransactionBag) = transaction.getValue(bag)
}

if (wrappers.isNotEmpty()) {
for (wrapper in wrappers) {
if (wrapper.tryInclude(transaction)) {
if (wrapperFactories.isNotEmpty()) {
var added = false
for (wrapperFactory in wrapperFactories) {
val (included, wrapper) = wrapperFactory.tryInclude(transaction)
if (included && wrapper != null) {
if (!wrappedTransactions.contains(wrapper)) {
wrappedTransactions.add(wrapper)
}
break
added = true
}
}
if (!added) {
wrappedTransactions.add(anonWrapper)
}
} else {
Expand All @@ -56,4 +62,4 @@ object TransactionWrapperHelper {

return wrappedTransactions
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/*
* Copyright 2024 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.transactions.coinjoin

import org.bitcoinj.coinjoin.utils.CoinJoinTransactionType
import org.bitcoinj.core.Transaction
import org.bitcoinj.script.Script
import org.bitcoinj.script.ScriptPattern
import org.bitcoinj.wallet.WalletEx
import org.dash.wallet.common.transactions.filters.TransactionFilter

open class CoinJoinTxFilter(private val wallet: WalletEx, val type: CoinJoinTransactionType) : TransactionFilter {
override fun matches(tx: Transaction): Boolean {
return CoinJoinTransactionType.fromTx(tx, wallet) == type
}
}

class CreateDenominationTxFilter(wallet: WalletEx) : CoinJoinTxFilter(
wallet,
CoinJoinTransactionType.CreateDenomination
)
class MakeCollateralTxFilter(wallet: WalletEx) : CoinJoinTxFilter(wallet, CoinJoinTransactionType.MakeCollateralInputs)
class MixingFeeTxFilter(wallet: WalletEx) : CoinJoinTxFilter(wallet, CoinJoinTransactionType.MixingFee)
class MixingTxFilter(wallet: WalletEx) : CoinJoinTxFilter(wallet, CoinJoinTransactionType.Mixing)
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package de.schildbach.wallet.transactions.coinjoin

import org.bitcoinj.core.*
import org.bitcoinj.wallet.WalletEx
import org.dash.wallet.common.transactions.TransactionComparator
import org.dash.wallet.common.transactions.TransactionWrapper
import org.dash.wallet.common.transactions.filters.TransactionFilter
import org.slf4j.LoggerFactory

open class CoinJoinMixingTxSet(
private val networkParams: NetworkParameters,
private val wallet: WalletEx
) : TransactionWrapper {
private val log = LoggerFactory.getLogger(CoinJoinMixingTxSet::class.java)
private var isFinished = false

private val coinjoinTxFilters = mutableListOf(
CreateDenominationTxFilter(wallet),
MakeCollateralTxFilter(wallet),
MixingFeeTxFilter(wallet),
MixingTxFilter(wallet)
)

private val matchedFilters = mutableListOf<TransactionFilter>()
override val transactions = sortedSetOf(TransactionComparator())

override fun tryInclude(tx: Transaction): Boolean {
if (isFinished || transactions.any { it.txId == tx.txId }) {
return false
}

val matchedFilter = coinjoinTxFilters.firstOrNull { it.matches(tx) }

if (matchedFilter != null) {
transactions.add(tx)
matchedFilters.add(matchedFilter)
return true
}

return false
}

override fun getValue(bag: TransactionBag): Coin {
var result = Coin.ZERO

for (tx in transactions) {
val value = tx.getValue(bag)
result = result.add(value)
}

return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/*
* Copyright 2024 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.transactions.coinjoin

import de.schildbach.wallet.ui.transactions.TxResourceMapper

import android.text.format.DateUtils
import androidx.annotation.StringRes
import de.schildbach.wallet_test.R
import org.bitcoinj.coinjoin.utils.CoinJoinTransactionType
import org.bitcoinj.core.Transaction
import org.bitcoinj.core.TransactionBag
import org.bitcoinj.wallet.WalletEx

class CoinJoinTxResourceMapper: TxResourceMapper() {
@StringRes
override fun getTransactionTypeName(tx: Transaction, bag: TransactionBag): Int {
if ((tx.type != Transaction.Type.TRANSACTION_NORMAL &&
tx.type != Transaction.Type.TRANSACTION_UNKNOWN) ||
tx.confidence.hasErrors() ||
tx.isCoinBase
) {
return super.getTransactionTypeName(tx, bag)
}

return when (CoinJoinTransactionType.fromTx(tx, bag as WalletEx)) {
CoinJoinTransactionType.CreateDenomination -> R.string.transaction_row_status_coinjoin_create_denominations
CoinJoinTransactionType.Mixing -> R.string.transaction_row_status_coinjoin_mixing
CoinJoinTransactionType.MixingFee -> R.string.transaction_row_status_coinjoin_mixing_fee
CoinJoinTransactionType.MakeCollateralInputs -> R.string.transaction_row_status_coinjoin_make_collateral
else -> super.getTransactionTypeName(tx, bag)
}
}

override fun getDateTimeFormat(): Int {
return DateUtils.FORMAT_SHOW_TIME
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* Copyright 2024 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.transactions.coinjoin

import org.bitcoinj.coinjoin.utils.CoinJoinTransactionType
import org.bitcoinj.core.NetworkParameters
import org.bitcoinj.core.Transaction
import org.bitcoinj.wallet.WalletEx
import org.dash.wallet.common.transactions.TransactionWrapper
import org.dash.wallet.common.transactions.TransactionWrapperFactory
import java.time.Instant
import java.time.LocalDateTime
import java.time.ZoneId

class CoinJoinTxWrapperFactory(val params: NetworkParameters, val wallet: WalletEx) : TransactionWrapperFactory {
private val wrapperMap = hashMapOf<Long, CoinJoinMixingTxSet>()
override val wrappers: List<TransactionWrapper>
get() = wrapperMap.values.toList()

override fun tryInclude(tx: Transaction): Pair<Boolean, TransactionWrapper?> {
return when (CoinJoinTransactionType.fromTx(tx, wallet)) {
CoinJoinTransactionType.None, CoinJoinTransactionType.Send -> { Pair(false, null) }
else -> {
val instant = Instant.ofEpochMilli(tx.updateTime.time)
val localDateTime = LocalDateTime.ofInstant(instant, ZoneId.systemDefault())
val startOfDay = localDateTime.toLocalDate().atStartOfDay(ZoneId.systemDefault())
val startOfDayTimestamp = startOfDay.toInstant().toEpochMilli()
val wrapper = wrapperMap[startOfDayTimestamp]
if (wrapper != null) {
Pair(wrapper.tryInclude(tx), wrapper)
} else {
val newWrapper = CoinJoinMixingTxSet(params, wallet)
val included = newWrapper.tryInclude(tx)
wrapperMap[startOfDayTimestamp] = newWrapper
Pair(included, newWrapper)
}
}
}
}
}
Loading

0 comments on commit 17e6983

Please sign in to comment.