From 92432af7e98b43d83a0b76d9b61185cd459d4b87 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Thu, 26 Dec 2024 07:50:57 -0800 Subject: [PATCH 01/26] refactor: move BaseWorker --- .../wallet/{ui/dashpay => service}/work/BaseWorker.kt | 2 +- .../wallet/ui/dashpay/work/BroadcastIdentityVerifyWorker.kt | 4 +--- .../ui/dashpay/work/BroadcastUsernameVotesOperation.kt | 1 + .../wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt | 2 +- .../de/schildbach/wallet/ui/dashpay/work/DeriveKeyWorker.kt | 1 + .../wallet/ui/dashpay/work/GetUsernameVotingResultsWorker.kt | 5 +---- .../wallet/ui/dashpay/work/SendContactRequestOperation.kt | 1 + .../wallet/ui/dashpay/work/SendContactRequestWorker.kt | 2 +- .../schildbach/wallet/ui/dashpay/work/SendInviteOperation.kt | 1 + .../de/schildbach/wallet/ui/dashpay/work/SendInviteWorker.kt | 1 + .../wallet/ui/dashpay/work/SingleWorkStatusLiveData.kt | 1 + .../schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt | 1 + 12 files changed, 12 insertions(+), 10 deletions(-) rename wallet/src/de/schildbach/wallet/{ui/dashpay => service}/work/BaseWorker.kt (97%) diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BaseWorker.kt b/wallet/src/de/schildbach/wallet/service/work/BaseWorker.kt similarity index 97% rename from wallet/src/de/schildbach/wallet/ui/dashpay/work/BaseWorker.kt rename to wallet/src/de/schildbach/wallet/service/work/BaseWorker.kt index 15ea4092c2..9931f7bc50 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BaseWorker.kt +++ b/wallet/src/de/schildbach/wallet/service/work/BaseWorker.kt @@ -1,4 +1,4 @@ -package de.schildbach.wallet.ui.dashpay.work +package de.schildbach.wallet.service.work import android.content.Context import androidx.work.CoroutineWorker diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastIdentityVerifyWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastIdentityVerifyWorker.kt index 2ef02b51a4..c59076881b 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastIdentityVerifyWorker.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastIdentityVerifyWorker.kt @@ -18,14 +18,12 @@ 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 dagger.assisted.Assisted import dagger.assisted.AssistedInject -import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.service.platform.PlatformBroadcastService -import de.schildbach.wallet.ui.dashpay.PlatformRepo +import de.schildbach.wallet.service.work.BaseWorker import org.bitcoinj.crypto.KeyCrypterException import org.bouncycastle.crypto.params.KeyParameter import org.dash.wallet.common.WalletDataProvider diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesOperation.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesOperation.kt index 120db3ed67..1ac44cbf1b 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesOperation.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesOperation.kt @@ -26,6 +26,7 @@ import androidx.work.* import de.schildbach.wallet.Constants import de.schildbach.wallet.livedata.Resource import de.schildbach.wallet.security.SecurityGuard +import de.schildbach.wallet.service.work.BaseWorker import org.bitcoinj.core.ECKey import org.dashj.platform.dpp.voting.ResourceVoteChoice import org.slf4j.LoggerFactory diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt index ed94c7ae18..5fd0393c6c 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/BroadcastUsernameVotesWorker.kt @@ -29,6 +29,7 @@ import de.schildbach.wallet.database.entity.UsernameRequest import de.schildbach.wallet.database.entity.UsernameVote import de.schildbach.wallet.service.platform.PlatformBroadcastService import de.schildbach.wallet.service.platform.PlatformSyncService +import de.schildbach.wallet.service.work.BaseWorker import org.bitcoinj.core.DumpedPrivateKey import org.bitcoinj.crypto.KeyCrypterException import org.bouncycastle.crypto.params.KeyParameter @@ -41,7 +42,6 @@ import org.dashj.platform.dpp.voting.ResourceVote import org.dashj.platform.dpp.voting.ResourceVoteChoice import org.dashj.platform.dpp.voting.TowardsIdentity import org.dashj.platform.dpp.voting.Vote -import org.dashj.platform.sdk.PlatformValue import org.slf4j.LoggerFactory @HiltWorker diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/DeriveKeyWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/DeriveKeyWorker.kt index a41c14488c..f806a8828e 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/DeriveKeyWorker.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/DeriveKeyWorker.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.work.WorkerParameters import androidx.work.workDataOf import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.service.work.BaseWorker import org.bitcoinj.crypto.KeyCrypterException class DeriveKeyWorker(context: Context, parameters: WorkerParameters) diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/GetUsernameVotingResultsWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/GetUsernameVotingResultsWorker.kt index d10fbf38d3..4a54799b1a 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/GetUsernameVotingResultsWorker.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/GetUsernameVotingResultsWorker.kt @@ -22,11 +22,8 @@ import androidx.work.WorkerParameters import androidx.work.workDataOf import dagger.assisted.Assisted import dagger.assisted.AssistedInject -import de.schildbach.wallet.service.platform.PlatformBroadcastService import de.schildbach.wallet.service.platform.PlatformSyncService -import org.bitcoinj.crypto.KeyCrypterException -import org.bouncycastle.crypto.params.KeyParameter -import org.dash.wallet.common.WalletDataProvider +import de.schildbach.wallet.service.work.BaseWorker import org.dash.wallet.common.services.analytics.AnalyticsService /** diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestOperation.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestOperation.kt index e28896e134..35a3e12598 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestOperation.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestOperation.kt @@ -8,6 +8,7 @@ import androidx.lifecycle.switchMap import androidx.work.* import de.schildbach.wallet.livedata.Resource import de.schildbach.wallet.security.SecurityGuard +import de.schildbach.wallet.service.work.BaseWorker import org.dash.wallet.common.services.analytics.AnalyticsService import org.slf4j.LoggerFactory diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestWorker.kt index 42ff3c40ca..a9ec1803c1 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestWorker.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendContactRequestWorker.kt @@ -25,7 +25,7 @@ import dagger.assisted.Assisted import dagger.assisted.AssistedInject import de.schildbach.wallet.WalletApplication import de.schildbach.wallet.service.platform.PlatformBroadcastService -import de.schildbach.wallet.ui.dashpay.PlatformRepo +import de.schildbach.wallet.service.work.BaseWorker import org.bitcoinj.crypto.KeyCrypterException import org.bouncycastle.crypto.params.KeyParameter import org.dash.wallet.common.services.analytics.AnalyticsService diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteOperation.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteOperation.kt index 0b2c82a542..59be2ff72c 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteOperation.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteOperation.kt @@ -23,6 +23,7 @@ import androidx.lifecycle.switchMap import androidx.work.* import de.schildbach.wallet.livedata.Resource import de.schildbach.wallet.security.SecurityGuard +import de.schildbach.wallet.service.work.BaseWorker import org.dash.wallet.common.services.analytics.AnalyticsService import org.slf4j.LoggerFactory diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteWorker.kt index df9a40d138..9d6a19acd9 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteWorker.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SendInviteWorker.kt @@ -29,6 +29,7 @@ import dagger.assisted.AssistedInject import de.schildbach.wallet.Constants import de.schildbach.wallet.database.entity.DashPayProfile import de.schildbach.wallet.data.InvitationLinkData +import de.schildbach.wallet.service.work.BaseWorker import de.schildbach.wallet.ui.dashpay.PlatformRepo import de.schildbach.wallet_test.R import org.bitcoinj.crypto.KeyCrypterException diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SingleWorkStatusLiveData.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SingleWorkStatusLiveData.kt index 63679a04ed..0e0a289ee1 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/SingleWorkStatusLiveData.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/SingleWorkStatusLiveData.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.Observer import androidx.work.WorkInfo import androidx.work.WorkManager import de.schildbach.wallet.livedata.Resource +import de.schildbach.wallet.service.work.BaseWorker abstract class SingleWorkStatusLiveData(val application: Application) : LiveData>(), Observer> { diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt index a866a4679d..d4c5a961d6 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/work/UpdateProfileWorker.kt @@ -34,6 +34,7 @@ import de.schildbach.wallet.ui.dashpay.PlatformRepo import de.schildbach.wallet.ui.dashpay.utils.GoogleDriveService import de.schildbach.wallet.security.SecurityGuard import de.schildbach.wallet.service.platform.PlatformBroadcastService +import de.schildbach.wallet.service.work.BaseWorker import org.bitcoinj.crypto.KeyCrypterException import org.bouncycastle.crypto.params.KeyParameter import org.dash.wallet.common.services.analytics.AnalyticsService From 88b5729bb513e11bbdd0f2b38e56c4703bf9148e Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Thu, 26 Dec 2024 07:51:13 -0800 Subject: [PATCH 02/26] feat: add BaseForegroundWorker --- .../service/work/BaseForegroundWorker.kt | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 wallet/src/de/schildbach/wallet/service/work/BaseForegroundWorker.kt diff --git a/wallet/src/de/schildbach/wallet/service/work/BaseForegroundWorker.kt b/wallet/src/de/schildbach/wallet/service/work/BaseForegroundWorker.kt new file mode 100644 index 0000000000..d949e91938 --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/work/BaseForegroundWorker.kt @@ -0,0 +1,100 @@ +package de.schildbach.wallet.service.work + +import android.Manifest +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.os.Build +import androidx.annotation.RequiresApi +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import de.schildbach.wallet_test.R + +abstract class BaseForegroundWorker( + context: Context, + parameters: WorkerParameters, + private val channelId: String, + private val notificationId: Int, + private val workName: String, + private val initialTitle: String, + private val initialContent: String +): BaseWorker(context, parameters) { + private val notificationManager = NotificationManagerCompat.from(context) + abstract suspend fun doWorkInForeground(): Result + + override suspend fun doWorkWithBaseProgress(): Result { + return try { + // Ensure permission is granted before proceeding + if (hasNotificationPermission()) { + setForegroundAsync(createForegroundInfo()) + doWorkInForeground() + } else { + // Log or handle the case where permission is not granted + Result.failure() + } + } catch (e: SecurityException) { + // Log and handle SecurityException gracefully + e.printStackTrace() + Result.failure() + } + } + + private fun hasNotificationPermission(): Boolean { + return notificationManager.areNotificationsEnabled() && if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + applicationContext.checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED + } else { + true + } + } + private fun createForegroundInfo(): ForegroundInfo { + createNotificationChannelIfNeeded() + val notification = NotificationCompat.Builder(applicationContext, channelId) + .setContentTitle(initialTitle) + .setContentText(initialContent) + .setSmallIcon(R.drawable.ic_dash_pay) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setProgress(0, 1, true) + .build() + + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ForegroundInfo(notificationId, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC) + } else { + ForegroundInfo(notificationId, notification) + + } + } + + protected fun updateNotification(contentTitle: String, contentText: String, progressMax: Int, progress: Int) { + if (!hasNotificationPermission()) return + + val updatedNotification = NotificationCompat.Builder(applicationContext, channelId) + .setContentTitle(contentTitle) + .setContentText(contentText) + .setSmallIcon(R.drawable.ic_dash_pay) + .setPriority(NotificationCompat.PRIORITY_LOW) + .setProgress(progressMax, progress, false) + .build() + + try { + notificationManager.notify(notificationId, updatedNotification) + } catch (e: SecurityException) { + e.printStackTrace() // Handle gracefully + } + } + + private fun createNotificationChannelIfNeeded() { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + val channel = NotificationChannel( + channelId, + workName, + NotificationManager.IMPORTANCE_LOW + ) + val notificationManager = applicationContext.getSystemService(NotificationManager::class.java) + notificationManager?.createNotificationChannel(channel) + } + } +} \ No newline at end of file From 52a1ad6a6e6ff2d8a64ed8ea95f5c0096213f479 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 1 Jan 2025 22:51:10 -0500 Subject: [PATCH 03/26] feat: add RestoreIdentityWorker --- wallet/res/values/strings-dashpay.xml | 1 + .../service/platform/PlatformSyncService.kt | 71 ++-- .../platform/work/RestoreIdentityOperation.kt | 64 +++ .../platform/work/RestoreIdentityWorker.kt | 364 ++++++++++++++++++ .../ui/dashpay/CreateIdentityService.kt | 16 - .../wallet/ui/dashpay/PlatformRepo.kt | 7 + .../ui/main/WalletTransactionsFragment.kt | 44 ++- 7 files changed, 503 insertions(+), 64 deletions(-) create mode 100644 wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityOperation.kt create mode 100644 wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityWorker.kt diff --git a/wallet/res/values/strings-dashpay.xml b/wallet/res/values/strings-dashpay.xml index 36d64e6c9c..76aa12816e 100644 --- a/wallet/res/values/strings-dashpay.xml +++ b/wallet/res/values/strings-dashpay.xml @@ -73,6 +73,7 @@ (3/3) Registering Username (3/3) Requesting Username (3/3) Recovering Username + Restore Identity Hello %s, Your account is ready Voting for your username has begun diff --git a/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt b/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt index fb8a8cb114..109c85c97b 100644 --- a/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt +++ b/wallet/src/de/schildbach/wallet/service/platform/PlatformSyncService.kt @@ -47,7 +47,7 @@ import de.schildbach.wallet.livedata.Status import de.schildbach.wallet.security.SecurityGuard import de.schildbach.wallet.service.BlockchainService import de.schildbach.wallet.service.BlockchainServiceImpl -import de.schildbach.wallet.ui.dashpay.CreateIdentityService +import de.schildbach.wallet.service.platform.work.RestoreIdentityOperation import de.schildbach.wallet.ui.dashpay.OnContactsUpdated import de.schildbach.wallet.ui.dashpay.OnPreBlockProgressListener import de.schildbach.wallet.ui.dashpay.PlatformRepo @@ -59,7 +59,6 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import org.bitcoinj.core.Coin import org.bitcoinj.core.Context -import org.bitcoinj.core.NetworkParameters import org.bitcoinj.core.Sha256Hash import org.bitcoinj.crypto.KeyCrypterException import org.bitcoinj.evolution.EvolutionContact @@ -76,7 +75,6 @@ import org.dashj.platform.dashpay.ContactRequest import org.dashj.platform.dashpay.UsernameRequestStatus import org.dashj.platform.dpp.identifier.Identifier import org.dashj.platform.dpp.voting.ContestedDocumentResourceVotePoll -import org.dashj.platform.sdk.PlatformValue import org.dashj.platform.sdk.platform.DomainDocument import org.dashj.platform.wallet.IdentityVerify import org.slf4j.Logger @@ -92,14 +90,14 @@ import kotlin.time.Duration.Companion.seconds interface PlatformSyncService { fun init() - fun initSync() + suspend fun initSync(runFirstUpdateBlocking: Boolean = false) fun resume() fun shutdown() fun updateSyncStatus(stage: PreBlockStage) fun preBlockDownload(future: SettableFuture) - suspend fun updateContactRequests() + suspend fun updateContactRequests(initialSync: Boolean = false) fun postUpdateBloomFilters() suspend fun updateUsernameRequestsWithVotes() suspend fun updateUsernameRequestWithVotes(username: String) @@ -134,7 +132,6 @@ class PlatformSynchronizationService @Inject constructor( private val usernameVoteDao: UsernameVoteDao, private val identityConfig: BlockchainIdentityConfig ) : PlatformSyncService { - companion object { private val log: Logger = LoggerFactory.getLogger(PlatformSynchronizationService::class.java) private val random = Random(System.currentTimeMillis()) @@ -167,7 +164,10 @@ class PlatformSynchronizationService @Inject constructor( // This method may not be required. initSync must be called by PreBlockDownload handler } - override fun initSync() { + override suspend fun initSync(runFirstUpdateBlocking: Boolean) { + if (runFirstUpdateBlocking) { + updateContactRequests(true) + } platformSyncJob = TickerFlow(UPDATE_TIMER_DELAY) .onEach { updateContactRequests() } .launchIn(syncScope) @@ -207,7 +207,7 @@ class PlatformSynchronizationService @Inject constructor( * This method should not use blockchainIdentity because in some cases * when the app starts, it has not yet been initialized */ - override suspend fun updateContactRequests() { + override suspend fun updateContactRequests(initialSync: Boolean) { // if there is no wallet or identity, then skip the remaining steps of the update if (!platformRepo.hasIdentity || walletApplication.wallet == null) { @@ -339,31 +339,32 @@ class PlatformSynchronizationService @Inject constructor( } updateSyncStatus(PreBlockStage.GetSentRequests) - // If new keychains were added to the wallet, then update the bloom filters - if (addedContact) { - postUpdateBloomFilters() - } - - // obtain profiles from new contacts - if (userIdList.isNotEmpty()) { - updateContactProfiles(userIdList.toList(), 0L) - } + if (!initialSync) { + // If new keychains were added to the wallet, then update the bloom filters + if (addedContact) { + postUpdateBloomFilters() + } - updateSyncStatus(PreBlockStage.GetNewProfiles) + // obtain profiles from new contacts + if (userIdList.isNotEmpty()) { + updateContactProfiles(userIdList.toList(), 0L) + } - coroutineScope { - awaitAll( - // fetch updated invitations - async { updateInvitations() }, - // fetch updated transaction metadata - async { updateTransactionMetadata() }, // TODO: this is skipped in VOTING state, but shouldn't be - // fetch updated profiles from the network - async { updateContactProfiles(userId, lastContactRequestTime) } - ) - } + updateSyncStatus(PreBlockStage.GetNewProfiles) - updateSyncStatus(PreBlockStage.GetUpdatedProfiles) + coroutineScope { + awaitAll( + // fetch updated invitations + async { updateInvitations() }, + // fetch updated transaction metadata + async { updateTransactionMetadata() }, // TODO: this is skipped in VOTING state, but shouldn't be + // fetch updated profiles from the network + async { updateContactProfiles(userId, lastContactRequestTime) } + ) + } + updateSyncStatus(PreBlockStage.GetUpdatedProfiles) + } // fire listeners if there were new contacts if (addedContact) { fireContactsUpdatedListeners() @@ -1334,13 +1335,9 @@ class PlatformSynchronizationService @Inject constructor( if (identity != null) { log.info("preBlockDownload: initiate recovery of existing identity ${identity.id}") - ContextCompat.startForegroundService( - walletApplication, - CreateIdentityService.createIntentForRestore( - walletApplication, - identity.id.toBuffer() - ) - ) + RestoreIdentityOperation(walletApplication) + .create(identity.id.toString()) + .enqueue() return@launch } else { log.info("preBlockDownload: no existing identity found") @@ -1353,7 +1350,7 @@ class PlatformSynchronizationService @Inject constructor( checkVotingStatus(identityData) if (!updatingContacts.get()) { - updateContactRequests() + updateContactRequests(initialSync = true) } } initSync() diff --git a/wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityOperation.kt b/wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityOperation.kt new file mode 100644 index 0000000000..1a478cafbe --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityOperation.kt @@ -0,0 +1,64 @@ +/* + * 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.service.platform.work + +import android.annotation.SuppressLint +import android.app.Application +import androidx.work.* +import de.schildbach.wallet.security.SecurityGuard +import org.bitcoinj.core.Coin +import org.slf4j.LoggerFactory + +class RestoreIdentityOperation(val application: Application) { + + class RestoreIdentityOperationException(message: String) : Exception(message) + + companion object { + private val log = LoggerFactory.getLogger(RestoreIdentityOperation::class.java) + + private const val WORK_NAME = "RestoreIdentityWorker.WORK#" + + fun uniqueWorkName(identityId: String) = WORK_NAME + identityId + } + + private val workManager: WorkManager = WorkManager.getInstance(application) + + /** + * Gets the list of all SendContactRequestWorker WorkInfo's + */ + val allOperationsData = workManager.getWorkInfosByTagLiveData(RestoreIdentityOperation::class.qualifiedName!!) + + @SuppressLint("EnqueueWork") + fun create(identity: String, retry: Boolean = false): WorkContinuation { + val password = SecurityGuard().retrievePassword() + val verifyIdentityWorker = OneTimeWorkRequestBuilder() + .setInputData( + workDataOf( + RestoreIdentityWorker.KEY_PASSWORD to password, + RestoreIdentityWorker.KEY_IDENTITY to identity + ) + ) + .addTag("identity:$identity") + .build() + + return WorkManager.getInstance(application) + .beginUniqueWork(uniqueWorkName(identity), + ExistingWorkPolicy.KEEP, + verifyIdentityWorker) + } +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityWorker.kt b/wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityWorker.kt new file mode 100644 index 0000000000..dd92993130 --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/platform/work/RestoreIdentityWorker.kt @@ -0,0 +1,364 @@ +/* + * 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.service.platform.work + +import android.content.Context +import android.content.pm.ServiceInfo +import androidx.annotation.StringRes +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import androidx.hilt.work.HiltWorker +import androidx.work.ForegroundInfo +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.CoinJoinConfig +import de.schildbach.wallet.database.dao.UsernameRequestDao +import de.schildbach.wallet.database.entity.BlockchainIdentityConfig +import de.schildbach.wallet.database.entity.BlockchainIdentityData +import de.schildbach.wallet.database.entity.UsernameRequest +import de.schildbach.wallet.service.platform.PlatformSyncService +import de.schildbach.wallet.service.work.BaseForegroundWorker +import de.schildbach.wallet.ui.dashpay.PlatformRepo +import de.schildbach.wallet.ui.dashpay.PreBlockStage +import de.schildbach.wallet.service.work.BaseWorker +import de.schildbach.wallet.ui.dashpay.work.GetUsernameVotingResultOperation +import de.schildbach.wallet_test.R +import org.bitcoinj.crypto.KeyCrypterException +import org.bitcoinj.evolution.AssetLockTransaction +import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension +import org.bouncycastle.crypto.params.KeyParameter +import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.common.services.analytics.AnalyticsService +import org.dashj.platform.dashpay.BlockchainIdentity +import org.dashj.platform.dashpay.UsernameInfo +import org.dashj.platform.dashpay.UsernameRequestStatus +import org.dashj.platform.dashpay.UsernameStatus +import org.dashj.platform.dpp.identifier.Identifier +import org.dashj.platform.dpp.identity.Identity +import org.dashj.platform.sdk.platform.DomainDocument +import org.dashj.platform.wallet.IdentityVerify +import org.slf4j.LoggerFactory + +@HiltWorker +class RestoreIdentityWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted parameters: WorkerParameters, + val walletApplication: WalletApplication, + val analytics: AnalyticsService, + val platformSyncService: PlatformSyncService, + val walletDataProvider: WalletDataProvider, + val platformRepo: PlatformRepo, + val coinJoinConfig: CoinJoinConfig, + val identityConfig: BlockchainIdentityConfig, + val usernameRequestDao: UsernameRequestDao +) : BaseForegroundWorker( + context, + parameters, + CHANNEL_ID, + NOTIFICATION_ID, + context.getString(R.string.restore_identity), + context.getString(R.string.processing_home_title), + context.getString(R.string.processing_home_step_1) +) { + companion object { + private val log = LoggerFactory.getLogger(RestoreIdentityWorker::class.java) + const val KEY_PASSWORD = "RestoreIdentityWorker.PASSWORD" + const val KEY_IDENTITY = "RestoreIdentityWorker.IDENTITY" + const val KEY_RETRY = "RestoreIdentityWorker.RETRY" + const val KEY_USERNAMES = "RestoreIdentityWorker.USERNAMES" + const val CHANNEL_ID = "restore_identity_work_channel" + const val NOTIFICATION_ID = 1000 + } + + override suspend fun doWorkInForeground(): Result { + val password = inputData.getString(KEY_PASSWORD) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_PASSWORD parameter")) + val identity = inputData.getString(KEY_IDENTITY) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_IDENTITY parameter")) + val retrying = inputData.getBoolean(KEY_RETRY, false) + val authGroupExtension = walletDataProvider.wallet!!.getKeyChainExtension(AuthenticationGroupExtension.EXTENSION_ID) as AuthenticationGroupExtension + + val encryptionKey: KeyParameter + try { + encryptionKey = walletDataProvider.wallet!!.keyCrypter!!.deriveKey(password) + } catch (ex: KeyCrypterException) { + analytics.logError(ex, "Restore Identity: failed to derive encryption key") + val msg = formatExceptionMessage("derive encryption key", ex) + return Result.failure(workDataOf(KEY_ERROR_MESSAGE to msg)) + } + + return try { + // restore identity and all other + restoreIdentity(Identifier.from(identity).toBuffer(), retrying) + Result.success( + workDataOf( + KEY_IDENTITY to identity, + ) + ) + } catch (ex: Exception) { + analytics.logError(ex, "Restore Identity: failed to restore identity") + Result.failure( + workDataOf( + KEY_IDENTITY to identity, + KEY_ERROR_MESSAGE to formatExceptionMessage("restore identity", ex) + ) + ) + } + } + + private suspend fun restoreIdentity(identity: ByteArray, retrying: Boolean) { + log.info("Restoring identity and username") + try { + updateNotification(applicationContext.getString(R.string.processing_home_title), applicationContext.getString(R.string.processing_home_step_1), 5, 0) + platformSyncService.updateSyncStatus(PreBlockStage.StartRecovery) + + // use an "empty" state for each + val blockchainIdentityData = BlockchainIdentityData(BlockchainIdentityData.CreationState.NONE, null, null, null, true) + + val authExtension = + walletDataProvider.wallet!!.getKeyChainExtension(AuthenticationGroupExtension.EXTENSION_ID) as AuthenticationGroupExtension + //authExtension.setWallet(walletApplication.wallet!!) // why is the wallet not set? we didn't deserialize it probably! + val cftxs = authExtension.assetLockTransactions + + val creditFundingTransaction: AssetLockTransaction? = + cftxs.find { it.identityId.bytes!!.contentEquals(identity) } + + val existingBlockchainIdentityData = identityConfig.load() + if (existingBlockchainIdentityData != null && !(existingBlockchainIdentityData.restoring /*&& existingBlockchainIdentityData.creationStateErrorMessage != null*/)) { + log.info("Attempting restore of existing identity and username; save credit funding txid") + val blockchainIdentity = platformRepo.blockchainIdentity + blockchainIdentity.assetLockTransaction = creditFundingTransaction + existingBlockchainIdentityData.creditFundingTxId = creditFundingTransaction!!.txId + platformRepo.updateBlockchainIdentityData(existingBlockchainIdentityData) + return + } + updateNotification(applicationContext.getString(R.string.processing_home_title), applicationContext.getString(R.string.processing_home_step_1), 5, 1) + val loadingFromAssetLockTransaction = creditFundingTransaction != null + val existingIdentity: Identity? + + if (!loadingFromAssetLockTransaction) { + existingIdentity = platformRepo.getIdentityFromPublicKeyId() + if (existingIdentity == null) { + throw IllegalArgumentException("identity $identity does not match a credit funding transaction or it doesn't exist on the network") + } + } + + val wallet = walletDataProvider.wallet!! + val encryptionKey = platformRepo.getWalletEncryptionKey() + ?: throw IllegalStateException("cannot obtain wallet encryption key") + val seed = wallet.keyChainSeed ?: throw IllegalStateException("cannot obtain wallet seed") + + // create the Blockchain Identity object + val blockchainIdentity = BlockchainIdentity(platformRepo.platform.platform, 0, wallet, authExtension) + // this process should have been done already, otherwise the credit funding transaction + // will not have the credit burn keys associated with it + platformRepo.addWalletAuthenticationKeysAsync(seed, encryptionKey) + platformSyncService.updateSyncStatus(PreBlockStage.InitWallet) + + // + // Step 2: The credit funding registration exists, no need to create it + // + + // + // Step 3: Find the identity + // + updateNotification(applicationContext.getString(R.string.processing_home_title), applicationContext.getString(R.string.processing_home_step_2), 5, 2) + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.IDENTITY_REGISTERING) + if (loadingFromAssetLockTransaction) { + platformRepo.recoverIdentityAsync(blockchainIdentity, creditFundingTransaction!!) + } else { + val firstIdentityKey = platformRepo.getBlockchainIdentityKey(0, encryptionKey)!! + platformRepo.recoverIdentityAsync( + blockchainIdentity, + firstIdentityKey.pubKeyHash + ) + } + platformRepo.updateBlockchainIdentityData(blockchainIdentityData, blockchainIdentity) + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.IDENTITY_REGISTERED) + platformSyncService.updateSyncStatus(PreBlockStage.GetIdentity) + updateNotification(applicationContext.getString(R.string.processing_home_title), applicationContext.getString(R.string.processing_home_step_3_restoring), 5, 3) + + // + // Step 4: We don't need to find the preorder documents + // + + // + // Step 5: Find the username + // + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.USERNAME_REGISTERING) + platformRepo.recoverUsernamesAsync(blockchainIdentity) + platformRepo.updateBlockchainIdentityData(blockchainIdentityData, blockchainIdentity) + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.USERNAME_REGISTERED) + platformSyncService.updateSyncStatus(PreBlockStage.GetName) + updateNotification(applicationContext.getString(R.string.processing_home_title), applicationContext.getString(R.string.processing_home_step_3_restoring), 5, 4) + + if (blockchainIdentity.currentUsername == null) { + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.REQUESTED_NAME_CHECKING) + + // check if the network has this name in the queue for voting + val contestedNames = platformRepo.platform.names.getAllContestedNames() + + contestedNames.forEach { name -> + val voteContenders = platformRepo.getVoteContenders(name) + val winner = voteContenders.winner + voteContenders.map.forEach { (identifier, documentWithVotes) -> + if (blockchainIdentity.uniqueIdentifier == identifier) { + blockchainIdentity.currentUsername = name + // load the serialized doc to get voting period and status... + val usernameRequestStatus = if (winner.isEmpty) { + UsernameRequestStatus.VOTING + } else { + val winnerInfo = winner.get().first + when { + winnerInfo.isLocked -> UsernameRequestStatus.LOCKED + winnerInfo.isWinner(blockchainIdentity.uniqueIdentifier) -> UsernameRequestStatus.APPROVED + else -> UsernameRequestStatus.LOST_VOTE + } + } + + blockchainIdentity.usernameStatuses.apply { + clear() + val usernameInfo = UsernameInfo( + null, + UsernameStatus.CONFIRMED, + blockchainIdentity.currentUsername!!, + usernameRequestStatus, + 0 + ) + put(blockchainIdentity.currentUsername!!, usernameInfo) + } + var votingStartedAt = -1L + var label = name + if (winner.isEmpty) { + val contestedDocument = DomainDocument( + platformRepo.platform.names.deserialize(documentWithVotes.serializedDocument!!) + ) + blockchainIdentity.currentUsername = contestedDocument.label + votingStartedAt = contestedDocument.createdAt!! + label = contestedDocument.label + } + val verifyDocument = IdentityVerify(platformRepo.platform.platform).get( + blockchainIdentity.uniqueIdentifier, + name + ) + + usernameRequestDao.insert( + UsernameRequest( + UsernameRequest.getRequestId( + blockchainIdentity.uniqueIdString, + blockchainIdentity.currentUsername!! + ), + label, + name, + votingStartedAt, + blockchainIdentity.uniqueIdString, + verifyDocument?.url, // get it from the document + documentWithVotes.votes, + voteContenders.lockVoteTally, + false + ) + ) + // what if usernameInfo would have been null, we should create it. + + var usernameInfo = blockchainIdentity.usernameStatuses[blockchainIdentity.currentUsername!!] + if (usernameInfo == null) { + usernameInfo = UsernameInfo( + null, + UsernameStatus.CONFIRMED, + blockchainIdentity.currentUsername!!, + UsernameRequestStatus.VOTING + ) + blockchainIdentity.usernameStatuses[blockchainIdentity.currentUsername!!] = usernameInfo + } + + // determine when voting started by finding the minimum timestamp + val earliestCreatedAt = voteContenders.map.values.minOf { + val document = documentWithVotes.serializedDocument?.let { platformRepo.platform.names.deserialize(it) } + document?.createdAt ?: 0 + } + + usernameInfo.votingStartedAt = earliestCreatedAt + usernameInfo.requestStatus = usernameRequestStatus + + // schedule work to check the status after voting has ended + GetUsernameVotingResultOperation(walletApplication) + .create( + usernameInfo.username!!, + blockchainIdentity.uniqueIdentifier.toString(), + earliestCreatedAt + ) + .enqueue() + } + } + } + + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.REQUESTED_NAME_CHECKED) + platformRepo.updateBlockchainIdentityData(blockchainIdentityData, blockchainIdentity) + + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.REQUESTED_NAME_CHECKING) + + // recover the verification link + + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.REQUESTED_NAME_CHECKED) + platformRepo.updateBlockchainIdentityData(blockchainIdentityData, blockchainIdentity) + + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.VOTING) + platformRepo.updateBlockchainIdentityData(blockchainIdentityData, blockchainIdentity) + } + updateNotification(applicationContext.getString(R.string.processing_home_title), applicationContext.getString(R.string.processing_home_step_3_restoring), 5, 5) + + // At this point, let's see what has been recovered. It is possible that only the identity was recovered. + // In this case, we should require that the user enters in a new username. + if (blockchainIdentity.identity != null && blockchainIdentity.currentUsername == null) { + blockchainIdentityData.creationState = BlockchainIdentityData.CreationState.USERNAME_REGISTERING + blockchainIdentityData.restoring = false + error("missing domain document for ${blockchainIdentity.uniqueId}") + } + + // + // Step 6: Find the profile + // + platformRepo.recoverDashPayProfile(blockchainIdentity) + // blockchainIdentity hasn't changed + platformSyncService.updateSyncStatus(PreBlockStage.GetProfile) + + platformRepo.addInviteUserAlert() + + // We are finished recovering + blockchainIdentityData.finishRestoration() + if (blockchainIdentityData.creationState != BlockchainIdentityData.CreationState.VOTING) { + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.DONE) + platformRepo.updateBlockchainIdentityData(blockchainIdentityData) + // Complete the entire process + platformRepo.updateIdentityCreationState(blockchainIdentityData, BlockchainIdentityData.CreationState.DONE_AND_DISMISS) + } + platformRepo.updateBlockchainIdentityData(blockchainIdentityData) + + platformSyncService.updateSyncStatus(PreBlockStage.RecoveryComplete) + platformRepo.init() + platformSyncService.initSync() + } catch (e: Exception) { + // triggering the end of the preBlockDownload stage as complete + // could be problematic, what if there were errors + platformSyncService.triggerPreBlockDownloadComplete() + throw e + } + } +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt index 73038dff1b..7185cf7c04 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt @@ -56,8 +56,6 @@ import org.dashj.platform.wallet.IdentityVerify import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit import javax.inject.Inject -import kotlin.coroutines.resumeWithException -import kotlin.coroutines.suspendCoroutine @AndroidEntryPoint class CreateIdentityService : LifecycleService() { @@ -75,8 +73,6 @@ class CreateIdentityService : LifecycleService() { private const val ACTION_RETRY_INVITE_WITH_NEW_USERNAME = "org.dash.dashpay.action.ACTION_RETRY_INVITE_WITH_NEW_USERNAME" private const val ACTION_RETRY_INVITE_AFTER_INTERRUPTION = "org.dash.dashpay.action.ACTION_RETRY_INVITE_AFTER_INTERRUPTION" - private const val ACTION_RESTORE_IDENTITY = "org.dash.dashpay.action.RESTORE_IDENTITY" - private const val EXTRA_USERNAME = "org.dash.dashpay.extra.USERNAME" private const val EXTRA_START_FOREGROUND_PROMISED = "org.dash.dashpay.extra.EXTRA_START_FOREGROUND_PROMISED" private const val EXTRA_IDENTITY = "org.dash.dashpay.extra.IDENTITY" @@ -131,14 +127,6 @@ class CreateIdentityService : LifecycleService() { putExtra(EXTRA_START_FOREGROUND_PROMISED, startForegroundPromised) } } - - @JvmStatic - fun createIntentForRestore(context: Context, identity: ByteArray): Intent { - return Intent(context, CreateIdentityService::class.java).apply { - action = ACTION_RESTORE_IDENTITY - putExtra(EXTRA_IDENTITY, identity) - } - } } private val walletApplication by lazy { application as WalletApplication } @@ -248,10 +236,6 @@ class CreateIdentityService : LifecycleService() { } handleCreateIdentityFromInvitationAction(null, null) } - ACTION_RESTORE_IDENTITY -> { - val identity = intent.getByteArrayExtra(EXTRA_IDENTITY)!! - handleRestoreIdentityAction(identity) - } } } else { log.info("work in progress, ignoring ${intent.action}") diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt index 93e77e5b8f..99d534690a 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt @@ -1237,4 +1237,11 @@ class PlatformRepo @Inject constructor( fun getIdentityBalance(identifier: Identifier): CreditBalanceInfo { return CreditBalanceInfo(platform.client.getIdentityBalance(identifier)) } + + suspend fun addInviteUserAlert() { + // this alert will be shown or not based on the current balance and will be + // managed by NotificationsLiveData + val userAlert = UserAlert(UserAlert.INVITATION_NOTIFICATION_TEXT, UserAlert.INVITATION_NOTIFICATION_ICON) + userAlertDao.insert(userAlert) + } } \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt b/wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt index 8f3265a626..b2a766c313 100644 --- a/wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt +++ b/wallet/src/de/schildbach/wallet/ui/main/WalletTransactionsFragment.kt @@ -42,6 +42,7 @@ import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import dagger.hilt.android.AndroidEntryPoint +import de.schildbach.wallet.service.platform.work.RestoreIdentityOperation import de.schildbach.wallet.ui.transactions.TransactionDetailsDialogFragment import de.schildbach.wallet.ui.transactions.TransactionGroupDetailsFragment import de.schildbach.wallet.ui.transactions.TransactionRowView @@ -261,23 +262,44 @@ class WalletTransactionsFragment : Fragment(R.layout.wallet_transactions_fragmen private fun openIdentityCreation() { viewModel.blockchainIdentity.value?.let { blockchainIdentityData -> if (blockchainIdentityData.creationStateErrorMessage != null) { - // Do we need to have the user request a new username - val errorMessage = blockchainIdentityData.creationStateErrorMessage - val needsNewUsername = blockchainIdentityData.creationState == BlockchainIdentityData.CreationState.USERNAME_REGISTERING && - (errorMessage.contains("Document transitions with duplicate unique properties") || - errorMessage.contains("missing domain document for")) - if (needsNewUsername || - // do we need this, cause the error could be due to a stale node - blockchainIdentityData.creationState == BlockchainIdentityData.CreationState.REQUESTED_NAME_CHECKING && - !errorMessage.contains("invalid quorum: quorum not found")) { - startActivity(CreateUsernameActivity.createIntentReuseTransaction(requireActivity(), blockchainIdentityData)) + // are we restoring? + if (blockchainIdentityData.restoring) { + RestoreIdentityOperation(requireActivity().application) + .create(blockchainIdentityData.userId!!, true) + .enqueue() } else { - Toast.makeText(requireContext(), blockchainIdentityData.creationStateErrorMessage, Toast.LENGTH_LONG).show() + // Do we need to have the user request a new username + val errorMessage = blockchainIdentityData.creationStateErrorMessage + val needsNewUsername = + blockchainIdentityData.creationState == BlockchainIdentityData.CreationState.USERNAME_REGISTERING && + (errorMessage.contains("Document transitions with duplicate unique properties") || + errorMessage.contains("missing domain document for")) + if (needsNewUsername || + // do we need this, cause the error could be due to a stale node + blockchainIdentityData.creationState == BlockchainIdentityData.CreationState.REQUESTED_NAME_CHECKING && + !errorMessage.contains("invalid quorum: quorum not found") + ) { + startActivity( + CreateUsernameActivity.createIntentReuseTransaction( + requireActivity(), + blockchainIdentityData + ) + ) + } else { + // we don't know what to do in this case? (not good) + Toast.makeText( + requireContext(), + blockchainIdentityData.creationStateErrorMessage, + Toast.LENGTH_LONG + ).show() + } } } else if (blockchainIdentityData.creationState == BlockchainIdentityData.CreationState.DONE) { startActivity(Intent(requireActivity(), SearchUserActivity::class.java)) // hide "Hello Card" after first click viewModel.dismissUsernameCreatedCard() + } else { + // not possible to get here? } } } From 6037097a851a2361d4d3f07863d74724d8cce627 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 1 Jan 2025 22:52:54 -0500 Subject: [PATCH 04/26] feat: topup backend --- build.gradle | 2 +- .../de/schildbach/wallet/di/DashPayModule.kt | 6 + .../service/platform/TopUpRepository.kt | 240 ++++++++++++++++++ .../platform/work/TopupIdentityOperation.kt | 148 +++++++++++ .../platform/work/TopupIdentityWorker.kt | 128 ++++++++++ .../ui/dashpay/CreateIdentityService.kt | 91 +------ .../wallet/ui/dashpay/PlatformRepo.kt | 62 ----- 7 files changed, 532 insertions(+), 145 deletions(-) create mode 100644 wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt create mode 100644 wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityOperation.kt create mode 100644 wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityWorker.kt diff --git a/build.gradle b/build.gradle index 670a20a8e3..b67bdf8368 100644 --- a/build.gradle +++ b/build.gradle @@ -4,7 +4,7 @@ buildscript { coroutinesVersion = '1.6.4' ok_http_version = '4.9.1' dashjVersion = '21.1.4' - dppVersion = "1.5.2" + dppVersion = "1.7.0-SNAPSHOT" hiltVersion = '2.51' hiltCompilerVersion = '1.2.0' hiltWorkVersion = '1.0.0' diff --git a/wallet/src/de/schildbach/wallet/di/DashPayModule.kt b/wallet/src/de/schildbach/wallet/di/DashPayModule.kt index 31c0fa4a49..3627694a31 100644 --- a/wallet/src/de/schildbach/wallet/di/DashPayModule.kt +++ b/wallet/src/de/schildbach/wallet/di/DashPayModule.kt @@ -32,6 +32,8 @@ import de.schildbach.wallet.service.platform.PlatformService import de.schildbach.wallet.service.platform.PlatformServiceImplementation import de.schildbach.wallet.service.platform.PlatformSyncService import de.schildbach.wallet.service.platform.PlatformSynchronizationService +import de.schildbach.wallet.service.platform.TopUpRepository +import de.schildbach.wallet.service.platform.TopUpRepositoryImpl import javax.inject.Singleton @Module @@ -56,6 +58,10 @@ abstract class DashPayModule { @Singleton abstract fun bindsCoinJoinService(coinJoinMixingService: CoinJoinMixingService): CoinJoinService + @Singleton // only want one of PlatformSyncService created + @Binds + abstract fun bindsTopupRepository(topUpRepositoryImpl: TopUpRepositoryImpl): TopUpRepository + //@Binds //@Singleton //abstract fun bindsBlockchainIdentityConfig(blockchainIdentityConfig: BlockchainIdentityConfig): BlockchainIdentityConfig diff --git a/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt b/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt new file mode 100644 index 0000000000..74d9e6624b --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/platform/TopUpRepository.kt @@ -0,0 +1,240 @@ +/* + * 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.service.platform + +import de.schildbach.wallet.Constants +import de.schildbach.wallet.WalletApplication +import de.schildbach.wallet.data.InvitationLinkData +import de.schildbach.wallet.payments.SendCoinsTaskRunner +import de.schildbach.wallet.ui.dashpay.PlatformRepo +import org.bitcoinj.core.Coin +import org.bitcoinj.core.Context +import org.bitcoinj.core.DumpedPrivateKey +import org.bitcoinj.core.RejectMessage +import org.bitcoinj.core.RejectedTransactionException +import org.bitcoinj.core.Sha256Hash +import org.bitcoinj.core.Transaction +import org.bitcoinj.core.TransactionConfidence +import org.bitcoinj.core.Utils +import org.bitcoinj.evolution.AssetLockTransaction +import org.bitcoinj.quorums.InstantSendLock +import org.bitcoinj.wallet.Wallet +import org.bouncycastle.crypto.params.KeyParameter +import org.dash.wallet.common.WalletDataProvider +import org.dashj.platform.dashpay.BlockchainIdentity +import org.dashj.platform.dpp.toHex +import org.dashj.platform.sdk.platform.Names +import org.slf4j.LoggerFactory +import javax.inject.Inject +import kotlin.coroutines.resumeWithException +import kotlin.coroutines.suspendCoroutine +import de.schildbach.wallet.ui.dashpay.CreateIdentityService +/** + * contains topup related functions that are used by [CreateIdentityService] to create + * an identity and by [TopupIdentityWorker] to topup an identity + */ +interface TopUpRepository { + fun createAssetLockTransaction( + blockchainIdentity: BlockchainIdentity, + username: String, + keyParameter: KeyParameter?, + useCoinJoin: Boolean + ) + + fun createTopupTransaction( + blockchainIdentity: BlockchainIdentity, + topupAmount: Coin, + keyParameter: KeyParameter?, + useCoinJoin: Boolean + ): AssetLockTransaction + + fun obtainAssetLockTransaction( + blockchainIdentity: BlockchainIdentity, + invite: InvitationLinkData + ) + + /** sends the transaction and waits for IS or CL */ + suspend fun sendTransaction(cftx: AssetLockTransaction): Boolean + + fun topUpIdentity( + topupAssetLockTransaction: AssetLockTransaction, + aesKeyParameter: KeyParameter + ) +} + +class TopUpRepositoryImpl @Inject constructor( + private val walletApplication: WalletApplication, + private val walletDataProvider: WalletDataProvider, + private val platformRepo: PlatformRepo, + private val sendTransaction: SendCoinsTaskRunner +) : TopUpRepository { + companion object { + private val log = LoggerFactory.getLogger(TopUpRepositoryImpl::class.java) + } + + val platform = platformRepo.platform + + override fun createAssetLockTransaction( + blockchainIdentity: BlockchainIdentity, + username: String, + keyParameter: KeyParameter?, + useCoinJoin: Boolean + ) { + Context.propagate(walletDataProvider.wallet!!.context) + val fee = if (Names.isUsernameContestable(username)) { + Constants.DASH_PAY_FEE_CONTESTED + } else { + Constants.DASH_PAY_FEE + } + val balance = walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.ESTIMATED_SPENDABLE) + val emptyWallet = balance == fee && balance <= (fee + Transaction.MIN_NONDUST_OUTPUT) + val cftx = blockchainIdentity.createAssetLockTransaction( + fee, + keyParameter, + useCoinJoin, + returnChange = true, + emptyWallet = emptyWallet + ) + blockchainIdentity.initializeAssetLockTransaction(cftx) + } + + override fun createTopupTransaction( + blockchainIdentity: BlockchainIdentity, + topupAmount: Coin, + keyParameter: KeyParameter?, + useCoinJoin: Boolean + ): AssetLockTransaction { + Context.propagate(walletDataProvider.wallet!!.context) + val balance = walletDataProvider.wallet!!.getBalance(Wallet.BalanceType.ESTIMATED_SPENDABLE) + val emptyWallet = balance == topupAmount && balance <= (topupAmount + Transaction.MIN_NONDUST_OUTPUT) + val topupTx = blockchainIdentity.createTopupFundingTransaction( + topupAmount, + keyParameter, + useCoinJoin, + returnChange = true, + emptyWallet = emptyWallet + ) + return topupTx + } + + // + // Step 2 is to obtain the credit funding transaction for invites + // + override fun obtainAssetLockTransaction(blockchainIdentity: BlockchainIdentity, invite: InvitationLinkData) { + Context.propagate(walletDataProvider.wallet!!.context) + var cftxData = platform.client.getTransaction(invite.cftx) + //TODO: remove when iOS uses big endian + if (cftxData == null) + cftxData = platform.client.getTransaction(Sha256Hash.wrap(invite.cftx).reversedBytes.toHex()) + val assetLockTx = AssetLockTransaction(platform.params, cftxData!!) + val privateKey = DumpedPrivateKey.fromBase58(platform.params, invite.privateKey).key + assetLockTx.addAssetLockPublicKey(privateKey) + + // TODO: when all instantsend locks are deterministic, we don't need the catch block + val instantSendLock = InstantSendLock(platform.params, Utils.HEX.decode(invite.instantSendLock), InstantSendLock.ISDLOCK_VERSION) + + assetLockTx.confidence.setInstantSendLock(instantSendLock) + blockchainIdentity.initializeAssetLockTransaction(assetLockTx) + } + + /** + * Send the credit funding transaction and wait for confirmation from other nodes that the + * transaction was sent. InstantSendLock, in a block or seen by more than one peer. + * + * Exceptions are returned in the case of a reject message (may not be sent with Dash Core 0.16) + * or in the case of a double spend or some other error. + * + * @param cftx The credit funding transaction to send + * @return True if successful + */ + override suspend fun sendTransaction(cftx: AssetLockTransaction): Boolean { + log.info("Sending credit funding transaction: ${cftx.txId}") + return suspendCoroutine { continuation -> + cftx.confidence.addEventListener(object : TransactionConfidence.Listener { + override fun onConfidenceChanged(confidence: TransactionConfidence?, reason: TransactionConfidence.Listener.ChangeReason?) { + when (reason) { + // If this transaction is in a block, then it has been sent successfully + TransactionConfidence.Listener.ChangeReason.DEPTH -> { + // TODO: a chainlock is needed to accompany the block information + // to provide sufficient proof + } + // If this transaction is InstantSend Locked, then it has been sent successfully + TransactionConfidence.Listener.ChangeReason.IX_TYPE -> { + // TODO: allow for received (IX_REQUEST) instantsend locks + // until the bug related to instantsend lock verification is fixed. + if (confidence!!.isTransactionLocked || confidence.ixType == TransactionConfidence.IXType.IX_REQUEST) { + log.info("credit funding transaction verified with instantsend: ${cftx.txId}") + confidence.removeEventListener(this) + continuation.resumeWith(Result.success(true)) + } + } + + TransactionConfidence.Listener.ChangeReason.CHAIN_LOCKED -> { + if (confidence!!.isChainLocked) { + log.info("credit funding transaction verified with chainlock: ${cftx.txId}") + confidence.removeEventListener(this) + continuation.resumeWith(Result.success(true)) + } + } + // If this transaction has been seen by more than 1 peer, then it has been sent successfully + TransactionConfidence.Listener.ChangeReason.SEEN_PEERS -> { + // being seen by other peers is no longer sufficient proof + } + // If this transaction was rejected, then it was not sent successfully + TransactionConfidence.Listener.ChangeReason.REJECT -> { + if (confidence!!.hasRejections() && confidence.rejections.size >= 1) { + confidence.removeEventListener(this) + log.info("Error sending ${cftx.txId}: ${confidence.rejectedTransactionException.rejectMessage.reasonString}") + continuation.resumeWithException(confidence.rejectedTransactionException) + } + } + TransactionConfidence.Listener.ChangeReason.TYPE -> { + if (confidence!!.hasErrors()) { + confidence.removeEventListener(this) + val code = when (confidence.confidenceType) { + TransactionConfidence.ConfidenceType.DEAD -> RejectMessage.RejectCode.INVALID + TransactionConfidence.ConfidenceType.IN_CONFLICT -> RejectMessage.RejectCode.DUPLICATE + else -> RejectMessage.RejectCode.OTHER + } + val rejectMessage = RejectMessage(Constants.NETWORK_PARAMETERS, code, confidence.transactionHash, + "Credit funding transaction is dead or double-spent", "cftx-dead-or-double-spent") + log.info("Error sending ${cftx.txId}: ${rejectMessage.reasonString}") + continuation.resumeWithException(RejectedTransactionException(cftx, rejectMessage)) + } + } + else -> { + // ignore + } + } + } + }) + walletApplication.broadcastTransaction(cftx) + } + } + + override fun topUpIdentity( + topupAssetLockTransaction: AssetLockTransaction, + aesKeyParameter: KeyParameter + ) { + platformRepo.blockchainIdentity.topUp( + topupAssetLockTransaction, + aesKeyParameter, + useISLock = true, + waitForChainlock = true + ) + } +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityOperation.kt b/wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityOperation.kt new file mode 100644 index 0000000000..8c8e2c609c --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityOperation.kt @@ -0,0 +1,148 @@ +/* + * 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.service.platform.work + +import android.annotation.SuppressLint +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.switchMap +import androidx.work.* +import de.schildbach.wallet.livedata.Resource +import de.schildbach.wallet.security.SecurityGuard +import de.schildbach.wallet.service.work.BaseWorker +import de.schildbach.wallet.ui.dashpay.work.BroadcastUsernameVotesOperation +import de.schildbach.wallet.ui.dashpay.work.BroadcastUsernameVotesWorker +import org.bitcoinj.core.Coin +import org.dash.wallet.common.services.analytics.AnalyticsService +import org.slf4j.LoggerFactory + +class TopupIdentityOperation(val application: Application) { + class TopupIdentityOperationException(message: String) : java.lang.Exception(message) + + companion object { + private val log = LoggerFactory.getLogger(TopupIdentityOperation::class.java) + + private const val WORK_NAME = "TopupIdentityWorker.WORK#" + fun uniqueWorkName(workId: String) = "${WORK_NAME}$workId}" + + fun operationStatus( + application: Application, + workId: String, + analytics: AnalyticsService + ): LiveData> { + val workManager: WorkManager = WorkManager.getInstance(application) + return workManager.getWorkInfosForUniqueWorkLiveData(uniqueWorkName(workId)).switchMap { + return@switchMap liveData { + if (it.isNullOrEmpty()) { + return@liveData + } + + if (it.size > 1) { + val e = RuntimeException("there should never be more than one unique work ${ + uniqueWorkName( + workId + ) + }") + analytics.logError(e) + throw e + } + emit(convertState(it.first())) + } + } + } + + fun allOperationsStatus(application: Application): LiveData>> { + val workManager: WorkManager = WorkManager.getInstance(application) + return workManager.getWorkInfosByTagLiveData(BroadcastUsernameVotesWorker::class.qualifiedName!!).switchMap { + return@switchMap liveData { + if (it.isNullOrEmpty()) { + return@liveData + } + + val result = mutableMapOf>() + it.filter { workInfo -> + val timestampTag = workInfo.tags.firstOrNull { it.startsWith("timestamp:") } + timestampTag?.let { + val timestamp = it.removePrefix("timestamp:").toLongOrNull() + timestamp != null && timestamp > BroadcastUsernameVotesOperation.lastTimestamp + } ?: false + }.forEach { workInfo -> + var toUserId = "" + workInfo.tags.forEach { tag -> + if (tag.startsWith("usernames:")) { + toUserId = tag.replace("usernames:", "") + } + } + result[toUserId] = convertState(workInfo) + } + emit(result) + } + } + } + + private fun convertState(workInfo: WorkInfo): Resource { + return when (workInfo.state) { + WorkInfo.State.SUCCEEDED -> { + Resource.success(workInfo) + } + WorkInfo.State.FAILED -> { + val errorMessage = BaseWorker.extractError(workInfo.outputData) + if (errorMessage != null) { + Resource.error(errorMessage, workInfo) + } else { + Resource.error(Exception(), workInfo) + } + } + WorkInfo.State.CANCELLED -> { + Resource.canceled(workInfo) + } + else -> { + Resource.loading(workInfo) + } + } + } + } + +// private val workManager: WorkManager = WorkManager.getInstance(application) +// +// /** +// * Gets the list of all SendContactRequestWorker WorkInfo's +// */ +// val allOperationsData = workManager.getWorkInfosByTagLiveData(TopupIdentityOperation::class.qualifiedName!!) + + @SuppressLint("EnqueueWork") + fun create(identity: String, amount: Coin): WorkContinuation { + val password = SecurityGuard().retrievePassword() + val verifyIdentityWorker = OneTimeWorkRequestBuilder() + .setInputData( + workDataOf( + TopupIdentityWorker.KEY_PASSWORD to password, + TopupIdentityWorker.KEY_IDENTITY to identity, + TopupIdentityWorker.KEY_VALUE to amount.value + ) + ) + .addTag("identity:$identity") + .build() + + return WorkManager.getInstance(application) + .beginUniqueWork(uniqueWorkName(identity), + ExistingWorkPolicy.KEEP, + verifyIdentityWorker) + } +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityWorker.kt b/wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityWorker.kt new file mode 100644 index 0000000000..671fb2cf73 --- /dev/null +++ b/wallet/src/de/schildbach/wallet/service/platform/work/TopupIdentityWorker.kt @@ -0,0 +1,128 @@ +/* + * 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.service.platform.work + +import android.content.Context +import androidx.hilt.work.HiltWorker +import androidx.work.WorkerParameters +import androidx.work.workDataOf +import dagger.assisted.Assisted +import dagger.assisted.AssistedInject +import de.schildbach.wallet.data.CoinJoinConfig +import de.schildbach.wallet.service.CoinJoinMode +import de.schildbach.wallet.service.platform.PlatformBroadcastService +import de.schildbach.wallet.service.platform.TopUpRepository +import de.schildbach.wallet.ui.dashpay.PlatformRepo +import de.schildbach.wallet.service.work.BaseWorker +import org.bitcoinj.core.Coin +import org.bitcoinj.core.InsufficientMoneyException +import org.bitcoinj.core.Sha256Hash +import org.bitcoinj.crypto.KeyCrypterException +import org.bitcoinj.wallet.authentication.AuthenticationGroupExtension +import org.bouncycastle.crypto.params.KeyParameter +import org.dash.wallet.common.WalletDataProvider +import org.dash.wallet.common.services.analytics.AnalyticsService +import org.slf4j.LoggerFactory + +@HiltWorker +class TopupIdentityWorker @AssistedInject constructor( + @Assisted context: Context, + @Assisted parameters: WorkerParameters, + val analytics: AnalyticsService, + val platformBroadcastService: PlatformBroadcastService, + private val topUpRepository: TopUpRepository, + val walletDataProvider: WalletDataProvider, + val platformRepo: PlatformRepo, + val coinJoinConfig: CoinJoinConfig +) : BaseWorker(context, parameters) { + companion object { + private val log = LoggerFactory.getLogger(TopupIdentityWorker::class.java) + const val KEY_PASSWORD = "TopupIdentityWorker.PASSWORD" + const val KEY_IDENTITY = "TopupIdentityWorker.IDENTITY" + const val KEY_TOPUP_TX = "TopupIdentityWorker.TOPUP_TX" + const val KEY_VALUE = "TopupIdentityWorker.VALUE" + const val KEY_BALANCE = "TopupIdentityWorker.BALANCE" + } + + override suspend fun doWorkWithBaseProgress(): Result { + val password = inputData.getString(KEY_PASSWORD) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_PASSWORD parameter")) + val identity = inputData.getString(KEY_IDENTITY) + ?: return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_IDENTITY parameter")) + val value = inputData.getLong(KEY_VALUE, 0) + if (value == 0L) { + return Result.failure(workDataOf(KEY_ERROR_MESSAGE to "missing KEY_VALUE parameter")) + } + val topupTxId = inputData.getString(KEY_TOPUP_TX)?.let { Sha256Hash.wrap(it) } + val authGroupExtension = walletDataProvider.wallet!!.getKeyChainExtension(AuthenticationGroupExtension.EXTENSION_ID) as AuthenticationGroupExtension + var topupTx = authGroupExtension.topupFundingTransactions.find { it.txId == topupTxId } + val coinValue = Coin.valueOf(value) + + val encryptionKey: KeyParameter + try { + encryptionKey = walletDataProvider.wallet!!.keyCrypter!!.deriveKey(password) + } catch (ex: KeyCrypterException) { + analytics.logError(ex, "Topup Identity: failed to derive encryption key") + val msg = formatExceptionMessage("derive encryption key", ex) + return Result.failure(workDataOf(KEY_ERROR_MESSAGE to msg)) + } + + return try { + if (topupTx == null) { + topupTx = topUpRepository.createTopupTransaction( + platformRepo.blockchainIdentity, + coinValue, + encryptionKey, + coinJoinConfig.getMode() != CoinJoinMode.NONE + ) + } + val wasTxSent = topupTx.confidence.isChainLocked || topupTx.confidence.isTransactionLocked || + topupTx.confidence.numBroadcastPeers() > 0 + if (!wasTxSent) { + topUpRepository.sendTransaction(topupTx) + } + log.info("topup tx sent: {}", topupTx.txId) + topUpRepository.topUpIdentity( + topupTx, + encryptionKey + ) + log.info("topup success: {}", topupTx.txId) + Result.success( + workDataOf( + KEY_IDENTITY to identity, + KEY_TOPUP_TX to topupTx.txId.toString(), + KEY_BALANCE to platformRepo.blockchainIdentity.creditBalance.value * 1000 + ) + ) + } catch (ex: Exception) { + analytics.logError(ex, "Topup Identity: failed to topup identity") + val args = when (ex) { + is InsufficientMoneyException -> arrayOf(ex.missing.toString()) + else -> arrayOf() + } + Result.failure( + workDataOf( + KEY_IDENTITY to identity, + KEY_TOPUP_TX to topupTx?.txId.toString(), + KEY_EXCEPTION to ex.javaClass.simpleName, + KEY_ERROR_MESSAGE to formatExceptionMessage("topup exception:", ex), + KEY_EXCEPTION_ARGS to args + ) + ) + } + } +} \ No newline at end of file diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt index 7185cf7c04..6c9257d75c 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/CreateIdentityService.kt @@ -21,6 +21,7 @@ import de.schildbach.wallet.security.SecurityFunctions import de.schildbach.wallet.security.SecurityGuard import de.schildbach.wallet.service.CoinJoinMode import de.schildbach.wallet.service.platform.PlatformSyncService +import de.schildbach.wallet.service.platform.TopUpRepository import de.schildbach.wallet.ui.dashpay.UserAlert.Companion.INVITATION_NOTIFICATION_ICON import de.schildbach.wallet.ui.dashpay.UserAlert.Companion.INVITATION_NOTIFICATION_TEXT import de.schildbach.wallet.ui.dashpay.work.GetUsernameVotingResultOperation @@ -133,6 +134,7 @@ class CreateIdentityService : LifecycleService() { @Inject lateinit var configuration: Configuration @Inject lateinit var platformRepo: PlatformRepo @Inject lateinit var platformSyncService: PlatformSyncService + @Inject lateinit var topUpRepository: TopUpRepository @Inject lateinit var userAlertDao: UserAlertDao @Inject lateinit var blockchainIdentityDataDao: BlockchainIdentityConfig @Inject lateinit var securityFunctions: SecurityFunctions @@ -254,7 +256,7 @@ class CreateIdentityService : LifecycleService() { } private suspend fun createIdentity(username: String?, retryWithNewUserName: Boolean) { - log.info("username registration starting") + log.info("username registration starting($username, $retryWithNewUserName)") org.bitcoinj.core.Context.propagate(walletApplication.wallet!!.context) val timerEntireProcess = AnalyticsTimer(analytics, log, AnalyticsConstants.Process.PROCESS_USERNAME_CREATE) val timerStep1 = AnalyticsTimer(analytics, log, AnalyticsConstants.Process.PROCESS_USERNAME_CREATE_STEP_1) @@ -364,7 +366,7 @@ class CreateIdentityService : LifecycleService() { // check to see if the funding transaction exists if (blockchainIdentity.assetLockTransaction == null) { val useCoinJoin = coinJoinConfig.getMode() != CoinJoinMode.NONE - platformRepo.createAssetLockTransactionAsync(blockchainIdentity, blockchainIdentityData.username!!, encryptionKey, useCoinJoin) + topUpRepository.createAssetLockTransaction(blockchainIdentity, blockchainIdentityData.username!!, encryptionKey, useCoinJoin) } } @@ -378,7 +380,7 @@ class CreateIdentityService : LifecycleService() { } ?: false if (!sent) { - sendTransaction(blockchainIdentity.assetLockTransaction!!) + topUpRepository.sendTransaction(blockchainIdentity.assetLockTransaction!!) } timerIsLock.logTiming() } @@ -505,10 +507,10 @@ class CreateIdentityService : LifecycleService() { // // Step 2: Create and send the credit funding transaction // - platformRepo.obtainAssetLockTransactionAsync(blockchainIdentity, blockchainIdentityData.invite!!) + topUpRepository.obtainAssetLockTransaction(blockchainIdentity, blockchainIdentityData.invite!!) } else { // if we are retrying, then we need to initialize the credit funding tx - platformRepo.obtainAssetLockTransactionAsync(blockchainIdentity, blockchainIdentityData.invite!!) + topUpRepository.obtainAssetLockTransaction(blockchainIdentity, blockchainIdentityData.invite!!) } if (blockchainIdentityData.creationState <= CreationState.CREDIT_FUNDING_TX_SENDING) { @@ -930,8 +932,8 @@ class CreateIdentityService : LifecycleService() { // determine when voting started by finding the minimum timestamp val earliestCreatedAt = voteContenders.map.values.minOf { - val document = platformRepo.platform.names.deserialize(documentWithVotes.serializedDocument!!) - document.createdAt ?: 0 + val document = documentWithVotes.serializedDocument?.let { platformRepo.platform.names.deserialize(it) } + document?.createdAt ?: 0 } usernameInfo.votingStartedAt = earliestCreatedAt @@ -999,81 +1001,6 @@ class CreateIdentityService : LifecycleService() { } } - /** - * Send the credit funding transaction and wait for confirmation from other nodes that the - * transaction was sent. InstantSendLock, in a block or seen by more than one peer. - * - * Exceptions are returned in the case of a reject message (may not be sent with Dash Core 0.16) - * or in the case of a double spend or some other error. - * - * @param cftx The credit funding transaction to send - * @return True if successful - */ - private suspend fun sendTransaction(cftx: AssetLockTransaction): Boolean { - log.info("Sending credit funding transaction: ${cftx.txId}") - return suspendCoroutine { continuation -> - cftx.confidence.addEventListener(object : TransactionConfidence.Listener { - override fun onConfidenceChanged(confidence: TransactionConfidence?, reason: TransactionConfidence.Listener.ChangeReason?) { - when (reason) { - // If this transaction is in a block, then it has been sent successfully - TransactionConfidence.Listener.ChangeReason.DEPTH -> { - // TODO: a chainlock is needed to accompany the block information - // to provide sufficient proof - } - // If this transaction is InstantSend Locked, then it has been sent successfully - TransactionConfidence.Listener.ChangeReason.IX_TYPE -> { - // TODO: allow for received (IX_REQUEST) instantsend locks - // until the bug related to instantsend lock verification is fixed. - if (confidence!!.isTransactionLocked || confidence.ixType == TransactionConfidence.IXType.IX_REQUEST) { - log.info("credit funding transaction verified with instantsend: ${cftx.txId}") - confidence.removeEventListener(this) - continuation.resumeWith(Result.success(true)) - } - } - - TransactionConfidence.Listener.ChangeReason.CHAIN_LOCKED -> { - if (confidence!!.isChainLocked) { - log.info("credit funding transaction verified with chainlock: ${cftx.txId}") - confidence.removeEventListener(this) - continuation.resumeWith(Result.success(true)) - } - } - // If this transaction has been seen by more than 1 peer, then it has been sent successfully - TransactionConfidence.Listener.ChangeReason.SEEN_PEERS -> { - // being seen by other peers is no longer sufficient proof - } - // If this transaction was rejected, then it was not sent successfully - TransactionConfidence.Listener.ChangeReason.REJECT -> { - if (confidence!!.hasRejections() && confidence.rejections.size >= 1) { - confidence.removeEventListener(this) - log.info("Error sending ${cftx.txId}: ${confidence.rejectedTransactionException.rejectMessage.reasonString}") - continuation.resumeWithException(confidence.rejectedTransactionException) - } - } - TransactionConfidence.Listener.ChangeReason.TYPE -> { - if (confidence!!.hasErrors()) { - confidence.removeEventListener(this) - val code = when (confidence.confidenceType) { - TransactionConfidence.ConfidenceType.DEAD -> RejectMessage.RejectCode.INVALID - TransactionConfidence.ConfidenceType.IN_CONFLICT -> RejectMessage.RejectCode.DUPLICATE - else -> RejectMessage.RejectCode.OTHER - } - val rejectMessage = RejectMessage(Constants.NETWORK_PARAMETERS, code, confidence.transactionHash, - "Credit funding transaction is dead or double-spent", "cftx-dead-or-double-spent") - log.info("Error sending ${cftx.txId}: ${rejectMessage.reasonString}") - continuation.resumeWithException(RejectedTransactionException(cftx, rejectMessage)) - } - } - else -> { - // ignore - } - } - } - }) - walletApplication.broadcastTransaction(cftx) - } - } - override fun onDestroy() { super.onDestroy() serviceJob.cancel() diff --git a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt index 99d534690a..2e55dcc886 100644 --- a/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt +++ b/wallet/src/de/schildbach/wallet/ui/dashpay/PlatformRepo.kt @@ -515,69 +515,7 @@ class PlatformRepo @Inject constructor( // // Step 2 is to create the credit funding transaction // - suspend fun createAssetLockTransactionAsync( - blockchainIdentity: BlockchainIdentity, - username: String, - keyParameter: KeyParameter?, - useCoinJoin: Boolean) - { - withContext(Dispatchers.IO) { - Context.propagate(walletApplication.wallet!!.context) - val fee = if (Names.isUsernameContestable(username)) { - Constants.DASH_PAY_FEE_CONTESTED - } else { - Constants.DASH_PAY_FEE - } - val balance = walletApplication.wallet!!.getBalance(Wallet.BalanceType.ESTIMATED_SPENDABLE) - val emptyWallet = balance == fee && balance <= (fee + Transaction.MIN_NONDUST_OUTPUT) - val cftx = blockchainIdentity.createAssetLockTransaction( - fee, - keyParameter, - useCoinJoin, - returnChange = true, - emptyWallet = emptyWallet - ) - blockchainIdentity.initializeAssetLockTransaction(cftx) - } - } - suspend fun createTopupTransactionAsync(blockchainIdentity: BlockchainIdentity, topupAmount: Coin, keyParameter: KeyParameter?, useCoinJoin: Boolean) { - withContext(Dispatchers.IO) { - Context.propagate(walletApplication.wallet!!.context) - val balance = walletApplication.wallet!!.getBalance(Wallet.BalanceType.ESTIMATED_SPENDABLE) - val emptyWallet = balance == topupAmount && balance <= (topupAmount + Transaction.MIN_NONDUST_OUTPUT) - val cftx = blockchainIdentity.createTopupFundingTransaction( - topupAmount, - keyParameter, - useCoinJoin, - returnChange = true, - emptyWallet = emptyWallet - ) - blockchainIdentity.initializeAssetLockTransaction(cftx) - } - } - - // - // Step 2 is to obtain the credit funding transaction for invites - // - suspend fun obtainAssetLockTransactionAsync(blockchainIdentity: BlockchainIdentity, invite: InvitationLinkData) { - withContext(Dispatchers.IO) { - Context.propagate(walletApplication.wallet!!.context) - var cftxData = platform.client.getTransaction(invite.cftx) - //TODO: remove when iOS uses big endian - if (cftxData == null) - cftxData = platform.client.getTransaction(Sha256Hash.wrap(invite.cftx).reversedBytes.toHex()) - val assetLockTx = AssetLockTransaction(platform.params, cftxData!!) - val privateKey = DumpedPrivateKey.fromBase58(platform.params, invite.privateKey).key - assetLockTx.addAssetLockPublicKey(privateKey) - - // TODO: when all instantsend locks are deterministic, we don't need the catch block - val instantSendLock = InstantSendLock(platform.params, Utils.HEX.decode(invite.instantSendLock), InstantSendLock.ISDLOCK_VERSION) - - assetLockTx.confidence.setInstantSendLock(instantSendLock) - blockchainIdentity.initializeAssetLockTransaction(assetLockTx) - } - } // // Step 3: Register the identity From 936d9fd76adce4de01a5e52cba652f7be58312e9 Mon Sep 17 00:00:00 2001 From: HashEngineering Date: Wed, 1 Jan 2025 22:53:08 -0500 Subject: [PATCH 05/26] feat: topup ui --- wallet/res/drawable/ic_credits.xml | 9 + wallet/res/drawable/ic_mail_icon.xml | 9 + wallet/res/drawable/ic_profile_icon.xml | 9 + wallet/res/layout/dialog_confirm_topup.xml | 145 +++++++++++ wallet/res/layout/dialog_what_are_credits.xml | 116 +++++++++ wallet/res/layout/fragment_tools.xml | 70 ++++- wallet/res/values/strings-dashpay.xml | 6 + wallet/res/values/strings.xml | 1 + .../wallet/payments/SendCoinsTaskRunner.kt | 20 ++ .../wallet/service/work/BaseWorker.kt | 2 + .../wallet/ui/dashpay/utils/DashPayConfig.kt | 9 + .../wallet/ui/more/ToolsFragment.kt | 19 ++ .../wallet/ui/more/ToolsViewModel.kt | 11 + .../more/tools/ConfirmTopUpDialogFragment.kt | 83 ++++++ .../more/tools/ConfirmTopupDialogViewModel.kt | 97 +++++++ .../tools/WhatAreCreditsDialogFragment.kt | 40 +++ .../wallet/ui/send/BuyCreditsFragment.kt | 245 +++++++++++------- .../wallet/ui/send/BuyCreditsViewModel.kt | 60 +++++ .../wallet/ui/send/SendCoinsViewModel.kt | 40 +++ 19 files changed, 899 insertions(+), 92 deletions(-) create mode 100644 wallet/res/drawable/ic_credits.xml create mode 100644 wallet/res/drawable/ic_mail_icon.xml create mode 100644 wallet/res/drawable/ic_profile_icon.xml create mode 100644 wallet/res/layout/dialog_confirm_topup.xml create mode 100644 wallet/res/layout/dialog_what_are_credits.xml create mode 100644 wallet/src/de/schildbach/wallet/ui/more/tools/ConfirmTopUpDialogFragment.kt create mode 100644 wallet/src/de/schildbach/wallet/ui/more/tools/ConfirmTopupDialogViewModel.kt create mode 100644 wallet/src/de/schildbach/wallet/ui/more/tools/WhatAreCreditsDialogFragment.kt create mode 100644 wallet/src/de/schildbach/wallet/ui/send/BuyCreditsViewModel.kt diff --git a/wallet/res/drawable/ic_credits.xml b/wallet/res/drawable/ic_credits.xml new file mode 100644 index 0000000000..2e6e38b605 --- /dev/null +++ b/wallet/res/drawable/ic_credits.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/res/drawable/ic_mail_icon.xml b/wallet/res/drawable/ic_mail_icon.xml new file mode 100644 index 0000000000..f5e53a292d --- /dev/null +++ b/wallet/res/drawable/ic_mail_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/res/drawable/ic_profile_icon.xml b/wallet/res/drawable/ic_profile_icon.xml new file mode 100644 index 0000000000..49534c20bc --- /dev/null +++ b/wallet/res/drawable/ic_profile_icon.xml @@ -0,0 +1,9 @@ + + + diff --git a/wallet/res/layout/dialog_confirm_topup.xml b/wallet/res/layout/dialog_confirm_topup.xml new file mode 100644 index 0000000000..d1bde05bfa --- /dev/null +++ b/wallet/res/layout/dialog_confirm_topup.xml @@ -0,0 +1,145 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +