Skip to content

Commit 5b34bb9

Browse files
committed
feat(multisig): Implement joint account creation flow
- Add CreateJointAccountFragment and ViewModel - Add NameJointAccountFragment for account naming - Add AddJointAccountFragment for participant selection - Implement JointAccountTransactionSignHelper - Add CreateJointAccountUseCase - Add participant selection UI components - Handle contact creation for external addresses
1 parent 4b6f926 commit 5b34bb9

102 files changed

Lines changed: 9948 additions & 41 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cursor/rules/design_system_rules.md

Lines changed: 742 additions & 0 deletions
Large diffs are not rendered by default.

app/src/main/kotlin/com/algorand/android/MainActivity.kt

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ import com.algorand.android.models.TransactionSignData
4141
import com.algorand.android.models.WalletConnectRequest
4242
import com.algorand.android.models.WalletConnectRequest.WalletConnectArbitraryDataRequest
4343
import com.algorand.android.models.WalletConnectRequest.WalletConnectTransaction
44+
import com.algorand.android.modules.addaccount.joint.transaction.ui.PendingSignaturesDialogFragment
4445
import com.algorand.android.modules.assetinbox.assetinboxoneaccount.ui.model.AssetInboxOneAccountNavArgs
4546
import com.algorand.android.modules.autolockmanager.ui.AutoLockManager
4647
import com.algorand.android.modules.deeplink.ui.DeeplinkHandler
@@ -393,7 +394,8 @@ class MainActivity :
393394
is TransactionManagerResult.OnTransactionRequestSigned -> {
394395
hideProgress()
395396
hideLedgerLoadingDialog()
396-
TODO("Implement this")
397+
PendingSignaturesDialogFragment.newInstance(result.signRequestId)
398+
.show(supportFragmentManager, PendingSignaturesDialogFragment.TAG)
397399
}
398400

399401
TransactionManagerResult.LedgerOperationCanceled -> {
@@ -576,7 +578,11 @@ class MainActivity :
576578

577579
private fun navToJointAccountImportDeepLink(address: String) {
578580
navToHome()
579-
TODO("Implement this")
581+
nav(
582+
HomeNavigationDirections.actionGlobalToJointAccountDetailFragment(
583+
accountAddress = address
584+
)
585+
)
580586
}
581587

582588
fun navToContactAdditionNavigation(address: String, label: String?) {

app/src/main/kotlin/com/algorand/android/core/transaction/JointAccountTransactionSignHelper.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ package com.algorand.android.core.transaction
1515
import com.algorand.algosdk.transaction.SignedTransaction
1616
import com.algorand.algosdk.util.Encoder
1717
import com.algorand.android.models.TransactionSignData
18+
import com.algorand.android.modules.addaccount.joint.transaction.domain.usecase.SignAndSubmitJointAccountSignature
1819
import com.algorand.android.utils.extensions.encodeBase64
1920
import com.algorand.android.utils.signTx
2021
import com.algorand.wallet.account.info.domain.usecase.GetAccountRekeyAdminAddress
@@ -25,6 +26,7 @@ import com.algorand.wallet.account.local.domain.usecase.GetLocalAccount
2526
import com.algorand.wallet.account.local.domain.usecase.GetLocalAccounts
2627
import com.algorand.wallet.algosdk.transaction.sdk.SignHdKeyTransaction
2728
import com.algorand.wallet.encryption.domain.utils.clearFromMemory
29+
import android.util.Log
2830
import com.algorand.wallet.foundation.PeraResult
2931
import com.algorand.wallet.jointaccount.domain.usecase.GetJointAccountProposerAddress
3032
import com.algorand.wallet.jointaccount.transaction.domain.model.JointSignRequestDTO
@@ -40,6 +42,7 @@ class JointAccountTransactionSignHelper @Inject constructor(
4042
private val getHdSeed: GetHdSeed,
4143
private val signHdKeyTransaction: SignHdKeyTransaction,
4244
private val proposeJointSignRequest: ProposeJointSignRequest,
45+
private val signAndSubmitJointAccountSignature: SignAndSubmitJointAccountSignature,
4346
private val getJointAccountProposerAddress: GetJointAccountProposerAddress,
4447
private val getAccountRekeyAdminAddress: GetAccountRekeyAdminAddress
4548
) {
@@ -126,7 +129,16 @@ class JointAccountTransactionSignHelper @Inject constructor(
126129
localAccount is LocalAccount.Algo25 || localAccount is LocalAccount.HdKey
127130
}
128131

129-
TODO("Implement auto sign with local accounts")
132+
for (signer in eligibleSigners) {
133+
val result = signAndSubmitJointAccountSignature(
134+
signRequestId = signRequestId,
135+
participantAddress = signer.algoAddress,
136+
rawTransactions = rawTransactions
137+
)
138+
if (result !is PeraResult.Success) {
139+
Log.w(TAG, "Failed to auto-sign for participant")
140+
}
141+
}
130142
}
131143

132144
companion object {

app/src/main/kotlin/com/algorand/android/core/transaction/TransactionSignBaseFragment.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ abstract class TransactionSignBaseFragment(
102102
}
103103

104104
protected open fun onJointAccountSignRequestCreated(signRequestId: String) {
105-
TODO("Implement this")
105+
nav(HomeNavigationDirections.actionGlobalToJointAccountSignRequestFragment(signRequestId))
106106
}
107107

108108
private val ledgerLoadingDialogListener = LedgerLoadingDialog.Listener { shouldStopResources ->

app/src/main/kotlin/com/algorand/android/modules/accountdetail/jointaccountdetail/domain/usecase/JointAccountInboxOperationsUseCase.kt

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,32 @@
1313
package com.algorand.android.modules.accountdetail.jointaccountdetail.domain.usecase
1414

1515
import com.algorand.android.deviceregistration.domain.usecase.DeviceIdUseCase
16-
import com.algorand.wallet.jointaccount.domain.repository.JointAccountRepository
16+
import com.algorand.android.models.Result
17+
import com.algorand.android.modules.addaccount.joint.core.domain.repository.JointAccountRepository
18+
import com.algorand.android.modules.addaccount.joint.creation.domain.usecase.DeleteInboxJointInvitationNotification
19+
import com.algorand.wallet.inbox.domain.model.InboxMessagesDTO
20+
import com.algorand.wallet.inbox.domain.model.InboxSearchDTO
1721
import javax.inject.Inject
1822
import javax.inject.Named
1923

2024
class JointAccountInboxOperationsUseCase @Inject constructor(
2125
private val deviceIdUseCase: DeviceIdUseCase,
2226
@param:Named(JointAccountRepository.INJECTION_NAME)
2327
private val jointAccountRepository: JointAccountRepository,
28+
private val deleteInboxJointInvitationNotification: DeleteInboxJointInvitationNotification
2429
) {
2530

2631
fun getDeviceId(): String? = deviceIdUseCase.getSelectedNodeDeviceId()
32+
33+
suspend fun getInboxMessages(addresses: List<String>): Result<InboxMessagesDTO> {
34+
val deviceId = getDeviceId()?.toLongOrNull()
35+
?: return Result.Error(Exception("Device ID not available"))
36+
return jointAccountRepository.getInboxMessages(deviceId, InboxSearchDTO(addresses))
37+
}
38+
39+
suspend fun deleteNotification(accountAddress: String): Boolean {
40+
val deviceId = getDeviceId()?.toLongOrNull() ?: return false
41+
val result = deleteInboxJointInvitationNotification(deviceId, accountAddress)
42+
return result is Result.Success
43+
}
2744
}

app/src/main/kotlin/com/algorand/android/modules/accountdetail/jointaccountdetail/ui/JointAccountDetailFragment.kt

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import android.view.LayoutInflater
1717
import android.view.View
1818
import android.view.ViewGroup
1919
import androidx.fragment.app.viewModels
20+
import com.algorand.android.HomeNavigationDirections
2021
import com.algorand.android.core.DaggerBaseFragment
2122
import com.algorand.android.models.FragmentConfiguration
2223
import com.algorand.android.modules.accountdetail.jointaccountdetail.viewmodel.JointAccountDetailViewModel
@@ -70,11 +71,23 @@ class JointAccountDetailFragment : DaggerBaseFragment(0), JointAccountDetailScre
7071
}
7172

7273
private fun navigateToNameJointAccount(event: ViewEvent.NavigateToNameJointAccount) {
73-
TODO("Implement this")
74+
nav(
75+
HomeNavigationDirections.actionGlobalToNameJointAccountFragment(
76+
threshold = event.threshold,
77+
participantAddresses = event.participantAddresses.toTypedArray()
78+
)
79+
)
7480
}
7581

7682
private fun navigateToEditContact(event: ViewEvent.NavigateToEditContact) {
77-
TODO("Implement this")
83+
nav(
84+
HomeNavigationDirections.actionGlobalEditContactFragment(
85+
contactName = event.contactName,
86+
contactPublicKey = event.contactPublicKey,
87+
contactDatabaseId = event.contactDatabaseId,
88+
contactProfileImageUri = event.contactProfileImageUri
89+
)
90+
)
7891
}
7992

8093
override fun onBackClick() {

app/src/main/kotlin/com/algorand/android/modules/accountdetail/jointaccountdetail/viewmodel/DefaultJointAccountDetailProcessor.kt

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212

1313
package com.algorand.android.modules.accountdetail.jointaccountdetail.viewmodel
1414

15+
import com.algorand.android.models.Result
1516
import com.algorand.android.modules.accountcore.ui.usecase.GetAccountDisplayName
1617
import com.algorand.android.modules.accountdetail.jointaccountdetail.domain.usecase.CreateJointAccountParticipantItem
18+
import com.algorand.android.modules.accountdetail.jointaccountdetail.domain.usecase.JointAccountInboxOperationsUseCase
1719
import com.algorand.android.modules.accountdetail.jointaccountdetail.ui.model.JointAccountParticipantItem
1820
import com.algorand.android.repository.ContactRepository
1921
import com.algorand.android.utils.toShortenedAddress
@@ -27,6 +29,7 @@ internal class DefaultJointAccountDetailProcessor @Inject constructor(
2729
private val getAccountDisplayName: GetAccountDisplayName,
2830
private val contactRepository: ContactRepository,
2931
private val createJointAccountParticipantItem: CreateJointAccountParticipantItem,
32+
private val inboxOperationsUseCase: JointAccountInboxOperationsUseCase
3033
) : JointAccountDetailProcessor {
3134

3235
override suspend fun createContentState(
@@ -71,8 +74,10 @@ internal class DefaultJointAccountDetailProcessor @Inject constructor(
7174
): JointAccountDetailProcessor.InvitationResult {
7275
val addresses = createJointAccountParticipantItem.getLocalAccountAddresses()
7376

74-
TODO("Implement this")
75-
return JointAccountDetailProcessor.InvitationResult.NetworkError
77+
return when (val result = inboxOperationsUseCase.getInboxMessages(addresses)) {
78+
is Result.Success -> parseInvitationFromInboxMessages(result.data, accountAddress)
79+
is Result.Error -> JointAccountDetailProcessor.InvitationResult.NetworkError
80+
}
7681
}
7782

7883
override suspend fun createParticipantItems(
@@ -82,8 +87,7 @@ internal class DefaultJointAccountDetailProcessor @Inject constructor(
8287
}
8388

8489
override suspend fun deleteInboxNotification(accountAddress: String) {
85-
// inboxOperationsUseCase.deleteNotification(accountAddress)
86-
TODO("Implement this")
90+
inboxOperationsUseCase.deleteNotification(accountAddress)
8791
}
8892

8993
override suspend fun isJointAccountExists(accountAddress: String): Boolean {

app/src/main/kotlin/com/algorand/android/modules/accountdetail/ui/AccountDetailFragment.kt

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,11 @@ class AccountDetailFragment :
256256
}
257257

258258
private fun navToJointAccountDetailFragment() {
259-
TODO("Implement this")
259+
nav(
260+
AccountDetailFragmentDirections.actionAccountDetailFragmentToJointAccountDetailFragment(
261+
accountDetailViewModel.accountAddress
262+
)
263+
)
260264
}
261265

262266
override fun onImageItemClick(nftAssetId: Long) {
@@ -559,7 +563,12 @@ class AccountDetailFragment :
559563
}
560564

561565
private fun navToInboxWithFilter() {
562-
TODO("Implement this")
566+
nav(
567+
AccountDetailFragmentDirections
568+
.actionAccountDetailFragmentToAssetInboxAllAccountsNavigation(
569+
filterAccountAddress = accountDetailViewModel.accountAddress
570+
)
571+
)
563572
}
564573

565574
private fun navToBuySellActionsBottomSheet() {

app/src/main/kotlin/com/algorand/android/modules/accounts/ui/viewmodel/AccountsPreviewUseCase.kt

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ import com.algorand.android.modules.parity.domain.model.SelectedCurrencyDetail
2525
import com.algorand.android.modules.peraconnectivitymanager.ui.PeraConnectivityManager
2626
import com.algorand.android.utils.CacheResult
2727
import com.algorand.wallet.banner.domain.usecase.GetBannerFlow
28-
import com.algorand.wallet.inbox.asset.domain.usecase.GetAssetInboxRequestCountFlow
2928
import com.algorand.wallet.privacy.domain.usecase.GetPrivacyModeFlow
30-
import com.algorand.wallet.remoteconfig.domain.usecase.IsFeatureToggleEnabled
3129
import com.algorand.wallet.spotbanner.domain.model.SpotBannerFlowData
3230
import com.algorand.wallet.spotbanner.domain.usecase.GetSpotBannersFlow
3331
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -44,12 +42,10 @@ class AccountsPreviewUseCase @Inject constructor(
4442
private val portfolioValueItemMapper: PortfolioValueItemMapper,
4543
private val peraConnectivityManager: PeraConnectivityManager,
4644
private val accountPreviewProcessor: AccountPreviewProcessor,
47-
private val getAssetInboxRequestCountFlow: GetAssetInboxRequestCountFlow,
4845
private val getAccountLiteCacheFlow: GetAccountLiteCacheFlow,
4946
private val getPrivacyModeFlow: GetPrivacyModeFlow,
5047
private val getBannerFlow: GetBannerFlow,
5148
private val getSpotBannersFlow: GetSpotBannersFlow,
52-
private val isFeatureToggleEnabled: IsFeatureToggleEnabled
5349
) {
5450

5551
suspend fun getInitialAccountPreview(): AccountPreview {
@@ -82,7 +78,6 @@ class AccountsPreviewUseCase @Inject constructor(
8278
}
8379

8480
private suspend fun getAccountPreviewInitializationFlow(accountLiteCacheData: Data): Flow<AccountPreview> {
85-
TODO("Implement this")
8681
return combine(
8782
getBannerFlow(),
8883
getSpotBannersFlow(getSpotBannerFlowData(accountLiteCacheData)),
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
/*
2+
* Copyright 2022-2025 Pera Wallet, LDA
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
6+
* Unless required by applicable law or agreed to in writing, software
7+
* distributed under the License is distributed on an "AS IS" BASIS,
8+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
* See the License for the specific language governing permissions and
10+
* limitations under the License
11+
*/
12+
13+
package com.algorand.android.modules.addaccount.intro.di
14+
15+
import com.algorand.android.modules.addaccount.intro.domain.usecase.CreateAlgo25Account
16+
import com.algorand.android.modules.addaccount.intro.domain.usecase.CreateAlgo25AccountUseCase
17+
import com.algorand.android.modules.addaccount.intro.domain.usecase.CreateHdKeyAccount
18+
import com.algorand.android.modules.addaccount.intro.domain.usecase.CreateHdKeyAccountUseCase
19+
import com.algorand.android.modules.addaccount.intro.domain.usecase.GetAddAccountIntroPreview
20+
import com.algorand.android.modules.addaccount.intro.domain.usecase.GetAddAccountIntroPreviewUseCase
21+
import dagger.Module
22+
import dagger.Provides
23+
import dagger.hilt.InstallIn
24+
import dagger.hilt.components.SingletonComponent
25+
26+
@Module
27+
@InstallIn(SingletonComponent::class)
28+
internal object AddAccountIntroUiModule {
29+
30+
@Provides
31+
fun provideGetAddAccountIntroPreview(
32+
impl: GetAddAccountIntroPreviewUseCase
33+
): GetAddAccountIntroPreview = impl
34+
35+
@Provides
36+
fun provideCreateHdKeyAccount(
37+
impl: CreateHdKeyAccountUseCase
38+
): CreateHdKeyAccount = impl
39+
40+
@Provides
41+
fun provideCreateAlgo25Account(
42+
impl: CreateAlgo25AccountUseCase
43+
): CreateAlgo25Account = impl
44+
}

0 commit comments

Comments
 (0)