diff --git a/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt b/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt index 489f5c63e..1d9648452 100644 --- a/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt +++ b/wallet/src/de/schildbach/wallet/service/platform/PlatformBroadcastService.kt @@ -17,28 +17,40 @@ package de.schildbach.wallet.service.platform +import com.google.common.base.Preconditions import de.schildbach.wallet.database.entity.DashPayContactRequest import de.schildbach.wallet.database.entity.DashPayProfile import de.schildbach.wallet.security.SecurityGuard import de.schildbach.wallet.ui.dashpay.PlatformRepo import org.bitcoinj.core.Context +import org.bitcoinj.core.ECKey +import org.bitcoinj.core.KeyId +import org.bitcoinj.core.Sha256Hash import org.bitcoinj.evolution.EvolutionContact import org.bouncycastle.crypto.params.KeyParameter import org.dash.wallet.common.WalletDataProvider import org.dash.wallet.common.services.analytics.AnalyticsConstants import org.dash.wallet.common.services.analytics.AnalyticsService import org.dash.wallet.common.services.analytics.AnalyticsTimer +import org.dashj.platform.dashpay.callback.SimpleSignerCallback import org.dashj.platform.dashpay.callback.WalletSignerCallback +import org.dashj.platform.dpp.identifier.Identifier +import org.dashj.platform.dpp.voting.ResourceVoteChoice +import org.dashj.platform.dpp.voting.Vote +import org.dashj.platform.sdk.Purpose import org.dashj.platform.wallet.IdentityVerifyDocument import org.slf4j.Logger import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream import javax.inject.Inject + interface PlatformBroadcastService { suspend fun broadcastUpdatedProfile(dashPayProfile: DashPayProfile, encryptionKey: KeyParameter): DashPayProfile suspend fun sendContactRequest(toUserId: String): DashPayContactRequest suspend fun sendContactRequest(toUserId: String, encryptionKey: KeyParameter): DashPayContactRequest suspend fun broadcastIdentityVerify(username: String, url: String, encryptionKey: KeyParameter?): IdentityVerifyDocument + suspend fun broadcastUsernameVotes(usernames: List, resourceVoteChoices: List, masternodeKeys: List, encryptionKey: KeyParameter?): List } class PlatformDocumentBroadcastService @Inject constructor( @@ -115,6 +127,50 @@ class PlatformDocumentBroadcastService @Inject constructor( return identityVerifyDocument } + override suspend fun broadcastUsernameVotes( + usernames: List, + resourceVoteChoices: List, + masternodeKeys: List, + encryptionKey: KeyParameter? + ): List { + Preconditions.checkArgument(usernames.size == resourceVoteChoices.size) + val votes = arrayListOf() + masternodeKeys.forEach { masternodeKeyBytes -> + // determine identity + val masternodeKey = ECKey.fromPrivate(masternodeKeyBytes) + val votingKeyId = KeyId.fromBytes(masternodeKey.pubKeyHash) + val boas = ByteArrayOutputStream(32 + 20) + val masternodes = walletDataProvider.wallet!!.context.masternodeListManager.masternodeList.getMasternodesByVotingKey(votingKeyId) + masternodes.forEach { masternode -> + try { + boas.write(masternode.proTxHash.bytes) + boas.write(masternodeKey.pubKeyHash) + val idBytes = Sha256Hash.of(boas.toByteArray()) + val identity = platform.identities.get(Identifier.from(idBytes.bytes)) + val votingIdentityPublicKey = identity!!.publicKeys.first { it.purpose == Purpose.VOTING } + + usernames.forEachIndexed { index, username -> + val resourceVoteChoice = resourceVoteChoices[index] + val vote = platform.names.broadcastVote( + resourceVoteChoice, + username, + masternode.proTxHash, + votingIdentityPublicKey, + SimpleSignerCallback( + mapOf(votingIdentityPublicKey to masternodeKey), + encryptionKey + ) + ) + votes.add(vote) + } + } catch (e: Exception) { + log.info("broadcast username vote failed:", e) + } + } + } + return votes + } + @Throws(Exception::class) override suspend fun broadcastUpdatedProfile(dashPayProfile: DashPayProfile, encryptionKey: KeyParameter): DashPayProfile { log.info("broadcast profile") diff --git a/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt b/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt index eda75f7e3..f868aa0d6 100644 --- a/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt +++ b/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt @@ -1046,7 +1046,8 @@ class PlatformSynchronizationService @Inject constructor( checkUsernameVotingStatus() try { val contestedNames = platform.platform.names.getContestedNames() - val myIdentifier = platformRepo.blockchainIdentity.uniqueIdentifier + // usernameRequestDao.clear() + // val myIdentifier = platformRepo.blockchainIdentity.uniqueIdentifier for (name in contestedNames) { try { val voteContender = platformRepo.getVoteContenders(name) diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesOperation.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesOperation.kt new file mode 100644 index 000000000..167abd709 --- /dev/null +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesOperation.kt @@ -0,0 +1,71 @@ +/* + * 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 . + */ + +package de.schildbach.wallet.ui.dashpay.work + +import android.annotation.SuppressLint +import android.app.Application +import androidx.work.* +import de.schildbach.wallet.Constants +import de.schildbach.wallet.security.SecurityGuard +import org.bitcoinj.core.ECKey +import org.dashj.platform.dpp.voting.ResourceVoteChoice +import org.slf4j.LoggerFactory + +class BroadcastUsernameVotesOperation(val application: Application) { + + class BroadcastUsernameVotesOperationException(message: String) : java.lang.Exception(message) + + companion object { + private val log = LoggerFactory.getLogger(BroadcastUsernameVotesOperation::class.java) + + private const val WORK_NAME = "BroadcastUsernameVotes.WORK#" + + fun uniqueWorkName(usernames: String) = WORK_NAME + usernames + } + + private val workManager: WorkManager = WorkManager.getInstance(application) + + /** + * Gets the list of all BroadcastUsernameVotesOperation WorkInfo's + */ + val allOperationsData = workManager.getWorkInfosByTagLiveData(BroadcastUsernameVotesWorker::class.qualifiedName!!) + + @SuppressLint("EnqueueWork") + fun create(usernames: List, voteChoices: List, masternodeKeys: List): WorkContinuation { + + val password = SecurityGuard().retrievePassword() + val verifyIdentityWorker = OneTimeWorkRequestBuilder() + .setInputData( + workDataOf( + BroadcastUsernameVotesWorker.KEY_PASSWORD to password, + BroadcastUsernameVotesWorker.KEY_USERNAMES to usernames.toTypedArray(), + BroadcastUsernameVotesWorker.KEY_VOTE_CHOICES to voteChoices.map { it.toString() }.toTypedArray(), + BroadcastUsernameVotesWorker.KEY_MASTERNODE_KEYS to masternodeKeys.map { + ECKey.fromPrivate(it).getPrivateKeyAsWiF(Constants.NETWORK_PARAMETERS) + }.toTypedArray() + ) + ) + .addTag("usernames:$usernames") + .build() + log.info("creating BroadcastUsernameVotesOperation({}, {})", usernames, voteChoices) + return WorkManager.getInstance(application) + .beginUniqueWork(uniqueWorkName(usernames.joinToString(",")), + ExistingWorkPolicy.KEEP, + verifyIdentityWorker) + } +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt new file mode 100644 index 000000000..40ad9d8cf --- /dev/null +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt @@ -0,0 +1,105 @@ +/* + * 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 . + */ +package de.schildbach.wallet.ui.dashpay.work + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.Data +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import com.bumptech.glide.load.engine.Resource +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import de.schildbach.wallet.Constants +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.service.platform.PlatformBroadcastService +import de.schildbach.wallet.service.platform.PlatformSyncService +import de.schildbach.wallet.ui.dashpay.PlatformRepo +import org.bitcoinj.core.DumpedPrivateKey +import org.bitcoinj.crypto.KeyCrypterException +import org.bouncycastle.crypto.params.KeyParameter +import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.common.services.analytics.AnalyticsService +import org.dashj.platform.dpp.voting.ResourceVoteChoice +import org.dashj.platform.sdk.ContestedDocumentResourceVotePoll +import org.slf4j.LoggerFactory + +@HiltWorker +class BroadcastUsernameVotesWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted parameters: WorkerParameters, + val analytics: AnalyticsService, + val platformBroadcastService: PlatformBroadcastService, + val platformSyncService: PlatformSyncService, + val walletDataProvider: WalletDataProvider +) : BaseWorker(context, parameters) { + + companion object { + private val log = LoggerFactory.getLogger(BroadcastUsernameVotesWorker::class.java) + + const val KEY_PASSWORD = "BroadcastUsernameVotesWorker.PASSWORD" + const val KEY_USERNAMES = "BroadcastUsernameVotesWorker.USERNAMES" + const val KEY_VOTE_CHOICES = "BroadcastUsernameVotesWorker.VOTE_CHOICES" + const val KEY_MASTERNODE_KEYS = "BroadcastUsernameVotesWorker.MASTERNODE_KEYS" + } + + override suspend fun doWorkWithBaseProgress(): Result { + val password = inputData.getString(KEY_PASSWORD) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_PASSWORD parameter")) + val usernames = inputData.getStringArray(KEY_USERNAMES) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_USERNAMES parameter")) + val voteChoices = inputData.getStringArray(KEY_VOTE_CHOICES) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_VOTE_CHOICES parameter")) + val masternodeKeys = inputData.getStringArray(KEY_MASTERNODE_KEYS) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_MASTERNODE_KEYS parameter")) + + val encryptionKey: KeyParameter + try { + encryptionKey = walletDataProvider.wallet!!.keyCrypter!!.deriveKey(password) + } catch (ex: KeyCrypterException) { + analytics.logError(ex, "Broadcast Username Vote: failed to derive encryption key") + val msg = formatExceptionMessage("derive encryption key", ex) + return Result.failure(workDataOf(KEY_ERROR_MESSAGE to msg)) + } + + return try { + log.info("creating BroadcastUsernameVotesWorker({}, {})", usernames, voteChoices) + + val votes = platformBroadcastService.broadcastUsernameVotes( + usernames.toList(), + voteChoices.map { ResourceVoteChoice.from(it) }, + masternodeKeys.map { DumpedPrivateKey.fromBase58(Constants.NETWORK_PARAMETERS, it).key.privKeyBytes }, + encryptionKey + ) + // this will update the DB and trigger observers + platformSyncService.updateUsernameRequestsWithVotes() + Result.success( + workDataOf( + KEY_USERNAMES to if(votes.isNotEmpty()) { + votes.map { (it.resourceVote.votePoll as? ContestedDocumentResourceVotePoll)?.index_values?.get(1) ?: "null" }.toTypedArray() + } else { + listOf("").toTypedArray() + }, + ) + ) + } catch (ex: Exception) { + analytics.logError(ex, "Username Voting: failed to broadcast votes") + Result.failure(workDataOf( + KEY_ERROR_MESSAGE to formatExceptionMessage("broadcast username vote", ex))) + } + } +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt index 4d0c17d34..a5c3c6861 100644 --- a/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt +++ b/wallet/src/de/schildbach/wallet/ui/username/UsernameRequestsViewModel.kt @@ -21,6 +21,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import de.schildbach.wallet.Constants +import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.database.dao.ImportedMasternodeKeyDao import de.schildbach.wallet.database.dao.UsernameRequestDao import de.schildbach.wallet.database.dao.UsernameVoteDao @@ -29,6 +30,7 @@ import de.schildbach.wallet.database.entity.UsernameRequest import de.schildbach.wallet.database.entity.UsernameVote import de.schildbach.wallet.service.platform.PlatformSyncService import de.schildbach.wallet.ui.dashpay.utils.DashPayConfig +import de.schildbach.wallet.ui.dashpay.work.BroadcastUsernameVotesOperation import de.schildbach.wallet.ui.username.adapters.UsernameRequestGroupView import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -57,6 +59,8 @@ import org.bitcoinj.wallet.AuthenticationKeyChain import org.bitcoinj.wallet.authentication.AuthenticationKeyStatus import org.bitcoinj.wallet.authentication.AuthenticationKeyUsage import org.dash.wallet.common.WalletDataProvider +import org.dashj.platform.dpp.identifier.Identifier +import org.dashj.platform.dpp.voting.ResourceVoteChoice import org.dashj.platform.sdk.platform.Names import java.util.UUID import javax.inject.Inject @@ -92,7 +96,8 @@ class UsernameRequestsViewModel @Inject constructor( private val usernameVoteDao: UsernameVoteDao, private val importedMasternodeKeyDao: ImportedMasternodeKeyDao, private val platformSyncService: PlatformSyncService, - private val walletDataProvider: WalletDataProvider + private val walletDataProvider: WalletDataProvider, + private val walletApplication: WalletApplication ): ViewModel() { private val workerJob = SupervisorJob() private val viewModelWorkerScope = CoroutineScope(Dispatchers.IO + workerJob) @@ -243,6 +248,12 @@ class UsernameRequestsViewModel @Inject constructor( viewModelScope.launch { usernameRequestDao.getRequest(requestId)?.let { request -> + BroadcastUsernameVotesOperation(walletApplication).create( + listOf(request.normalizedLabel), + listOf(ResourceVoteChoice.towardsIdentity(Identifier.from(request.identity))), + masternodes.value.map { it.votingPrivateKey } + ).enqueue() + usernameRequestDao.removeApproval(request.username) usernameRequestDao.update(request.copy(votes = request.votes + keysAmount, isApproved = true)) _uiState.update { it.copy(voteSubmitted = true) } @@ -264,6 +275,11 @@ class UsernameRequestsViewModel @Inject constructor( viewModelScope.launch { usernameRequestDao.getRequest(requestId)?.let { request -> + BroadcastUsernameVotesOperation(walletApplication).create( + listOf(request.normalizedLabel), + listOf(ResourceVoteChoice.abstain()), + masternodes.value.map { it.votingPrivateKey } + ).enqueue() usernameRequestDao.update(request.copy(votes = request.votes - keysAmount, isApproved = false)) _uiState.update { it.copy(voteCancelled = true) } usernameVoteDao.insert( @@ -296,6 +312,21 @@ class UsernameRequestsViewModel @Inject constructor( keysAmount ) _uiState.update { it.copy(voteSubmitted = true) } + + val usernames = arrayListOf() + val voteChoices = arrayListOf() + requestIds.forEach { + usernameRequestDao.getRequest(it)?.let { request -> + usernames.add(request.normalizedLabel) + voteChoices.add(ResourceVoteChoice.towardsIdentity(Identifier.from(request.identity))) + } + } + + BroadcastUsernameVotesOperation(walletApplication).create( + usernames, + voteChoices, + masternodes.value.map { it.votingPrivateKey } + ).enqueue() } } @@ -472,23 +503,28 @@ class UsernameRequestsViewModel @Inject constructor( } } - fun block(requestId: String) { + fun block(username: String) { if (keysAmount == 0) { return } viewModelScope.launch { - usernameRequestDao.getRequest(requestId)?.let { request -> - usernameRequestDao.update(request.copy(lockVotes = request.lockVotes + keysAmount, isApproved = true)) + // usernameRequestDao.getRequest(requestId)?.let { request -> + BroadcastUsernameVotesOperation(walletApplication).create( + listOf(username), + listOf(ResourceVoteChoice.lock()), + masternodes.value.map { it.votingPrivateKey } + ).enqueue() + //usernameRequestDao.update(request.copy(lockVotes = request.lockVotes + keysAmount, isApproved = true)) _uiState.update { it.copy(voteSubmitted = true) } usernameVoteDao.insert( UsernameVote( - request.username, - request.identity, + username, + "", UsernameVote.LOCK ) ) } - } + //} } }